Spring Boot (六) 配置详解及源码分析

本章讲解Spring Boot的配置文件,即我们创建项目时在resources目录下生成的application.properties。其实这个配置文件不仅仅只能放在resources目录下,也不仅仅只能是properties文件。

1 配置文件位置

1 resources目录
2 resources/config目录
3 项目根目录
4 项目根目录下config目录

假如我们四个目录下都有配置文件默认是取哪一个呢?下面我们演示一下,现在将四个目录都放拷贝一份文件。结构如下:

在这里插入图片描述
文件中都配置一个属性,值为目录,比如resources目录下。

在这里插入图片描述
测试代码如下:

@SpringBootTest
class SpringBootDemoApplicationTests {
    
    

	@Value("${demo.location}")
	private String location;
	@Test
	void contextLoads() {
    
    
		System.out.println(location);
	}

	public String getLocation() {
    
    
		return location;
	}

	public void setLocation(String location) {
    
    
		this.location = location;
	}
}

运行测试方法,可见最先读取的是项目根目录下的config目录。

在这里插入图片描述
然后把项目根目录下config目录下的文件删除,再次运行。一一测试后,配置文件位置的优先级为:
项目根目录下config目录 > 项目根目录 > resources/config > resources

2 配置文件类型

Spring Boot默认支持3种配置文件:
1.properties
2.yaml
3.xml(不推荐)

上文中已经演示了properties,这里就不再次演示,下面我们看看yaml也可以是yml:

在这里插入图片描述
注意:yaml文件的缩进只能使用空格,且冒号后面也必须有空格。

xml配置:
在这里插入图片描述
如果目录下同时存在yaml、yml、properties、xml四种类型的文件,如:
在这里插入图片描述
优先级为: properties > xml > yml > yaml

由上面可以看出xml配置起来比较麻烦,所以基本没有使用。对于properties和yaml各有优缺点把,看自己喜好。

3 配置详解

3.1 多环境配置

在开发当中,通常一个项目都至少会经过开发、测试、生产三个环境,不同的环境数据库,服务器端口等配置可能都不一样,如果在切环境的时再来一一修改,比较麻烦,也容易出错,所以一般是一套环境对于一个配置文件。

3.1.1 多文件配置

Spring Boot也给我们提供了这样的功能。针对多环境场景下,我们会给每个环境创建一个配置文件application-${profile}.yaml。其中,${profile} 为环境名,对应到 Spring Boot 项目生效的Profile。
创建3个文件application.yaml、application-dev.yaml、application-prod.yaml,如下:
在这里插入图片描述
application.yaml

spring:
 profiles:
  active: prod  #生效的环境

application-dev.yaml

server:
  port: 8081
demo:
  location: yaml配置 dev
  filename: application dev

application-prod.yaml

server:
  port: 8082
demo:
  location: yaml配置 prod
  filename: application prod

如上在application.yaml 中配置生效的环境为prod,所以加载application-prod.yaml文件,所以服务端口为8082。
在这里插入图片描述
激活指定环境配置:

  • 直接在application.yml的配置文件中使用 spring.profiles.active=dev|prod
  • 设置虚拟机参数 -Dspring.profiles.active=dev|prod
  • 设置命令行参数 --spring.profiles.active=dev|prod
  • 也可直接启用多个文件spring.profiles.active=dev,prod,用逗号分隔开

3.1.2 多模块文件

上面是将不同的配置放到了不同的配置文件中,Spring Boot还支持在同一个配置文件中分模块配置,然后指定启用的模块。

spring:
 profiles:
  active: prod #启用模块

---
spring:
 profiles: dev #指定模块名称
demo:
 location: yaml配置 dev
 filename: application dev
 
server:
 port: 8084
---
spring:
 profiles: prod #指定模块名称
demo:
 location: yaml配置 prod
 filename: application prod
 
server:
 port: 8085

可以通过三个短横线为分隔,将其分为不同模块。由上面注释的方式指定模块名称,和指定启用模块。也可同时启用两个模块的文件用逗号分隔。

spring:
 profiles:
  active: prod,dev #启用模块

3.2 配置文件名称

