SpringBoot配置管理


SpringBoot 配置管理

1. SpringBoot 启动流程简析

SpringBoot 可以轻松创建独⽴的、⽣产级的基于 Spring 的应⽤程序,只需要很少的⼀些 Spring 配置。为什么可以做到呢?

我们从 SpringBoot 的启动流程⻆度,简要分析 SpringBoot 启动过程中主要做了哪些事情。

SpringBoot 启动简要流程图

原始大图链接

⼀、启动流程概述

启动流程从⻆度来看,主要分两个步骤。

第⼀个步骤是构造⼀个 SpringApplication 应⽤;

第⼆个步骤是调⽤它的 run ⽅法,启动应⽤;

⼆、构造 SpringApplication 应⽤

public SpringApplication(ResourceLoader resourceLoader, Class<?>...
primarySources) {
 //资源加载器默认为null
 this.resourceLoader = resourceLoader;
 Assert.notNull(primarySources, "PrimarySources must not be null");
 //primarySources这⾥指的是执⾏main⽅法的主类
 this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
 //根据classpath推断出应⽤类型,主要有NONE,SERVLET,REACTIVE三种类型
 this.webApplicationType = WebApplicationType.deduceFromClasspath();
 //通过SpringFactoriesLoader⼯具类获取META-INF/spring.factories中配置的⼀系列
 //ApplicationContextInitializer接⼝的⼦实现类
 setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
 //基本同上,也是通过SpringFactoriesLoader⼯具类获取配置的ApplicationListener
 //接⼝的⼦实现类
 setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
 //通过⽅法调⽤堆栈获取到main执⾏⽅法的主类
 this.mainApplicationClass = deduceMainApplicationClass();
 }

SpringApplication 的构造函数中为 SpringApplication 做了⼀些初始化配置,包括

主资源类(⼀般是启动类 primarySources )、

应⽤类型(webApplicationType )、

应⽤环境上下⽂初始化器(initializers)、

应⽤监听器(listeners)、

main ⽅法主类(mainApplicationClass )

initializers 将在 ConfigurableApplicationContext 的 refresh ⽅法之前调⽤,⽐如针对 web 应⽤来说,需要在 refresh 之前注册属性源或者激活指定的配置⽂件。

listeners 将在 AbstractApplicationContext 的 refresh() ⽅法中,先被注册到 IOC 容器中,IOC 容器中剩下的⾮懒加载的单例被实例化后,IOC 容器发布相应的事件,这些事件最终会调⽤与之相关联的 AplicationListener 的 onApplicationEvent ⽅法

可对照顶部的流程图和源码分析

三、运⾏ SpringApplication

/**创建并刷新应⽤上下⽂**/
 public ConfigurableApplicationContext run(String... args) {
 //spring-core包下的计时器类,在这⾥主要⽤来记录应⽤启动的耗时
 StopWatch stopWatch = new StopWatch();
 stopWatch.start();
 ConfigurableApplicationContext context = null;
 Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();

 //系统配置设置为headless模式
 configureHeadlessProperty();
 //通过SpringFactoriesLoader⼯具类获取META-INF/spring.factories
 //⽂件中SpringApplicationRunListeners接⼝的实现类,在这⾥是
 //EventPublishingRunListener
 SpringApplicationRunListeners listeners = getRunListeners(args);

 //⼴播ApplicationStartingEvent事件使应⽤中的ApplicationListener
 //响应该事件
 listeners.starting();
 try {
   ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
   //准备应⽤环境,这⾥会发布ApplicationEnvironmentPreparedEvent事件
   //并将environment绑定到SpringApplication中
   ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);

   configureIgnoreBeanInfo(environment);
   //打印彩蛋
   Banner printedBanner = printBanner(environment);
   //根据成员变量webApplicationType创建应⽤上下⽂
   context = createApplicationContext();
   exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context);
   //准备上下⽂,⻅⽂章顶部流程图
   prepareContext(context, environment, listeners, applicationArguments, printedBanner);
   //刷新上下⽂,⻅⽂章顶部流程图
   refreshContext(context);
   //上下⽂后处理,空实现
   afterRefresh(context, applicationArguments);
   //计时停⽌
   stopWatch.stop();
   if (this.logStartupInfo) {
      new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
   }
   //⼴播ApplicationStartedEvent事件使应⽤中的ApplicationListener
   //响应该事件
   listeners.started(context);

   //应⽤上下⽂中的ApplicationRunner,CommandLineRunner执⾏run⽅法
   callRunners(context, applicationArguments);
 }
 catch (Throwable ex) {
   handleRunFailure(context, ex, exceptionReporters, listeners);
   throw new IllegalStateException(ex);
 }
 try {
   //⼴播ApplicationReadyEvent事件使应⽤中的ApplicationListener
   //响应该事件
   listeners.running(context);
 }
 catch (Throwable ex) {
   handleRunFailure(context, ex, exceptionReporters, null);
   throw new IllegalStateException(ex);
 }
 //返回应⽤上下⽂
 return context;
 }

1 在启动过程中,SpringApplicationListener 在不同阶段通过调⽤⾃身的不同⽅法(如 starting()、 environmentPrepared() )发布相应事件,通知 ApplicationListener 进⾏响应。

2 refreshContext(context) ⽅法是构建 IOC 容器最复杂的⼀步,绝⼤多数 bean 的定义加载以及实例化都在这⼀步执⾏。包括但不限于 BeanFactoryPostProcessor、BeanPostProcessor、 ApplicationEventMulticaster、@Controller, @Component 等注解的组件。

