Spring系列(16-19)

烟雨 3年前 (2022-09-23) 阅读数 413 #Spring
文章标签 Spring

十六、@Configration和@Bean注解

1、@Configuration注解

@Configuration这个注解可以加在类上,让这个类的功能等同于一个bean xml配置文件,可通过 AnnotationConfigApplicationContext 来加载 @Configuration 修饰的类,如下:
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigBean.class);
@Configuration注解修饰的类,会被Spring通过cglib做增强处理,通过cglib会生成一个代理对象,代理会拦截所有被@Bean注解修饰的方法,可以确保一些bean是单例的。
注意:
  • 有没有@Configuration注解,@Bean都会起效,都会将@Bean修饰的方法作为bean注册到容器中。被@Configuration修饰的bean最后输出的时候带有EnhancerBySpringCGLIB 的字样,而没有@Configuration注解的bean没有Cglib的字样;有 EnhancerBySpringCGLIB 字样的说明这个bean被cglib处理过的,变成了一个代理对象。

  • 被@Configuration修饰的类,Spring容器中会通过cglib给这个类创建一个代理,代理会拦截所有被@Bean 修饰的方法,默认情况(bean为单例)下确保这些方法只被调用一次,从而确保这些bean是同一个bean,即单例的。

2、@Bean注解

这个注解类似于bean xml配置文件中的bean元素,用来在Spring容器中注册一个bean。
@Bean注解用在方法上,表示通过方法来定义一个bean,默认将方法名称作为bean名称,将方法返回值作为bean对象,注册到Spring容器中。
@Bean
public User user1() {
	return new User();
}

十七、@ComponentScan、@ComponentScans注解

1、@ComponentScan