上文提到过Spring Boot会在启动时默认加载resources目录下名为application的文件(如:application.properties、application.yaml),当然我们也可自定义文件名。

启动类设置属性:
System.setProperty(ConfigFileApplicationListener.CONFIG_NAME_PROPERTY, “demo,application”);

@SpringBootApplication
public class SpringBootDemoApplication {
    
    

	public static void main(String[] args) {
    
    
		// 设置加载的配置文件名称
		System.setProperty(ConfigFileApplicationListener.CONFIG_NAME_PROPERTY, "demo,application");
		SpringApplication.run(SpringBootDemoApplication.class, args);
	}

}

以上配置将同时加载demo.yaml和appliation.yaml。

3.3 配置随机值

Spring Boot提供随机值配置功能,通过RandomValuePropertySource 实现。
在application.yaml中配置如下:

demo:
 randomInt: ${
    
    random.int} #随机int类型整数
 randomLong: ${
    
    random.long} #随机Long类型整数

注入属性配置类

@Component
public class SpringbootConfig {
    
    

	@Value("${demo.randomInt}")
	private int randomInt;
	@Value("${demo.randomLong}")
	private long randomLong;

	public int getRandomInt() {
    
    
		return randomInt;
	}

	public void setRandomInt(int randomInt) {
    
    
		this.randomInt = randomInt;
	}

	public long getRandomLong() {
    
    
		return randomLong;
	}

	public void setRandomLong(long randomLong) {
    
    
		this.randomLong = randomLong;
	}
}
@RestController
public class ConfigController {
    
    
	@Autowired
	private SpringbootConfig springbootConfig;

	@RequestMapping("/config")
	public String configName() {
    
    
		return "随机int:"+springbootConfig.getRandomInt()+",随机long:"+springbootConfig.getRandomLong();
	}
}

结果如下:
在这里插入图片描述
RandomValuePropertySource提供了如下随机值:

${
    
    random.int} #随机int型
${
    
    random.long} #随机long型
${
    
    random.int(m)} #随机小于m的int型
${
    
    random.int[n,m]} #随机大于n小于m的int型
${
    
    random.uuid} #uuid字符串
${
    
    random.value} #随机字符串

其中随机字符串不一定是value,只要不等于其它物种类型,就属于随机字符串。

3.3 加载自定义properties

在resources目录下新建tes.properties文件,内容如下:

person.name=tom
person.age=19
person.address=中国

PersonConfig

@Component
@PropertySource("classpath:test.properties")
public class PersonConfig {
    
    
	@Value("${person.name}")
	private String name;
	@Value("${person.age}")
	private int age;
	@Value("${person.address}")
	private String address;

	@Override
	public String toString() {
    
    
		return "PersonConfig{" +
				"name='" + name + '\'' +
				", age=" + age +
				", address='" + address + '\'' +
				'}';
	}
}
@RestController
public class ConfigController {
    
    
	@Autowired
	private PersonConfig personConfig;
	
	@RequestMapping("/person")
	public String getPerson() {
    
    
		return  personConfig.toString();
	}
}

结果如下:
在这里插入图片描述
这种@value方式如果属性比较多,配置起来就显得很麻烦,这时候可以采用@EnableConfigurationProperties加@ConfigurationProperties注解

启动类配置如下:

@SpringBootApplication
@EnableConfigurationProperties(PersonConfig.class)
@PropertySource("classpath:test.properties")
public class SpringBootDemoApplication {
    
    

	public static void main(String[] args) {
    
    
	
	SpringApplication.run(SpringBootDemoApplication.class, args);
	}

}

PersonConfig

@ConfigurationProperties(prefix = "person")
public class PersonConfig {
    
    
//	@Value("${person.name}")
	private String name;
//	@Value("${person.age}")
	private int age;
//	@Value("${person.address}")
	private String address;

	public void setName(String name) {
    
    
		this.name = name;
	}

	public void setAge(int age) {
    
    
		this.age = age;
	}

	public void setAddress(String address) {
    
    
		this.address = address;
	}

	public String getName() {
    
    
		return name;
	}

	public int getAge() {
    
    
		return age;
	}

	public String getAddress() {
    
    
		return address;
	}

