在前面的课程中,我们给大家分享过SpringBoot精妙的启动配置,主要阐述的是spring的IoC容器在SpringBoot中的加载过程,并与传统项目中Spring的IoC容器加载过程进行了一个对比.我们在开发的过程中,除了IoC容器的配置之外,当然还有许多其他的配置,诸如数据库的链接信息,端口,以及项目的内部使用的一些个性化信息等.那SpringBoot是如何管理这些配置呢?我今天呢,就从以下这三个方面来给大家分享一下SpringBoot是如何管理配置信息的.
配置文件和属性获取
配置文件的名字、目录和优先级
传统的properties与YAML
1.1 传统配置文件的值获取与SpringBoot中的值获取
在传统的项目里,我们的配置信息一般都写在配置文件中,然后关于spring需要的信息,我们就在spring的xml文件里引用,大略如下所示:
classpath下的config里创建一个jdbc.properties
jdbc.driverClassName=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/testdb?serverTimezone=UTC&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true jdbc.username=develop jdbc.password=&dT$BvYdOlH4*m9G
然后在我们的application.xml里引入我们需要的这个数据源:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <context:property-placeholder location="classpath:config/jdbc.properties" /> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driverClassName}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </bean> ... ...其他配置 </beans>
当然除了spring的数据源信息之外,我们往往也会有一些其他的信息,比如项目返回给前端的一些报错信息等,此时我们通常的做法是使用java的原生方法去加载。如以下所示:
在classpath的config下有个webreslt.properties
0=SUCCESS 100000=参数有误 100001=AppKey不能为空 100002=Signature不能为空 100003=参数列表(paramMap)不能为空 100004=secret不能包含在参数列表中 100005=参数签名不合法 100006=手机号不能为空 100007=验证码不能为空 100008=邮件内容不能为空 100009=收件人不能为空 100010=邮件主题不能为空 200000=应用无权限 200001=应用未注册 300000=Api异常 300001=短信发送失败 300002=短信验证失败 300003=邮件发送失败 300004=短信发送超过最大条数限制
获取值的方法如下:
package com.ailu.paas.common.utils; import org.apache.commons.lang3.StringUtils; import java.util.Locale; import java.util.ResourceBundle; public class PropertyFileReader { public static String getItem(String key) { return getItem(key, ""); } public static String getItem(String key, String defaultValue) { ResourceBundle rb = ResourceBundle.getBundle("config/webresult"); String value = ""; try { value = new String(rb.getString(key).getBytes("ISO-8859-1"), "UTF-8"); } catch (Exception e) { e.printStackTrace(); } if (StringUtils.isEmpty(value)) { value = defaultValue; } return value.trim(); } }
然后在其他的地方,我们就可以直接使用以下这样的方式去获取属性文件中的值了.
String value=PropertyFileReader.getItem(key);
而在SpringBoot中呢,我们则可以使用@Component+@Value这两个组合,来快速的读取配置文件中的值,
仍然是在classpath下,我们在配置文件里写上如下配置:
name="Lianmengtu"
然后我们创建一个java类,并加上@Component和@Value,如下所示:
package top.lianmengtu.testprofile.common; import lombok.Getter; import lombok.Setter; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component @Getter @Setter public class TestProperty { @Value("${name}") private String name; }
其中@Component是为了把TestProperty作为组件放入到Spring的IoC容器中,而@Value("${name}")则是为了从配置文件中取值,${name}中的name即是配置文件中的key.然后我们就可以在其他地方通过注入直接使用了:
@Autowired private TestProperty testProperty; public void test(){ System.out.println(testProperty.getName()); }
1.2 随机值的绑定
在某些场景下,我们可能需要在项目的配置中添加一些随机值,并且这些值在我们项目启动后就自动的初始化,而SpringBoot就考虑到了这种情况,于是给我们准备了一些工具,方便我们的使用.使用情况如下:
# 随机字符串 secret=${random.value} #随机数 setup=${random.int} #0-10之间的随机数 range-int=${random.int[0,10]} #生成uuid uuid=${random.uuid}
获取方式和其他的属性相同,如下所示:
package top.lianmengtu.testprofile.common; import lombok.Getter; import lombok.Setter; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component @Getter @Setter public class TestProperty { @Value("${secret}") private String secret; @Value("${setup}") private Integer setup; @Value("${range-int}") private Integer rangeInt; @Value("${uuid}") private String uuid; }
1.3 变量的引用与占位符
有些时候我们会在配置项中引用另一项的值,当然,是以变量的形式进行引用.如下所示:
protocal=http:// domain=${protocal}ask.lianmengtu.top
这里我们看到,在springBoot中使用的变量占位符是${key}.这时候就有一个问题,因为我们现在大多数的开发环境都是maven,我们都知道maven也是支持变量占位的,我们可以在打包不同环境的时候可以激活不同的profile,然后对变量值进行替换,而我们往常在使用Maven的时候,用的变量占位符正好是${key},那此时,我们该怎么办呢?SpringBoot是不是不支持Maven的变量占位呢?我们必须要二选其一吗?
当然不是.SpringBoot也考虑到了这个问题,因此给maven占位提供了另外一个符号即@key@,如下所示:
protocal=http:// domain=${protocal}ask.lianmengtu.top username=@username@ password=@password@
然后我们就可以在我们的pom里这么写了,而当我们激活某一个profile的时候,相应的maven变量就会被替换了
<profiles> <profile> <id>dev</id> <properties> <username>dev</username> <password>dev123</password> </properties> </profile> <profile> <id>test</id> <properties> <username>test</username> <password>test123</password> </properties> </profile> </profiles>
SpringBoot是除了支持properties这种方式之外,它也支持YAML这种方式,并且因为yml结构清晰,又可以继承,所以使用yml这种方式的人也越来越多了.而我就是这么对yml路转粉的.
2.1 yml结构化
yml的第一个好处是结构化,这与properties是明显的差别.这里放上两份同样的配置文件来个视觉对比,首先是properties
environments.dev.url= environments.dev.name=Developer Setup environments.prod.url= environments.prod.name=My Cool App my.servers[0]=dev.example.com my.servers[1]=another.example.com
对比yml
environments: dev: url: http://dev.example.com name: Developer Setup prod: url: http://another.example.com name: My Cool App my: servers: - dev.example.com - another.example.com
这里只是举个例子,所以可能这两三行大家看起来没什么感觉,但要知道在实际的项目中,我们的配置可能包含各种各样的信息,数据库的,缓存的,第三方平台的等等,那时候,如果我们还用properties,那么看着将会非常头大.
2.2 yml的继承
在我们使用配置文件的时候,尤其是分环境使用的时候,常常会碰到这么一个问题: 大多数的项目配置都一样,只有少数的不一样,此时,如果是使用properties,那么我们就只能每个文件各写一份,而在yml里,我们就不用,我们只需要将通用的那部分写到application-common.yml里,然后少量不同的,我们在分环境进行描述,application-dev.yml,application-prod.yml里,这样我们只需要激活一份文件,剩下的就会自动的进行继承和使用了,具体方式如下:
application.yml
app: name: "lianmengtu" country: "China" username: "melon" password: "melon123"
application-dev.yml:
app: username: "jacobdev" password: "dev123456"
application-prod.yml
app: username: "LMTprod" password: "prod456"
这样当我们在启动时,激活不同的配置时,username和password会不同,但name和country则是从默认的yml中继承过来的.
2.3 指定配置文件的名字和地址
刚刚我们提到过多种环境,配置文件之所以要区分环境,就是因为有些信息是需要保密的,无法公开.而SpringBoot则允许从外部,通过命令行的形式,对配置文件进行指定,当然也可以指定变量.
2.3.1 指定配置文件的名字
我们可以使用--spring.config.name=xxx 这样的参数形式指定配置文件的名字:
$ java -jar myproject.jar --spring.config.name=myproject
2.3.2 配置指定目录下的配置文件
我们可以使用--spring.config.location=xxxxx这样的参数形式来配置指定目录下的配置文件,如下文则指定了classpath下的config目录下的test.yml
java -jar myproject.jar --spring.config.location=classpath:/config/test.yml
当然从1.x转过来的人可能更喜欢指定目录,这里要特别说明一下,如果--spring.config.location是以目录结尾的,则必须加/ 如下所示:
java -jar myproject.jar --spring.config.location=classpath:/config/
2.3.3 配置的优先级
在我们没有明确指定文件名字的时候,springBoot会按着以下顺序进行考虑
当前目录下的config目录里的application.properties
当前目录下的application.properties
classpath下的config目录下的application.properties
classpath下的application.properties
当然除了这些之外,命令行也是可以传参数的,并且命令行参数的优先级是最高的.
使用@Value()来进行属性注入有些时候会显得比较笨重,尤其是使用多个配置或者我们的配置项是呈垂直结构化的数据时,更是这样.SpringBoot提供了另外一种方法来处理这类比较复杂的数据.这就是我们要说的@ConfigurationProperties
.
首先我们有这样一个配置文件:
app: name: "lianmengtu" enabled: false security: username: "jacob" password: "jacob123" roles: - USER - ADMIN
我们看到在这个配置文件里,app下有name属性,有enabled属性,其中较为特殊的是security,因为他还包含了些其他的属性,包括username,包括password,还有一个类型为String的roles集合,那么此时,我们可以对应的写成下面这个属性类
package top.lianmengtu.testprofile.common; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Collections; import java.util.List; @ConfigurationProperties("app") public class ComplexProperty { private String name; private boolean enabled; private final Security security=new Security(); public String getName() { return name; } public void setName(String name) { this.name = name; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public Security getSecurity() { return security; } public static class Security { private String username; private String password; private List<String> roles =new ArrayList<>(Collections.singleton("USER")); public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public List<String> getRoles() { return roles; } public void setRoles(List<String> roles) { this.roles = roles; } } }
之后,我们可以在我们的service层引用它:
package top.lianmengtu.testprofile.service.impl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import top.lianmengtu.testprofile.common.ComplexProperty; import java.util.List; @Service public class PropertyService { private final ComplexProperty complexProperty; @Autowired public PropertyService(ComplexProperty complexProperty){ this.complexProperty=complexProperty; } public String name(){ return complexProperty.getName(); } public boolean enabled(){ return complexProperty.isEnabled(); } public String userName(){ return complexProperty.getSecurity().getUsername(); } public String password(){ return complexProperty.getSecurity().getPassword(); } public String roles(){ StringBuffer roles=new StringBuffer(); List<String> roleArray=complexProperty.getSecurity().getRoles(); roleArray.forEach(role->{ roles.append(role).append("----"); }); return roles.toString(); } }
这里的构造函数注入,我们也可以换成相应的@Autowried注入.为了使这个配置生效,我们需要在Application上加上@EnableConfigurationProperties(ComplexProperty.class)
package top.lianmengtu.testprofile; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import top.lianmengtu.testprofile.common.ComplexProperty; @SpringBootApplication @EnableConfigurationProperties(ComplexProperty.class) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class,args); } }
刚刚举的那种类型可以说是比较复杂的一个类型了,除了那种内嵌的形式之外,如果说我们只想得到内嵌类里面的属性或者说只有内嵌类里面有属性,则我们可以写成以下这种形式,其他的地方都不用变.这种形式我们称之为relaxed binding
@ConfigurationProperties("app.security") public class ComplexProperty { private String username; private String password; private List<String> roles; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public List<String> getRoles() { return roles; } public void setRoles(List<String> roles) { this.roles = roles; } }
并且也支持map类型,配置文件如下所示,其中key可以有两种指定方式,一种是"[/key]",一种是/key:
app: name: "lianmengtu" enabled: false security: username: "jacob" password: "jacob123" roles: - USER - ADMIN work: "[/position]": "CEO" "[/company]": "bat" /address: "BeiJing"
而我们的配置类则如下所示:
package top.lianmengtu.testprofile.common; import org.springframework.boot.context.properties.ConfigurationProperties; import java.util.List; import java.util.Map; @ConfigurationProperties("app.security") public class ComplexProperty { private String username; private String password; private List<String> roles; private Map<String,String> work; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public List<String> getRoles() { return roles; } public void setRoles(List<String> roles) { this.roles = roles; } public Map<String, String> getWork() { return work; } public void setWork(Map<String, String> work) { this.work = work; } }
这两种方式都是Ok的,
总结今天的内容,其实到这里就已经结束了,在使用配置文件的过程中,我们还碰到了一些问题,首先,我们在使用这种配置方式,无论是@Component+@Value还是后来的@ConfigurationProperties这两种方式,他都是在进入spring的时候进行初始化的,这也就意味着,如果我们没有从Spring的容器中去取我们的属性容器的话,那么我们的属性值是没有办法注入的,这一点希望大家能够注意,其次,今天只是讲了主要的几种方式,还有一些像复杂类型的属性合并,以及属性验证,这些希望大家可以研究一下,如果有不明白的,欢迎大家在论坛上提出来,我们可以一起探讨.以下是@ConfigurationProperties和@Value的一些对比:
Feature | @ConfigurationProperties | @Value |
---|---|---|
Yes | No | |
Yes | No | |
| No | Yes |