Spring boot运行原理-自定义自动配置类

Java框架

浏览数:105

2019-9-7

AD:资源代下载服务

在前面SpringBoot的文章中介绍了SpringBoot的基本配置,今天我们将给大家讲一讲SpringBoot的运行原理,然后根据原理我们自定义一个starter pom。

本章对于后续继续学习SpringBoot至关重要,了解SpringBoot运行原理对于我们深入学习SpringBoot有着非常重要的作用。

SpringBoot的自动配置从何而来

要想了解SpringBoot的自动配置,我们可以在源码看到相关代码和配置。
SpringBoot关于自动配置的源码在spring-boot-autoconfigure-2.1.x.jar内,打开maven依赖我们可以看见

如果想了解SpringBoot为我们做了哪些自动配置,可以通过下面方式查看当前项目中已启用和未启用的自动配置的报告。
  1. 运行jar时增加–debug参数:
java -jar xx.jar --debug
  1. 在application.properties中设置属性:
debug=true
启动时,通过控制台我们可以看到哪些配置已使用自动配置,哪些配置没有自动配置。

已启用自动配置

未启用自动配置

仔细看上图我们可以发现,相关如

@ConditionalOnClass found required class ...    \   @ConditionalOnClass did not find required class ...

的字眼非常多,可见@ConditionalOnClass注解可能在自动配置中起着主要作用,那究竟是如何起作用的呢?

运行原理

关于SpringBoot的运作原理,我们还是回归到@SpringBootApplication注解上来,这个注解是一个组合注解,它的核心功能是一个开启自动配置注解@EnableAutoConfiguration

下面我们来看下@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 {};
}

这里我们重点关注@Import的导入功能,AutoConfigurationImportSelector使用SpringFactoriesLoader.loadFactoryNames方法来扫描META-INF/spring.factories文件中描述的jar包,所以我们立马到刚刚打开的自动配置类中的META-INF/spring.factories中找一下是否真的有这样一个文件。

好家伙,还真的有

马上打开看一看

核心注解

我们打开上面配置的任何其中一个注解,一般都有下面的条件注解,打开源码spring-boot-autoconfigure下的org/springframework/boot/condition看看都有哪些注解

简单介绍一下每个注解代表代表的条件:
@ConditionalOnBean: 当容器里有指定的Bean条件下。
@ConditionalOnClass: 当类路径下有指定的类的条件下。
@ConditionalOnExpression: 基于SpEL表达式作为判断条件。
@ConditionalOnJava: 基于JVM版本作为判断条件。
@ConditionalOnjndi: 在基于JNDI存在的条件下查找指定的位置。
@ConditionalOnMissingBean: 当容器里没有Bean的情况下。
@ConditionalOnMIssingClass: 当类路径下没有指定的类的条件下。
@ConditionalOnNotWebApplication: 当前项目不是Web项目的条件下。
@ConditionalOnProperty: 指定的属性是否有指定的值。
@ConditionalOnResource: 类路径是否有指定的值。
@ConditionalOnSingleCandidate: 当指定Bean在容器中只有一个,或者虽然有多个但是指定首选的Bean
@ConditionalOnWebApplication: 当前项目是web项目的条件下
这些注解都是组合了@Conditional元注解,只是使用了不同的条件(Condition)
下面我们简单分析一下@ConditionalOnWebApplication注解。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional({OnWebApplicationCondition.class})
public @interface ConditionalOnWebApplication {
    ConditionalOnWebApplication.Type type() default ConditionalOnWebApplication.Type.ANY;

    public static enum Type {
        ANY,
        SERVLET,
        REACTIVE;

        private Type() {
        }
    }
}
看看OnWebApplicationCondition.class 是如何定义条件的

这里我们主要看isWebApplication方法,判断条件主要如下:
(1)GenericWebApplicationContext是否在类路径中;
(2)容器中是否有名为session的scope;
(3)当前容器的Environment是否为ConfigurableWebEnvironment
(4)当前的ResourceLoader是否为WebApplicationContext(ResourceLoader是ApplicationContext的顶级接口之一);
(5)构造ConditionOutcom类的isMatch方法返回布尔值来确定条件。

==这里的isWebApplication()方法,spring-boot-1.x和2.x有区别,在1.x中只判断了servlet,而在2.x增加了了reative和any的webapplication判断==

简单示例

在以往的web项目中,通常需要在web.xml中配置一个filter,对请求进行编码,如下所示:
<filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter
        </filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
因此如果要实现自动配置的话,需要满足一下条件:
(1)能配置CharacterEncodingFilter这个Bean;
(2)能配置encoding和forceEncoding这两个参数。

参数配置

在上一章我们讲到了类型安全的配置,Spring Boot的自动配置也是基于这一点实现的,这里的配置可以在application.properties中直接配置。
源码如下图:

代码解释:

(1)在application.properties配置的前缀是spring.http.encoding;
(2)默认编码方式为UTF-8,若修改可配置spring.http.encoding.charset=编码;
(3)设置force,默认为true,若修改可配置spring.http.encoding.force=false;

配置Bean