@ComponentScan用于批量注册bean。这个注解会让Spring去扫描某些包及其子包中所有的类,然后将满足一定条件的类作为bean注册到Spring容器容器中。
具体需要扫描哪些包?以及这些包中的类满足什么条件时被注册到容器中,这些都可以通过这个注解中的参数动态配置。
先来看一下这个注解的定义:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
    @AliasFor("basePackages")
    String[] value() default {};
    
    @AliasFor("value")
    String[] basePackages() default {};
    
    Class<?>[] basePackageClasses() default {};
    
    Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
    
    Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;
    
    ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;
    
    String resourcePattern() default "**/*.class";
    
    boolean useDefaultFilters() default true;
    
    Filter[] includeFilters() default {};
    
    Filter[] excludeFilters() default {};
    
    boolean lazyInit() default false;
}
定义上可以看出此注解可以用在任何类型上面,不过我们通常将其用在类上面。
常用参数:
  1. value:指定需要扫描的包。

  2. basePackages:作用同value;value和basePackages不能同时存在设置,可二选一。

  3. basePackageClasses:指定一些类,spring容器会扫描这些类所在的包及其子包中的类。

  4. nameGenerator:自定义bean名称生成器。

  5. resourcePattern:需要扫描包中的那些资源,默认是:**/*.class,即会扫描指定包中所有的class文件。

  6. useDefaultFilters:对扫描的类是否启用默认过滤器,默认为true。

  7. includeFilters:过滤器:用来配置被扫描出来的那些类会被作为组件注册到容器中。

  8. excludeFilters:过滤器,和includeFilters作用刚好相反,用来对扫描的类进行排除的,被排除的类不会被注册到容器中。

  9. lazyInit:是否延迟初始化被注册的bean。

  10. @Repeatable(ComponentScans.class),这个注解可以同时使用多个。

@ComponentScan工作的过程:

Spring会扫描指定的包,且递归下面子包,得到一批类的数组然后这些类会经过上面的各种过滤器,最后剩下的类会被注册到容器中所以玩这个注解,主要关注2个问题:
第一个:需要扫描哪些包?通过 value、backPackages、basePackageClasses 这3个参数来控制。
第二个:过滤器有哪些?通过 useDefaultFilters、includeFilters、excludeFilters 这3个参数来控制过滤器。
这两个问题搞清楚了,就可以确定哪些类会被注册到容器中。
---------------------------------------------------------------------------------------------------------------------------------
默认情况下,任何参数都不设置的情况下,此时,会将@ComponentScan修饰的类所在的包作为扫描包;默认情况下useDefaultFilters为true,这个为true的时候,Spring容器内部会使用默认过滤器。
规则是:凡是类上有 @Repository、@Service、@Controller、@Component 这几个注解中的任何一个的,那么这个类就会被作为bean注册到Spring容器中,所以默认情况下,只需在类上加上这几个注解中的任何一个,这些类就会自动交给Spring容器来管理了。

2、@Component、@Repository、@Service、@Controller

spring提供这4个注解,是为了让系统更清晰,通常情况下,系统是分层结构的,多数系统一般分为controller层、service层、dao层。
@controller通常用来标注controller层组件,@service注解标注service层的组件,@Repository标注dao层的组件,这样可以让整个系统的结构更清晰,当看到这些注解的时候,会和清晰的知道属于哪个层,对于Spring来说,将这3个注解替换成@Component注解,对系统没有任何影响,产生的效果是一样的。

3、includeFilters的使用

过滤器:用来配置被扫描出来的那些类会被作为组件注册到容器中。再来看一下includeFilters这个参数的定义:
Filter[] includeFilters() default {};

是一个 Filter 类型的数组,多个Filter之间为或者关系,即满足任意一个就可以了,看一下 Filter 的代码:

@Retention(RetentionPolicy.RUNTIME)
@Target({})
@interface Filter {
    FilterType type() default FilterType.ANNOTATION;
    
    @AliasFor("classes")
    Class<?>[] value() default {};
    
    @AliasFor("value")
    Class<?>[] classes() default {};
    
    String[] pattern() default {};
}
可以看出Filter也是一个注解,参数:

type

过滤器的类型,是个枚举类型,5种类型
  1. ANNOTATION:通过注解的方式来筛选候选者,即判断候选者是否有指定的注解。

  2. ASSIGNABLE_TYPE:通过指定的类型来筛选候选者,即判断候选者是否是指定的类型。

  3. ASPECTJ:ASPECTJ表达式方式,即判断候选者是否匹配ASPECTJ表达式。

  4. REGEX:正则表达式方式,即判断候选者的完整名称是否和正则表达式匹配。

  5. CUSTOM:用户自定义过滤器来筛选候选者,对候选者的筛选交给用户自己来判断。

value

和参数classes效果一样,二选一。

classes

3种情况如下
  1. 当type=FilterType.ANNOTATION时,通过classes参数可以指定一些注解,用来判断被扫描的类上是否有classes参数指定的注解。

  2. 当type=FilterType.ASSIGNABLE_TYPE时,通过classes参数可以指定一些类型,用来判断被扫描的类是否是classes参数指定的类型。

  3. 当type=FilterType.CUSTOM时,表示这个过滤器是用户自定义的,classes参数就是用来指定用户自定义的过滤器,自定义的过滤器需要实现org.springframework.core.type.filter.TypeFilter接口。

pattern

2种情况如下
  1. 当type=FilterType.ASPECTJ时,通过pattern来指定需要匹配的ASPECTJ表达式的值。

  2. 当type=FilterType.REGEX时,通过pattern来自正则表达式的值。

案例:扫描包含某个注解的类

我们自定义一个注解,让标注有这些注解的类自动注册到容器中
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component //@1
public @interface MyBean {
    @AliasFor(annotation = Component.class) //@2
	String value() default ""; //@3
}
重点在于@1和@2这2个地方的代码,通过上面的参数可以间接给@Component注解中的value设置值。
创建一个类,使用这个注解标注
@MyBean
public class Service1 {
}

使用@CompontentScan标注

@ComponentScan(
    useDefaultFilters = false, //不启用默认过滤器
    includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes =MyBean.class)})
public class ScanBean3 {
}
上面指定了Filter的type为注解的类型,只要类上面有 @MyBean 注解的,都会被作为bean注册到容器中。

案例:扫描包含指定类型的类

让Spring来进行扫描,类型满足IService的都将其注册到容器
public interface IService {
}

@CompontentScan标注的类

@ComponentScan(
	useDefaultFilters = false, //不启用默认过滤器
	includeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes= IService.class)}
)
public class ScanBean4 {
}

4、自定义Filter

有时候我们需要用到自定义的过滤器,使用自定义过滤器的步骤:
  1. 设置@Filter中type的类型为:FilterType.CUSTOM。

  2. 自定义过滤器类,需要实现接口:org.springframework.core.type.filter.TypeFilter。

  3. 设置@Filter中的classses为自定义的过滤器类型。

来看一下 TypeFilter 这个接口的定义:
@FunctionalInterface
public interface TypeFilter {
	boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException;
}
是一个函数式接口,包含一个match方法,方法返回boolean类型,有2个参数,都是接口类型的,下面介绍一下这2个接口。

MetadataReader接口

类元数据读取器,可以读取一个类上的任意信息,如类上面的注解信息、类的磁盘路径信息、类的class对象的各种信息,Spring进行了封装,提供了各种方便使用的方法。
public interface MetadataReader {
    /**
    * 返回类文件的资源引用
    */
    Resource getResource();
    /**
    * 返回一个ClassMetadata对象,可以通过这个读想获取类的一些元数据信息,如类的class对象、
    是否是接口、是否有注解、是否是抽象类、父类名称、接口名称、内部包含的之类列表等等,可以去看一下源
    码
    */
    ClassMetadata getClassMetadata();
    /**
    * 获取类上所有的注解信息
    */
    AnnotationMetadata getAnnotationMetadata();
}

