这篇文章与@EnableAutoConfiguration、@ComponentScan和@Configuration是相关联的。

Spring是如何处理@EnableXXXXX系列的注解的?

本质上来说,在@EnableXXXX系列的注解里面的@Import是@EnableXXX系列的核心。以下是@Import注解的文档注释(Deepl机器翻译的)

表示一个或多个要导入的组件类--通常是@Configuration类。
提供的功能相当于Spring XML中的<import/>元素。允许导入@Configuration类、ImportSelector和ImportBeanDefinitionRegistrar实现,以及常规组件类(从4.2开始;类似于AnnotationConfigApplicationContext.register(java.lang.Class<?>...))。

在导入的@Configuration类中声明的@Bean定义应该通过使用@Autowired注入来访问。要么bean本身可以被自动注入,要么声明bean的配置类实例可以被自动注入。后一种方法允许在@Configuration类方法之间进行明确的、IDE友好的导航。

可以在类级或作为元注解声明。

如果需要导入XML或其他非@Configuration Bean定义的资源,请使用@ImportResource注解代替。

看完这个文档你大概知道了这个注解的作用就是导入Configration类。而所有的Configration类都是交由ConfigurationClassParser解析的,他里面有一个重要的方法————parse(),它经过层层调用会调用processImports()方法处理@Import注解,processImport()方法处理4种类型的import类:

  • 1.DeferredImportSelector实现类,它是ImportSelector的子接口。
    它不是立刻处理的,他会等下面的ImportSelector和Configuration类处理完成后才会处理。一般处理@Configration+@Conditional系列的注解使用,因为你可以保证其他的@Configration @Bean都处理完了。
  • 2.ImportSelector实现类,
    他是立即处理的,里面有一个方法selectImports(),返回你要加载的Configration类的全限定类名数组。
  • 3.ImportBeanDefinitionRegistrar
    它是最后处理的,也就是在DeferredImportSelector之后处理,它有两个registerBeanDefinitions()方法可以用来注册BeanDefinition。
  • 4.普通类(Configuration类)
    非前三种情况的所有import的类都被认为是Configuration类,不论是否加上@Configuration注解,又因为在Spring中everything is a bean,故如果你import一个普通类这个普通类的实例也可以注入到其他容器。

注:前三种一般要配合EnvironmentAware,BeanFactoryAware,BeanClassLoaderAware,ResourceLoaderAware中的一个或者多个使用,因为他们可以帮助你寻找资源。

而调用ConfigurationClassParser的对象是ConfigurationClassPostProcessor,这是一个专门用来处理Configuration类的的后置处理器,它先于BeanFactoryPostProcessor,因为处理Configuration类对象的本质是向容器插入BeanDefinition。从这里你也应该知道了处理@Import——也就是@EnableXXX——需要和@Configuration一起使用,因为处理@Import是处理@Configuration的一个附带过程。

于是我们可以得到相应的处理步骤:

  • 1.ConfigurationClassPostProcessor将所有的扫描到的非Import引入的@Configuration注解的类做成set交给ConfigurationClassParser解析。
  • 2.ConfigurationClassParser遇到@Import注解,根据上述四种方式处理不同的import的类。
  • 3.不同的导入虽然处理方式不同,但是它们的目的都是将BeanDefinition添加到容器。

注:而对于@EnableAutoConfiguration来说,它导入的是AutoConfigurationImportSelector,它会使用SpringFactoriesLoader将所有的需要导入的Configuration类从spring.factories导入,然后将解析出的Configuration类继续处理。

如何自定义一个@EnableXXXX注解?

看完上述,你大概知道了一个@EnableXXX注解是如何实现的。那么我们该如何自定义呢?
假设我们注入一个TestClass对象,如下:

public class TestClass {
    public void say() {
        System.out.println("hello world!!!");
    }
}

定义TestConfigration类(实际上,如果你只是想注入TestClass那Configuration不是必要的,但是在下文的优雅实现Starter会用到),如下:

@Configuration
public class TestConfigration {
    @Bean
    public TestClass testClass() {
        return new TestClass();
    }
}

