一、外部化配置
配置分为编译时和运行时,而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 in random.*. 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的方式 使用自定义事件时会生效的原因。
本次整个分析到此结束