MetadataReaderFactory接口

类元数据读取器工厂,可以通过这个类获取任意一个类的MetadataReader对象。
public interface MetadataReaderFactory {
    /**
    * 返回给定类名的MetadataReader对象
    */
    MetadataReader getMetadataReader(String className) throws IOException;
    /**
    * 返回指定资源的MetadataReader对象
    */
    MetadataReader getMetadataReader(Resource resource) throws IOException;
}

我们来个自定义的Filter,判断被扫描的类如果是 IService 接口类型的,就让其注册到容器中。

public class MyFilter implements TypeFilter {
    /**
    * @param metadataReader
    * @param metadataReaderFactory
    * @return
    * @throws IOException
    */
    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
    	Class curClass = null;
        try {
        //当前被扫描的类
        curClass = Class.forName(metadataReader.getClassMetadata().getClassName());
        } catch (ClassNotFoundException e) {
        	e.printStackTrace();
        }
        
        //判断curClass是否是IService类型
        boolean result = IService.class.isAssignableFrom(curClass);
        return result;
    }
}

@CompontentScan标注的类

@ComponentScan(
    useDefaultFilters = false, //不启用默认过滤器
    includeFilters = {@ComponentScan.Filter(type = FilterType.CUSTOM, classes =MyFilter.class)}
)
public class ScanBean5 {
}
type为FilterType.CUSTOM,表示Filter是用户自定义的,classes为自定义的过滤器。

5、excludeFilters

配置排除的过滤器,满足这些过滤器的类不会被注册到容器中,用法上面和includeFilters用一样。

十八、@Import批量注册bean

1、@Import出现的背景

目前为止,注解的方式批量注册bean,前面我们介绍了2种方式:
  1. @Configuration结合@Bean注解的方式

  2. @CompontentScan扫描包的方式