	@Override
	public String toString() {
    
    
		return "PersonConfig{" +
				"name='" + name + '\'' +
				", age=" + age +
				", address='" + address + '\'' +
				'}';
	}
}

这种方式就避免了每个属性都写一个@value。

3.4 加载自定义yaml

@PropertySource默认是不支持yaml文件的,因为其加载properties文件是在PropertySourceFactory接口种完成的,而该接口只有一个默认实现类DefaultPropertySourceFactory,所以要加载yaml文件需要们自定义实现PropertySourceFactory接口。

PropertySourceFactory的作用是将创建的PropertiesSource暴露到spring 的environment种,那如何读取yaml文件呢?spring提供了两个类加载yaml文档,YamlPropertiesFactoryBean和YamlMapFactoryBean。YamlPropertiesFactoryBean将yaml加载为properties,YamlMapFactoryBean将yaml加载为map。
示例代码如下:

public class MyPropertySourceFactory extends DefaultPropertySourceFactory {
    
    
	@Override
	public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
    
    
		String resourceName = name == null?resource.getResource().getFilename():name;
		// 判断是否yaml文件
		if (resource.getResource().getFilename().endsWith("yaml")) {
    
    
		// 加载yaml文档
			YamlPropertiesFactoryBean yamlPropertiesFactoryBean = new YamlPropertiesFactoryBean();
			yamlPropertiesFactoryBean.setResources(resource.getResource());
			// 初始化
			yamlPropertiesFactoryBean.afterPropertiesSet();
			//加载为properties
			Properties properties = yamlPropertiesFactoryBean.getObject();
			//封装成 PropertiesPropertySource 返回
			return new PropertiesPropertySource(resourceName, properties);
		}
		// properties文件
		return super.createPropertySource(resourceName, resource);
	}
}

在使用@PropertySource时,指定自定义的PropertySourceFactory实现。

@SpringBootApplication
@PropertySource(value = "classpath:yamlTest.yaml",factory = MyPropertySourceFactory.class)
public class SpringBootDemoApplication {
    
    
	public static void main(String[] args) {
    
    
		SpringApplication.run(SpringBootDemoApplication.class, args);
	}
}

注入文件属性:

@Component
public class PersonConfig {
    
    
	@Value("${person.name}")
	private String name;
	@Value("${person.age}")
	private int age;
	@Value("${person.address}")
	private String address;

	@Override
	public String toString() {
    
    
		return "PersonConfig{" +
				"name='" + name + '\'' +
				", age=" + age +
				", address='" + address + '\'' +
				'}';
	}
}

4 配置加载过程源码分析

Spring Boot加载application配置文件是由ConfigFileApplicationListener来完成的。

在这里插入图片描述
由上图可以看出ConfigFileApplicationListener既是一个应用监听器(ApplicationListener),也是一个环境的后置处理器(EnvironmentPostProcessor)。

ConfigFileApplicationListener部分属性如下:

private static final String DEFAULT_PROPERTIES = "defaultProperties";
	// 默认的配置文件路径
    private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/";
    // 默认的配置文件名称
    private static final String DEFAULT_NAMES = "application";
    private static final Set<String> NO_SEARCH_NAMES = Collections.singleton((Object)null);
    private static final Bindable<String[]> STRING_ARRAY = Bindable.of(String[].class);
    private static final Bindable<List<String>> STRING_LIST = Bindable.listOf(String.class);
    private static final Set<String> LOAD_FILTERED_PROPERTY;
    
    // 配置生效配置文件的属性名称 如spring.profiles.active=dev则表示application-dev.properties 参考前文3.1
    public static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";
    public static final String INCLUDE_PROFILES_PROPERTY = "spring.profiles.include";
    
    // 自定义配置文件名称的属性名称,如设置spring.config.name=dubbo,则加载dubbo.properties
    public static final String CONFIG_NAME_PROPERTY = "spring.config.name";
    
    // 自定义配置文件位置,如设置spring.config.location=classpath:/config/app,则可以将配置文件放如/resources/config/app目录下
    public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";
    public static final String CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location";
    public static final int DEFAULT_ORDER = -2147483638;
    private final DeferredLog logger = new DeferredLog();
    private static final Resource[] EMPTY_RESOURCES;
    private static final Comparator<File> FILE_COMPARATOR;
    private String searchLocations;
    private String names;
    private int order = -2147483638;

