SpringBoot原理(一)自动装配机制初探

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/liman65727/article/details/100052856

前言

经常用到springboot,其配置步骤及其简单,但这背后的原理是什么或许值得我们深入去探讨一下,依稀记得一句话,springboot中其实没有新技术,只是在已有的框架上封装了一下,减少了开发者的工作量。我们常提到的springboot中常见的四大组件是:AutoConfiguration,Starter,Actuator,Springboot CTL(用的较少)。

从一些简单注解入手

让人着迷的@SpringBootApplication

我们跟踪进去发现,这个其实是一个复合注解,具体如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

会发现主要有三个注解,@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan三个注解组成。我们最熟悉的还是@ComponentScan这个在spring中就用的很多了,无非就是扫描指定的包,然后从这些包中取出对应的bean。这个注解后续不会重点介绍

@SpringBootConfiguration中源代码如下:

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

}

会发现,这个实际就是一个@Configuration注解。似乎也没有什么新鲜的,后面会举一个@Configuration的实例。

@EnableAutoConfiguration注解中的源码如下

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

	String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

	Class<?>[] exclude() default {};

	String[] excludeName() default {};

}

其实springboot实现动态注入的核心就是这个注解中的@Import注解,这个后面会详细探讨。所以,从这里我们可以看出,@SpringBootApplication其实就是由三个注解组成——1.@ComponentScan,2、@Configuration,3、@EnableAutoConfiguration,第一个比较简单这里就不探讨了,下面先给出一些实例,然后逐步总结一下这三个注解。

@Configuration实例

其实一句话就可以说明@Configuration的作用:一个@Configuration就相当于一个applicationContext.xml(这个是我个人理解)在XML时代,我们配置spring容器,都必须线程applicationContext.xml入手,各种bean之间的依赖都写在了applicationContext.xml中,后来有了注解,@Configuration就好比一个applicationContext.xml。下面还是看一下@Configuration的简单demo吧

这个是需要托管的实例对象

/**
 * autor:liman
 * createtime:2019/8/19
 * comment:
 */
public class DemoClass {

    public void sayDemoClass(){
        System.out.println("demo class");
    }
}

一个标有@Configuration的类,就好比一个IOC容器

@Configuration
public class ConfigurationAnno {

    //这个方法就会返回对应的bean
    @Bean
    public DemoClass getDemoClass(){
        return new DemoClass();
    }

}

这样就可以通过spring获取这个生成的bean了

/**
 * autor:liman
 * createtime:2019/8/19
 * comment:
 */
public class ConfigurationDemo {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = 
                new AnnotationConfigApplicationContext(ConfigurationAnno.class);
        DemoClass bean =  applicationContext.getBean(DemoClass.class);
        bean.sayDemoClass();
    }

}

这里说明一下,上述在getBean的时候是通过类型获取的,如果需要通过bean的name从IOC容器中获取bean,则bean的名称不是所谓的类名首字母小写,而是与@Configuration中返回该bean实例的方法名为准,如下所示

/**
 * autor:liman
 * createtime:2019/8/19
 * comment:
 */
public class ConfigurationDemo {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = 
            new AnnotationConfigApplicationContext(ConfigurationAnno.class);
        DemoClass bean = (DemoClass) applicationContext.getBean("getTestClass");
        bean.sayDemoClass();
    }

}

如果写成如下代码,会抛出无法找到指定bean的异常:

DemoClass bean = (DemoClass) applicationContext.getBean("demoClass");//这样是无法找到指定bean的

@ComponentScan比较简单,这里就不做介绍了,无非就是扫描指定的包,然后获取包下面的bean。重点是@EnableAutoConfiguration注解,这个注解就要复杂的多了。springboot中的动态注入,主要功能就是这个注解完成,针对这个注解的问题,在下文动态注入中进行探讨。

动态注入

要想了解动态注入,还是先从我们了解的@Import注解开始说起。