3 SpringFactoriesLoader ⼯具类,在 SpringApplication 的构造过程中、运⾏过程中都起到了极其重要的作⽤。SpringBoot 的⾃动化配置功能⼀个核⼼依赖点就在该类上,该类通过读取类路径下的 META-INF/spring.factories ⽂件获取各种各样的⼯⼚接⼝的实现类,通过反射获取这些类的类对象、构造⽅法,最终⽣成实例。

四、总结

1 SpringApplication 的构造过程中,配置了 SpringApplication 应⽤上下⽂的⼀些基本元素,如应⽤类型 webApplicationType、应⽤初始化器 ApplicationContextInitializer、应⽤监听器 ApplicationListener 等。这些元素在 SpringApplication 的执⾏ run() 过程中,都会在不同阶段发挥不同的作⽤。

2 SpringApplication 的执⾏ run() 过程中,⼀⽅⾯要初始化 IOC 容器(主要是 bean 的加载与初始化), ⼀⽅⾯要在不同的阶段直接或间接回调 ApplicationListener 或 ApplicationRunner 等其他接⼝的⽅法。

3 SpringFactoriesLoader 是 SpringBoot ⾃动化配置功能极其关键的⼀环,它读取类路径下 META-INF/spring.factories ⽂件中⼯⼚接⼝的实现类,来获取各种各种的 Bean ⼯⼚实例,最终获取到⾃动化配置的 bean。

2. Bean ⾃动装配原理

SpringBoot ⼤量简化了配置。实际上这些配置还都存在,只是将⼀些不常⽤的配置隐藏了起来, 不需要我们像以往⼀样逐⼀配置,从⽽减少了配置量、提⾼了开发效率。

⼀、Bean ⾃动装载的核⼼问题

SpringBoot ⾥⾯的各种 Bean (类对象)能够实现⾃动装载,⾃动装载帮我们减少了 XML 的配置,和 ⼿动编码进⾏ Bean 的加载⼯作。从⽽极⼤程度上帮我们减少了配置量和代码量。

要实现 Bean 的⾃动装载,需要解决两个问题

  • 如何保证 Bean ⾃动装载的灵活性?这个问题通过配置⽂件来解决,在配置 A 情况下去装载 BeanY ;在配置 B 情况下去装载 BeanZ 。(通常情况下配置 A 和 B 会有默认值,来决定默认的装载⾏为,这样就不需要我们配置了,进⼀步减少配置量)
  • 如何保证 Bean 装载的顺序性?当 BeanA 装载完成之后再去装载 BeanY ,BeanY 装载完成之后才去装载 BeanX。这个装载顺序问题由 @ConditionOnXXXXXXX 注解来解决。

⼆、全局配置⽂件

SpringBoot 使⽤⼀个全局的配置⽂件,配置⽂件名是固定的:

  • application.properties
  • application.yml

全局配置⽂件的作⽤:修改 SpringBoot ⾃动配置的默认值,通过配置来影响 SpringBoot ⾃动加载⾏为。

三、配置加载原理源码解析

所有的 SpringBoot 应⽤程序都是以 SpringApplication.run() 作为应⽤程序⼊⼝的。

我们可以跟踪⼀下这个⽅法:

run ⽅法传⼊了 SpringApplication 对象和⼀些运⾏期参数。

如图所示⽅法,可以看到是获得 SpringFactories 的实例,⽽它需要通过⼀个类叫: SpringFactoriesLoader

这个类⾥⾯体现了 SpringBoot 加载配置⽂件的核⼼逻辑。

SpringFactoriesLoader 类的主要作⽤是通过类路径下的 META-INF/spring.factories ⽂件获取⼯ ⼚类接⼝的实现类,初始化并保存在缓存中,以供 Springboot 启动过程中各个阶段的调⽤。Spring 的⾃动化配置功能,也与此息息相关。

从上图可以看到:

  • 先从 META-INF/spring.factories ⽂件夹下加载了 spring.factories ⽂件资源。
  • 然后读取⽂件中的 ClassName 作为值放⼊ Properties 。

然后通过反射机制,对 spring.factories ⾥⾯的类资源进⾏实例化。

四、@EnableAutoConfiguration 作⽤

SpringBoot ⼊⼝启动类使⽤了 SpringBootApplication ,实际上就是开启了⾃动配置功能 @EnableAutoConfiguration 。

SpringFactoriesLoader 会以 @EnableAutoConfiguration 的包名和类名 org.springframework.boot.autoconfigure.EnableAutoConfiguration 为 Key 查找 spring.factories ⽂件,并将 value 中的类名实例化加载到 Spring Boot 应⽤中。

spring.factories ⽂件中的每⼀⾏都是⼀个⾃动装配类。

五、Bean 的⾃动装配实现原理简述

每⼀个⾃动配置类进⾏⾃动配置功能( spring.factories 中的每⼀⾏对应的类),我们以 HttpEncodingAutoConfiguration 为例:

