今天我們了解SpringBoot Profiles特性
一、外部化配置
配置分為編譯時和運行時,而Spring采用后者,在工作中有時也會兩者一起使用。
何為“外部化配置”官方沒有正面解釋。通常,對於可擴展性應用,尤其是中間件,它們的功能性組件是可配置化的,如線程池配置及數據庫連接信息等。
假設設置Spring應用的Profile為dev,通過 ConfigurableEnvironment#setDefaultProfiles
方法實現,這種通過代碼的方式配置,配置數據來源於應用內部實現的稱為“內部化配置”。
SpringBoot內置了17種外部化配置,並規定了其調用順序。實際不止17種,也並不是必須按官方規定的順序。
官方說明:
4.2. Externalized Configuration
Spring Boot lets you externalize your configuration so that you can work with the same application code in different environments. You can use properties files, YAML files, environment variables, and command-line arguments to externalize configuration. Property values can be injected directly into your beans by using the
@Value
annotation, accessed through Spring’sEnvironment
abstraction, or be bound to structured objects through@ConfigurationProperties
.Spring Boot uses a very particular
PropertySource
order that is designed to allow sensible overriding of values. Properties are considered in the following order:
- Devtools global settings properties in the
$HOME/.config/spring-boot
folder when devtools is active.@TestPropertySource
annotations on your tests.properties
attribute on your tests. Available on@SpringBootTest
and the test annotations for testing a particular slice of your application.- Command line arguments.
- Properties from
SPRING_APPLICATION_JSON
(inline JSON embedded in an environment variable or system property).ServletConfig
init parameters.ServletContext
init parameters.- JNDI attributes from
java:comp/env
.- Java System properties (
System.getProperties()
).- OS environment variables.
- A
RandomValuePropertySource
that has properties only inrandom.*
.- Profile-specific application properties outside of your packaged jar (
application-{profile}.properties
and YAML variants).- Profile-specific application properties packaged inside your jar (
application-{profile}.properties
and YAML variants).- Application properties outside of your packaged jar (
application.properties
and YAML variants).- Application properties packaged inside your jar (
application.properties
and YAML variants).@PropertySource
annotations on your@Configuration
classes.- Default properties (specified by setting
SpringApplication.setDefaultProperties
).
配置的引用方式
1)XML 文件
根據spring規范,元信息存放在META-INF目錄下。
示例: 在resources目錄下創建META-INF/spring/context.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="xmlPerson" class="com.example.profiledemo.property.XmlPerson">
<property name="name" value="xml name" />
<property name="age" value="10" />
</bean>
</beans>
定義JavaBean
/*
* @auth yuesf
* @data 2019/11/23
*/
public class XmlPerson {
private String name;
private String age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
}
使用xml配置屬性
/*
* @auth yuesf
* @data 2019/11/23
*/
@RestController
@ImportResource(locations = { "META-INF/spring/context.xml" })
public class ContextController {
@Autowired
private XmlPerson xmlPerson;
@GetMapping("/xml")
public XmlPerson xml() {
return xmlPerson;
}
}
啟動服務運行結果如下
{
"name": "xml name",
"age": "10"
}
2)Annotation
官方提供兩種方式 @Value、@ConfigurationProperties
(1)@Value
@Value是綁定application配置文件的屬性變量
示例:
applicaton.properties文件配置
person.name=yuesf
使用Annotation配置屬性
@Value("${person.name:defaultValue}")
private String name;
@Value在Spring中是強校驗,使用時必須在配置中存在,否則會無法啟動,示例中采用容錯的方式,不存在使用默認值。
@Value的語義可以參考java.util.Properties#getProperty(java.lang.String, java.lang.String)
方法, 如果變量存在,則取變量值,若不存在取默認值
(2)@ConfigurationProperties
官方說明:
4.2.8. Type-safe Configuration Properties
Using the
@Value("${property}")
annotation to inject configuration properties can sometimes be cumbersome, especially if you are working with multiple properties or your data is hierarchical in nature. Spring Boot provides an alternative method of working with properties that lets strongly typed beans govern and validate the configuration of your application.
使用@Value來表達多個屬性時特別麻煩,官方說明使用與JavaBean綁定的方式聯合使用,使用方式如下:
使用@ConfigurationProperties 需要兩步完成使用
- 必須要定義一個類來與屬性做綁定。
示例說明:
/*
* @auth yuesf
* @data 2019/11/22
*/
@ConfigurationProperties("person")
public class Person {
private String name;
private String age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
}
- 使用@EnableConfigurationProperties 激活Person配置
示例說明:
@SpringBootApplication
@EnableConfigurationProperties(Person.class)
public class ProfileDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ProfileDemoApplication.class, args);
}
}
3)Java Code (硬編碼)
(1) 實現EnvironmentAware
示例通過實現EnvironmentAware
接口來自定義server.port
端口號為7070:
/*
* @auth yuesf
* @data 2019/11/26
*/
@Component
public class CustomizedEnvironment implements EnvironmentAware {
@Override
public void setEnvironment(Environment environment) {
System.out.println("當前激活profile文件是:"+Arrays.asList(environment.getActiveProfiles()));
if(environment instanceof ConfigurableEnvironment){
ConfigurableEnvironment env = ConfigurableEnvironment.class.cast(environment);
MutablePropertySources propertySources = env.getPropertySources();
Map<String, Object> source = new HashMap<>();
source.put("server.port","7070");
PropertySource propertySource = new MapPropertySource("javacode",source);
propertySources.addFirst(propertySource);
}
}
}
啟動后驗證端口號未發生變更,不是我們想要的效果
...
The following profiles are active: dev
2019-11-26 18:04:26.850 INFO 54924 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
...
當前激活profile文件是:[dev]
...
通過actuator查看端口號已經變更
http://127.0.0.1:8080/actuator/env/server.port
"propertySources":
[
{
"name":"server.ports"
},
{
"name":"javacode",
"property":{
"value":"7070"
}
},
{
"name": "commandLineArgs"
},
...
]
問題:
這里會遇到一個問題,請問為什么這里的7070端口號沒有使用呢?
文中
javacode
是我們代碼中指定的名稱。propertySources的取值邏輯是順序讀取,一但有值就會返回。而返回后又對propertySources做了addFirst操作,所以會造成相互覆蓋。源碼地址: org.springframework.core.env.PropertySourcesPropertyResolver#getProperty(java.lang.String, java.lang.Class
, boolean) 要想使用改后的屬性,我們可以仿照源碼使用下面這種自定義事件ApplicationListener
的方式。
(2)自定義事件ApplicationEnvironmentPreparedEvent
/*
* @auth yuesf
* @data 2019/11/23
*/
public class CustomizedSpringBootApplicationListener
implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
ConfigurableEnvironment env = event.getEnvironment();
MutablePropertySources propertySources = env.getPropertySources();
Map<String, Object> source = new HashMap<>();
source.put("server.port","6060");
PropertySource propertySource = new MapPropertySource("customizedListener",source);
propertySources.addFirst(propertySource);
}
}
添加Spring SPI配置 META-INF/spring.factories
# Application Listeners
org.springframework.context.ApplicationListener=\
com.example.profiledemo.listener.CustomizedSpringBootApplicationListener
啟動后驗證結果,啟動端口已經生效
...
The following profiles are active: dev
Tomcat initialized with port(s): 6060 (http)
...
當前激活profile文件是:[dev]
...
通過actuator查看端口號,發現7070為第一個,6060為第二個。
"propertySources":
[
{
"name":"server.ports"
},
{
"name":"javacode",
"property":{
"value":"7070"
}
},
{
"name":"customizedListener",
"property":{
"value":"6060"
}
},
{
"name": "commandLineArgs"
},
...
]
根據結果猜測,這樣結果雖然已經修改過來了,但由於后使用addFirst方法對順序做了改動。把javacode
放在了第一位。
Profiles使用場景
1)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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd"
profile="test">
...
</beans>
spring中對xml做了屬性封裝,使用profile方式來加載,示例中使用的是profile="test"
2)Properties文件
properties文件名按 application-{profile}.properties 規約來命名
3)Annotation使用
通過 @Profile 方式指定一個或多個 profile
4)命令行
通過--spring.profiles.active 命令行指定使用的profile,還可以使用 --spring.profiles.include引用多個profile
三、裝配原理
通過上面說明並沒有講清楚他的裝配原理是什么,那么我們通過源碼了解下裝配原理。
1.首先第一步是查看spring-boot源碼#META-INF/spring.factories
# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader
PropertySourceLoader
接口有兩個實現
-
PropertiesPropertySourceLoader 解析properties和xml
public class PropertiesPropertySourceLoader implements PropertySourceLoader { private static final String XML_FILE_EXTENSION = ".xml"; @Override public String[] getFileExtensions() { return new String[] { "properties", "xml" }; } ... }
-
YamlPropertySourceLoader 解析 yml和yaml
public class YamlPropertySourceLoader implements PropertySourceLoader { @Override public String[] getFileExtensions() { return new String[] { "yml", "yaml" }; } ... }
2.不管哪種解析,查下load方法是由誰來調用
本文使用idea查看代碼,查看代碼需要下載源碼才可以查看。
本文中提到查看源碼的方法調用統一使用idea自帶的快捷鍵Alt+F7,或鼠標右鍵Find Usages
發現load方法是由ConfigFileApplicationListener.Loader#loadDocuments 方法調用。
再次查看ConfigFileApplicationListener
這個類是被誰調用,同樣使用鼠標右鍵Find Usages ,發現會出來很多,那么我們會有選擇性的查看。看哪一個呢?使勁找就能找到我們剛才看到的那個文件/META-INF/spring.factories
文件中有個ApplicationListener
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener
查到這里就涉及到spring的事件,如果你不清楚Spring事件可以看下相關文檔。
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
/**
* Handle an application event.
* @param event the event to respond to
*/
void onApplicationEvent(E event);
}
ApplicationListener
只有一個方法 onApplicationEvent
同樣查看剛才我們定位到spring.factories文件查看 ConfigFileApplicationListener#onApplicationEvent
方法,
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent(
(ApplicationEnvironmentPreparedEvent) event);
}
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent(event);
}
}
這個方法非常簡單,通過判斷如果是 ApplicationEnvironmentPreparedEvent
類型時怎么做,意思是當應用環境准備時怎么做。第二個是如果是 ApplicationPreparedEvent
類型怎么做,意思是應用准備時怎么做。
3.反過來在看下我們Java Code的方式 使用自定義事件時會生效的原因。
本次整個分析到此結束
本文由博客一文多發平台 OpenWrite 發布!
再次感謝!!! 您已看完全文,歡迎關注微信公眾號猿碼
,你的支持是我持續更新文章的動力!