下面我们来看几个问题。
问题1
如果需要注册的类是在第三方的jar中,那么我们如果想注册这些bean有2种方式:
  1. 通过@Bean标注方法的方式,一个个来注册。

  2. @CompontentScan的方式:默认的@CompontentScan是无能为力的,默认情况下只会注册@Compontent标注的类,此时只能自定义@CompontentScan中的过滤器来实现了。

这2种方式都不是太好,每次有变化,调整的代码都比较多。
问题2
通常我们的项目中有很多子模块,可能每个模块都是独立开发的,最后通过jar的方式引进来,每个模块中都有各自的@Configuration、@Bean标注的类,或者使用@CompontentScan标注的类。
被@Configuration、@Bean、@CompontentScan标注的类,我们统称为bean配置类,配置类可以用来注册bean,此时如果我们只想使用其中几个模块的配置类,怎么办?
@Import可以很好的解决这2个问题,下面我们来看@Import怎么玩的。

2、@Import使用

先看Spring对它的注释,总结下来作用就是和xml配置的 <import />标签作用一样,允许通过它引入@Configuration标注的类 , 引入ImportSelector接口和ImportBeanDefinitionRegistrar接口的实现,也包括@Component注解的普通类。
总的来说:@Import可以用来批量导入需要注册的各种类,如普通的类、配置类,完后完成普通类和配置类中所有bean的注册。
@Import的源码:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
    /**
    * {@link Configuration @Configuration}, {@link ImportSelector},
    * {@link ImportBeanDefinitionRegistrar}, or regular component classes to
    import.
    */
    Class<?>[] value();
}
@Import可以使用在任何类型上,通常情况下,类和注解上用的比较多。
value:一个Class数组,设置需要导入的类,可以是@Configuration标注的类,可以是ImportSelector接口或者ImportBeanDefinitionRegistrar接口类型,或者需要导入的普通组件类。

使用步骤

  1. 将@Import标注在类上,设置value参数。

  2. 将@Import标注的类作为AnnotationConfigApplicationContext构造参数创建AnnotationConfigApplicationContext对象。

  3. 使用AnnotationConfigApplicationContext对象。

@Import的value常见的有5种用法

  1. value为普通的类。

  2. value为@Configuration标注的类。

  3. value为@CompontentScan标注的类。

  4. value为ImportBeanDefinitionRegistrar接口类型。

  5. value为ImportSelector接口类型。

  6. value为DeferredImportSelector接口类型。

ImportBeanDefinitionRegistrar接口

这个接口提供了通过Spring容器api的方式直接向容器中注册bean。
public interface ImportBeanDefinitionRegistrar {
    default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
    	registerBeanDefinitions(importingClassMetadata, registry);
    }
    
    default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    }
}
2个默认方法,都可以用来调用Spring容器api来注册bean。
2个方法中主要有3个参数
  1. importingClassMetadata:AnnotationMetadata类型的,通过这个可以获取被@Import注解标注的类所有注解的信息。

  2. registry:BeanDefinitionRegistry类型,是一个接口,内部提供了注册bean的各种方法。

  3. importBeanNameGenerator:BeanNameGenerator类型,是一个接口,内部有一个方法,用来生成bean的名称。

关于BeanDefinitionRegistry和BeanNameGenerator接口在来细说一下。

BeanDefinitionRegistry接口:bean定义注册器

bean定义注册器,提供了bean注册的各种方法,来看一下源码:
public interface BeanDefinitionRegistry extends AliasRegistry {
    /**
    * 注册一个新的bean定义
    * beanName:bean的名称
    * beanDefinition:bean定义信息
    */
    void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException;
    /**
    * 通过bean名称移除已注册的bean
    * beanName:bean名称
    */
    void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;
    /**
    * 通过名称获取bean的定义信息
    * beanName:bean名称
    */
    BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;
    /**
    * 查看beanName是否注册过
    */
    boolean containsBeanDefinition(String beanName);
    /**
    * 获取已经定义(注册)的bean名称列表
    */
    String[] getBeanDefinitionNames();
    /**
    * 返回注册器中已注册的bean数量
    */
    int getBeanDefinitionCount();
    /**
    * 确定给定的bean名称或者别名是否已在此注册表中使用
    * beanName:可以是bean名称或者bean的别名
    */
    boolean isBeanNameInUse(String beanName);
}
基本上所有bean工厂都实现了这个接口,让bean工厂拥有bean注册的各种能力。上面我们用到的 AnnotationConfigApplicationContext 类也实现了这个接口。

