5.SpringBoot学习(五)——Spring Boot Profile用法
1.简介
1.1 概述
Spring Profiles provide a way to segregate parts of your application configuration and make it be available only in certain environments. Any
@Component
,@Configuration
or@ConfigurationProperties
can be marked with@Profile
to limit when it is loaded
Spring Profiles 提供了一种隔离应用程序配置部分并使之仅在某些环境中可用的方法。任何 @Component,@ Configuration 或 @ConfigurationProperties 都可以用 @Profile 标记来限制它的加载。
1.2 特点
- 可在 xml 中使用;
- 可以修饰类、注解、方法;
- 可以通过 命令行、系统属性、启动变量、配置文件 等方式激活;
2.环境
- JDK 1.8.0_201
- Spring Boot 2.2.0.RELEASE
- 构建工具(apache maven 3.6.3)
- 开发工具(IntelliJ IDEA )
3.代码
3.1 代码说明
提供了 dev、prod 两套环境的配置和代码,测试激活不同的 profile,功能是否如预期
3.2 代码结构
3.3 maven 依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3.4 配置文件
application.properties
spring.profiles.active=dev
application-dev.properties
spring.application.name=spring-boot-profile
server.port=8080
user.age=20
application-prod.properties
spring.application.name=spring-boot-profile
server.port=9090
user.age=30
3.5 java代码
UserModel.java
public class UserModel {
private String name;
@Value("${user.age}")
private Integer age;
public UserModel(String name, Integer age) {
this.name = name;
this.age = age;
}
public UserModel() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "UserModel{name='" + name + '\'' + ", age='" + age + '\'' + '}';
}
}
DevConfig.java
@Profile("dev")
@Configuration
public class DevConfig {
@Bean
public UserModel userModel() {
UserModel userModel = new UserModel();
userModel.setName("zhangsan");
return userModel;
}
}
ProdConfig.java
@Profile("prod")
@Configuration
public class ProdConfig {
@Bean
public UserModel userModel() {
UserModel userModel = new UserModel();
userModel.setName("lisi");
return userModel;
}
}
UserController.java
@RestController
public class UserController {
@Autowired
private final ApplicationContext applicationContext;
public UserController(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@GetMapping(value = "/user/get")
public UserModel getUser() {
return applicationContext.getBean(UserModel.class);
}
@Profile("dev")
@GetMapping(value = "/user/dev")
public UserModel devUser() {
return new UserModel("dev-user", 22);
}
@Profile("prod")
@GetMapping(value = "/user/prod")
public UserModel prodUser() {
return new UserModel("prod-user", 33);
}
}
3.6 git 地址
spring-boor/spring-boot-05-basis/spring-boot-profile
4.测试
4.1 profile 的几种激活方式
-
使用 jar 包启动时,指定为命令行参数
java -jar spring-boot-profile-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev
-
使用 jar 包启动时,指定为启动参数
java -Dspring.profiles.active=dev -jar spring-boot-profile-0.0.1-SNAPSHOT.jar
-
通过配置文件指定
spring.profiles.active=dev
-
通过代码设定为系统变量
System.setProperty("spring.profiles.active", "dev");
-
启动类中指定
new SpringApplicationBuilder(SpringBootProfileApplication.class) .properties("spring.profiles.active=dev").run(args);
-
idea 启动时指定(配置任意一处即可)
-
web.xml 中配置
<context-param> <param-name>spring.profiles.active</param-name> <param-value>dev</param-value> </context-param>
优先级
命令行方式 > Java系统属性方式 > 系统变量方式 > 配置文件方式
profile 值可以指定多个,例如: --spring.profiles.active=dev,test
4.2 profile 的测试结果
按照上面任意一种方式激活 profile,分别设置为 dev 和 prod,启动 SpringBootProfileApplication.main 方法,在 spring-boot-profile.http 访问下列地址,观察输出信息是否符合预期。
4.2.1 profile=dev
### GET /user/get
GET http://localhost:8080/user/get
### GET /user/dev
GET http://localhost:8080/user/dev
4.2.1 profile=prod
### GET /user/get
GET http://localhost:9090/user/get
### GET /user/prod
GET http://localhost:9090/user/prod
5.源码分析
5.1 Spring Boot 中 Profile 是如何生效的?
在启动类启动的时候,按照如下顺序调用 SpringApplication.run -> prepareEnvironment ->
listeners.environmentPrepared -> listener.environmentPrepared -> initialMulticaster.multicastEvent
-> getApplicationListeners -> invokeListener -> doInvokeListener -> listener.onApplicationEvent
在 ApplicationListener 中,有一个 ConfigFileApplicationListener,这个监听器用来解析配置文件,所以会调用它的 onApplicationEvent 方法,它的实现如下
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
}
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent(event);
}
}
启动时这里的 event 是 ApplicationEnvironmentPreparedEvent,所以会执行 onApplicationEnvironmentPreparedEvent 方法
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
// 从 spring.factories 中获取 EnvironmentPostProcessor
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
// 将当前 ConfigFileApplicationListener 加入到 postProcessors 中
postProcessors.add(this);
// 根据 @Ordered 配置的顺序进行排序
AnnotationAwareOrderComparator.sort(postProcessors);
// 触发 postProcessEnvironment 方法
for (EnvironmentPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
}
}
ConfigFileApplicationListener 的 postProcessEnvironment 实现如下
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
// 添加配置文件
addPropertySources(environment, application.getResourceLoader());
}
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
RandomValuePropertySource.addToEnvironment(environment);
// 加载配置文件
new Loader(environment, resourceLoader).load();
}
Loader 的构造函数中,使用 SpringFactoriesLoader 加载 PropertySourceLoader,它有两个实现类:PropertiesPropertySourceLoader 和 YamlPropertySourceLoader,前者解析 .properties 和 .xml,后者解析 .yml 和 .yaml,在 spring.factories 中,PropertiesPropertySourceLoader 在前,所以先解析 .properties 文件,YamlPropertySourceLoader中 .yml 在前,先解析 .yml
Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
this.environment = environment;
this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
getClass().getClassLoader());
}
在 load() 方法中,获取所有的 profiles,然后通过 load 方法记在对应的配置文件
void load() {
FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY, (defaultProperties) -> {
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
// 初始化 Profiles
initializeProfiles();
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
if (isDefaultProfile(profile)) {
addProfileToEnvironment(profile.getName());
}
// 加载配置文件
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
addLoadedPropertySources();
applyActiveProfiles(defaultProperties);
});
}
initializeProfiles()
private void initializeProfiles() {
// The default profile for these purposes is represented as null. We add it
// first so that it is processed first and has lowest priority.
this.profiles.add(null);
// 获取 spring.profiles.active 值
Set<Profile> activatedViaProperty = getProfilesFromProperty(ACTIVE_PROFILES_PROPERTY);
// 获取 spring.profiles.include 值
Set<Profile> includedViaProperty = getProfilesFromProperty(INCLUDE_PROFILES_PROPERTY);
// 从其他途径获取 profile
List<Profile> otherActiveProfiles = getOtherActiveProfiles(activatedViaProperty, includedViaProperty);
this.profiles.addAll(otherActiveProfiles);
// Any pre-existing active profiles set via property sources (e.g.
// System properties) take precedence over those added in config files.
this.profiles.addAll(includedViaProperty);
addActiveProfiles(activatedViaProperty);
if (this.profiles.size() == 1) { // only has null profile
for (String defaultProfileName : this.environment.getDefaultProfiles()) {
Profile defaultProfile = new Profile(defaultProfileName, true);
this.profiles.add(defaultProfile);
}
}
}
load()
private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
getSearchLocations().forEach((location) -> {
boolean isFolder = location.endsWith("/");
Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
// 加载配置文件
names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
});
}
private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
DocumentConsumer consumer) {
if (!StringUtils.hasText(name)) {
for (PropertySourceLoader loader : this.propertySourceLoaders) {
if (canLoadFileExtension(loader, location)) {
// 加载配置文件
load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
return;
}
}
throw new IllegalStateException("File extension of config file location '" + location + "' is not known to any PropertySourceLoader. If the location is meant to reference " + "a directory, it must end in '/'");
}
Set<String> processed = new HashSet<>();
for (PropertySourceLoader loader : this.propertySourceLoaders) {
for (String fileExtension : loader.getFileExtensions()) {
if (processed.add(fileExtension)) {
loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory, consumer);
}
}
}
}