//加载application全局配置⽂件内的部分配置到HttpEncodingProperties⾥⾯
@Configuration
@EnableConfigurationProperties({HttpEncodingProperties.class})
//当web容器类型是servlet的时候执⾏本类中的⾃动装配代码
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
//当有⼀个CharacterEncodingFilter的这样⼀个类的字节码⽂件时时执⾏本类中的⾃动装配代码
@ConditionalOnClass({CharacterEncodingFilter.class})
//当spring.http.encoding配置值为enabled的时候执⾏本类中的⾃动装配代码
@ConditionalOnProperty(
    prefix = "spring.http.encoding",
    value = {"enabled"},
    matchIfMissing = true   //如果application配置⽂件⾥⾯不配置,默认为true
)
public class HttpEncodingAutoConfiguration {
    private final HttpEncodingProperties properties;
    public HttpEncodingAutoConfiguration(HttpEncodingProperties properties) {
        this.properties = properties;
   }
    @Bean
    //当没有CharacterEncodingFilter这个Bean就实例化CharacterEncodingFilter为⼀个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.HttpEncodingProperties.Type.REQUEST));

filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpEncodingProperties.Type.RESPONSE));
        return filter;
   }
    @Bean
    public HttpEncodingAutoConfiguration.LocaleCharsetMappingsCustomizer localeCharsetMappingsCustomizer() {
 			return new HttpEncodingAutoConfiguration.LocaleCharsetMappingsCustomizer(this.properties);
   }

   //此处省略与⾃动加载⽆关的代码:HttpEncode的逻辑及其他
}

在配置类加载过程中,⼤量的使⽤到了条件加载注解:

条件注解 使⽤说明
@ConditionalOnClass classpath 中存在该类字节码⽂件时,才执⾏实例化⽅法或将类实例化
@ConditionalOnMissingClass classpath 中不存在该类字节码⽂件时,才执⾏实例化⽅法(不存在 A 的时候去初始化 B)
@ConditionalOnBean DI 容器中存在该类型 Bean 时,才执⾏实例化⽅法或将类实例化
@ConditionalOnMissingBean DI 容器中不存在该类型 Bean 时,才执⾏实例化⽅法或将类实例化
@ConditionalOnSingleCandidate DI 容器中该类型 Bean 只有⼀个或@Primary 的只有⼀个时,才执⾏实例化⽅法或将类实例化
@ConditionalOnExpression SpEL 表达式结果为 true 时,才执⾏实例化⽅法或将类实例化
@ConditionalOnProperty 参数设置或者值⼀致时,才执⾏实例化⽅法或将类实例化
@ConditionalOnResource 指定的⽂件存在时,才执⾏实例化⽅法或将类实例化
@ConditionalOnJndi 指定的 JNDI 存在时,才执⾏实例化⽅法或将类实例化
@ConditionalOnJava 指定的 Java 版本存在时,才执⾏实例化⽅法或将类实例化
@ConditionalOnWebApplication Web 应⽤环境下,才执⾏实例化⽅法或将类实例化
@ConditionalOnNotWebApplication ⾮ Web 应⽤环境下,才执⾏实例化⽅法或将类实例化

这个实现原理实际上就是⼀个⾃定义 spring-boot-starter 的实现原理,在以上的⾃动装配过程中,依赖于 HttpEncodingProperties 的⾃定义属性,我们后⾯会讲如何读取⾃定义配置属性。

3. 详解 YAML 语法及占位符语法

我们已经介绍过 application.yml 是我们项⽬⾥⾯的全局配置⽂件,在全局的配置⽂件中我们可以通过配置来改变程序的加载⾏为和运⾏⾏为。

YAML 是 “YAML Ain’t a Markup Language”(YAML 不是⼀种标记语⾔)的递归缩写。表明其不是⼀种标记语⾔( xml、html 是标记语⾔)。它是⼀种数据序列化语⾔,通过⼀定的格式表示数据结构。由于其良好的数据结构表现能⼒,既⽅便程序处理,也⽅便程序员阅读,所以其常常被⽤于书写配置⽂件。那么它能够表示哪些数据结构呢?

⼀、设计⼀个 YAML 数据结构

⾸先我们提出这样⼀个需求:

#   1. ⼀个家庭有爸爸、妈妈、孩⼦。
#   2. 这个家庭有⼀个名字(family-name)叫做“happy family”
#   3. 爸爸有名字(name)和年龄(age)两个属性
#   4. 妈妈有两个别名
#   5. 孩⼦除了名字(name)和年龄(age)两个属性,还有⼀个friends的集合
#   6. 每个friend有两个属性:hobby(爱好)和性别(gender)

上⾯的数据结构⽤ yaml 该如何表示呢?

family:
  family-name: "happy family"
  father:
    name: tom
    age: 38
  mother:
    alias:
      - rose
      - alice
  child:
    name: jack
    age: 5
    friends:
      - hobby: football
        gender: male
      - hobby: sing
        gender: female

或者 friends 的部分也可以写成:

friends:
     - {hobby: football,sex:  male}
     - {hobby: basketball,sex: female}

规则 1:字符串的单引号与双引号

  • 双引号: 会转义字符串⾥⾯的特殊字符,如下⾯\n 被转义为换⾏: name: “zhangsan \n lisi”:输出:zhangsan 换⾏ lisi
  • 单引号: 不会转义特殊字符,特殊字符最终只是作为⼀个普通的字符串数据,如:name: ‘zhangsan \n lisi’:输出:zhangsan \n lisi

规则 2:⽀持松散的语法

在 SpringBoot 应⽤中 YAML 数据格式⽀持松散的绑定语法,也就是说下⾯的三种 key 都是⼀样的。

family-name = familyName  = family_name

但是不绝对,通常建议使⽤中划线分隔的这种语法。

⼆、配置⽂件占位符

SpringBoot 配置⽂件⽀持占位符,如:为 person.age 设置⼀个随机数

person:
  age: ${random.int}

