Spring是如何处理@EnableXXXXXX注解的,以及该如何优雅得实现自己得starter?
这篇文章与@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.转载请标名出处与作者。已经或者建议请评论区留言。
二次阅读,收益泼猴