Spring Boot学习笔记(二):Spring Boot 运行原理

看一下 Spring 实现自动配置的原理~~

全部章节传送门:
Spring Boot学习笔记(一):Spring Boot 入门基础
Spring Boot学习笔记(二):Spring Boot 运行原理
Spring Boot学习笔记(三):Spring Boot Web开发
Spring Boot学习笔记(四):Spring Boot 数据访问
Spring Boot学习笔记(五):Spring Boot 企业级开发
Spring Boot学习笔记(六):Spring Boot 应用监控

Spring Boot 启动原理

任何一个 Spring Boot 项目都会有一个启动类,其中有一个 @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}
)}
)
public @interface SpringBootApplication {
    ...
}

其中,比较关键的注解:

  • @SpringBootConfiguration:标记当前类为配置类
  • @EnableAutoConfiguration:开启自动配置
  • @ComponentScan:扫描主类所在的同级包以及下级包里的Bean

在进入核心注解 @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 使用 getCandidateConfigurations 方法得到待配置的class的类名集合。

protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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;
}

SpringFactoriesLoader.loadFactoryNames 方法会扫描 META-INF/spring.factories 文件。

查看 spring-boot-autoconfigure-2.1.3.RELEASE.jar 中的 spring.factories 。


Spring Boot学习笔记(二):Spring Boot 运行原理_第1张图片
spring-factories.png

可以看到其中的自动配置。

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
...

核心注解

打开任意的 AutoConfiguration 文件(spring-boot-autoconfigure-2.1.3.RELEASE.jar 中),可以看到很多的条件注解,这些注解包含在 org.springframework.boot.autoconfigure.condition 包中,如下所示:

  • @ConditionalOnBean:当容器里有指定Bean的条件下
  • @ConditionalOnClass:当类路径下有指定的类的条件下
  • @ConditionalOnExpression:基于SpEL表达式作为判断条件
  • @ConditionalOnJava:基于JVM版本作为判断条件
  • @ConditionalOnJndi:在JNDI存在的条件下查找指定的位置
  • @ConditionalOnMissingBean:当容器里没有指定Bean的情况下
  • @ConditionalOnMissingClass:当容器里没有指定类的情况下
  • @ConditionalOnWebApplication:当前项目时Web项目的条件下
  • @ConditionalOnNotWebApplication:当前项目不是Web项目的条件下
  • @ConditionalOnProperty:指定的属性是否有指定的值
  • @ConditionalOnResource:类路径是否有指定的值
  • @ConditionalOnOnSingleCandidate:当指定Bean在容器中只有一个,或者有多个但是指定首选的Bean
  • @ConditionalOnWebApplication: 当前项目是在 Web 项目的条件下。

这些注解都组合了@Conditional注解,只是使用了不同的条件。

实例分析

下面通过一个简单的 Spring Boot 内置的自动配置: http的编码配置来讲解一下配置流程。

在常规项目中配置 Http 编码的时候是在 web.xml 中配置一个 filter 。

 
    CharacterEncodingFilter
    org.springframework.web.filter.CharacterEncodingFilter
     
        encoding
        utf-8 
     
     
        forceEncoding
        true 
     
 

自动配置需要满足两个条件:

  1. 能配置 CharacterEncodingFilter 这个 Bean;
  2. 能配置 encoding 和 forceEncoding 两个参数。

配置参数的时候使用了在SpringBoot基础章节中讲述的类型安全配置,Spring Boot 也是基于这一点实现的。双击shift全局搜索 HttpProperties(这里需要注意,不是老版本中的 HttpEncodingProperties)。

@ConfigurationProperties(
    prefix = "spring.http"
) // 配置前缀
public class HttpProperties {
    private boolean logRequestDetails;
    private final HttpProperties.Encoding encoding = new HttpProperties.Encoding();

    public HttpProperties() {
    }

    public boolean isLogRequestDetails() {
        return this.logRequestDetails;
    }

    public void setLogRequestDetails(boolean logRequestDetails) {
        this.logRequestDetails = logRequestDetails;
    }

    public HttpProperties.Encoding getEncoding() {
        return this.encoding;
    }

    public static class Encoding {
        public static final Charset DEFAULT_CHARSET;
        private Charset charset;
        private Boolean force;
        private Boolean forceRequest;
        private Boolean forceResponse;
        private Map mapping;

        public Encoding() {
            this.charset = DEFAULT_CHARSET;
        }

        public Charset getCharset() {
            return this.charset;
        }

        public void setCharset(Charset charset) {
            this.charset = charset;
        }

        public boolean isForce() {
            return Boolean.TRUE.equals(this.force);
        }

        public void setForce(boolean force) {
            this.force = force;
        }

        public boolean isForceRequest() {
            return Boolean.TRUE.equals(this.forceRequest);
        }

        public void setForceRequest(boolean forceRequest) {
            this.forceRequest = forceRequest;
        }

