zl程序教程

您现在的位置是:首页 >  Javascript

当前栏目

重学SpringBoot系列之配置管理

2023-04-18 14:29:34 时间

重学SpringBoot系列之配置管理

Bean自动装载的核心问题

我们之前为大家介绍了,Spring Boot里面的各种Bean(类对象)能够实现自动装载,自动的装载帮我们减少了XML的配置,和手动编码进行Bean的加载工作。从而极大程度上帮我们减少了配置量和代码量

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

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

全局配置文件

SpringBoot使用一个全局的配置文件,配置文件名是固定的;

  • application.properties
  • application.yml

全局配置文件的作用:修改SpringBoot自动配置的默认值,通过配置来影响SpringBoot自动加载行为。


配置加载原理源码解析

所有的Spring Boot应用程序都是以SpringApplication.run()作为应用程序入口的。下面我们来一步一步跟踪一下这个函数。

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
    return run(new Class[]{primarySource}, args);
}

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
    return (new SpringApplication(primarySources)).run(args);
}

run方法传入了SpringApplication对象和一些运行期参数。继续向前跟进,我们发现一个类叫做SpringFactoriesLoader,这里面体现了Spring Boot加载配置文件的核心逻辑。

从上图可以看到:

  • META-INF/spring.factories文件夹下下面加载了spring.factories文件资源
  • 然后读取文件中的ClassName作为值放入Properties

然后通过反射机制,对spring.factories里面的类资源进行实例化,所以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的逻辑及其他
}

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

我们讲的这个实现原理实际上就是一个自定义spring-boot-starter的实现原理,我们会在后面章节中自己编码实现一个分布式文件系统fastdfs与spring boot整合的starter。大家届时会有更深一步的理解。在以上的自动装配过程中依赖于HttpEncodingProperties的自定义属性,我们后面会讲如何读取自定义配置属性。


YAML

YAML语法及占位符语法

设计一个YAML数据结构

首先我们提出这样一个需求:

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

上面的数据结构用yaml该如何表示呢?

family:
  family-name: "happy family"
  father:
    name: dhy
    age: 18
  mother:
    alias:
      - lovely
      - ailice
  child:
    name: xpy
    age: 5
    friends:
      - hobby: football
        sex:  male
      - hobby: basketball
        sex: female

或者是friends的部分写成

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

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

  • 双引号: 会转义字符串里面的特殊字符,如下面 被转义为换行:
​ name: “zhangsan 
 lisi”:输出:zhangsan 换行 lisi
  • 单引号: 不会转义特殊字符,特殊字符最终只是作为一个普通的字符串数据,如:
​ name: ‘zhangsan 
 lisi’:输出:zhangsan 
 lisi

规则2:支持松散的语法

在spring Boot应用中YAML数据格式支持松散的绑定语法,也就是下面的三种key都是一样的。

family-name = familyName  = family_name

但是不绝对,笔者印象中曾经遇到过某些写法不被兼容的情况。我通常使用中划线分隔的这种语法。


配置文件占位符

Spring Boot配置文件支持占位符,一些用法如下:为persopn.age设置一个随机数

person:
    age: ${random.int}

随机数占位符

  • ${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 范围以内的数

默认值

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

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

${xxxxx.yyyy:默认值}

比如为配置father.best属性

father:
    best: ${family.father.name:dhy}

如果family.father.name存在则father.best=${family.father.name},family.father.name这个配置不存在,则取值father.best=dhy


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 {

    private String familyName;   //成员变量名称要和yml配置项key一一对应
    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(爱好)和性别(sex)
@Data
public class Friend {
    private String hobby;
    private String sex;
}

测试用例

写一个测试用例测试一下,看看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=zimug, age=18), 
mother=Mother(alias=[lovely, ailice]), child=Child(name=zimug2, age=5, 
friends=[Friend(hobby=football, sex=male), Friend(hobby=basketball, sex=female)]))

