[笔记]SpringBoot实战(3)-Spring Boot

系列笔记:

Spring Boot 2 'application.properties' 全部配置示例

约定优于配置,是 Spring Boot 的主导思想。

Spring Boot 基础

随着动态语言的流行(Ruby、Groory、Scala、Node.js),Java的开发显得格外的笨重: 繁多的配置、低下的开发效率、复杂的部署流程以及第三方技术集成难度大。
在上述环境下,Spring Boot应运而生。它使用"习惯优于配置"(项目中存在大量的配置,此外还内置一个习惯性的配置,让你无须手动进行配置)的理念让你的项目快速运行起来。使用Sprine Boot很容易创建一个独立运行(运行jar,内嵌Servlet容器)、准生产级别的基于Spring框架的项目,使用SpringBoot你可以不用或者只需要很少的配置。

Spring Boot 核心功能

  • 1.独立运行的Spring项目

    Spring Boot 可以以jar包的形式独立运行,运行一个Spring Boot项目只需通过java xx.jar来运行。

  • 2.内嵌Servlet容器

    Spring Boot 可选择内嵌Tomcat、Jetty 或者Undertow,这样我们无须以war包形式部署项目。

  • 3.提供 starter 简化配置

    当对应的starter 被选中后,与这些技术相关的Spring的Bean将会被自动配置。

  • 4.自动配置Spring

  • 5.准生产的应用监控

    提供基于http、ssh、telnet 对运行时的项目进行监控

  • 6.无代码生成和xml配置

Spring 4.x 提倡使用Java配置和注解配置组合,而Sping Boot不需要任何xml配置即可实现Spring的所有配置。

Spring Boot CLI 是SpringBoot提供的控制台命令工具。

Spring Boot 核心

@SpringBootApplication

@SpringBootApplication 是 SpringBoot项目的核心注解,主要目的是开启自动配置。它是一个组合注解,主要组合了@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan。其中,@EnableAutoConfiguration让SpringBoot根据类路径中的jar包依赖为当前项目进行自动配置。

SpringBoot会自动扫描@SpringBootApplication所在类的同级包以及下级包里的Bean。建议入口类防止的位置在groupId+arctifactId组合的包名下。

还可以使用exclude参数关闭特定的自动配置。例如:

@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})

可参考Spring Boot学习2——自定义banner一文。

从Spring Boot 2.0开始 启动时的 ASCII 图像 Spring Boot banner 现已支持 GIF

配置文件

使用一个全局的配置文件 application.properties 或 application.yml,放置在src/main/resources目录或者类路径的/config下。

yaml 是以数据为中心的语言,在配置数据的时候具有面向对象的特征。

# properties 
server.port=8080  
server.context-path=/  
## VS
# yml
server:  
  port: 8080
  contextPath: /

Spring Boot 的参数配置会按照下列的优先级顺序进行加载

  • 命令行参数
  • 来自 java:comp/env 的JNDI 属性
  • Java 系统属性(System.getProperties())
  • 操作系统环境变量 :
  • RandomValuePropertySource 配置的 random.* 属性值
  • jar 包外部的 application-{profile}.properties 或 application.yml ( 带 spring.profile )配置文件
  • jar 包内部的 application-{profile}.properties 或 application.yml (带 spring.profile )配置文件
  • jar 包外部的 application.properties 或 application.yml (不带 spring.profile )配置文件
  • jar 包内部的 application.properties 或 application.ym (不带 spring.profile )配置文件:
  • @Configuration 注解类上的 @PropertySource
  • 通过 SpringApplication.setDefaultProperties 指定的默认属性

starter pom

Spring Boot 为我们提供了简化企业级开发绝大多数场景的 starter pom,只要使用了应用场景所需要的starter pom,相关的技术配置将会消除,就可以得到SpringBoot为我们提供的自动配置的Bean。

官方和第三方都有为SpringBoot提供 startrt pom。

使用XML配置

提倡零配置,即无xml配置,特殊要求必须使用xml配置时,可以通过Spring 提供的@ImpoerResource来加载xml配置,例如:

@ImportResource({"classpath:xxx-context.xml", "classpath:yyy-context.xml"})

外部配置

Spring Boot允许使用 properties 文件,yaml 文件或者命令行参数作为外部配置。
例如:

java -jar xx.jar --server.port=8080  

在SpringBoot里,我们可以通过@Value注入application.properties文件中定义的属性。而在常规的Spring环境下,需要通过 @PropertySource 指明 properties 文件的位置,然后再通过@Value注入。