@Import注解

在XML配置时代,一个项目中肯定不会只有一个applicationContext.xml,如果要将其他applicationContext.xml导入,则会用到<import>标签,这里的@Import注解其实作用和<import>标签作用一样。先来看一个实例:

先给出包的结构,在configuration实例包中,这个包中的代码,就是上面总结@Configuration的时候的代码,OtherBean只是另一个实体对象,OtherConfiguration只是在另一个包中的一个配置类,如下所示:

@Configuration
public class OtherConfiguration {

    @Bean
    public OtherBean getOtherBean(){
        return new OtherBean();
    }

}

现在有一个问题,如果在ConfigurationDemo中获取OtherBean,这个能顺利获取到么?

public class ConfigurationDemo {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ConfigurationAnno.class)
        DemoClass bean =  applicationContext.getBean(DemoClass.class);
        bean.sayDemoClass();
        //在ConfigurationDemo中获取OtherBean
        OtherBean otherBean = applicationContext.getBean(OtherBean.class);
        System.out.println(otherBean.otherBean());
    }

}

下面是运行结果:

很明显,无法获取OtherBean,这个时候,如果在对应的Configuration类中加入Import注解,就可以获取到OtherBean了

其实如果对spring了解很多的,并不用看这里的繁琐文章,这里对Import的介绍显得有点啰嗦。还是回归到动态注解的本质上来,动态注解,无非就是让在spring注入bean的之前,增加一些条件判断,这些条件判断决定什么时候注入什么bean,什么时候不注入什么bean。这些工作都交由@EnableAutoConfiguration注解完成。

@EnableAutoConfiguration注解

@EnableAutoConfiguration注解可以理解为由以下两个注解复合而成,

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)

@AutoConfigurationPackage

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {

}

从这里可以看到,这无非也就是两个相关的类,一个是以Selector结尾的,一个是以Registrar结尾的,这里两个类就完成了动态注入即根据条件注入。

下面我们举一个简单的实例来说明Selector和Registrar动态注入的方式。

Selector注入

先自定义两个业务类 CacheService和LoggerService(任意两个都行,不用实现),之后再定义一个注解,这个注解中加入了Import注解,同时定义了一个exclude的属性。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
//这里可以指定registrar或者selector类型的class,根据这个完成动态的注入,这里先指定SomeServiceImport
@Import({SomeServiceImport.class})
public @interface EnableDefineService {

    //配置一些方法
    Class<?>[] exclude() default {};

}

新建一个Selector类

/**
 * autor:liman
 * createtime:2019/8/19
 * comment:selector的动态导入机制
 */
public class SomeServiceImport implements ImportSelector {

    private String excludeClassName="com.learn.springbootstarter.AutoImportSelector.CacheService";

    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {

        String name = EnableDefineService.class.getName();
        //将注解中的元数据转换成attributes
        AnnotationAttributes attributes = AnnotationAttributes.fromMap(annotationMetadata.getAnnotationAttributes(name, true));
        //获取注解元数据中的值
        String[] excludes = (String[]) attributes.get("exclude");
        String excludeName = excludes[0];//获得需要屏蔽加载的类;
        String[] services=null;
        if(excludeClassName.equals(excludeName)){//这里过滤掉exclude指定的类
            services = new String[]{LoggerService.class.getName()};
        }else{
            services = new String[]{CacheService.class.getName()};
        }
        return services;
    }
}

测试主类如下:

/**
 * autor:liman
 * createtime:2019/8/19
 * comment:
 */
@SpringBootApplication
//这个注解中,指定exclude属性为CacheService.class
@EnableDefineService(exclude = CacheService.class)
public class EnableDemoMain {
    public static void main(String[] args) {
        ConfigurableApplicationContext ca=SpringApplication.run(EnableDemoMain.class,args);

        String[] beanDefinitionNames = ca.getBeanDefinitionNames();

        Predicate<String> predicate = str->str.equals("com.learn.springbootstarter.AutoImportSelector.CacheService")
                || str.equals("com.learn.springbootstarter.AutoImportSelector.LoggerService");

        Arrays.asList(beanDefinitionNames).stream().filter(predicate).forEach(System.out::println);
    }
}