比较一下二者


配置属性值数据绑定校验

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

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

比如:

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

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


如何对绑定的属性值进行校验

比如:我们希望对之前章节定义的family类里面爸爸的年龄,进行校验。让其不能小于21岁,小于21就是不合理的配置,也就是错误配置。那我们该怎么做呢?

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

@Data
@Component
@Validated
@ConfigurationProperties(prefix = "family")
public class Family {
  • 校验父亲的年龄,必须大于21岁
public class Father {
    private String name;
    @Min(21)
    private Integer age;
}
  • 校验familyName,必须不能为空
@NotEmpty
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>5.2.4.Final</version>
</dependency>

在之前的Spring Boot 版本中,hibernate-validator是作为默认引入的web开发的集成package,但是在我最新使用的Spring Boot 2.3.0.RELEASE已经不是默认引入的了,所以需要通过上面的maven坐标单独引入。


当校验失败的时候抛出异常

针对Family的属性校验,只需要写一个测试类,将Family类注入就可以

//随机端口启动
@SpringBootTest(classes = Application.class,webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith({SpringExtension.class})
public class test
{
    //注入启动的端口
    @LocalServerPort
    private Integer port;

    @Resource
    private Family family;

    @Test
    public void test()
    {
        System.out.println("启动的端口:  "+port);
        System.out.println(family);
    }
}

如果我们修改family.father.age=18,也就是说不满足最小值是21的这样一个校验规则


其他参考例子:

  • @size (min=6, max=20, message=“密码长度只能在6-20之间”)
  • @pattern(regexp="[a-za-z0-9._%±]+@[a-za-z0-9.-]+.[a-za-z]{2,4}",message=“请输入正确的邮件格式”)
  • @Length(min = 5, max = 20, message = “用户名长度必须位于5到20之间”)
  • @Email(message = “请输入正确的邮箱”)
  • @NotNull(message = “用户名称不能为空”)
  • @Max(value = 100, message = “年龄不能大于100岁”)
  • @Min(value= 18 ,message= “必须年满18岁!” )
  • @AssertTrue(message = “bln4 must is true”)
  • @AssertFalse(message = “blnf must is falase”)
  • @DecimalMax(value=“100”,message=“decim最大值是100”)
  • @DecimalMin(value=“100”,message=“decim最小值是100”)
  • @NotNull(message = “身份证不能为空”)
  • @Pattern(regexp="^(d{18,18}|d{15,15}|(d{17,17}[x|X]))$",message=“身份证格式错误”)

JSR303校验详细

参考我之前写的文章


附录、常用校验注解

实际上这些校验注解不仅可以校验配置属性值,也可以校验HTTP请求参数值,我们后面的章节会为大家再次介绍

官方JSR 303规范(国外网址,国内访问比较慢,需要耐心等)


加载额外配置文件的两种方式

使用@PropertySource加载自定义yml或properties文件

properties配置文件加载

family.properties这种格式的配置文件,在之前章节的代码基础之上,使用如下的注解就可以将文件中的配置属性进行加载,非常简单!

@PropertySource(value = {"classpath:family.properties"})
public class Family {

然后配合@Value注解,进行注入即可

也可以配合@ConfigurationProperties将properties中指定前缀的值和当前类进行绑定

Spring高级之注解@PropertySource详解(超详细)

@PropertySource配置的用法


spring 官方文档明确说明不支持使用@PropertySource加载YAML配置文件,但是我们仍然有办法,跟着我继续。

  • 我们新建一个配置文件family.yml,把上一节用到的YAML数据结构放里面。用来模拟第三方jar包的额外配置文件(非application配置文件)。
#    1. 一个家庭有爸爸、妈妈、孩子。
#    2. 这个家庭有一个名字(family-name)叫做“happy family”
#    3. 爸爸有名字(name)和年龄(age)两个属性
#    4. 妈妈有两个别名
#    5. 孩子除了名字(name)和年龄(age)两个属性,还有一个friends的集合
#    6. 每个friend有两个属性:hobby(爱好)和性别(sex)

family:
  family-name: "happy family"
  father:
    name: zimug
    age: 18
  mother:
    alias:
      - lovely
      - ailice
  child:
    name: zimug2
    age: 5
    friends:
      - hobby: football
        sex:  male
      - hobby: basketball
        sex: female
  • 笔者通过阅读代码了解到,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();
  }

}
  • 然后基于上一节的代码,在Family类的上面加上如下注解即可。通过factory属性明确的指定使用我们自定义的MixPropertySourceFactory加载yml配置文件。
@PropertySource(value = {"classpath:family.yml"}, factory = MixPropertySourceFactory.class)
public class Family {

使用@ImportResource加载Spring的xml配置文件

在没有Spring注解的时代,spring的相关配置都是通过xml来完成的,如:beans.xml。下面的XML配置的含义是:

将com.dhy.bootlaunch.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="com.dhy.bootlaunch.service.TestBeanService"></bean>
</beans>
  • 新建一个空类,com.dhy.bootlaunch.service.TestBeanService。
  • 测试用例,测试Spring上下文环境中是否有testBeanService这样一个bean,有的话表示xml配置文件已经生效,成功将testBeanService实例化并注入到Spring上下文环境中:
@RunWith(SpringRunner.class)
@SpringBootTest
public class ImportResourceTests {

