Spring注解专题系列

Spring注解应用篇--IOC容器Bean组件注册

这是Spring注解专题系类文章,本系类文章适合Spring入门者或者原理入门者,小编会在本系类文章下进行企业级应用实战讲解以及spring源码跟进。

环境准备

  • 编译器IDEA
  • maven依赖spring-context version:4.3.12.RELEASE
  • maven依赖junit version:4.11

Bean && Configuration注解

小编经历过xml文件配置的方式,后来使用springboot后发现开箱即用的零xml配置方式(除了框架外中间件等配置~)简直不要太清爽。然后基于注解驱动开发的特性其实spring早就存在了(
Spring的特性包括IOC和DI(依赖注入)
传统的xml Bean注入方式:

xml式Bean注入

<bean id="exampleBean" class="xxxx.ExampleBean"/>

或者注入Bean的同时进行属性注入

<bean id="exampleBean" class="xxxx.ExampleBean">
<property name="age" value="666"></property>
<property name="name" value="evinhope"></property>
</bean>

上面传统的代码其实就是等价于:
配置类注册Bean

@Configuration
public class BaseConfig {
    @Bean("beanIdDefinition")
    public ExampleBean exampleBean(){
        return new ExampleBean("evinhope",666);
    }
}

@ Configuration等价于xml配置文件,表示它是一个配置类,@ bean等价于xml的bean标签,告诉容器这个bean需要注册到IOC容器当中。几乎xml的每一个标签或者标签属性都可以对应一个注解。其中使用bean注解时,默认bean id为方法名(exampleBean),当然也可以通过@ Bean(xxxx)来指定bean的id。
测试用例

@Test
    public void shouldAnswerWithTrue()
    {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(BaseConfig.class);//创建IOC容器
        System.out.println("IOC容器创建完成...");
        ExampleBean exampleBean = (ExampleBean) ctx.getBean("beanIdDefinition");//获取id为beanIDDefinition的Bean
        System.out.println(exampleBean);
    }

output:

IOC容器创建完成...
ExampleBean{name='evinhope', age=666}

创建IOC容器并且获取后,与getBean相关的方法:
Spring注解专题系列
这一些IOC容器方法后续在其他注解证明上可能会用得上,这里挑几个说明一下:
getBeanNamesForType(bean.class),根据bean的类型返回此类型bean的所有id(ret String[])
getBeanDefinitionNames(),获取容器中定义的所有bean id(ret String[])
output:

[beanIdDefinition, exampleBean02]  [IOC容器在创建过程中往里面注册的bean, baseConfig, beanIdDefinition, exampleBean02]
//其中beanIdDefinition和exampleBean02为同类型的bean,不同id;baseConfig为配置类~

@ Configuration源码点进去,这个注释上还有@ Component注释,说明配置类注释其实也是一个组件bean

componentScan注解自动扫描组件&指定扫描规则

这个注解等价于xml的content:component-scan标签
componentScan注解包扫描,只要标注了@Controller、@Service、@Repository、@component四大注解的都会自动扫描加入到IOC中。
注解解读
注释源码点进去后可以看到包含一个@Repeatable注解,跟着点进去,可以得知Repeatable始于JDK1.8,表示其声明的注释类型,说明@componentScan可以重复使用,来扫描多个包路径。
这里关注几个有意思的注解属性:
value/basePackages:在xxxx包路径下扫描组件
includeFilters:指定扫描的时候包含符合规则的组件(类型声明为Filter[])
excludeFilters:指定哪些类型不符合组件扫描的条件(类型声明为Filter[])
在来看Filter的定义信息:
Filter为componentScan注解下的嵌套注解。包含几个重要的属性:
FilterType type(默认为FilterType.ANNOTATION):使用过滤的类型
其中FilterType为枚举类,包含以下值:ANNOTATION(按照注解类型过滤组件)ASSIGNABLE_TYPE(按照主键类型过滤组件)ASPECTJ(按照切面表达式)REGEX(按照正则表达式)CUSTOM(自定义)
classes:定义完过滤类型后需要针对过滤类型来解释过滤的类
pattern:用于过滤器的模式,主要和FilterType为按照切面表达式和按照正则表达式来组合使用。
用法:
先创建3个bean 组件,ControllerBean,ServiceBean,DaoBean(分别在类上加上@Controller、@Service、@Repository注解)。
测试前先用ApplicationContext的getBeanDefinitionNames()方法查看可知ioc中的确不存在上面3个bean组件。

@Configuration
@ComponentScan(value= "cn.edu.scau")
public class BaseConfig {

使用ApplicationContext的getBeanDefinitionNames()方法打印后,发现3个bean组件已经加进来容器中了,其中,bean id为首字母小写的类名(controllerBean, daoBean, serviceBean)
进行FilterType的使用。

@ComponentScan(value= "cn.edu.xxx",includeFilters = {
        @ComponentScan.Filter(type=FilterType.ANNOTATION,classes = {Controller.class})
        })

按照上面的说明,此时容器应该只有controller组件,service和dao应该不在容器中,然而事实却是3种组件都在容器中,这个源码中说的不一样???再回过头看componentScan源码。
发现有一个属性boolean useDefaultFilters() default true源码注释这样说的:自动检测使用@controller@service@component@repository组件。然后上面的代码再修改一下

@ComponentScan(value= "cn.edu.scau",includeFilters = {
        @ComponentScan.Filter(type=FilterType.ANNOTATION,classes = {Controller.class}),
},useDefaultFilters = false)

再使用getBeanDefinitionNames查看容器bean,发现只剩下了controller注解标注的bean,过滤成功。
由上面说明可知,includeFilter为Filter数组,则可定义多个过滤规则

@ComponentScan(value= "cn.edu.scau",includeFilters = {
        @ComponentScan.Filter(type=FilterType.ANNOTATION,classes = {Controller.class}),
        @ComponentScan.Filter(type=FilterType.ASSIGNABLE_TYPE,classes = {ServiceBean.class})
},useDefaultFilters = false)

结果就是容器中新增类型为ServiceBean的组件。
excludeFilters用法同includeFilters一样,只不过它是过滤掉不符合条件的bean,同时需要搭配userDefaultFilters=false来使用
下面来试试FilterType为自定义的用法:
点进去FilterType源码后发现CUSTOM上面有注解{@link org.springframework.core.type.filter.TypeFilter} implementation.
这说明自定义规则需要实现TypeFilter接口
再来看看TypeFilter源码:
接口定义了一个match方法:该方法用于确定包扫描下的类是否匹配
其中带有2个参数以及返回类型:
@ Param(MetadataReader):当前目标类读取信息
@ Param (MetadataReaderFactory):这个一个类信息读取器工厂,可以获取其他类信息
@ Return(boolean):返回当前类是否符合过滤的要求

public class MyFilter implements TypeFilter {
    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
        return false;
    }
}
/*
* 同时在配置类中配置
*/
@ComponentScan(value= {"cn.edu.scau.controller","cn.edu.scau.service","cn.edu.scau.dao"},includeFilters = {
        @ComponentScan.Filter(type=FilterType.CUSTOM,classes = {MyFilter.class})
},useDefaultFilters = false)

在测试类中使用ApplicationContext的getBeanDefinitionNames方法发现controller、service、dao三个组件全部不在容器中或者调用Application的getBeanDefinitionCount方法发现比之前的少了3个bean。证明重写TypeFilter 接口的match方法起作用了,false代表全部不匹配。
源码点进去看看MetadataReader的属性描述:
getResource():返回当前类资源引用(类路径)
getClassMetadata():获取当前类的类信息
getAnnotationMetadata():获取当前类的注解信息

//return false的逻辑替换成
Resource resource = metadataReader.getResource();
        AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
        ClassMetadata classMetadata = metadataReader.getClassMetadata();
        String className = classMetadata.getClassName();
        if(className.contains("Dao")){
            return true;
        } //只返回类型带有Dao的类
        return false; //其他类一律过滤掉

同理,查看结果,容器中只存在dao的bean组件,另外2个都没有在容器中出现,完成包扫描的过滤。
当需要多包路径多扫描规则的时候,可以使用多个componentScan(jdk8 支持,带有repeatable元注解)或者使用一个componentScans(源码跟进可知,其实就是一个componentScan数组)

Scope注解设置组件的作用域

这个注解相当于xml配置文件下bean标签的scope属性。
IOC容器的Bean都是单实例,证明测试一下:

/*
* 还是上面注册的那个Bean(id为beanIdDefinition)
*/
@Test
    public void shouldAnswerWithTrue()
    {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(BaseConfig.class);
        System.out.println("IOC容器创建完成...");
        System.out.println(ctx.getBean("beanIdDefinition") == ctx.getBean("beanIdDefinition"));
    }

结果为true。说明多次从容器中获取的bean为同一个,即为单实例。
Scope源码跟进去可以看到属性value的值可以为singleton、prototype、request、session。分别代表该注解下的bean为单实例(ioc容器启动后会调用方法创建对象放到容器中,以后需要该对象就从容器中获取),为多实例(容器创建启动时不会去调用方法创建对象放进容器中,只有在需要该对象的时候才会去new一个新对象),request代表同一个请求创建一个实例,session同一个session创建一个实例。
同时在bean中增加一个无参构造器

public ExampleBean(){
        System.out.println("exampleBean constructor......");
    }

测试再跑一次,output:
exampleBean constructor......
IOC容器创建完成...
这说明单实例bean在容器初始化创建的过程中已经注册了。
在配置类bean中添加@Scope("prototype")
再跑一次,output:
IOC容器创建完成...
exampleBean constructor......
exampleBean constructor......
false
也说明了多实例容器创建启动时不会去调用方法创建对象放进容器中,只有在需要该对象的时候才会去new一个新对象。

Lazy注解Bean懒加载

这个注解主要针对单实例bean来说的,上面说过,默认在容器启动就创建了对象,懒加载ioc启动后不创建对象,第一次获取bean的时候再来创建bean,并进行初始化。
在添加懒加载后再测试output:
IOC容器创建完成...
exampleBean constructor......
true
说明对象还是同一个,只是bean的创建容器注册往后挪了。

Conditional注解按照条件来注册bean

代码跟进去,发现只有一个Condition属性为一个Class数组。(所有的组件必须匹配才能被注册)再condition点进去
Condition是一个接口,需要被实现,实现里面的matches方法用来判断该组件是否条件匹配。
分析到此,思路几乎清晰,条件匹配类:

public class MyCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //TODO 等一下进行代码填充
        return false;
    }
}