        public boolean isForceResponse() {
            return Boolean.TRUE.equals(this.forceResponse);
        }

        public void setForceResponse(boolean forceResponse) {
            this.forceResponse = forceResponse;
        }

        public Map getMapping() {
            return this.mapping;
        }

        public void setMapping(Map mapping) {
            this.mapping = mapping;
        }

        public boolean shouldForce(HttpProperties.Encoding.Type type) {
            Boolean force = type != HttpProperties.Encoding.Type.REQUEST ? this.forceResponse : this.forceRequest;
            if (force == null) {
                force = this.force;
            }

            if (force == null) {
                force = type == HttpProperties.Encoding.Type.REQUEST;
            }

            return force;
        }

        static {
            DEFAULT_CHARSET = StandardCharsets.UTF_8;//默认编码是UTF8
        }

        public static enum Type {
            REQUEST,
            RESPONSE;

            private Type() {
            }
        }
    }
}

通过调用上述配置,然后根据条件配置 CharacterEncodingFilter 的 Bean 。

@Configuration 
@EnableConfigurationProperties({HttpProperties.class}) 
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@ConditionalOnClass({CharacterEncodingFilter.class})
@ConditionalOnProperty(
    prefix = "spring.http.encoding",
    value = {"enabled"},
    matchIfMissing = true
)
public class HttpEncodingAutoConfiguration {
    private final Encoding properties;

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

    @Bean
    @ConditionalOnMissingBean
    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;
    }
    ...
}

看上面的注解:

  • @Configuration:标明为配置类
  • @EnableConfigurationProperties(HttpEncodingProperties.class)声明开启属性注入
  • @ConditionalOnClass(CharacterEncodingFilter.class)当CharacterEncodingFilter在类路径的条件下
  • @ConditionalOnProperty(prefix = “spring.http.encoding”, value = “enabled”, matchIfMissing = true)当spring.http.encoding=enabled的情况下,如果没有设置则默认为true,即条件符合
  • @ConditionalOnMissingBean当容器中没有这个Bean时新建Bean 。

实现 starter pom

使用 idea 创建 Maven 的 quickstart 项目,项目信息如下。

com.wyk
spring-boot-starter-hello
1.0-SNAPSHOT

在 pom.xml 中添加 spring-boot-autoconfigure 依赖。


    org.springframework.boot
    spring-boot-autoconfigure
    2.1.2.RELEASE

创建属性配置类,用来获取类型安全的属性。

package com.wyk.springbootstarterhello;

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

/**
 * hello属性配置
 */
@ConfigurationProperties(prefix="hello")
public class HelloServiceProperties {
    private static final String MSG = "world";
    //添加默认值
    private String msg = MSG;

    public String getMsg() {
        return msg;
    }

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

创建判断依据类。

package com.wyk.springbootstarterhello;

/**
 * 判断依据类
 */
public class HelloService {
    private String msg;

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

    public String getMsg() {
        return msg;
    }

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

创建自动配置类。

package com.wyk.springbootstarterhello;

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", value="enabled", matchIfMissing=true)
public class HelloServiceAutoConfiguration {
    @Autowired
    private HelloServiceProperties helloServiceProperties;

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

若要配置类生效,还需要注册自动配置类,在src/main/resources 下新建 META-INF/spring.factories,并在其中填写如下内容:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.wyk.springbootstarterhello.HelloServiceAutoConfiguration

多个配置的话需要使用逗号隔开。

另外,在idea中默认没有resources目录,需要手工创建,然后点击 File->Project Structure...,在弹出的窗口中选择 Modules,然后右击resources目录将其设置为资源。

[图片上传失败...(image-5b5c5b-1553269214786)]

然后需要将项目添加到 Maven 中方便引用。

点击 View->Tool Window->Maven Projects ,会在右侧弹出Maven的工具栏。

[图片上传失败...(image-a6ee64-1553269214786)]

点击Lifecycle中的install即可构建Maven工程并安装到本地仓库,需要清除的话需点击clean 。

接下来创建一个 Spring Boot 的Web项目用来使用前面的 starter 。创建好之后需要添加starter依赖。


    com.wyk
    spring-boot-starter-hello
    1.0-SNAPSHOT

然后简单的修改一下运行类。

@RestController
@SpringBootApplication
public class StatertestdemoApplication {

    @Autowired
    HelloService helloService;

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

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

运行程序,访问 http://localhost:8080 ,效果如下图。

Spring Boot学习笔记(二):Spring Boot 运行原理_第2张图片
starter-hello-world.png

在application.properties中添加配置。

hello.msg=wyk

重新运行, 访问 http://localhost:8080 ,效果如下图。

Spring Boot学习笔记(二):Spring Boot 运行原理_第3张图片
starter-hello-wyk.png

你可能感兴趣的:(Spring Boot学习笔记(二):Spring Boot 运行原理)