1 随机数占位符

  • ${random.value} - 类似 uuid 的随机数,没有”-“连接
  • ${random.int}- 随机取整型范围内的⼀个值
  • ${random.long} - 随机取⻓整型范围内的⼀个值
  • ${random.long(100,200)} - 随机⽣成⻓整型 100-200 范围内的⼀个值
  • ${random.uuid} - ⽣成⼀个 uuid,有短杠连接
  • ${random.int(10)} - 随机⽣成⼀个 10 以内的数
  • ${random.int(100,200)} - 随机⽣成⼀个 100-200 范围以内的数

2 默认值

占位符获取之前配置的值,如果没有可以是⽤“冒号”指定默认值。

格式如:xxxxx.yyyy 是属性层级及名称,如果该属性不存在,冒号后⾯填写默认值。

${xxxxx.yyyy:默认值}

⽐如为配置 father.best 属性,如果 family.father.name 存在则 father.best=${family.fath er.name} , family.father.name 这个配置不存在,则取值 father.best=tom

4. YAML 配置绑定变量两种⽅式

YAML 语法可以清晰地表达类、成员变量、集合以及它们之间的嵌套关系。

但如果配置只停留在配置⽂件⾥⾯是没有意义的,我们需要将配置绑定到内存变量上,从⽽⽤变量值去影响程序的运⾏。

接下来我们来学习配置与变量绑定的两种⽅法。

⼀、使⽤@Value 获取配置值

通过 @Value 注解将 family.family-name 属性的值绑定到 familyName 成员变量上⾯。

@Data
@Component
public class Family {
    @Value("${family.family-name}")
    private String familyName;
}

就是这么简单,完了!

⼆、使⽤@ConfigurationProperties 获取配置值

下⾯是⽤于接收上⼀节中 yml 配置的 Java 实体类,看看你⾃⼰能不能根据 yml 的嵌套结构,写出来对应的 Java 实体类:

//   1. ⼀个家庭有爸爸、妈妈、孩⼦。
//   2. 这个家庭有⼀个名字(family-name)叫做“happy family”
@Data
@Component
@ConfigurationProperties(prefix = "family")   //表示配置的整体前缀
public class Family {
    //成员变量名称要和yml配置项key⼀⼀对应
    private String familyName;
    private Father father;
    private Mother mother;
    private Child child;
}

// 3. 爸爸有名字(name)和年龄(age)两个属性
@Data
public class Father {
    private String name;
    private Integer age;
}

// 4. 妈妈有两个别名
@Data
public class Mother {
    private String[] alias;
}

//5. 孩⼦除了名字(name)和年龄(age)两个属性,还有⼀个friends的集合
@Data
public class Child {
    private String name;
    private Integer age;
    private List<Friend> friends;
}

// 6. 每个friend有两个属性:hobby(爱好)和性别(gender)
@Data
public class Friend {
    private String hobby;
    private String gender;
}

三、测试⽤例

写⼀个测试⽤例测⼀下,看看 yml 配置属性是否真的绑定到了类对象的成员变量上⾯。

// @RunWith(SpringRunner.class) Junit4
@ExtendWith(SpringExtension.class)  //Junit5
@SpringBootTest
public class CustomYamlTest {

    @Autowired
     Family family;

    @Test
    public void hello(){
        System.out.println(family.toString());
   }
}

测试结果,不能有为 null 的输出字段,如果有表示你的 Java 实体数据结构写得不正确:

Family(familyName=happy family, father=Father(name=tom, age=38),
mother=Mother(alias=[rose, alice]),
child=Child(name=jack, age=5, friends=[Friend(hobby=football,
gender=male), Friend(hobby=sing, gender=female)]))

四、⽐较⼀下⼆者

@ConfigurationProperties @Value
功能 批量注⼊属性到 java 类 ⼀个个属性指定注⼊
松散语法绑定 ⽀持 不⽀持
复杂数据类型(对象、数组) ⽀持 不⽀持
JSR303 数据校验 ⽀持 不⽀持
SpEL 不⽀持 ⽀持

数据校验和 SPEL 的内容,我们后⾯学习。

5. 配置属性值数据绑定校验

⼀、为什么要对配置属性值校验

配置⽂件是需要开发⼈员⼿动来修改的,只要是⼈为参与就会有出错的可能。为了避免⼈为配置出错的可能,我们需要对配置属性值做校验。

⽐如:

  • 针对数据库密码配置:需要限定最⼩⻓度或者复杂度限制
  • 针对系统对外发邮件,邮件发送⽅的邮箱地址配置:字符串配置要符合⼀定的邮件正则表达式规则
  • 针对某些不能为空的配置:开发⼈员有可能忘了为它赋值等等场景

我们不能等到程序上线之后,才发现相关的配置错误。所以通常对配置属性与类对象的成员变量绑定的时候,就要加上⼀些校验规则。如果配置值不符合校验规则,在应⽤程序在启动的时候就会抛出异常。

⼆、如何对绑定的属性值进⾏校验

我们希望对之前定义的 family 类⾥⾯爸爸的年龄进⾏校验。让其不能⼩于 22 岁,否则就是错误配置。那我们该怎么做呢?

在需要校验的属性装配类上加@Validated 注解