先创建一个Computer的Bean对象,然后在配置类中进行容器注册

@Bean("window")
    @Conditional(MyCondition.class)
    public Computer window(){
        return new Computer();
    }
    @Bean("linux")
    @Conditional(MyCondition.class)
    public Computer linux(){
        return new Computer();
    }

测试跑起来后发现,容器中没有id为linux和window的bean对象。在Conditon的matches方法中,false表示不匹配,ture代表匹配。
现实开发中可能有这样的需求,不同的环境注册不同的bean。
因此,尝试在Condition的matches方法中看看里面的参数代表啥意思.
@ Param ConditionContext:获取条件上下文环境
@ Param AnnotatedTypeMetadata:注解信息读取

Environment environment = context.getEnvironment();
        String property = environment.getProperty("os.name");
        //获取到bean定义的注册类.BeanDefinitionRegistry可以用来判断bean的注册信息,也可以在容器中注册bean,后续文章会分析这个类
        BeanDefinitionRegistry registry = context.getRegistry();
        if(property.contains("windows")){
            return false;
        }
        return true;

测试跑起来,小编的电脑系统为Windows 10,则2个computer bean全部没被注册。
Junit测试可以调整改变JVM的参数,步骤如下:
1、IDE找到Edit Configurations
2、在configuration这里找到VM options,这里可以设置JVM参数。这里我们改变运行的环境,改成linux.。写法:-Dos.name=linux
测试再跑起来,2个bean又被注册到容器中了。
可以在测试类获取容器后再拿到环境确认环境已经改变了

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(BaseConfig.class);
        ConfigurableEnvironment environment = ctx.getEnvironment();
        String property = environment.getProperty("os.name");
        System.out.println(property);
        System.out.println("IOC容器创建完成...");