BeanNameGenerator接口:bean名称生成器

bean名称生成器,这个接口只有一个方法,用来生成bean的名称:
public interface BeanNameGenerator {
	String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry);
}
spring内置了3个实现
  1. DefaultBeanNameGenerator:默认bean名称生成器,xml中bean未指定名称的时候,默认就会使用这个生成器,默认为:完整的类名#bean编号。

  2. AnnotationBeanNameGenerator:注解方式的bean名称生成器,比如通过@Component(bean名称)的方式指定bean名称,如果没有通过注解方式指定名称,默认会将完整的类名作为bean名称。

  3. FullyQualifiedAnnotationBeanNameGenerator:将完整的类名作为bean的名称。

BeanDefinition接口:bean定义信息

用来表示bean定义信息的接口,我们向容器中注册bean之前,会通过xml或者其他方式定义bean的各种配置信息,bean的所有配置信息都会被转换为一个BeanDefinition对象,然后通过容器中BeanDefinitionRegistry接口中的方法,将BeanDefinition注册到spring容器中,完成bean的注册操作。

value为ImportBeanDefinitionRegistrar接口类型用法(4个步骤)

  1. 定义ImportBeanDefinitionRegistrar接口实现类,在registerBeanDefinitions方法中使用registry来注册bean。

  2. 使用@Import来导入步骤1中定义的类。

  3. 使用步骤2中@Import标注的类作为AnnotationConfigApplicationContext构造参数创建Spring容器。

  4. 使用AnnotationConfigApplicationContext操作bean。

案例

来2个普通的类。
public class Service1 {
}

public class Service2 {
    private Service1 service1;
    
    public Service1 getService1() {
    	return service1;
    }
    
    public void setService1(Service1 service1) {
    	this.service1 = service1;
    }
}

来个类实现ImportBeanDefinitionRegistrar接口,然后在里面实现上面2个类的注册,如下:

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //定义一个bean:Service1
        BeanDefinition service1BeanDinition = BeanDefinitionBuilder.genericBeanDefinition(Service1.class).getBeanDefinition();
        //注册bean
        registry.registerBeanDefinition("service1", service1BeanDinition);
        //定义一个bean:Service2,通过addPropertyReference注入service1
        BeanDefinition service2BeanDinition = BeanDefinitionBuilder.genericBeanDefinition(Service2.class).addPropertyReference("service1", "service1").getBeanDefinition();
        //注册bean
        registry.registerBeanDefinition("service2", service2BeanDinition);
        }
}
注意上面的registerBeanDefinitions方法,内部注册了2个bean,Service1和Service2。
上面使用了BeanDefinitionBuilder这个类,这个是BeanDefinition的构造器,内部提供了很多静态方法方便构建BeanDefinition对象。

value为ImportSelector接口类型

导入选择器,看一下源码:
public interface ImportSelector {
    /**
    * 返回需要导入的类名的数组,可以是任何普通类,配置类(@Configuration、@Bean、@CompontentScan等标注的类)
    * @importingClassMetadata:用来获取被@Import标注的类上面所有的注解信息
    */
    String[] selectImports(AnnotationMetadata importingClassMetadata);
}
用法(4个步骤)
  1. 定义ImportSelector接口实现类,在selectImports返回需要导入的类的名称数组。

  2. 使用@Import来导入步骤1中定义的类。

  3. 使用步骤2中@Import标注的类作为AnnotationConfigApplicationContext构造参数创建spring容器。

  4. 使用AnnotationConfigApplicationContext操作bean。