@Data
@Component
@Validated
@ConfigurationProperties(prefix = "family")
public class Family {
  • 校验⽗亲的年龄,必须⼤于 22 岁
public class Father {
    private String name;
    @Range(min = 22, message = "必须年满22岁!")
    private Integer age;
}
  • 校验 familyName ,⻓度必须在指定范围内
@Length(min = 5, max = 20, message = "家庭名⻓度必须位于5到20之间")
private String familyName;

这些校验规则注解是在 JSR 303(java) 规范中定义的,但是 JSR 303 只是⼀个规范,⽬前通常都是使⽤ hibernate-validator 进⾏统⼀参数校验,hibernate-validator 是对 JSR 303 规范的实现。

注意:当你使⽤注解的时候,如果 org.hibernate.validator.constraints 包和 javax.validation.constraints 包同时存在某个校验注解,要 import 使⽤ org.hibernate.validator.constraints 包。

<dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-validator</artifactId>
   <version>6.2.0.Final</version>
</dependency>

在之前的 SpringBoot 版本中,hibernate-validator 是作为默认引⼊的 Web 开发的集成 package, 但是在新版的 SpringBoot 已经不是默认引⼊的了,所以需要通过上⾯的 maven 坐标单独引⼊。

三、校验失败异常

如果我们修改 family.father.age=18 ,也就是说不满⾜最⼩值是 22 的这样⼀个校验规则。 校验失败,会有如下异常。

四、附录说明

Hibernate Validator 是 Bean Validation 的参考实现,它提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有⼀些附加的 constraint。

在⽇常开发中,Hibernate Validator 经常⽤来验证 bean 的字段,基于注解,⽅便快捷⾼效。

1. Bean Validation 中内置的 constraint

注解 作⽤
@Valid 被注释的元素是⼀个对象,需要检查此对象的所有字段值
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是⼀个数字,其值必须⼤于等于指定的最⼩值
@Max(value) 被注释的元素必须是⼀个数字,其值必须⼩于等于指定的最⼤值
@DecimalMin(value) 被注释的元素必须是⼀个数字,其值必须⼤于等于指定的最⼩值
@DecimalMax(value) 被注释的元素必须是⼀个数字,其值必须⼩于等于指定的最⼤值
@Size(max, min) 被注释的元素的⼤⼩必须在指定的范围内
@Digits (integer, fraction) 被注释的元素必须是⼀个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是⼀个过去的⽇期
@Future 被注释的元素必须是⼀个将来的⽇期
@Pattern(value) 被注释的元素必须符合指定的正则表达式
@Email 被注释的元素必须是电⼦邮箱地址

2. Hibernate Validator 附加的 constraint

注释 作用
@NotBlank 被注释的字符串的必须⾮空
@NotEmpty 被注释的字符串的必须⾮空
@Length(min=, max=) 被注释的字符串的⼤⼩必须在指定的范围内
@Range(min=, max=) 被注释的元素必须在合适的范围内
@URL(protocol=,host=, port=, regexp=, flags=) 被注释的字符串必须是⼀个有效的 url
@CreditCardNumber 被注释的字符串必须通过 Luhn 校验算法,银⾏卡,信⽤卡等号码⼀般都⽤ Luhn 计算合法性
@ScriptAssert(lang=, script=, alias=) 要有 Java Scripting API 即 JSR 223 (“Scripting for the JavaTM Platform”)的实现
@SafeHtml(whitelistType=, additionalTags=) classpath 中要有 jsoup 包

hibernate 补充的注解中,最后 3 个不常⽤,可忽略。

注意区分⼀下 @NotNull @NotEmpty @NotBlank 3 个注解的区别:

@NotNull 任何对象的 value 不能为 null

@NotEmpty 集合对象的元素不为 0,即集合不为空,也可以⽤于字符串不为 null

@NotBlank 只能⽤于字符串不为 null ,并且字符串 trim() 以后 length 要⼤于 0

注意:HTTP 请求的校验,需要在 Controller 类头部加上 @Validated 注解

6. 加载额外配置⽂件的两种⽅式

⼀、为什么要加载额外配置⽂件

有⼀些⽼的项⽬⾥⾯的 jar 包并未主动去与 SpringBoot 融合,很多 jar 包都有⾃⼰的配置⽂件。如 果我们在 SpringBoot 项⽬中使⽤这些 jar 包就必须得使⽤它们的配置⽂件,那就⾯临⼀个问题: SpringBoot 项⽬默认只有⼀个全局配置⽂件:application.yml 或 application.properties 。该 如何加载额外的配置⽂件?

⼆、使⽤@PropertySource 加载⾃定义 yml 或 properties ⽂件

1. properties 配置⽂件加载

resouces ⽬录新建 family.properties

在线 yml 和 properties 互转

将之前的 family 相关的 yml 内容粘贴,转换后的内容

family.family-name=happy family
family.father.name=tom
family.father.age=38
family.mother.alias[0]=rose
family.mother.alias[1]=alice
family.child.name=jack
family.child.age=${random.int(5,16)}
family.child.friends[0].hobby=play
family.child.friends[0].gender=male
family.child.friends[1].hobby=sing
family.child.friends[1].gender=female

family.properties 这种格式的配置⽂件,在之前的代码基础之上,加上如下的注解就可以将⽂件中的配置属性进⾏加载。

注意在 application.yml 中把 family 相关的配置删掉

@Data
@Component
@ConfigurationProperties(prefix = "family")
@Validated
@PropertySource(value = {"classpath:family.properties"})
public class Family {

}

⼀样可以测试通过

2. yaml 配置⽂件加载

spring 官⽅⽂档明确说明不⽀持使⽤@PropertySource 加载 YAML 配置⽂件

