Spring Boot 系列之六:深入理解Spring Boot的自动配置

我们知道,Spring Boot自动配置功能可以根据不同情况来决定Spring配置应该用哪个,不应该用哪个,举个例子:

  • Spring的JdbcTemplate是不是在Classpath里面?如果是,并且DataSource也存在,就自动配置一个JdbcTemplate的Bean。
  • Thymeleaf是不是在Classpath里面?如果是,则自动配置Thymeleaf的模板解析器、视图解析器、模板引擎。

那个这个是怎么实现的呢?原因就在于它利用了Spring的条件化配置,条件化配置允许配置存在于应用中,但是在满足某些特定条件前会忽略这些配置。

要实现条件化配置我们要用到@Conditional条件化注解。

本篇从如下三个方面进行展开:

  1. @Conditional小例子,来说明条件化配置的实现方式
  2. Spring Boot 的条件化配置详解
  3. Spring Boot 自动配置源码分析
  4. 自己动手实现Spring Boot Starter pom

一、@Conditional小例子

我们知道在windows下显示列表的命令是dir,而在linux系统下显示列表的命令是ls,基于条件配置,我们可以实现在不同的操作系统下返回不同的值。

1、判断条件定义

(1)Windows下的判定条件

/**
 * 实现spring 的Condition接口,并且重写matches()方法,如果操作系统是windows就返回true
 *
 */
public class WindowsCondition implements Condition{

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        
        return context.getEnvironment().getProperty("os.name").contains("Windows");
    }
}

(2)Linux下的判定条件

/**
 * 实现spring 的Condition接口,并且重写matches()方法,如果操作系统是linux就返回true
 *
 */
public class LinuxCondition implements Condition{

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        
        return context.getEnvironment().getProperty("os.name").contains("Linux");
    }    
}

(3)Mac 下的判定条件

/**
 * 实现spring 的Condition接口,并且重写matches()方法,如果操作系统是Mac就返回true
 *
 */
public class MacCondition implements Condition{
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        return conditionContext.getEnvironment().getProperty("os.name").contains("Mac");
    }
}

2、不同系统下的的Bean类

(1)接口

public interface ListService {

    public String showListLine();
}

(2)windows下的Bean类

public class WindowsListService implements ListService{

    @Override
    public String showListLine() {
        return "dir";
    }
}

(3)Linux下的Bean类

public class LinuxListService implements ListService{

    @Override
    public String showListLine() {
        return "ls";
    }
}

(4)Mac下的Bean类 

3、配置类

@Configuration
public class ConditionConfig {

    @Bean
    @Conditional(WindowsCondition.class) // 通过@Conditional注解,符合Windows条件则实例化WindowsListService
    public ListService windowsListService(){
        return new WindowsListService();
    }

    @Bean
    @Conditional(LinuxCondition.class) // 通过@Conditional注解,符合Linux条件则实例化LinuxListService
    public ListService linuxListService(){
        return new LinuxListService();
    }

    @Bean
    @Conditional(MacCondition.class) // 通过@Conditional注解,符合Mac条件则实例化MacListService
    public ListService macListService(){
        return new MacListService();
    }
}

4、运行

public class ConditionTest {

    public static void main(String[] args) {

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConditionConfig.class);
        ListService listService = context.getBean(ListService.class);
        System.out
                .println(context.getEnvironment().getProperty("os.name") + " 系统下的列表命令为: " + listService.showListLine());
    }
}

5、运行Main,由于我的是Windows10系统,因此结果是:

 二、Spring Boot的条件化配置

在spring boot项目中会存在一个名为spring-boot-autoconfigure的jar包

条件化配置就是在这个jar里面实现的,它用到了如下的条件化注解,这些注解都是以@ConditionalOn开头的,他们都是应用了@Conditional的组合注解:

接下来我们看个源码的列子:

以JdbcTemplateAutoConfiguration为例,它里面有这段代码:

@Bean
    @Primary
    @ConditionalOnMissingBean(JdbcOperations.class)
    public JdbcTemplate jdbcTemplate() {
        return new JdbcTemplate(this.dataSource);
    }

只有在不存在JdbcOperations(如果查看JdbcTemplate的源码,你会发现JdbcTemplate类实现了JdbcOperations接口)实例的时候,才会初始化一个JdbcTemplate 的Bean。

基于以上内容,我们就可以阅读自动配置相关的源码了。

三、Spring Boot 自动配置源码分析

Spring Boot项目的启动类用的注解--@SpringBootApplication是一个组合注解,其中@EnableAutoConfiguration是自动配置相关的。以下源码截取自spring-boot-autoconfigure-2.1.1.RELEASE.jar