首先这个类是在创建SpringApplication实例时,通过spring的spi机制加载到SpringApplication中的。
我们知道Spring Boot的启动类就是SpringApplication调用其静态run方法,静态run方法中创建SpringApplication实例调用成员run方法。

SpringApplication构造方法:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    
    
		this.resourceLoader = resourceLoader;
		Assert.notNull(primarySources, "PrimarySources must not be null");
		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
		this.webApplicationType = WebApplicationType.deduceFromClasspath();
		setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
		// 从META-INF/spring.factories中加载所有ApplicationListener,并添加到SpringApplication的listeners集合中。
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		this.mainApplicationClass = deduceMainApplicationClass();
	}

上面代码中调用getSpringFactoriesInstances(ApplicationListener.class)方法加载了ConfigFileApplicationListener
META-INF/spring.factories 配置如下:
在这里插入图片描述
SpringApplication的run方法

public ConfigurableApplicationContext run(String... args) {
    
    
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		configureHeadlessProperty();

		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();
		try {
    
    
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			// 准备环境,加载Properties、yaml文件
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
			configureIgnoreBeanInfo(environment);
			Banner printedBanner = printBanner(environment);
			
			context = createApplicationContext();
			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class[] {
    
     ConfigurableApplicationContext.class }, context);
		
			prepareContext(context, environment, listeners, applicationArguments, printedBanner);
			
			refreshContext(context);
			
			afterRefresh(context, applicationArguments);
			stopWatch.stop();
			if (this.logStartupInfo) {
    
    
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
			
			listeners.started(context);

			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
    
    
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}

		try {
    
    
			listeners.running(context);
		}
		catch (Throwable ex) {
    
    
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}

该方法里调用了prepareEnvironment(listeners, applicationArguments)方法准备环境,其中调用了 listeners.environmentPrepared(environment)方法,发布ApplicationEnvironmentPreparedEvent事件,触发了ConfigFileApplicationListener的onApplicationEvent(ApplicationEvent event)方法。

public void onApplicationEvent(ApplicationEvent event) {
    
    
		if (event instanceof ApplicationEnvironmentPreparedEvent) {
    
    
		//加载配置文件
			onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
		}
		if (event instanceof ApplicationPreparedEvent) {
    
    
			onApplicationPreparedEvent(event);
		}
	}

事件类型为onApplicationEnvironmentPreparedEvent进入onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
    
    
		List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
		将ConfigFileApplicationListener添加到后置处理器集合
		postProcessors.add(this);
		AnnotationAwareOrderComparator.sort(postProcessors);
		for (EnvironmentPostProcessor postProcessor : postProcessors) {
    
    
			postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
		}
	}

调用了环境的后置处理器方法postProcessEnvironment,上文说过ConfigFileApplicationListener自身也是一个环境的后置处理器,进入器postProcessEnvironment方法,该方法什么也没做,调用了自身addPropertySources(environment, application.getResourceLoader())方法。

protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
    
    
		// 向环境中添加RandomValuePropertySource,支持获取随机数,参考上文3.3
		RandomValuePropertySource.addToEnvironment(environment);
		// 创建Loader实例,利用spring的spi机制加载了读取配置文件的两个Loader,PropertiesPropertySourceLoader、YamlPropertySourceLoader
	
		new Loader(environment, resourceLoader).load();
	}

该方法中添加了加载随机数的RandomValuePropertySource,通过该SPI
加载了读取配置文件的PropertiesPropertySourceLoader、YamlPropertySourceLoader,最终调用这两个Loader的load方法读取配置文件。

在这里插入图片描述

PropertiesPropertySourceLoader

在这里插入图片描述
YamlPropertySourceLoader

在这里插入图片描述
其中的getFileExtensions方法返回的是支持的配置文件类型,所以Spring Boot默认支持四种类型的配置文件,参考上文2。

最后调用了PropertiesPropertySourceLoader和YamlPropertySourceLoader的load(String name, Resource resource)方法读取了配置文件。

猜你喜欢

转载自blog.csdn.net/qq_36706941/article/details/108622014
今日推荐