ouput:linux
回到Conditional注解的源码的元注解:@Target({ElementType.TYPE, ElementType.METHOD})。Conditional这个注解可以用于方法和配置类上面,可以延伸如@Conditional注解放在配置上,若不符合条件,那么配置类下的所有bean都不会注册到IOC容器中。
现实开发场景可以这个判断条件需要大量使用,在每一个Bean上都写上@Conditional(MyCondition.class)不太方便和比较繁琐,因此可以尝试把他再封装一层,代码看起来更加清爽:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(MyCondition.class)
public @interface MyDefinitionConditional {

}

这样一来,凡是需要使用@Conditional(MyCondition.class)的地方都可以用@MyDefinitionConditional来代替。

Profile注解环境切换

在文档中是这样描述这个注解的:@Profile注解事实上是由一个更加灵活的@Conditional注解来实现了。
由源码切入@Profile,发现此注解上还有@Conditional注解,@Conditional(ProfileCondition.class),ProfileCondition跟进去,发现实现了Condition这个接口(和上面讲的@Conditional一样),下面为源码中重写了Condition的matches方法:

@Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        if (context.getEnvironment() != null) {
            MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
            if (attrs != null) {
                for (Object value : attrs.get("value")) {
                    if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
                        return true;
                    }
                }
                return false;
            }
        }
        return true;
    }