我们都知道springboot最核心的注解 @SpringBootApplication等于@SpringBootConfiguration、@EnableAutoConfiguration 、@ComponentScan等注解的作用,顾名思义,@EnableAutoConfiguration 显然就是springboot实现自动配置的核心所在。

@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) })
public @interface SpringBootApplication {

	@AliasFor(annotation = EnableAutoConfiguration.class)
	Class<?>[] exclude() default {};

	@AliasFor(annotation = EnableAutoConfiguration.class)
	String[] excludeName() default {};

	@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
	String[] scanBasePackages() default {};

	@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
	Class<?>[] scanBasePackageClasses() default {};

}

而这个@EnableAutoConfiguration注解里面有个@Import注解导入了AutoConfigurationImportSelector用来实现具体的功能。

@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 {};

}

我们来看看AutoConfigurationImportSelector类,重点是selectImports方法。方法selectImports()调用了方法getAutoConfigurationEntry(),进而调用了getCandidateConfigurations()方法,获取候选配置,由此我们可以猜想这一步就是springboot获取所有用@Configuration注解修饰的配置类的名称,那么为什么叫做“候选”配置呢?往下看,根据方法名,我们就能知道方法做了什么,接下来就是从这里获取的候选配置的list里,剔除重复部分,再剔除一开始我们@SpringbootApplication 注解里exclude掉的配置,才得到最终的配置类名集合。

接下来细看getCandidateConfigurations方法是如何拿到这些配置类名称的。

调用了SpringFactoriesLoader.loadFactoryNames()方法。

 在SpringFactoriesLoader.loadFactoryNames()方法里面,我们看到会查询META-INF/spring.factories这个配置文件。

SpringFactoriesLoader.loadFactoryNames方法会扫描具有META-INF/spring.factories文件的jar包,而我们的spring-boot-autoconfigure.jar里面就有一个这样的文件,此文件中声明了具体有哪些自动配置:

我们上面提到的JdbcTemplateAutoConfiguration自动配置类就在里面。

四、编写自己的Spring Boot Starter pom

接下来,我们就来写一个简单的Spring Boot Starter pom

1、新建项目,工程名demo;

2、修改pom文件。

        <!-- 这里需要引入spring boot的自动配置作为依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>

3、配置属性

package auto.config.demo.service;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @ConfigurationProperties
 * 自动匹配application.properties文件中hello.msg的值,然后赋值给类属性msg,这里的msg默认值为“spring boot”
 *
 */
@ConfigurationProperties(prefix="hello")
public class HelloServiceProperties {

    private static final String MSG = "spring boot";

    private String msg = MSG;

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

4、判定依据类

package auto.config.demo.service;

/**
 * 后面的代码会依据此类是否存在,来决定是否生产对应的Bean
 *
 */
public class HelloService {

    private String msg;

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public String sayHello() {
        return "hello " + msg;
    }
}

5、自动配置类

package auto.config.demo.config;

import auto.config.demo.service.HelloService;
import auto.config.demo.service.HelloServiceProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(HelloServiceProperties.class)
@ConditionalOnClass(HelloService.class)
@ConditionalOnProperty(prefix = "hello", matchIfMissing = true, value = "enabled")
public class HelloServiceAutoConfiguration {

    @Autowired
    HelloServiceProperties helloServiceProperties;

    @Bean
    @ConditionalOnMissingBean(HelloService.class)
    public HelloService helloService() {
        HelloService service = new HelloService();
        service.setMsg(helloServiceProperties.getMsg());
        return service;
    }
}

根据HelloServiceProperties提供的参数,并通过@ConditionalOnClass(HelloService.class)判定HelloService这个类在Classpath中是否存在,存在并且还没有对应的Bean,就生成对应的helloService Bean。

6、注册配置,需要到META-INF/spring.factories文件中注册改自动配置类:在src/main/source目录下新建改文件,然后进行配置。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
auto.config.demo.config.HelloServiceAutoConfiguration

7、对该工程进行mvn clean install,将jar推送到本地maven仓库,供后续使用。

mvn clean install -Dmaven.test.skip=true

打包进maven仓,再工程目录下,使用cmd命令,使用上述命令进行运行。

8、使用demo ,使用我们这个demo 需要新建一个或使用既存的一个spring boot工程(这里我用的是既存的),然后pom中依赖:

        <dependency>
            <groupId>auto.config</groupId>
            <artifactId>demo</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

9、实现controller

package auto.config.demo.controller;

import auto.config.demo.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    //代码中没有配置这个helloService Bean,但是自动配置能够帮忙实例化,因此可以直接注入
    @Autowired
    HelloService helloService;

    @RequestMapping(value="/helloService")
    public String sayHello() {
        return helloService.sayHello();
    }
}

10、浏览器访问:http://localhost:8080/helloService

11、在application.properties里面配置hello.msg=sam,然后再次访问/helloService接口。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值