    @Autowired
    private ConfigurableApplicationContext  ioc;

    @Test
    public void testHelloService() {
        //测试Spring上下文环境中是否有testBeanService这样一个bean,有的话表示xml配置文件生效
        boolean testBeanService= ioc.containsBean("testBeanService");
        System.out.println(testBeanService);
    }
}

因为还没使用@ImportResource加载beans.xml,此时执行测试用例,输出false表示beans.xml配置文件并未加载,所以没有testBeanService的存在

在spring boot应用入口启动类上加@ImportResource(locations = {"classpath:beans.xml"}),该注解用来加载Spring XML配置文件。 此时再试一下测试用例,输出:true。表示beans.xml配置文件被正确加载。


使用SpEL表达式绑定配置项

使用SpEL表达式绑定字符串集合

创建一个配置文件employee.properties,内容如下:

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

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

@Data
@Configuration
@PropertySource(name = "employeeProperties",<H
        value = "classpath:employee.properties",
        encoding = "utf-8")
public class Employee {

    //使用SpEL读取employee.properties配置文件
    @Value("#{'${employee.names}'.split(',')}")
    private List<String> employeeNames;

}

测试用例

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

//随机端口启动
@SpringBootTest(classes = Application.class,webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith({SpringExtension.class})
public class test
{
    @Resource
    Employee employee;

    @Test
    public void valueBindTests2() throws Exception {
        System.out.println(employee.toString());
    }
}

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


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,如果不存在就获得默认值31
@Value ("#{${employee.age}['five'] ?: 31}") 
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还可以完成算术运算、逻辑运算、正则匹配运算、条件运算等功能

官方文档


读取properties文件中文乱码问题的解决

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

使用PropertySource注解时指定encoding


profile不同环境使用不同配置

配置文件规划

我们开发的服务通常会部署在不同的环境中,例如开发环境、测试环境,生产环境等,而不同环境需要不同的配置。最典型的场景就是在不同的环境下需要连接不同的数据库,需要使用不同的数据库配置。我们期待实现的配置效果是:

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

Spring Boot 默认的配置文件是 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。

https://www.baeldung.com/spring-profiles


配置及配置文件的加载优先级

全局配置文件加载优先级

spring boot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件.数值越小的标号优先级越高。

  • file:./config/ (当前项目路径config目录下);
  • file:./ (当前项目路径下);
  • classpath:/config/ (类路径config目录下);
  • 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=D:/application.yml

项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置。


配置加载优先级

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

  • 命令行参数
  • 来自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.yml(不带spring.profile)配置文件
  • @Configuration注解类上的@PropertySource
  • 通过SpringApplication.setDefaultProperties指定的默认属性

其实大家关于配置的优先级不用特别的去记忆。用到的时候查一下、体验一下、一般来说:特殊指定配置(命令行、环境变量)大于通用配置、外部配置优先级高于内部配置、局部环境配置(带profile)大于全局普适性配置

官方文档


配置文件敏感字段加密

Jasypt是什么

官网:http://www.jasypt.org/

Jasypt是一个Java库,允许开发人员以很简单的方式添加基本加密功能,而无需深入研究加密原理。利用它可以实现高安全性的,基于标准的加密技术,无论是单向和双向加密。加密密码,文本,数字,二进制文件。

  • 高安全性的,基于标准的加密技术,无论是单向和双向加密。加密密码,文本,数字,二进制文件…
  • 集成Hibernate的。
  • 可集成到Spring应用程序中,与Spring Security集成。
  • 集成的能力,用于加密的应用程序(即数据源)的配置。
  • 特定功能的高性能加密的multi-processor/multi-core系统。
  • 与任何JCE(Java Cryptography Extension)提供者使用开放的API

说了这么多,我们spring boot 配置管理到底用Jasypt做什么?

  • 出于安全考量,使用“密钥”加密敏感字符串(如数据库密码),并将加密后的字符串保存到配置文件中。
  • spring boot集成Jasypt后实现加密字符串的自动解密配置值,不需要人为参与。当然spring boot需要密钥才能进行解密。
  • “密钥”与配置文件分开存放,分开使用,从而保证应用配置的安全性

使用bat脚本生成加密串

为了方便,简单编写了一个bat脚本方便使用。

@echo off
set/p input=待加密的明文字符串:
set/p password=加密密钥(盐值):
echo 加密中......
java -cp jasypt-1.9.2.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI  ^
input=%input% password=%password% ^
algorithm=PBEWithMD5AndDES
pause
  • 使用jasypt-1.9.2.jar中的org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI类进行加密
  • input参数是待加密的字符串,password参数是加密的密钥(盐值)
  • 使用PBEWithMD5AndDES算法进行加密

注意:jasypt-1.9.2.jar 文件需要和bat脚本放在相同目录下。此包可直接在示例项目中直接下载。


使用示例,双击上面的bat脚本文件,输入待加密内容和密钥,得到加密结果:

注意:相同的盐值(密钥),每次加密的结果是不同的。


Jasypt与spring boot整合

首先引入Jasypt的maven坐标

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

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

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

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

“密钥”与配置文件分开存放

本身加解密过程都是通过盐值进行处理的,所以正常情况下盐值加密串是分开存储的。出于安全考量,盐值应该放在系统属性命令行或是环境变量来使用,而不是放在同一个配置文件里面。

命令行存储方式示例

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

环境变量存储方式示例

设置环境变量(linux):

# 打开/etc/profile文件
vim /etc/profile
# 文件末尾插入
export JASYPT_PASSWORD = xxxx

启动命令:

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

这样真的安全么?

有的同学会问这样的问题:如果的linux主机被攻陷了怎么办,黑客不就知道了密钥?

对于这个问题:我只能这么说,如果你的应用从内部被攻陷,在这个世界上没有一种加密方法是绝对安全的。这种加密方法只能做到:防君子不防小人。大家可能都听说过,某著名互联网公司将明文数据库密码上传到了github上面,导致用户信息被泄露的问题。这种加密方式,无非是将密钥与加密结果分开存放,减少个人疏忽导致的意外,增加破解难度。

如果密钥被从内部渗透暴露了,任何加密都是不安全的。就像你的组织内部有离心离德的人,无论你如何加密都不安全,你需要做的是把他找出来干掉,或者防范他加入你的组织!