Spring的注解@configuration和@Configurable两者长得非常相似,但用途却有很大的差异。

@configuration 注解大家已经非常熟悉了,配合@bean注解就可以轻松减少xml配置,这里不再过多介绍;

@Configurable 平时用的却较少,它用于解决非Spring容器管理的Bean中却依赖Spring Bean的场景,也就是说Bean A依赖了一个Spring的Bean B,但是A不是Spring 得Bean所以无法进行属性注入拿不到B的情况。

本文就来详细聊一聊这个注解的使用方法和底层原理。

使用方法

在使用之前,咱们先看看@Configurable注解的源码(Spring5.x)

/**
 * Marks a class as being eligible for Spring-driven configuration.
 *
 * <p>Typically used with the AspectJ {@code AnnotationBeanConfigurerAspect}.
 *
 * @author Rod Johnson
 * @author Rob Harrop
 * @author Adrian Colyer
 * @author Ramnivas Laddad
 * @since 2.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Configurable {
   /**
    * The name of the bean definition that serves as the configuration template.
    */
   String value() default "";
   /**
    * Are dependencies to be injected via autowiring?
    */
   Autowire autowire() default Autowire.NO;
   /**
    * Is dependency checking to be performed for configured objects?
    */
   boolean dependencyCheck() default false;
   /**
    * Are dependencies to be injected prior to the construction of an object?
    */
   boolean preConstruction() default false;
}

咱们从源码可以看到,这个注解必须打在类上,Autowire类型默认为Autowire.NO(实际上此值不生效,不需要修改为BY_TYPE也能注入成功,下文会提到)。其他的咱们暂时不用关心。

先新建一个类AA如下:

@Configurable
public class AA {

    @Autowired
    private BB b;

    @Autowired
    ApplicationContext applicationContext;

    public BB getB() {
        return b;
    }

    public ApplicationContext getApplicationContext() {
        return applicationContext;
    }
}

AA类增加了注解@Configurable,没有标记@Component等Spring Bean注解,依赖了类BB、ApplicationContext并实现了Getter方法,看看BB类的源码:

@Component
public class BB {

    private Long id = 12306L;

    public Long getId() {
        return id;
    }
}

BB类非常简单,@Component标记为Spring扫描类,然后返回id属性的值为12306。

在applicationContext.xml中添加包扫描,注解,aspectj:

<!-- 扫描com.biyao.api.ordergroup下注解 -->
<context:component-scan base-package="com.kevin"/>
<!-- 允许apo注解 -->
<context:annotation-config/>

<bean class="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect" factory-method="aspectOf"/>

再配置一下maven的编译插件(必须配置,否则会注入失败,后面会讲解):

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.7</version>

    <configuration>
        <complianceLevel>1.8</complianceLevel>
        <source>1.8</source>
        <target>1.8</target>
        <outxml>true</outxml>
        <verbose>true</verbose>
        <showWeaveInfo>true</showWeaveInfo>
        <aspectLibraries>
            <aspectLibrary>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aspects</artifactId>
            </aspectLibrary>
        </aspectLibraries>
    </configuration>

    <executions>
        <execution>
            <phase>process-classes</phase>
            <goals>
                <goal>compile</goal>
            </goals>
        </execution>
    </executions>
</plugin>

好了,代码都写完成了,咱们来跑一个单元测试:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:applicationContext.xml"})
public class T1 {

    @Test
    public void t1() {
        AA aa = new AA();
        BB b = aa.getB();
        System.out.println("aa.getB() is .." + b);
        System.out.println("bb.id = " + b.getId());
    }
}

测试结果如下:


aa.getB() is ..com.kevin.beans.BB@1c88ec8
bb.id = 12306

实验证明通过@Configurable注解能往new 出来的对象注入spring bean;以后咱们有些不想Spring IOC管理的bean也能注入了。这样可以随用随取,在一些非单例场景下特别有用,比如定时任务,每次执行的时候需要new一个新的业务对象,这样也能拿到IOC里的JDBC对象(场景仅举例,通过ApplicationContext等Spring接口也能获取)。接下来咱们来分析下原理。