  • 新建⼀个配置⽂件 family.yml,⽤来模拟第三⽅ jar 包的额外配置⽂件(⾮ application 配置⽂件)。
family:
  family-name: happy family
  father:
    age: 38
    name: tom
  mother:
    alias:
      - rose
      - alice
  child:
    name: jack
    age: 6
    friends:
      - gender: male
        hobby: play
      - gender: female
        hobby: sing
  • DefaultPropertySourceFactory 是进⾏配置⽂件加载的⼯⼚类。
  • 尽管其默认不⽀持读取 YAML 格式外部配置⽂件,但是我们可以通过继承 DefaultPropertySourceFactory ,然后对它的 createPropertySource 进⾏⼀下改造。就可以 实现 YAML 的“额外”配置⽂件加载。
public class MixPropertySourceFactory extends
DefaultPropertySourceFactory {
  @Override
  public PropertySource<?> createPropertySource(@Nullable String name, EncodedResource resource) throws IOException {
    String sourceName = name != null ? name : resource.getResource().getFilename();
    if (sourceName != null
          &&(sourceName.endsWith(".yml") || sourceName.endsWith(".yaml"))) {
      Properties propertiesFromYaml = loadYml(resource);
      //将YML配置转成Properties之后,再⽤PropertiesPropertySource绑定
      return new PropertiesPropertySource(sourceName, propertiesFromYaml);
   } else {
      return super.createPropertySource(name, resource);
   }
 }

  //将YML格式的配置转成Properties配置
  private Properties loadYml(EncodedResource resource) throws IOException{
    YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
    factory.setResources(resource.getResource());
    factory.afterPropertiesSet();
    return factory.getObject();
  }
}

3. 练习:仿做⾃定义 User、Book 的属性绑定校验

三、使⽤@ImportResource 加载 Spring 的 xml 配置⽂件

新建⼀个类,top.syhan.boot.config.service.TestBeanService

@Data
public class TestBeanService {
    private String name;
}

在没有 Spring 注解的时代,spring 的相关配置都是通过 xml 来完成的。

resource ⽬录新建 beans.xml

下⾯的 XML 配置的含义是:将 top.syhan.boot.config.service.TestBeanService ⼀个带属性值的实例注⼊到 Spring 上下⽂环境中。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
     <bean id="testBeanService" class="top.syhan.boot.config.service.TestBeanService">
        <property name="name" value="SpringBoot"/>
    </bean>
</beans>
  • 测试⽤例,测试 Spring 上下⽂环境中是否有 testBeanService 这样⼀个 bean,有的话表示 xml 配置⽂件已经⽣效,成功将 testBeanService 实例化并注⼊到 Spring 上下⽂环境中:
@Slf4j
@SpringBootTest
@ExtendWith(SpringExtension.class)
class TestBeanServiceTest {
    //注⼊Spring上下⽂环境
    @Resource
    private ConfigurableApplicationContext ioc;
    @Test
    public void testLoadService() {
        //测试Spring上下⽂环境中是否有testBeanService这样⼀个bean,有的话表示xml配置⽂件⽣效
        boolean flag = ioc.containsBean("testBeanService");
        assertTrue(flag);
        TestBeanService testBeanService = (TestBeanService) ioc.getBean("testBeanService");
        log.info(String.valueOf(testBeanService));
        assertEquals("SpringBoot", testBeanService.getName());
   }
}
  • 因为还没使⽤ @ImportResource 加载 beans.xml,此时执⾏测试⽤例会失败,表示 beans.xml 配置⽂件并未加载,所以没有 testBeanService 的存在。

  • 在 spring boot 应⽤⼊⼝启动类上加上注解

@ImportResource(locations = {"classpath:beans.xml"}) ,该注解⽤来加载 Spring XML 配置⽂件。

此时再测,测试可以通过,beans.xml 配置⽂件被正确加载,并输出了 testBeanService 实例的信息。

7. 使⽤ SpEL 表达式绑定配置项

Spring Expression Language (SpEL) 是⼀种功能⾮常强⼤的表达式语⾔,可⽤于在运⾏时查询和操作对象。 SpEL 书写在 XML 配置⽂件或者 Annotation 注解上,在 Spring Bean 的创建过程中 ⽣效。

SpEL 能⽤在很多的场景下,现在给⼤家介绍⼀下在 Spring Boot 中如何使⽤ SpEL 表达式读取配置属性。

⼀、使⽤ SpEL 表达式绑定字符串集合

创建⼀个配置⽂件 employee.properties,内容如下:

employee.names=james,curry,zhangsan,姚明
employee.type=教练,球员,经理
employee.age={one:'27', two : '35', three : '34', four: '26'}
  • 上⽂中 names 和 type 属性分别代表雇员 employee 的名字和分类,是字符串类型属性
  • age 属性代表雇员的年龄,是⼀组键值对、类对象数据结构

创建⼀个配置类 Employee ,代码如下:

@Data
@Configuration
@PropertySource (name = "employeeProperties",
        value = "classpath:employee.properties",
        encoding = "utf-8")
public class Employee {
     /**
     * 使⽤SpEL读取employee.properties配置⽂件
     */
    @Value("#{'${employee.names}'.split(',')}")
    private List<String> employeeNames;
}
  • @Value 注解和 @PropertySource 注解参考前⾯的学习内容。

⼆、测试⽤例

使⽤如下测试⽤例,将属性值绑定到 Employee 类对象上,并将其打印

@ExtendWith(SpringExtension.class)
//@RunWith(SpringRunner.class)   //Junit4开发者使⽤这个注解
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class ValueBindTests {
    @Resource
    Employee employee;
    @Test
    public void valueBindTests2() throws Exception {
        System.out.println(employee.toString());
   }
}

输出结果如图所示,说明使⽤ SpEL 读取 employee.properties 配置⽂件,并绑定属性值到 Employee 对象⽣效。

上⾯的例⼦中,我们使⽤ SpEL 表达式读取了 employee.names 属性,并将其从字符串属性,以逗号为分隔符转换为 List 类型。属性值注⼊完成之后,employeeNames=[james, curry, zhangsan, 姚明]

三、SpEL 结合@Value 注解读取配置⽂件属性–更多示例