案例

public class Service1 {
}

@Configuration标注的配置类:Module1Config

@Configuration
public class Module1Config {
    @Bean
    public String name() {
    	return "公众号:路人甲java";
    }
    @Bean
    public String address() {
    	return "上海市";
    }
}

下面自定义一个ImportSelector,然后返回上面2个类的名称

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{
       		Service1.class.getName(),
        	Module1Config.class.getName()
    	};
    }
}

来个@Import标注的类,导入MyImportSelector

/**
* 通过@Import导入MyImportSelector接口实现类
*/
@Import({MyImportSelector.class})
public class MainConfig5 {
}

测试

@Test
public void test5() {
    //1.通过AnnotationConfigApplicationContext创建spring容器,参数为@Import标注的类
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig5.class);
    //2.输出容器中定义的所有bean信息
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
    }
}

部分输出如下:

com.javacode2018.lesson001.demo24.test5.Service1-
>com.javacode2018.lesson001.demo24.test5.Service1@45b4c3a9
name->公众号:路人甲java
address->上海市

DeferredImportSelector接口

DeferredImportSelector是ImportSelector的子接口,既然是ImportSelector的子接口,所以也可以通过@Import进行导入,这个接口和ImportSelector不同地方有两点:
  • 延迟导入。

    • 比如@Import的value包含了多个普通类、多个@Configuration标注的配置类、多个ImportSelector接口的实现类,多个ImportBeanDefinitionRegistrar接口的实现类,还有DeferredImportSelector接口实现类,此时Spring处理这些被导入的类的时候,会将DeferredImportSelector类型的放在最后处理,会先处理其他被导入的类,其他类会按照value所在的前后顺序进行处理。

    • 那么我们是可以做很多事情的,比如我们可以在DeferredImportSelector导入的类中判断一下容器中是否已经注册了某个bean,如果没有注册过,那么再来注册。

    • 以后我们会讲到另外一个注解@Conditional,这个注解可以按条件来注册bean,比如可以判断某个bean不存在的时候才进行注册,某个类存在的时候才进行注册等等各种条件判断,通过@Conditional来结合DeferredImportSelector可以做很多事情。

  • 指定导入的类的处理。

指定导入的类的处理顺序

通使用Order注解的方式即可
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Documented
public @interface Order {
	int value() default Ordered.LOWEST_PRECEDENCE;
}
value的值越小,优先级越高。

3、总结

  1. @Import可以用来批量导入任何普通的组件、配置类,将这些类中定义的所有bean注册到容器中。

  2. @Import常见的5种用法需要掌握。

  3. 掌握ImportSelector、ImportBeanDefinitionRegistrar、DeferredImportSelector的用法。

  4. DeferredImportSelector接口可以实现延迟导入、按序导入的功能。

  5. Spring中很多以@Enable开头的都是使用@Import集合ImportSelector方式实现的。

  6. BeanDefinitionRegistry接口:bean定义注册器,这个需要掌握常见的。

十九,@Conditional通过条件来控制bean的注册

@Conditional注解是从Spring4.0才有的,可以用在任何类型或者方法上面,通过@Conditional注解可以配置一些条件判断,当所有条件都满足的时候,被@Conditional标注的目标才会被Spring容器处理。
@Conditional源码:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
	Class<? extends Condition>[] value();
}
这个注解只有一个value参数,Condition类型的数组,Condition是一个接口,表示一个条件判断,内部有个方法返回true或false,当所有Condition都成立的时候,@Conditional的结果才成立。

1、Condition接口

用来表示条件判断的接口,源码如下:
@FunctionalInterface
public interface Condition {
    /**
    * 判断条件是否匹配
    * context:条件判断上下文
    */
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
是一个函数式接口,内部只有一个matches方法,用来判断条件是否成立的,2个参数:
  • context:条件上下文,ConditionContext接口类型的,可以用来获取容器中的各种信息。