@Configurable注解的实现原理

为什么要在POM文件里引入aspectj-maven-plugin呢?

我们可以先注释掉aspectj-maven-plugin插件,然后看看AA类的反编译代码。

@Configurable
public class AA {
    @Autowired
    private BB b;
    @Autowired
    ApplicationContext applicationContext;

    public AA() {
    }

    public BB getB() {
        return this.b;
    }

    public ApplicationContext getApplicationContext() {
        return this.applicationContext;
    }
}

可以看到AA没有编译后再反编译回来代码没有差别:

image-20201220220650003

使用了插件之后再反编译回来:

image-20201220220930348

可以看到多了构造函数AA():

public AA() {
    JoinPoint var2 = Factory.makeJP(ajc$tjp_1, this, this);
    JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
    if (this != null && this.getClass().isAnnotationPresent(Configurable.class) && AnnotationBeanConfigurerAspect.ajc$if$bb0((Configurable)this.getClass().getAnnotation(Configurable.class))) {
        AnnotationBeanConfigurerAspect.aspectOf().ajc$before$org_springframework_beans_factory_aspectj_AbstractDependencyInjectionAspect$1$e854fa65(this);
    }

    if (this != null && this.getClass().isAnnotationPresent(Configurable.class) && (this == null || !this.getClass().isAnnotationPresent(Configurable.class) || !AnnotationBeanConfigurerAspect.ajc$if$bb0((Configurable)this.getClass().getAnnotation(Configurable.class))) && AbstractDependencyInjectionAspect.ajc$if$6f1(var1)) {
        AnnotationBeanConfigurerAspect.aspectOf().ajc$afterReturning$org_springframework_beans_factory_aspectj_AbstractDependencyInjectionAspect$2$1ea6722c(this);
    }

    if (!AnnotationBeanConfigurerAspect.ajc$if$bb0((Configurable)this.getClass().getAnnotation(Configurable.class)) && AbstractDependencyInjectionAspect.ajc$if$6f1(var2)) {
        AnnotationBeanConfigurerAspect.aspectOf().ajc$afterReturning$org_springframework_beans_factory_aspectj_AbstractDependencyInjectionAspect$2$1ea6722c(this);
    }

}

这个方法比较复杂,但经过一些列调用最终会指向org.springframework.beans.factory.wiring.BeanConfigurerSupport#configureBean方法,摘录部分关键代码:

// 解决给定bean实例的BeanWiringInfo。 beanInstance用于解析信息的bean实例
BeanWiringInfo bwi = bwiResolver.resolveWiringInfo(beanInstance);


// 按名称或类型自动装配给定bean实例的bean属性。 也可以使用AUTOWIRE_NO调用,以便仅应用实例化后的回调(例如,用于注释驱动的注入)。
// 不应用标准BeanPostProcessors回调或对bean进行任何进一步的初始化。 为此,此接口提供了不同的细粒度操作,例如initializeBean 。 但是,如果适用于实例的配置,则将应用InstantiationAwareBeanPostProcessor回调。
beanFactory.autowireBeanProperties(beanInstance, bwi.getAutowireMode(), bwi.getDependencyCheck());
// 试图将Bean放置到Spring IOC容器中
beanFactory.initializeBean(beanInstance, (beanName != null ? beanName : ""));

做了两件事:1.进行spring bean的自动注入;2.试图将新生成的bean放置到IOC容器中(会放置失败)。感兴趣的同学可以进一步追踪源码,这里往后已经进入spring bean的生命周期了。

原理总结

配置了aspectj-maven-plugin插件之后,插件会辅助将含有@Configurable注解的类,在编译时AnnotationBeanConfigurerAspect相关操作写入构造方法,这样当开发者使用new方法创建对象时就调用BeanConfigurerSupport#configureBean()方法,实现spring bean的注入。还记得我们在applicationContext.xml中配置了AnnotationBeanConfigurerAspect类吗?这就是配置的原因,如果不配置@Configurable注解不会生效。