  • 假如我们需要获取第⼀位(数组下标从 0 开始)雇员的姓名,可以使⽤如下的 SpEL 表达式:
@Value ("#{'${employee.names}'.split(',')[0]}")
private String firstEmployeeName;

属性值注⼊完成之后,firstEmployeeName=‘’james‘’

  • 我们还可以使⽤ @Value 注解将键值对、类对象的数据结构转换为 Java 的 Map 数据类型
@Value ("#{${employee.age}}")
private Map<String, Integer> employeeAge;

属性值注⼊完成之后,employeeAge={one=27, two=35, three=34, four=26}

  • 假如我们需要根据 Map 的 Key 获取 Value 属性值,可以使⽤如下的 SpEL 表达式:
@Value ("#{${employee.age}.two}")
// @Value ("#{${employee.age}['two']}") //这样写也可以
private String employeeAgeTwo;

属性值注⼊完成之后,employeeAgeTwo=35

  • 如果我们不确定,Map 中的某个 key 是否存在,可以使⽤如下的 SpEL 表达式。如果 key 存在就获取对应的 value ,如果不存在就获得默认值 30。
@Value("#{${employee.age}['three'] != null ? ${employee.age} ['three']: 30}")
private Integer ageWithDefaultValue;

属性值注⼊完成之后,ageWithDefaultValue=31

四、SpEL 结合 @Value 注解读取系统环境变量

可以使⽤ SpEL 表达式读取系统环境变量,示例:获取 JAVA_HOME ⽬录:

@Value ("#{systemProperties['java.home']}")
private String javaHome;

同理,可以获取系统⽤户⼯作⽬录

@Value ("#{systemProperties['user.dir']}")
private String userDir;

运⾏结果:

当然,除了以上在 Spring Boot 中使⽤ SpEL 的常⽤⽤法,SpEL 还可以完成算术运算、逻辑运算、 正则匹配运算、条件运算等功能,建议⼤家参照官⽅⽂档学习。

更多内容可以参考:https://docs.spring.io/spring/docs/4.3.10.RELEASE/springframework-reference/html/expressions.html

五、读取 properties ⽂件中⽂乱码问题的解决

File->settings->File Encoding->图所示选项及勾选

使⽤ PropertySource 注解时指定 encoding

8. profile 不同环境使⽤不同配置

⼀、配置⽂件规划

我们开发的服务通常会部署在不同的环境中,例如开发环境、测试环境,⽣产环境等,⽽不同环境需要不同的配置。

最典型的场景就是在不同的环境下需要连接不同的数据库,需要使⽤不同的数据库配置。 我们期待实现的配置效果是:

  • 减少配置修改次数
  • ⽅便环境配置切换

SpringBoot 默认的配置⽂件是 application.properties(或 yml)。那么如何实现不同的环境使⽤不同的配置⽂件呢?⽐较好的做法是为不同的环境定义不同的配置⽂件

全局配置⽂件:application.yml

开发环境配置⽂件:application-dev.yml

测试环境配置⽂件:application-test.yml

⽣产环境配置⽂件:application-prod.yml

⼆、切换环境的⽅式

1. 通过配置 application.yml

application.yml 是默认使⽤的配置⽂件,在其中通过 spring.profiles.active 设置使⽤哪⼀个配置⽂件,下⾯代码表示使⽤ application-prod.yml 配置,如果 application-prod.yml 和 application.yml 配置了相同的配置,⽐如都配置了运⾏端⼝,那 application-prod.yml 的优先级更⾼。

#需要使⽤的配置⽂件
spring:
  profiles:
    active: prod

2. VM options、Program arguments、Active Profile

  • VM options 设置启动参数 -Dspring.profiles.active=prod
  • Program arguments 设置 –spring.profiles.active=prod
  • Active Profile 设置 prod

这三个参数不要⼀起设置,会引起冲突,选⼀种即可,如下图

3. 命令⾏⽅式

将项⽬打成 jar 包,在 jar 包的⽬录下打开命令⾏,使⽤如下命令启动:

java -jar spring-boot-profile.jar --spring.profiles.active=prod

关于 Spring Profiles 更多信息可以参⻅:Spring Profiles

9. 配置及配置⽂件的加载优先级

⼀、全局配置⽂件加载优先级

SpringBoot 启动会扫描以下位置的 application.properties 或者 application.yml ⽂件作为默认配置⽂件,数值越⼩的标号优先级越⾼。

  1. file:./config/ (当前项⽬路径 config ⽬录下);
  2. file:./ (当前项⽬路径下);
  3. classpath:/config/ (类路径 config ⽬录下);
  4. classpath:/ (类路径下).

按照优先级从⾼到低的顺序,所有位置的⽂件都会被加载,⾼优先级配置内容会覆盖低优先级配置内容

SpringBoot 会从这四个位置全部加载主配置⽂件,如果⾼优先级中配置⽂件属性与低优先级配置⽂件不冲突的属性,则会共同存在—互补配置。

假如我们在上⾯的四个配置⽂件分别设置 server.port=6666、7777、8888、9999 ,然后启动应⽤,最终的启动端⼝为 6666,因为 file:./config/ (当前项⽬路径 config ⽬录下配置⽂件)优先级是最⾼的。

⾃定义改变全局配置⽂件的加载位置:(优先级最⾼)

我们也可以通过配置 spring.config.location 来改变默认配置。

java -jar boot-launch-1.0.jar --spring.config.location=/Users/apple/Desktop/application.yml

项⽬打包好以后,我们可以使⽤命令⾏参数的形式,在启动项⽬的时候来指定配置⽂件的位置。

打包的时候遇到测试代码问题失败的解决⽅案

有时候可能在你的测试代码中存在⼀些问题,默认是扫描测试代码打包的,那就会出现打包错误的现象。

解决⽅法

1、⽤ maven 命令打包,跳过测试

mvn package -Dmaven.test.skip=true

2、pom 中添加配置

<properties>
  <java.version>17</java.version>
  <skipTests>true</skipTests>
</properties>

3、IDEA 设置中指定

⼆、配置加载优先级

SpringBoot 也可以从以下位置加载配置:优先级从⾼到低;⾼优先级的配置覆盖低优先级的配置, 所有的配置会形成互补配置。