使用@EnableXXX方法注入有四种方式,如下:
1.ImportBeanDefinitionRegistrar方式:定义@EnableXXX注解,导入ImportBeanDefinitionRegistrar实现。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({EnableAnnotationTestImportBeanDefinitionRegistrar.class})
public @interface EnableAnnotationTest {}
public class EnableAnnotationTestImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
    private ResourceLoader resourceLoader;
    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader=resourceLoader;
    }
    
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //注册Configuration类的BeanDefinition
        BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(TestConfigration.class).getBeanDefinition();
        registry.registerBeanDefinition("testConfigration", beanDefinition);
    }
    
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
            BeanNameGenerator importBeanNameGenerator) {
        ImportBeanDefinitionRegistrar.super.registerBeanDefinitions(importingClassMetadata, registry, importBeanNameGenerator);
    }

}

2.ImportSelector方式:定义注解,导入ImportSelector实现:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({EnableAnnotationTest2ImportSelector.class})
public @interface EnableAnnotationTest2 {}
public class EnableAnnotationTest2ImportSelector implements ImportSelector, ResourceLoaderAware {
    private ResourceLoader resourceLoader;
    private static final String[] NO_IMPORTS = {};
    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader=resourceLoader;
    }
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        //你要导入的Configuration类的全限定类名
        return new String[]{"com.bigbrotherlee.enable.TestConfigration"};
    }
}

3.DeferredImportSelector方式:定义注解,导入DeferredImportSelector实现:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({EnableAnnotationTestDeferredImportSelector.class})
public @interface EnableAnnotationTest3 {}
public class EnableAnnotationTestDeferredImportSelector implements DeferredImportSelector, ResourceLoaderAware {
    private ResourceLoader resourceLoader;
    private static final String[] NO_IMPORTS = {};
    
    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader=resourceLoader;
    }

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return NO_IMPORTS;
    }    
    
    @Override
    public Class<? extends Group> getImportGroup() {
        return TestGroup.class;
    }
    
    public static class  TestGroup implements Group{
        
        private AnnotationMetadata metadata;
        
        @Override
        public void process(AnnotationMetadata metadata, DeferredImportSelector selector) {
            this.metadata = metadata;
        }

        @Override
        public Iterable<Entry> selectImports() {
            //你要导入的Configuration类
            List<Entry> result = new CopyOnWriteArrayList<DeferredImportSelector.Group.Entry>();
            result.add(new Entry(metadata, "com.bigbrotherlee.enable.TestConfigration"));
            return result;
        }
    } 

}

4.直接导入:定义@EnableXXX注解,导入对应的Configuration类或者直接导入TestClass:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({TestConfigration.class})
public @interface EnableAnnotationTest4 {}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({TestClass.class})
public @interface EnableAnnotationTest4 {}

注:第4种,这种方式是不推荐的,它不够动态,扩展性不够好。本质上来说@Import导入的类如果没有实现特殊的接口的话会直接当作Configuration类来处理。@Configuration是一个复合注解,会当做一个Component注入到容器,如下:


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {}

注:通常,前三种需要配合XXXXXAware感知接口一起使用,感知接口对是搜索资源很有帮助。
注:DeferredImportSelector会在其他类型导入处理完成后调用,在配合@ConditionalXXX一起使用的时候很有帮助;AutoConfigurationImportSelector就是使用的这种实现。
注:@SpringBootApplication本身是一个@Configuration注解,在@EnableAutoConfiguration、@ComponentScan和@Configuration文中有提到,所以@EnableXXX注解一般会放在@SpringBootApplication下面。

如何写一个自己的@ConditionalXXX注解?

前文中提到DeferredImportSelector与@ConditionalXXX一起使用会有奇效,那么我们如果自己定义一个这样的注解该如何实现呢?实际上它与@EnableXXX系列的注解是类似的,它依赖@Conditional注解实现,其文档如下:

表示只有当所有指定的条件都匹配时,组件才有资格注册。