上面我们已经配置好相关参数,现在根据条件配置CharacterEncodingFilter的Bean,下面看看源码:
@Configuration
@EnableConfigurationProperties({HttpProperties.class}) //1
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@ConditionalOnClass({CharacterEncodingFilter.class})//2
@ConditionalOnProperty(
    prefix = "spring.http.encoding",
    value = {"enabled"},
    matchIfMissing = true
)//3
public class HttpEncodingAutoConfiguration {
    private final Encoding properties;

    public HttpEncodingAutoConfiguration(HttpProperties properties) {
        this.properties = properties.getEncoding();
    }

    
    @Bean//4
    @ConditionalOnMissingBean//5
    public CharacterEncodingFilter characterEncodingFilter() {
        CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
        filter.setEncoding(this.properties.getCharset().name());
        filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
        filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
        return filter;
    }

代码解释:

(1)开启属性注入,通过@EnableCondigurationProperties声明

(2)当CharacterEncodingFilter在类路径的条件下;

(3)当设置spring.http.encoding.enabled的情况下,如果没有设置则默认为true,即条件符合;

(4)像使用Java配置的方式配置CharacterEncoding这个Bean;

(5)当容器中没有这个Bean的时候新建Bean

实战-自定义一个starter pom

前面我们已经详细讲述了spring boot是如何实现自动化配置的,现在我们来动手自己写一个starter pom实现自动化配置。

要求:当某个类存在的时候,自动配置这个类的Bean,并可将Bean的属性在application.properties中配置

创建一个普通MAVEN工程

创建一个普通MAVEN工程,并加入spring boot自动配置依赖

属性配置

我们仿照HttpProperties来配置,我们自定义starter 的配置文件
@ConfigurationProperties(prefix = "xicent.service")
public class MyServiceProperties {
    private MyServiceProperties.MyProperties myProperties = new MyServiceProperties.MyProperties();

    public MyProperties getMyProperties() {
        return myProperties;
    }

    public void setMyProperties(MyProperties myProperties) {
        this.myProperties = myProperties;
    }

    public static class MyProperties{
        public static final String DEFAULT_NAME;
        private String author;
        private String age;
        
        static {
            DEFAULT_NAME = "wjx";
        }
        
    省略 get/set..

使用类型安全的方式获取属性。author如果不设置,会给默认值。

判断依据类

public class MyService {
    private String name;
    private String age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }
}

这个类作为我们的判断依据类,如果存在,则创建这个类的Bean。

自动配置类(关键)

@Configuration
@EnableConfigurationProperties(MyServiceProperties.class)
@ConditionalOnClass(MyService.class)
@ConditionalOnProperty(prefix = "xicent.service",value = "enabled",matchIfMissing = true)
public class MyServiceAutoConfiguration {
    private MyServiceProperties.MyProperties properties;

    public MyServiceAutoConfiguration(MyServiceProperties properties) {
        this.properties = properties.getMyProperties();
    }
    
    @Bean
    @ConditionalOnMissingBean(MyService.class)
    public MyService myService(){
        MyService myService = new MyService();
        myService.setName(properties.getAuthor());
        myService.setAge(properties.getAge());
        
        return myService;
    }
}

这里我们仿照了HttpEncodingAutoConfiguration的写法,其实MyServiceProperties也可以直接用@Autowired直接注入的。

@ConditionalOnClass判断MyService这个类是否在类路径中存在,并且容器中没有这个Bean的情况下,我们对这个Bean进行自动配置。

注册配置

在前面我们也带大家看过,每个自动配置的包中,在src/main/resources/META-INF下都会有一个spring.factories配置文件。
下面我们就将我们刚刚写好的自动配置类,在这个配置文件中进行注册。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.xicent.starter.config.MyServiceAutoConfiguration
如果有多个自动配置,用","隔开,此处的"\"是为了换行后仍能读到属性。

添加在仓库

如果是公司提供给其他项目使用,则可以直接上传到公司私服。这里为了方便测试,我们就打包到本地仓库。
直接点击idea->maven project->lifecycle->install

新建Spring Boot项目加入依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.xicent</groupId>
            <artifactId>mystarter</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>

mystarter是我们刚刚手动写好的自动配置,加入web是为了待会用接口访问,方便测试。

添加配置

刚刚我们的自动配置中允许配置两个参数,其中author如果不配置,提供默认值。
xicent.service.my-properties.age=23
xicent.service.my-properties.author=kris

注入依赖

@SpringBootApplication
@RestController
public class TeststarterApplication {

    @Autowired
    MyService myService;

    @GetMapping("/")
    String testStarter(){
        return myService.getName()+":"+myService.getAge();
    }

    public static void main(String[] args) {
        SpringApplication.run(TeststarterApplication.class, args);
    }

}

访问接口

如果不配置author

到这里,我们自定义的starter pom就大功告成啦~ 是不是感觉其实挺简单的,Spring Boot自动配置的神秘面纱也就被我们悄悄揭开了。

如果您觉得有用记得分享喔~
公众号搜索:喜讯XiCent 获取更多福利~

作者:kriswu