类型安全的配置(基于properties)

使用@Value注入每个配置显得很麻烦,Spring Boot 还提供了基于类型安全的配置方式,通过@ConfigurationProperties将properties属性和一个Bean及其属性关联,从而实现类型安全的配置。SpringBoot的自动配置就是基于此实现的。

具体使用示例可以参考Spring Boot学习1——配置文件

日志配置

SpringBoot支持Java Util Logging、Log4j、log4j2、Logback作为日志框架,且为各日志框架的控制台输出和文件输出做好了配置。

默认情况下,SpringBoot使用Logback作为日志框架。

示例:

## 配置日志文件
logging.file=D:/xxx/log.log  
## 配置日志级别,格式为logging.level.包名=级别
logging.level.org.springframework.web=DEBUG  
## 开发中经常出现和参数类型相关的4XX错误,设置此项我们会看到更详细的错误信息。

具体可参考Spring Boot学习3——日志管理一文。

Profile 配置

Profile是Spring用来针对不同的环境对不同的配置提供支持的,全局Profile配置使用 application-{profile}.proeprties (如:application-prod.proeprties)。
通过在application.proeprties中设置spring.profiles.active=prod来指定活动的Profile。 也可以通过命令行来设置:

java -jar xxx.jar --spring.profiles.active=prod  

除了spring.profiles.active来激活一个或者多个profile之外,还可以用spring.profiles.include来叠加profile。

@Profile使用示例:

@Component
@Profile("test")
public class TestDBConnector  {  
    @Override
    public void configure() {
        System.out.println("testdb");
    }
}

可以如下总结多环境的配置思路:
application.properties 中配置通用内容,并设置 spring.profiles.active=dev,代表以开发环境为默认配置。
application-{profile}.properties 中配置各个环境不同的内容,然后通过命令行方式去激活不同环境的配置。

SpringBoot运行原理

SpringBoot中关于自动配置的源码在spring-boot-autoconfigure-x.x.x.jar 内。若想知道SpringBoot为我们做了哪些自动配置,可以查看这里的源码。
可以通过以下三种方式查看当前项目中已启用和未启用的自动配置的报告:

  • 1.运行jar时增加 --debug 参数
java -jar xx.jar --debug  
  • 2.在 application.properties 中设置属性 debug=true
  • 3.在 STS 中设置 VM arguments:-Ddebug

启动会在控制台看到已启用的自动配置为:

============================
CONDITIONS EVALUATION REPORT  
============================
Positive matches: 启用的自动配置  
-----------------
 ....
Negative matches: 未启动的  
-----------------
 ....

运作原理

前面提到过@SpringBootApplication是一个组合注解,它的核心功能是由@EnableAutoConfiguration注解提供的。其摘要源码如下(SpringBoot 2.1.1):

@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包,而前面提到的spring-boot-autoconfigure-x.x.x.jar里就有一个spring.factories文件,此文件中声明了有哪些自动配置,摘要如下:

# Initializers
org.springframework.context.ApplicationContextInitializer=\  
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\  
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener  
# Application Listeners
org.springframework.context.ApplicationListener=\  
org.springframework.boot.autoconfigure.BackgroundPreinitializer  
# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\  
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener  
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\  
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\  
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition  
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\  
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\  
org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration  
# Failure analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\  
org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinitionFailureAnalyzer,\  
org.springframework.boot.autoconfigure.session.NonUniqueSessionRepositoryFailureAnalyzer  
# Template availability providers
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\  
org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider  

核心注解

打开上面任意一个AutoConfiguation文件,一般都有下面的条件注解,在spring-boot-autoconfigure-x.x.x.jarorg.springframwork.boot.autoconfigure.condition包下,条件注解如下:

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

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

分析 @ConditionOnWebApplication 注解

@ConditionOnWebApplication 主要源码分析:

package org.springframework.boot.autoconfigure.condition;  
import java.lang.annotation.Documented;  
import java.lang.annotation.ElementType;  
import java.lang.annotation.Retention;  
import java.lang.annotation.RetentionPolicy;  
import java.lang.annotation.Target;  
import org.springframework.context.annotation.Conditional;  
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 使用了 OnWebApplicationCondition 条件
@Conditional(OnWebApplicationCondition.class)
public @interface ConditionalOnWebApplication {  
    // ...
}

OnWebApplicationCondition 源码分析:

SpringBoot2/Spring5 引入了ReactiveWeb编程,所以要比 SpringBoot1/Spring4要复杂一些,Spring Boot1 只需要判断isWebApplication() 方法

  • 其中 isServletWebApplication() 判断条件是:

    • org.springframework.web.context.support.GenericWebApplicationContext 是否在类路径中
    • 容器中是否有名为session的scope
    • 当前容器的 Enviroment 是否为 ConfigurableWebEnvironment
    • 当前的 ResourceLoader 是否为 WebApplicationContext (ResourceLoader 是 ApplicationCOntext 的顶级接口之一)
  • isReactiveWebApplication() 判断条件是:

    • org.springframework.web.reactive.HandlerResult 是否在类路径中
    • 当前容器的 Enviroment 是否为 ConfigurableReactiveWebEnvironment
    • 当前的 ResourceLoader 是否为 ReactiveWebApplicationContext

最终通过 ConditionOutcome.isMatch() 方法返回布尔值来确定条件

简单分析 HttpEncodingAutoConfiguration 自动配置源码

源码摘要:

@Configuration
// 开启属性注入,通过@EnableConfigurationProperties声明
@EnableConfigurationProperties(HttpProperties.class)
// 使用了 OnWebApplicationCondition 条件,且要求为servlet web application
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
// 当 CharacterEncodingFilter 在类路径的条件下
@ConditionalOnClass(CharacterEncodingFilter.class)
// 当设置 spring.http.encoding=enabled 的情况下,如果没有设置则默认为 true,即符合条件
@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true)
public class HttpEncodingAutoConfiguration {  
    @Bean // 像使用 Java 配置的方式配置 CharacterEncodingFilter  这个 Bean
    @ConditionalOnMissingBean // 当容器中没有这个Bean的时候新建Bean。
    public CharacterEncodingFilter characterEncodingFilter() {
        CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
        filter.setEncoding(this.properties.getCharset().name());
        filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST));
        filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE));
        return filter;
    }
}

再接着分析下 HttpProperties 配置类(SpringBoot1中为HttpEncodingProperties):

// 在 application.properties 配置的时候前缀是 spring.http
@ConfigurationProperties(prefix = "spring.http")
public class HttpProperties {  
    private final Encoding encoding = new Encoding();
    public static class Encoding {
        // 默认编码方式是 UTF-8,若修改可以使用spring.http.encoding.charset=编码
        public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
        private Charset charset = DEFAULT_CHARSET;
        // forceEncoding,若修改可以使用spring.http.encoding.force=true/false
        private Boolean force;
    }
}

启动引导:Spring Boot 应用启动的秘密

1.SpringApplication 初始化

SpringBoot 整个启动流程分为两个步骤:初始化一个org.springframework.boot.SpringApplication 对象,执行该对象的 run 方法。看下 SpringApplication 的初始化流程, SpringApplication 的构造方法代码如下:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {  
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    // 判断是否是web项目
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    setInitializers((Collection) getSpringFactoriesInstances(
            ApplicationContextInitializer.class));
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    // 找到入口类
    this.mainApplicationClass = deduceMainApplicationClass();
}

2.Spring Boot 启动流程

Spring Boot 应用的整个启动流程都封装在 SpringApplication.run() 方法中,其整个流程真的是太长太长了,但本质上就是在 Spring 容器启动的基础上做了大量的扩展,按照这个思路来看看源码:

public ConfigurableApplicationContext run(String... args) {  
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    configureHeadlessProperty();
    // 1
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    try {
        // 2
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                args);
        ConfigurableEnvironment environment = prepareEnvironment(listeners,
                applicationArguments);
        configureIgnoreBeanInfo(environment);
        // 3
        Banner printedBanner = printBanner(environment);
        // 4
        context = createApplicationContext();
        // 5
        exceptionReporters = getSpringFactoriesInstances(
                SpringBootExceptionReporter.class,
                new Class[] { ConfigurableApplicationContext.class }, context);
        // 6
        prepareContext(context, environment, listeners, applicationArguments,
                printedBanner);
        // 7
        refreshContext(context);
        // 8
        afterRefresh(context, applicationArguments);
        stopWatch.stop();
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass)
                    .logStarted(getApplicationLog(), stopWatch);
        }
        // 9
        listeners.started(context);
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }

    try {
        listeners.running(context);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, null);
        throw new IllegalStateException(ex);
    }
    return context;
}