最后的运行结果也很给力

加入到spring ioc中的之后LoggerService,CacheService被排除了。这个排除的操作就是在我们自己实现的SomeServiceSelector中实现的,SpringBoot中的Selector注入方式也是如此,在真正源码中,过滤的操作是在AutoConfigurationImportSelector中实现的,这个会在后面详细介绍。

Registrar动态注入

还是沿用上述实例,在上述实例中增加一个Registrar类,具体代码如下:

/**
 * autor:liman
 * createtime:2019/8/21
 * comment:
 */
public class SomeServiceRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
        Class beanClass = LoggerService.class;
        //封装成RootBeanDefinition
        RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass);
        //首字母小写
        String beanName = StringUtils.uncapitalize(beanClass.getName());
        System.out.println(beanName);
        //直接注册到IOC容器中
        beanDefinitionRegistry.registerBeanDefinition(beanName,beanDefinition);
    }
}

将我们定义的注解EnableDefineService中import指定的类换成这个SomeServiceRegistar,如下所示:

之后运行启动实例,会得到如下输出,因为这里只注册了一个LoggerService所以,并没有其他业务类。

上述是自己写的两种动态注入的一个实例,但是真正的动态输入还会用到SPI机制,这个在后面。

SPI

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。(这句话来自百度百科)

先细看AutoConfigurationImportSelector

下面一段是AutoConfigurationImportSelector中的源码,selectImports方法会最终返回需要注入的类的名称,AnnotationMetadata这个参数代表的是@EnableAutoConfiguration注解中的元数据,根据这些元数据进行一些判断,导入指定的类。这就是AutoConfigurationImportSelector完成的工作。

public class AutoConfigurationImportSelector implements DeferredImportSelector{


	@Override
	public String[] selectImports(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return NO_IMPORTS;
		}

        //加载spring-autoconfigure-metadata.properties配置文件中的数据
		AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
				.loadMetadata(this.beanClassLoader);
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
				annotationMetadata);

        //返回需要交给spring进行注入的类
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}

}

上述方法就返回了需要springboot自动装配的一些bean,通过String[]的形式返回需要装配的bean的name,但是这个方法的在真正返回需要装配的bean的name之前,还做了很多操作。做了些动态过滤的操作。

第一步是通过loadMetadata加载当前classpath下的spring-autoconfigure-metadata.properties文件,这个文件里面配置了所有动态加载的条件。这个后面会有详细的例子。第二步是通过getAutoConfigurationEntry获取需要动态加载的class,这一步具体源码如下:

protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
		AnnotationMetadata annotationMetadata) {
	if (!isEnabled(annotationMetadata)) {
		return EMPTY_ENTRY;
	}
	AnnotationAttributes attributes = getAttributes(annotationMetadata);

    //这一步是去加载classpath下的spring.factories文件中的实例,这个就是配置了spi动态加载的类
    //下面的可以忽略
	List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
	configurations = removeDuplicates(configurations);
	Set<String> exclusions = getExclusions(annotationMetadata, attributes);
	checkExcludedClasses(configurations, exclusions);
	configurations.removeAll(exclusions);
	configurations = filter(configurations, autoConfigurationMetadata);
	fireAutoConfigurationImportEvents(configurations, exclusions);
	return new AutoConfigurationEntry(configurations, exclusions);
}

getCandidateConfigurations这个详细源码如下:

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
	List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
			getBeanClassLoader());
	Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
			+ "are using a custom packaging, make sure that file is correct.");
	return configurations;
}