  • metadata:用来获取被@Conditional标注的对象上的所有注解信息。

2、ConditionContext接口

这个接口中提供了一些常用的方法,可以用来获取spring容器中的各种信息,看一下源码:
public interface ConditionContext {
    /**
    * 返回bean定义注册器,可以通过注册器获取bean定义的各种配置信息
    */
    BeanDefinitionRegistry getRegistry();
    /**
    * 返回ConfigurableListableBeanFactory类型的bean工厂,相当于一个ioc容器对象
    */
    @Nullable
    ConfigurableListableBeanFactory getBeanFactory();
    /**
    * 返回当前spring容器的环境配置信息对象
    */
    Environment getEnvironment();
    /**
    * 返回资源加载器
    */
    ResourceLoader getResourceLoader();
    /**
    * 返回类加载器
    */
    @Nullable
    ClassLoader getClassLoader();
}

3、比较关键性的问题:Condition条件判断在什么时候执行?

Spring对配置类的处理主要分为2个阶段:
  • 配置类解析阶段:会得到一批配置类的信息,和一些需要注册的bean。

  • bean注册阶段:将配置类解析阶段得到的配置类和需要注册的bean注册到Spring容器中。

什么是配置类

类中有下面任意注解之一的就属于配置类:
  • 类上有@Compontent注解

  • 类上有@Configuration注解

  • 类上有@CompontentScan注解

  • 类上有@Import注解

  • 类上有@ImportResource注解

  • 类中有@Bean标注的方法

判断一个类是不是一个配置类,是否的是下面这个方法:
org.springframework.context.annotation.ConfigurationClassUtils#isConfigurationCandidate
Spring中处理这2个过程会循环进行,直到完成所有配置类的解析及所有bean的注册。

Spring对配置类处理过程

源码位置:
org.springframework.context.annotation.ConfigurationClassPostProcessor#processConfigBeanDefinitions
整个过程大致的过程如下:
  1. 通常我们会通过new AnnotationConfigApplicationContext()传入多个配置类来启动Spring容器。

  2. Spring对传入的多个配置类进行解析。

  3. 配置类解析阶段:这个过程就是处理配置类上面6中注解的过程,此过程中又会发现很多新的配置类,比如@Import导入的一批新的类刚好也符合配置类,而被@CompontentScan扫描到的一些类刚好也是配置类;此时会对这些新产生的配置类进行同样的过程解析。

  4. bean注册阶段:配置类解析完成后,会得到一批配置类和一批需要注册的bean,此时Spring容器会将这批配置类作为bean注册到spring容器,同样也会将这批需要注册的bean注册到Spring容器中。

  5. 经过上面第3个阶段之后,Spring容器中会注册很多新的bean,这些新的bean中可能又有很多新的配置类。

  6. Spring从容器中将所有bean拿出来,遍历一下,会过滤得到一批未处理的新的配置类,继续交给第3步进行处理。直到完成所有配置类的解析和bean的注册。

从上面过程中可以了解到:
  1. 可以在配置类上面加上@Conditional注解,来控制是否需要解析这个配置类,配置类如果不被解析,那么这个配置上面6种注解的解析都会被跳过。

  2. 可以在被注册的bean上面加上@Conditional注解,来控制这个bean是否需要注册到Spring容器中。

