我们之前为大家介绍了,Spring Boot里面的各种Bean(类对象)能够实现自动装载,自动的装载帮我们减少了XML的配置,和手动编码进行Bean的加载工作。从而极大程度上帮我们减少了配置量和代码量
要实现Bean的自动装载,需要解决两个问题
SpringBoot使用一个全局的配置文件,配置文件名是固定的;
全局配置文件的作用:修改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
文件里面究竟写了什么类?这些类是做什么的?就是我们下一步要探究的问题了。
SpringBoot入口启动类使用了SpringBootApplication,实际上就是开启了自动配置功能@EnableAutoConfiguration。
SpringFactoriesLoader会以@EnableAutoConfiguration的包名和类名org.springframework.boot.autoconfigure.EnableAutoConfiguration为Key查找spring.factories文件,并将value中的类名实例化加载到Spring Boot应用中。如下图:
spring.factories文件中的每一行都是一个自动装配类。
每一个自动配置类进行自动配置功能(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数据结构
首先我们提出这样一个需求:
# 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}
name: “zhangsan \n lisi”:输出:zhangsan 换行 lisi
name: ‘zhangsan \n lisi’:输出:zhangsan \n lisi
在spring Boot应用中YAML数据格式支持松散的绑定语法,也就是下面的三种key都是一样的。
family-name = familyName = family_name
但是不绝对,笔者印象中曾经遇到过某些写法不被兼容的情况。我通常使用中划线分隔的这种语法。
Spring Boot配置文件支持占位符,一些用法如下:为persopn.age设置一个随机数
person:
age: ${random.int}
占位符获取之前配置的值,如果没有可以是用“冒号”指定默认值
格式例如,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
通过@Value注解将family.family-name属性的值绑定到familyName成员变量上面。
@Data
@Component
public class Family {
@Value("${family.family-name}")
private String familyName;
}
下面是用于接收上一节中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 {
public class Father {
private String name;
@Min(21)
private Integer age;
}
@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的这样一个校验规则
实际上这些校验注解不仅可以校验配置属性值,也可以校验HTTP请求参数值,我们后面的章节会为大家再次介绍
官方JSR 303规范(国外网址,国内访问比较慢,需要耐心等)
family.properties
这种格式的配置文件,在之前章节的代码基础之上,使用如下的注解就可以将文件中的配置属性进行加载,非常简单!
@PropertySource(value = {"classpath:family.properties"})
public class Family {
然后配合@Value注解,进行注入即可
也可以配合@ConfigurationProperties将properties中指定前缀的值和当前类进行绑定
Spring高级之注解@PropertySource详解(超详细)
spring 官方文档明确说明不支持使用@PropertySource加载YAML配置文件,但是我们仍然有办法,跟着我继续。
# 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
是进行配置文件加载的工厂类。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();
}
}
@PropertySource(value = {"classpath:family.yml"}, factory = MixPropertySourceFactory.class)
public class Family {
在没有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>
@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配置文件被正确加载。
创建一个配置文件employee.properties,内容如下:
employee.names=james,curry,zimug,姚明
employee.type=教练,球员,经理
employee.age={one:'27', two : '35', three : '34', four: '26'}
创建一个配置类 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, 姚明
@Value ("#{'${employee.names}'.split(',')[0]}")
private String firstEmployeeName;
属性值注入完成之后,firstEmployeeName=‘’james‘’
@Value ("#{${employee.age}}")
private Map<String, Integer> employeeAge;
属性值注入完成之后,employeeAge={one=27, two=35, three=34, four=26}
@Value ("#{${employee.age}.two}")
// @Value ("#{${employee.age}['two']}") //这样写也可以
private String employeeAgeTwo;
属性值注入完成之后,employeeAgeTwo=35
@Value ("#{${employee.age}['five'] ?: 31}")
private Integer ageWithDefaultValue;
属性值注入完成之后,ageWithDefaultValue=31
还可以使用SpEL表达式读取系统环境变量,示例如下,获取JAVA_HOME目录:
@Value ("#{systemProperties['java.home']}")
private String javaHome;
同理,可以获取系统用户工作目录
@Value ("#{systemProperties['user.dir']}")
private String userDir;
当然,除了以上在Spring Boot中使用SpEL的常用用法,SpEL还可以完成算术运算、逻辑运算、正则匹配运算、条件运算等功能
File->settings->File Encoding->图所示选项及勾选
使用PropertySource注解时指定encoding
我们开发的服务通常会部署在不同的环境中,例如开发环境、测试环境,生产环境等,而不同环境需要不同的配置。最典型的场景就是在不同的环境下需要连接不同的数据库,需要使用不同的数据库配置。我们期待实现的配置效果是:
Spring Boot 默认的配置文件是 application.properties(或yml)。那么如何实现不同的环境使用不同的配置文件呢?一个比较好的实践是为不同的环境定义不同的配置文件,如下所示:
全局配置文件:application.yml
开发环境配置文件:application-dev.yml
测试环境配置文件:application-test.yml
生产环境配置文件:application-prod.yml
application.yml是默认使用的配置文件,在其中通过spring.profiles.active设置使用哪一个配置文件,下面代码表示使用application-prod.yml配置,如果application-prod.yml和application.yml配置了相同的配置,比如都配置了运行端口,那application-prod.yml的优先级更高
#需要使用的配置文件
spring:
profiles:
active: prod
VM options设置启动参数 -Dspring.profiles.active=prod
Program arguments设置 --spring.profiles.active=prod
Active Profile 设置 prod
这三个参数不要一起设置,会引起冲突,选一种即可,如下图
将项目打成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的默认配置文件.数值越小的标号优先级越高。
以上是按照优先级从高到低的顺序,所有位置的文件都会被加载,高优先级配置内容会覆盖低优先级配置内容。
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也可以从以下位置加载配置:优先级从高到低;高优先级的配置覆盖低优先级的配置,所有的配置会形成互补配置
。
其实大家关于配置的优先级不用特别的去记忆。用到的时候查一下、体验一下、一般来说:特殊指定配置(命令行、环境变量)大于通用配置、外部配置优先级高于内部配置、局部环境配置(带profile)大于全局普适性配置
Jasypt是一个Java库,允许开发人员以很简单的方式添加基本加密功能,而无需深入研究加密原理。利用它可以实现高安全性的,基于标准的加密技术,无论是单向和双向加密。加密密码,文本,数字,二进制文件。
说了这么多,我们spring boot 配置管理到底用Jasypt做什么?
为了方便,简单编写了一个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 文件需要和bat脚本放在相同目录下。此包可直接在示例项目中直接下载。
使用示例,双击上面的bat脚本文件,输入待加密内容和密钥,得到加密结果:
注意:相同的盐值(密钥),每次加密的结果是不同的。
首先引入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上面,导致用户信息被泄露的问题。这种加密方式,无非是将密钥与加密结果分开存放,减少个人疏忽导致的意外,增加破解难度。
如果密钥被从内部渗透暴露了,任何加密都是不安全的。就像你的组织内部有离心离德的人,无论你如何加密都不安全,你需要做的是把他找出来干掉,或者防范他加入你的组织!
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有