条件是指在Bean定义被注册之前可以通过编程确定的任何状态(详情请看Condition接口)。

条件注解可以通过以下任何方式使用。
-作为任何直接或间接使用@Component注解的类的类型级注解,包括@Configuration类。
-作为元注解,用于组成自定义定型注解。
-作为任何@Bean方法的方法级注解。

如果一个@Configuration类被标记为@Conditional,那么与该类相关的所有@Bean方法、@Import注解和@ComponentScan注解都将受到条件的限制。

注意:不支持继承 @Conditional 注解;任何来自超类或来自重载方法的条件将不被考虑。为了执行这些语义,@Conditional 本身不会被声明为 @Inherited;此外,任何使用 @Conditional 进行元注解的自定义组成注解都不能被声明为 @Inherited。

ConfigurationClassParser在处理Configuration之前,会新调用ConditionEvaluator的shouldSkip()方法判断是否需要处理该类,该方法会将该元素上的所有@ConditionalXX一一处理,如果全部match则满足处理条件。
以上是SpringBoot处理步骤,我们如果要自定义一个处理类则应该如下处理:

  • 1.定义一个@ConditionalXXX注解:

    @Target({ ElementType.TYPE, ElementType.METHOD })
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Conditional(OnTestCondition.class)//关键
    public @interface ConditionalOnTest {}
  • 2.写Conditional处理类:

    public class OnTestCondition extends SpringBootCondition{
      @Override
      public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
          //符合条件
          return ConditionOutcome.match("符合条件-------------------");
      }
    
    }

    然后你加在相应的目标上就OK了。
    注:很显然你可以通过ConditionContext对象获得你想要的容器内的资源。从这里你也可以想到为什么要与DeferredImportSelector配合使用要好一些,因为其他资源都已经加载完毕。
    注:ConditionOutcome.match("")与ConditionOutcome.noMatch("")分别表示匹配与不匹配,里面的内容会记录到日志里面。
    注:SpringBootCondition是Condition接口的一个抽象类实现,你要自定义建议继承SpringBootCondition使用。
    注:在绝大多数时候@ConditionalXXXXX注解是完全可以满足需求的。

@EnableAutoConfiguration、@ComponentScan和@Configuration中有提到一些条件的推断过程,你可以回头看看。

如何优雅地实现一个starter

前文中提到@EnableAutoConfiguration是配合DeferredImportSelector来实现的,并且他会他会使用SpringFactoriesLoader加载配置。所以,很显然实现一个starter需要如下几个步骤:

  • 1.编写你需要导入的Configuration类与你需要导入的bean:

    public class StarterClass {
      public void doSome() {
          System.out.println("--------run doSome-----------");
      }
    }
    @Configuration
    public class TestConfig {
      @Bean
      public StarterClass starterClass() {
          return new StarterClass();
      }
    }
  • 2.配置你需要导入的configuration类到META-INF/spring.factories配置文件:

    # Auto Configure
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.example.starter.configuration.TestConfig

    然后你直接导入该starter就可以直接使用了。

这里有两个要点需要提示:假如你需要动态导入Bean,就像@EnableAutoConfiguration一样,在编写程序的时候你是不知道需要导入哪些bean,那你就需要自己写一个@EnableXXX注解(也就是@Import导入合适的类去发现并注册你要注册的类),然后自己处理这个注解;假如你知道需要导入哪些bean而这些bean的导入时需要满足一定条件的,那你需要配合@Conditional系列的注解使用,甚至需要自定义@ConditionalXXX注解。

注:
1.在@EnableAutoConfiguration、@ComponentScan和@Configuration也有提到,SpringApplication.run(Class,String[])传入的都是配置。而作为@SpringBootApplication作为三个注解地复合,@EnableAutoConfiguration能够将非同包的资源有选择地导入到容器,@ComponentScan可以将本包为besePackage扫到地资源导入到容器。
2.转载请标名出处与作者。已经或者建议请评论区留言。

标签: springboot, spring

仅有一条评论

  1. 二次阅读,收益泼猴

添加新评论