  3. 如果配置类不会被注册到容器,那么这个配置类解析所产生的所有新的配置类及所产生的所有新的bean都不会被注册到容器。

一个配置类被Spring处理有2个阶段:配置类解析阶段、bean注册阶段。
如果将Condition接口的实现类作为配置类上@Conditional中,那么这个条件会对两个阶段都有效,此时通过Condition是无法精细的控制某个阶段的,如果想控制某个阶段,比如可以让他解析,但是不能让他注册,此时就就需要用到另外一个接口了:ConfigurationCondition

4、ConfigurationCondition接口

源码:
public interface ConfigurationCondition extends Condition {
    /**
    * 条件判断的阶段,是在解析配置类的时候过滤还是在创建bean的时候过滤
    */
    ConfigurationPhase getConfigurationPhase();
    /**
    * 表示阶段的枚举:2个值
    */
    enum ConfigurationPhase {
        /**
        * 配置类解析阶段,如果条件为false,配置类将不会被解析
        */
        PARSE_CONFIGURATION,
        /**
        * bean注册阶段,如果为false,bean将不会被注册
        */
        REGISTER_BEAN
    }
}
ConfigurationCondition接口相对于Condition接口多了一个getConfigurationPhase方法,用来指定条件判断的阶段,是在解析配置类的时候过滤还是在创建bean的时候过滤。

5、@Conditional使用的3步骤

  1. 自定义一个类,实现Condition或ConfigurationCondition接口,实现matches方法。

  2. 在目标对象上使用@Conditional注解,并指定value的指为自定义的Condition类型。

  3. 启动Spring容器加载资源,此时@Conditional就会起作用了。

6、案例

bean不存在的时候才注册

public class OnMissingBeanCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //获取bean工厂
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        //从容器中获取IService类型bean
        Map<String, IService> serviceMap = beanFactory.getBeansOfType(IService.class);
        //判断serviceMap是否为空
        return serviceMap.isEmpty();
    }
}

根据环境选择配置类

自定义一个条件的注解
@Conditional(EnvCondition.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EnvConditional {
    //环境(测试环境、开发环境、生产环境)
    enum Env {
    TEST, DEV, PROD
    }
    //环境
    Env value() default Env.DEV;
}
注意这个注解比较特别,这个注解上面使用到了@Conditional注解,这个地方使用到了一个自定义Condition类:EnvCondition.java
测试环境配置类
@Configuration
@EnvConditional(EnvConditional.Env.TEST)
public class TestBeanConfig {
    @Bean
    public String name() {
    	return "我是测试环境!";
    }
}

开发环境配置类

@Configuration
@EnvConditional(EnvConditional.Env.DEV)
public class DevBeanConfig {
    @Bean
    public String name() {
    	return "我是开发环境!";
    }
}

生产环境配置类

@Configuration
@EnvConditional(EnvConditional.Env.PROD)
public class ProdBeanConfig {
    @Bean
    public String name() {
    	return "我是生产环境!";
    }
}
下面来看一下条件类:EnvCondition
条件类会解析配置类上面@EnvConditional注解,得到环境信息。然后和目前的环境对比,决定返回true还是false,如下:
public class EnvCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //当前需要使用的环境
        EnvConditional.Env curEnv = EnvConditional.Env.DEV; // 可以修改这个改变环境
        //获取使用条件的类上的EnvCondition注解中对应的环境
        EnvConditional.Env env = (EnvConditional.Env)
        metadata.getAllAnnotationAttributes(EnvConditional.class.getName()).get("value")
        .get(0);
        return env.equals(curEnv);
    }
}

Condition指定优先级

@Condtional中value指定多个Condtion的时候,默认情况下会按顺序执行,可以通过@Order注解来指定顺序。

总结

  • @Conditional注解可以标注在Spring需要处理的对象上(配置类、@Bean方法),相当于加了个条件判断,通过判断的结果,让Spring觉得是否要继续处理被这个注解标注的对象。

  • Spring处理配置类大致有2个过程:解析配置类、注册bean,这两个过程中都可以使用@Conditional来进行控制Spring是否需要处理这个过程。

  • Condition默认会对2个过程都有效。

  • ConfigurationCondition控制得更细一些,可以控制到具体那个阶段使用条件判断。


参考:路人甲-Spring系列

版权声明

非特殊说明,本文由Zender原创或收集发布,欢迎转载。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

作者文章
热门