  1. 命令⾏参数
  2. 来⾃ java:comp/env 的 JNDI 属性
  3. Java 系统属性(System.getProperties())
  4. 操作系统环境变量
  5. RandomValuePropertySource 配置的 random.* 属性值
  6. jar 包外部的 application-{profile}.properties 或 application.yml(带 spring.profile) 配置⽂件
  7. jar 包内部的 application-{profile}.properties 或 application.yml(带 spring.profile) 配置⽂件
  8. jar 包外部的 application.properties 或 application.yml(不带 spring.profile) 配置⽂件
  9. jar 包内部的 application.properties 或 application.yml(不带 spring.profile) 配置⽂件
  10. @Configuration 注解类上的 @PropertySource
  11. 通过 SpringApplication.setDefaultProperties 指定的默认属性

关于配置的优先级不⽤特别去记忆,⽤到的时候查⼀下即可。

⼀般来说:特殊指定配置(命令⾏、环境变量)⼤于通⽤配置、外部配置优先级⾼于内部配置、局部 环境配置(带 profile)⼤于全局普适性配置。

参考:官⽅⽂档,获得更多关于配置优先级的内容

10. 配置⽂件敏感字段加密

⼀、说明

使⽤过 SpringBoot 配置⽂件的⼈都知道,资源⽂件中的内容通常情况下是明⽂显示,安全性就⽐较差⼀些。

打开 application.properties 或 application.yml ,⽐如 MySql 登陆密码,Redis 登陆密码以及第三⽅的密钥等等⼀览⽆余,这⾥介绍⼀个加解密组件,提⾼⼀些属性配置的安全性。

jasypt 是由⼀个国外⼤神写的⼀个 SpringBoot 的⼯具包,⽤来加密配置⽂件中的信息。

GitHub Demo 地址:https://github.com/jeikerxiao/spring-boot2/tree/master/springboot-encrypt

⼆、数据⽤户名和数据库密码加密为例

1. 引⼊包

查看最新版本可以到:https://github.com/ulisesbocchio/jasypt-spring-boot

<dependency>
  <groupId>com.github.ulisesbocchio</groupId>
  <artifactId>jasypt-spring-boot-starter</artifactId>
  <version>3.0.4</version>
</dependency>

2. 配置加密解密的秘钥

application.yml

# jasypt加密的密匙
jasypt:
  encryptor:
    password: Y6M9fAJQdU7jNp5MW

3. 写⼀个单元测试,⽤来⽣成加密后的秘钥

package top.syhan.boot.service;
import lombok.extern.slf4j.Slf4j;
import org.jasypt.encryption.StringEncryptor;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* @description:
* @author: syhan
* @date: 2022-03-14
**/
@Slf4j
@SpringBootTest
@ExtendWith(SpringExtension.class)
public class EncryptorTest {
    @Autowired
    private StringEncryptor encryptor;
    @Test
    public void getEncryptor() {
        String result = encryptor.encrypt("happy family");
        log.info(result);
   }
}

下⾯是输出的加密字符串:

在 properties 或 yml ⽂件中需要对明⽂进⾏加密的地⽅使⽤ ENC() 包裹,如原值:”happy family”, 加密后使⽤ ENC(密⽂) 替换。程序中像往常⼀样使⽤ @Value(“${}”) 获取该配置即可,获取的是解密之后的明⽂值。

⽂本被加密之后,我们需要告知 SpringBoot 该如何解密,因为 SpringBoot 要读取该配置的明⽂内容。在 application.properties 或 yml ⽂件中,做如下配置:

# 设置盐值(加密解密密钥),我们配置在这⾥只是为了测试⽅便
# ⽣产环境中,切记不要这样直接进⾏设置,可通过环境变量、命令⾏等形式进⾏设置。下⽂会讲
jasypt:
encryptor:
  password: 123456

运⾏单元测试,打印 family 对象

三、“密钥”与配置⽂件分开存放

本身加解密过程都是通过盐值进⾏处理的,所以正常情况下盐值和加密串是分开存储的。出于安全考量,盐值应该放在系统属性、命令⾏或是环境变量来使⽤,⽽不是放在同⼀个配置⽂件⾥⾯

1. 命令⾏存储⽅式示例

java -jar xxx.jar --jasypt.encryptor.password=xxx &;

2. 环境变量存储⽅式示例

设置环境变量(Linux)

# 打开/etc/profile⽂件
vim /etc/profile
# ⽂件末尾插⼊
export JASYPT_PASSWORD = xxxx

启动命令

java -jar xxx.jar --jasypt.encryptor.password=${JASYPT_PASSWORD} &;

文章作者: Syhan
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Syhan !
评论
  目录