这段代码先通过上下文环境获取所有带有Profile注解的类方法信息,存在Profile注解的话,就会遍历MultiValueMap字典,判断一个或者更加多的Profile属性值是否被当前上下文环境激活。
现实开发中可能会有开发环境、测试环境、线上环境甚至更加多的环境,他们使用的数据源或者一些配置等等都是有差异的,因此它的使用场景也就出来了。
模拟数据源配置几个Bean:

@Bean("test")
    @Profile("test")
    public ExampleBean exampleBeanOfTest(){
        return new ExampleBean();
    }

    @Bean("dev")
    @Profile("dev")
    public ExampleBean exampleBeanOfDev(){
        return new ExampleBean();
    }

    @Bean("prod")
    @Profile("prod")
    public ExampleBean exampleBeanOfProd(){
        return new ExampleBean();
    }

测试:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ConfigurableEnvironment environment = ctx.getEnvironment();
        environment.setActiveProfiles("test","dev");//使用代码配置环境变量
        ctx.register(BaseConfig.class);
        ctx.refresh();

运行后发现测试和开发环境的2个bean已经注册到容器中了~
或者像上面说的IDE设置JVM参数来达到目的:VM:-Dspring.profiles.active="test","dev"
或者使用@PropertySource加.properties文件同样可以切换
在resources文件目录下新建一个application.properties属性文件,指明环境变量:spring.profiles.active=prod
后再配置类头上添加注解@PropertySource("classpath:/application.properties")也可达到相同的结果。

import 注解给容器中快速导入一个组件

总结上面容器注册bean的方法:1、@Bean注解 2、ComponentScan包扫描+组件标注注解 3、import注解
源码文档是这样说的,import能够导入一个或更多的bean,也可以通过实现ImportSelector和ImportBeanDefinitionRegistrar接口来进行bean注册,如果是xml或者其他非bean定义的资源需要被import,可以使用@ImportResource。
这就说明使用import注册bean组件有3种方式。

  • 直接快速导入
@Configuration
@Import(Computer.class)
//下面是配置类

测试后发现bean注册在容器了,bean id为全类名(cn.xxx.xxx.Computer)

  • 实现ImportSelector 接口
public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return null;
    }
}

在配置类中:

@Configuration
@Import(MyImportSelector.class)
//下面是配置类

测试跑起来,理论上应该是没有任何bean在容器中注册的,因为重写的方法返回null,事实却报错了。
::: danger 报错信息
Failed to process import candidates for configuration class [cn.edu.scau.config.BaseConfig]; nested exception is java.lang.NullPointerException
:::
大致的意思就是空指针异常导致import异常。
源码跟一下,查看一下方法调用栈后发现异常是由一个叫ConfigurationClassParse类捕获并且抛出来的,查看try代码块,有这样一段代码:

for (SourceClass candidate : importCandidates) {
                    if (candidate.isAssignable(ImportSelector.class)) {
                        //省略部分源代码
                            String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
                            Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
                    }
                    else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
                        //省略此逻辑代码}
                    else {
                        // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
                        // process it as an @Configuration class
                        this.importStack.registerImport(
                                currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
                        processConfigurationClass(candidate.asConfigClass(configClass));
                    }
                }

结合这段代码不难理解,获取import注解里面类的信息进行循环遍历,若是实现ImportSelector接口的是一种情况,实现ImportBeanDefinitionRegistrar的也是另一种情况,剩下的就是把他当作常规import进行处理。我们这里是实现ImportSelector接口属于第一种情况,调用我们重写selectImports的方法,我们返回给他null,得到一个名为importClassNames的数组,数组作为asSourceClasses参数,importClassNames.length,为null的对象使用length当然会返回空指针异常,修改一下上面的代码

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[0];
    }
}

selectImports方法参数:
@ Param AnnotationMetadata :标注@Import类(这里为配置类)的类注解信息。
@ Return:返回需要在容器中注册的bean。bean的id为全类名。如:

return new String[]{"cn.xxx.xxx.bean.Computer"};
  • 实现importBeanDefinitionRegister接口

importBeanDefinitionRegister接口方法参数:
@ Param AnnotationMetadata:同上(注解import这个类的信息)
@ Param BeanDefinitionRegistry:BeanDefinition注册类,可以使用registerBeanDefinition方法手动注册进来

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        boolean b = registry.containsBeanDefinition("cn.xxx.xxx.bean.Computer");
        if(!b){//没有Computer这bean就注册进来
            BeanDefinition beanDefinition = new RootBeanDefinition(Computer.class);
            registry.registerBeanDefinition("computer666",beanDefinition);
        }else{//存在的话就移除注册,容器中也会跟着移除
            registry.removeBeanDefinition("cn.xxx.xxx.bean.Computer");
        }
    }
}
@Configuration
@Import({Computer.class,MyImportBeanDefinitionRegistrar.class})
//配置类

运行后Computer这Bean不在容器中,先import了进去后,在MyImportBeanDefinitionRegistrar中又被注册器给移除了。

FactoryBean注解注册Bean

这是Spring的工厂bean,实现Factory接口,重写里面的方法

public class ComputerFactoryBean implements FactoryBean<Computer>{
    @Override
    public Computer getObject() throws Exception {
        return new Computer();
    }

    @Override
    public Class<?> getObjectType() {
        return Computer.class;
    }
    /*
    * false:代表多实例    true:代表单实例
    */
    @Override
    public boolean isSingleton() {
        return false;
    }
}

`测试:

Object myFactoryBean01 = ctx.getBean("myFactoryBean");
        Object myFactoryBean02 = ctx.getBean("myFactoryBean");
        System.out.println(myFactoryBean01 == myFactoryBean02);
        System.out.println(myFactoryBean01.getClass()+"   "+myFactoryBean02.getClass());

测试后发现,容器中注册的是ComputerFactoryBean这个代理工厂bean,然而根据代理工厂的Bean id去容器中取bean对象时又是Computer被代理的bean。那么如何获取容器中工厂Bean(ComputerFactoryBean)呢。源码跟一下:
从getBean源码入手更进去在AbstractBeanFactory这个类中发现:

if (!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name)) {
            return beanInstance;
        }

其中name为getBean(id)我们传进去的,BeanInstance这个对象由AbstractBeanFactory这个类的doGetBean方法里面调用getSingleton(beanName)这个函数进行获取,其中beanName由name处理过后的参数,判断name是否以FACTORY_BEAN_PREFIX(值为&)开头,不断循环去掉&头得到beanName,返回BeanInstance对象(这个对象就是代理工厂bean),进而可以知道想要获取容器中代理bean通过加&进行处理。

//获取代理的bean   value=Computer
        Object myFactoryBean01 = ctx.getBean("myFactoryBean");
       //getBean前面加上大于等于1的&符号代表获取FactoryBean   value = ComputerFactoryBean
        Object myFactoryBean02 = ctx.getBean("&&myFactoryBean");

实际中可能会使用工厂Bean来代理某一个Bean,对该对象的所有方法做一个拦截,进行定制化的处理。个人认为倒不如使用基于注解的aspectJ做AOP更加来得方便。

欢迎大家关注一波我的公众号,嘤嘤嘤(你们的支持是我写下去的最大动力呜呜呜呜)
Spring注解专题系列