可以看出这里就是加载当前classpath下的所有的spring.factories文件中的内容。下面就是spring.factories中EnableAutoConfiguration的配置。这些如果没有AutoConfigurationImportSelector的过滤操作,这里所有配置的值,都会在getCandidateConfigurations方法中会返回给IOC容器,springboot会自动装载这些类。

来看看SPI

前面说了太多啰嗦的东西,这里开始真正说一下SPI机制。

1、先建立一个单独的工程,一个简单的maven工程,引入spring-context,结构如下所示,但是要在resource目录下建立META-INF文件夹,在其中建立spring.factories文件,因为上文已经说到,springboot启动的时候会去扫描classpath下的META-INF文件夹下的spring.factories文件中的内容。动态注入这个文件中的扩展点。

2、看一下spring.factories文件以及对应的业务类

/**
 * autor:liman
 * createtime:2019/8/25
 * comment:
 */
public class SPIDemoService {

    public String SPISayHello(String param){
        return "hello this is SPI core service "+param;
    }

}

交给IOC管理

/**
 * autor:liman
 * createtime:2019/8/25
 * comment:
 */
@Configuration
public class SPIConfig {

    @Bean
    public SPIDemoService getSpiDemoService(){
        return new SPIDemoService();
    }
}

spring.factories中加入下面一行就行

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.learn.service.SPIConfig

3、然后在另外一个项目中引用这个项目的依赖

<dependency>
    <groupId>com.learn</groupId>
    <artifactId>SpiCore</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

之后在另一个项目中写一个启动测试了,会发现springboot会自动给我们注入这个bean。

@SpringBootApplication
public class SPIDemo {

    public static void main(String[] args) {
        ConfigurableApplicationContext applicationContext = SpringApplication.run(SPIDemo.class);
        SPIDemoService sp = applicationContext.getBean(SPIDemoService.class);
        String test = sp.SPISayHello("test");
        System.out.println(test);
    }

}

运行结果:

其实这里我们没有用任何的@Autowired直接,springboot自动帮我们注入了这个实例,我们真正做到了开箱即用。

什么是spring-autoconfigure-metadata.properties

之前说过,这个配置文件是配置了自动载入的条件(有一种说法叫元数据,这个就不用纠结了),简单点理解就是这个文件配置了动态注入的相关条件依赖。可能还是有点晕,下面还是直接上个实例吧。

1、还是在上一步建立的那个项目中,还是在resource文件夹下的META-INF文件夹下。建立一个spring-autoconfigure-metadata.properties文件。内容如下:

com.learn.service.SPIConfig.ConditionalOnClass=com.learn.springbootstarter.EnableAutoFromSpi.TestService

2、另一个项目,还是之前的代码,会有什么样的情况发生呢?

运行以后会出现很经典的错误,就是无法找到类,看来第一步中的配置有些魔力啊。

这就是因为ConditionOnClass就是指定了加载SPIDemoService的条件,如果存在TestService,SPIDemoService才会加载,才会被springboot动态注入,如果不存在就不会被动态注入,很显然,这里我们只是加了这个配置,还没有加这ConditionOnClass所指定的类,因此当然springboot当然就不会为我们加载这个类

3、在指定路径下加入TestService

然后运行会发现,我们熟悉的结果又出现了,这里就不贴图了。

总结

写到这里,这篇博客基本上都顾及到了springboot自动装配的所有内容,但是附上了一些示例,使得这篇博客整体结果有些凌乱,但是从@Configuration和@Import等常见的注解入手,到最后的动态条件自动注入,如果耐心看完会有很大的收获,总体来说这篇文章重点为了介绍后面的动态选择注入写了很多无关的篇幅,但至少提供了一些主要关键字,如果不太理解可以直接先参考其他大牛的博客之后再实现以下本文中的小小实例,收获会更大。

本文中所涉及到的实例代码地址:https://download.csdn.net/download/liman65727/11612632

展开阅读全文

没有更多推荐了,返回首页