1.SpringApplication与Spring容器
类配置与XML配置
传统Spring框架大多采用XML文件作为配置文件,但Spring Boot推荐使用Java配置类(使用@Configuration注解修饰的类)作为配置文件。
(1)如果希望Spring Boot能加载其他配置类或者扫描其他包下的配置类或Bean组件,则可使用如下两个注解。
➢@Import:该注解显式指定Spring Boot要加载的配置类。
➢@ComponentScan:该注解指定 Spring Boot 扫描指定包及其子包下所有的配置类或 Bean组件。(也可以在@SpringBootApplication注解中使用scanBasePackages属性指定包路径)
(2)如果项目不可避免地要用到XML配置文件,则可用@ImportResource 注解来导入XML配置文件。
//额外指定扫描com.bnuz.boot包及其子包下所以配置类和Bean组件
@SpringBootApplication(scanBasePackages = "com.bnuz.bean")
//加载MyConfig类(使用@Configuration注解的配置类)作为配置类
@Import(MyConfig.class)
//加载 类加载路径下的beans.xml文件作为配置文件
@ImportResource("classpath:beans.xml")
public class MainApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
}
}
点击进入@SpringBootApplication的源码:
@SpringBootApplication 的 scanBasePackages 其实就是@ComponentScan的basePackages属性。
接着点击@SpringBootConfiguration注解的源码:
可以发现该注解是带有@Configuration注解的,所以这个带main()方法的主类是Spring Boot应用的主配置源。
除了加载主配置源中所有的配置,Spring Boot 还会自动扫描主配置源所在的包及其子包下所有带@Component注解(@Configuration、@Controller、@Service、@Repository都是@Component的变体)的配置类或Bean组件。
如果不为@SpringBootApplication指定scanBasePackages属性,则Spring Boot默认会加载主配置类(@SpringBootApplication修饰的类)所在的包及其子包下的所有配置类和Bean组件,这相当于scanBasePackages属性的默认值就是主配置类所在的包。
Spring Boot加载配置文件的三种不同方式
(1)通过@ComponentScan的basePackages指定额外要扫描的包及其子包:
在com.bnuz.bean包下定义一个简单的类并用@Component注解修饰,它将会被扫描`配置成Spring容器中的Bean。
package com.bnuz.bean;
@Component
public class User {
private String name;
private Integer age;
private Pet pet;
...
}
然后在主配置源指定要扫描的包,如同上面那个例子:
//额外指定扫描com.bnuz.boot包及其子包下所以配置类和Bean组件
@SpringBootApplication(scanBasePackages = "com.bnuz.bean")
(2)使用@ImportResource注解指定加载XML配置文件。
//加载 类加载路径下的beans.xml文件作为配置文件
@ImportResource("classpath:beans.xml")
(3)使用@Import注解指定加载其他Java配置类。
@Configuration()
public class MyConfig {
//给容器添加组件,以方法名作为组件的id,返回值类型就是组件的class,返回的值就是容器创建的实例
@Bean
public User user(){
User lyq = new User("lyq", 22);
return lyq;
}
}
然后在主配置源使用@Import指定加载的配置类,如同上面那个例子:
//加载MyConfig类(使用@Configuration注解的配置类)作为配置类
@Import(MyConfig.class)
启动日志和失败分析器
启动日志
如果使用SpringApplication的静态run()方法来运行Spring Boot应用,则默认显示INFO级别的日志消息,包括一些与启动相关的详情。
在resources目录下新建一个application.properties文件,如果希望Spring Boot显示完整的错误报告,则可通过这个文件开启debug属性:
logging.level.org.springframework.boot.autoconfigure.logging=debug
如果想关闭启动信息的日志,则可在properties文件中将如下属性设为false:
spring.main.log-startup-info=false
失败分析器
开发自定义的失败分析器,自定义的失败分析器应继承AbstractFailureAnalyzer<T>,该基类中的泛型T代表该失败分析器要处理的异常。
package com.bnuz.boot.app;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import java.net.BindException;
public class MyAnalyzer extends AbstractFailureAnalyzer<BindException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, BindException cause) {
cause.printStackTrace();
return new FailureAnalysis("程序出错,端口被占用:" + cause.getMessage(),
"先停用8080的应用再运行本应用,server.port改为本应用端口", cause);
}
}
继承 AbstractFailureAnalyzer<T>抽象基类就要实现它的 analyze()抽象方法,该方法返回的FailureAnalysis代表了对该异常的分析结果;如果不想让该失败分析器分析该异常,而是希望将该异常留给下一个分析器进行分析,则可让该方法返回null。
上面失败分析器的 analyze()方法返回了一个 FailureAnalysis 对象,表明该失败分析器会对BindException进行分析。FailureAnalysis的本质就是包装三个信息。
➢ description:失败的描述信息。第一个构造参数。
➢ action:对该失败的修复建议。第二个构造参数。
➢ cause:导致失败的异常。第三个构造参数。
自定义失败分析器需要在META-INF/spring.factories文件中注册,首先在项目的resources目录下创建META-INF文件夹(注意大小写和短横线),然后在META-INF文件夹内创建spring.factories文件,该文件的内容如下:
org.springframework.boot.diagnostics.FailureAnalyzer=com.bnuz.boot.app.MyAnalyzer
在另一个项目启动Tomcat后,运行本程序,在控制台输出如下:
延迟初始化
Spring Boot使用ApplicationContext作为Spring容器,因此Spring Boot默认会对容器中所有的singleton Bean执行预初始化——这其实就和Spring Ioc容器一个原理。
在某些特殊需求下,Spring Boot也允许将取消ApplicationContext的预初始化行为改为延迟初始化。要将Spring容器设为延迟初始化,有如下三种方式。
➢ 调用SpringApplicationBuilder 对象的lazyInitialization(true)方法。
➢ 调用SpringApplication对象的setLazyInitialization(true)方法。
➢ 在application.properties文件中配置如下代码:
spring.main.lazy-initialization=true
经典白学:一般来说,如果不是有非常特殊的原因,则不建议启用Spring Boot的延迟初始化。
Banner
在启动Spring Boot应用时,默认会显示一个大大的Spring的Banner。如果要关闭该Banner,则可通过application.properties文件中的spring.main.banner-mode属性进行设置。该属性支持如下三个属性值。
➢ console:在控制台输出Banner。
➢ log:在日志文件中输出Banner。
➢ off:彻底关闭Banner。
老实说我目前还不能理解banner有什么用,就感觉能搞一些花里胡哨的图案就没了。
Spring Boot 也允许指定自定义的 Banner,详情自行查阅(P45)。
SpringApplication还提供了setBanner()方法以编程式的方式来设置Banner,调用该方法需要传入一个Banner参数,该Banner对象的printBanner()负责输出Banner内容—简单来说,想用什么内容作为自定义的Banner,就让该方法输出什么内容。
流式API
Spring Boot提供了SpringApplicationBuilder工具类,通过该工具类能以流式API创建SpringApplication,并启动Spring Boot应用。
此外,SpringApplicationBuilder还提供了如下方法来加载配置文件:
➢ sources(Class<?>...sources):为应用添加配置类。
➢ child(Class<?>...sources):为当前容器添加子容器配置类。
➢ parent(Class<?>...sources):为当前容器添加父容器配置类。
从这些方法可以看出,使用SpringApplicationBuilder可以构建ApplicationContext的层次结构(让 Spring 容器具有父子关系)。
让多个 Spring 容器具有层次结构具有很多好处。比如,由于子容器是由父容器负责加载的,因此子容器中的Bean可访问父容器中的Bean,但父容器中的Bean不能访问子容器中的Bean。
事件监听器Listener
采用传统方式注册的事件监听器都是部署在ApplicationContext容器(Spring容器)中的,它们必须等到容器被创建之后才会出现,而SpringApplication的有些事件是在容器被创建之前被触发的。
当Spring Boot应用启动时,SpringApplication会依次触发如下事件:
(1)在应用刚刚开始、还未进行任何处理之时,触发ApplicationStartingEvent事件,除非监听器和初始化器注册失败。
(2)当ApplicationContext要使用的Environment已经确定,但ApplicationContext还未被创建之时,触发ApplicationEnvironmentPreparedEvent事件。
(3) 当ApplicationContext准备完成且初始化器(ApplicationContextInitializer)已被调用,但未加载任何Bean定义之前,触发ApplicationContextInitializedEvent事件。
(4)在所有Bean定义被加载之后、Spring容器刷新之前,触发ApplicationPreparedEvent事件。
(5)在Spring容器刷新之后,ApplicationRunner、CommandLineRunner的接口方法被调用之前,触发ApplicationStartedEvent事件。
(6) 当应用程序的 LivenessState 状态变成 CORRECT,表明应用程序进入 live 状态时,触发AvailabilityChangeEvent事件。
(7)当所有 ApplicationRunner、CommandLineRunner 的接口方法被调用完成后,触发ApplicationReadyEvent事件。
(8)当应用程序的ReadinessState状态变成ACCEPTING_TRAFFIC,表明应用程序准备接受服务请求时,触发AvailabilityChangeEvent事件。
(9)如果启动遇到异常,则触发ApplicationFailedEvent事件。
上面列出的都是绑定到 SpringApplication 的 SpringApplicationEvent 事件。
此外,还会在ApplicationPreparedEvent事件与ApplicationStartedEvent事件之间触发如下两个容器事件:
➢ Web服务器初始化完成后触发WebServerInitializeEvent事件;如果使用Servlet Web服务器,则触发 ServletWebServerInitializedEvent 事件;如果使用反应式 Web 服务器,则触发ReactiveWebServerInitializedEvent事件。
➢ 在刷新ApplicationContext时触发ContextRefreshedEvent事件。
事件监听器采用同一个线程来执行,因此不应该在事件监听器中执行某些耗时的操作;如果确实需要完成某些耗时的操作,则建议使用 ApplicationRunner 或CommandLineRunner接口。
为了让事件监听器能区分事件到底来自哪个容器,可以用事件监听器依赖注入的容器与事件来自的容器进行比较;为了将容器依赖注入事件监听器,可通过如下两种方式:
(1)接口注入:让事件监听器实现 ApplicationContextAware 接口,Spring容器将会被注入监听器。
(2)普通注入:如果事件监听器是容器中的 Bean,则可直接使用@Autowired 注解来完成依赖注入。
为了监听SpringApplication触发的事件,SpringApplication提供了如下方式来注册事件监听器:
➢ 调用SpringApplication的addListeners()方法或SpringApplicationBuilder的listeners()方法添加事件监听器。
➢ 使用 META-INF/spring.factories 文件来配置事件监听器。在该文件中添加如下代码即可注册事件监听器:
org.springframework.context.ApplicationListener=com.bnuz.boot.app.MyListener
初始化器
Spring Boot 还提供了 ApplicationContextInitializer 对 Spring 容器执行初始化。
有了ApplicationContextInitializer实现类之后,接下来可通过如下方式来注册初始化器:
➢ 调用SpringApplication的addInitializers()方法或SpringApplicationBuilder的initializers()方法添加初始化器。
➢ 使用 META-INF/spring.factories 文件来配置初始化器。在该文件中添加如下代码即可注册初始化器:
org.springframework.context.ApplicationContextInitializer=\
com.bnuz.boot.app.MyInitializer
配置环境后处理器
如果想在Environment对象创建之后、Spring容器刷新之前对Environment对象(配置环境)进行定制,Spring Boot提供了配置环境后处理器—实现EnvironmentPostProcessor接口的类被称为“配置环境后处理器”。
有了EnvironmentPostProcessor实现类之后,可通过META-INF/spring.factories文件来注册配置环境后处理器。例如如下代码:
org.springframework.boot.env.EnvironmentPostProcessor=\
com.bnuz.boot.app.实现EnvironmentPostProcessor接口的类
监听器、初始化器和配置环境后处理器案例
本例开发一个事件监听器,监听SpringApplication触发的ApplicationStartedEvent事件。该监听器类的代码如下:
package com.bnuz.boot.app;
import org.springframework.beans.BeansException;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
public class MyListener implements ApplicationContextAware,
ApplicationListener<ApplicationStartedEvent> {
private ApplicationContext ctx;
//接口注入方法,通过该方法可访问Spring容器
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.ctx = applicationContext;
}
@Override
public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) {
ConfigurableApplicationContext context = applicationStartedEvent.getApplicationContext();
if (ctx == context){
System.out.println("触发事件的容器与监听器所在的容器相同");
}
//后面可编写自定义处理
}
}
上面的监听器类实现了 ApplicationListener<ApplicationStartedEvent>接口,表明该监听器会监听ApplicationStartedEvent事件;而且该类实现了ApplicationContextAware接口,通过该接口可让
该监听器访问它所在的容器。
该监听器类中的onApplicationEvent()方法就是事件处理方法,SpringApplication的事件将会触发监听器的onApplicationEvent()方法,该方法中的粗体字代码对发布事件的容器与监听器所在的容器进行判断。当SpringApplication加载了层次结构的容器时,子容器发布的事件会触发父容器中的事件监听器,此时发布事件的容器和监听器所在的容器可能不是同一个容器。
接下来定义一个初始化器,初始化器可对 Spring 容器执行初始化设置。下面是该初始化器类的代码:
package com.bnuz.boot.app;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
public class MyInitializer implements
ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
System.out.println("模拟对Spring容器执行初始化");
}
}
初始化器在实现 ApplicationContextInitializer 接口时必须实现 initialize()方法,该方法可对Spring 容器执行任意初始化。
再定义一个配置环境后处理器,配置环境后处理器可对 Environment 进行设置,Spring Boot会使用设置、修改后的Environment来创建ApplicationContext(Spring容器)。配置环境后处理器最常见的用法就是:通过它来加载自定义的配置文件,该配置文件中的配置信息最终将被应用于Spring容器。
public class FkEnvironmentPostProcessor implements EnvironmentPostProcessor
{
private final PropertiesPropertySourceLoader loader = new PropertiesPropertySourceLoader();
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application)
{
// 指定自定义的配置文件
Resource path = new ClassPathResource("fk.properties");
// 加载自定义配置文件
PropertySource<?> ps = loadProperty(path);
System.out.println("fkjava.name: " + ps.getProperty("fkjava.name"));
System.out.println("fkjava.age: " +ps.getProperty("fkjava.age"));
// 将PropertySource中的属性添加到Environment配置环境中
environment.getPropertySources().addLast(ps);
}
private PropertySource<?> loadProperty(Resource path)
{
if (!path.exists())
{
throw new IllegalArgumentException("资源: " + path + " 不存在");
}
try
{
// 加载path对应的配置文件
return this.loader.load("custom-resource", path).get(0);
}
catch (IOException ex)
{
throw new IllegalStateException("加载配置文件出现错误: " + path, ex);
}
}
}
运行结果如下:
从上面的执行过程可以看到,由配置环境后处理器负责对 Environment 进行处理,因此在Environment被创建之后,该后处理器立即得到执行的机会,所以它可以在Spring Banner被打印之前执行。
当Spring容器被创建之后,容器初始化器会获得执行的机会,它可以对Spring容器进行初始化设置。当Spring容器被初始化之后,程序开始初始化容器中的Bean。
当容器被刷新之后,容器发布ApplicationStartedEvent事件,触发MyListener监听器,这就是上面的事件监听器、初始化器和配置环境后处理器的执行过程。
通过上面的执行过程可以看到,虽然Spring Boot进行了高度封装,但它依然提供了大量的初始化API:事件监听器`容器初始化器和配置环境后处理器。通过这些初始化API,开发者完全可以在恰当的时机,对Spring容器执行任何自己想要的初始化。
2.外部配置源
Spring Boot支持如下不同形式的配置源。
➢ 属性文件(前面介绍的application.properties就是属性文件)。
➢ YAML文件(后缀可以是.yml或.yaml)。
➢ 环境变量。
➢ 命令行参数。
获取这些外部化的属性主要有如下几种方式:
➢ 使用@Value注解将属性值注入任何Bean组件。
➢ 使用Spring的Environment抽象层进行访问。
➢ 使用@ConfigurationProperties注解将一批特定属性绑定到指定Java对象。
配置源的加载顺序与优先级
各种外部配置源的加载顺序如下,所有先加载的配置源都可能被后加载的配置源覆盖,因此可认为后加载的配置源的优先级更高:
(1)默认属性(通过SpringApplication.setDefaultProperties()方法指定)。
(2)配置类(@Configuration修饰的类)上用@PropertySource注解加载的属性文件中的属性值。注意:采用这种方式配置的属性只有等到Spring容器刷新时才会被添加到Environment中,因此通过这种方式来配置某些属性(如logging.*`spring.main.*等)就太迟了,因为这些属性需要在Spring容器刷新之前起作用。
(3)配置数据(如application.properties文件等)。
(4)RandomValuePropertySource,只包含random.*中的属性。
(5)操作系统环境变量。
(6)Java系统属性(System的getProperties()方法返回的属性)。
(7)来自java:comp/env的JNDI属性。
(8)ServletContext的初始化参数(在web.xml文件中通过<context-param.../>元素设置的初始化参数)。
(9)ServletConfig的初始化参数(在web.xml文件中通过<initparam.../>元素设置的初始化参数,或者通过@Servlet注解设置的初始化参数)。
(10)来自SPRING_APPLICATION_JSON的属性(嵌套在环境变量或系统属性中的JSON文本)。
(11)命令行参数。
(12)测试用例类上通过@SpringBootTest注解的properties所指定的属性。
(13)测试用例类上用@TestPropertySource注解加载的属性文件中的属性值。
(14)当Spring Boot的devtools工具处于激活状态时,用户Home目录中.config/spring-boot/子目录下spring-boot-devtools.properties或spring-boot-devtools.yml文件中设置的属性。
(7)(8) 只有在Web服务器或应用服务器中才有用;(12)(13)仅对单元测试有效;(14)需要在用户Home目录(在Windows平台下对应于“C:\Users\用户名”目录)下添加配置文件,因此上面这些配置项中优先级最高的可认为是命令行参数。
利用JSON参数配置(P59)
Spring Boot 还支持一个特殊的系统属性(环境变量):spring.application.json (SPRING_APPLICATION_JSON),通过这个特殊的系统属性或环境变量可以传入一段 JSON 文本,而这段JSON文本将会被解析成配置属性—该配置属性在加载顺序中排在第10位。
@SpringBootApplication
public class App
{
public static void main(String[] args)
{
// 设置spring.application.json系统属性
System.setProperty("spring.application.json",
"{\"fkjava\":{\"name\":\"疯狂软件\", \"age\":20, " +
"\"servers\":[\"fkjava.org\", \"crazyit.org\"]}}");
SpringApplication.run(App.class, args);
}
}
使用YAML配置文件
application.properties文件的另一种形式是application.yml,这两种文件只是载体形式不同,其本质是一样的。
Spring Boot 使用SnakeYAML来解析YAML文件,且spring-boot-starter自动依赖SnakeYAML,所以可以直接解析YAML文件。
有如下application.properties配置片段:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost/sys
spring.datasource.username=root
spring.datasource.password=123456
server.port=8081
如果写成YAML配置片段,则对应于如下形式:
application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost/sys
username: root
password: 123456
server:
port: 8080
如果程序要加载YAML文件配置的属性,则Spring Boot提供了如下工具类。
➢ YamlPropertiesFactoryBean:它将YAML文件加载为Properties对象。
➢ YamlMapFactoryBean:它将YAML文件加载为Map对象。
➢ YamlPropertySourceLoader:它将YAML文件加载为PropertySource。
需要说明的是,@PropertySource和@TestPropertySource这两个注解都只能加载属性文件,不能加载YAML文件,这是YAML文件的局限之一。
yml示例1
本例定义两份YAML文件,其中第一份是系统会自动加载的 application.yml文件,该文件的内容如下:
fkjava:
server:
name: lyq
post: 9000
第二份是系统不会自动加载的fk.yml文件,该文件的内容如下:
fkjava:
name: lyq01
age: 20
servers:
- www.baidu.com
- www.bilibili.com
修改在上面示例中配置环境后处理器来加载这份fk.yml文件,该后处理器的代码如下:
public class FKEnvironmentPostProcessor implements EnvironmentPostProcessor {
//创建了YamlPropertySourceLoader对象
private final YamlPropertySourceLoader loader =new YamlPropertySourceLoader();
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
//定义自定义配置文件
Resource path = new ClassPathResource("fk.yml");
PropertySource<?> ps = null;
try {
//调用了该对象的load()方法来加载YAML配置文件
ps = this.loader.load("custom-resource", path).get(0);
}
catch (IOException e){
e.printStackTrace();
}
System.out.println("fkjava.name:" + ps.getProperty("fkjava.name"));
System.out.println("fkjava.age:" + ps.getProperty("fkjava.age"));
System.out.println("fkjava.servers[0]:" + ps.getProperty("fkjava.servers[0]"));
System.out.println("fkjava.servers[1]" + ps.getProperty("fkjava.servers[1]"));
//将PropertySource中的属性添加到Environment配置环境中
environment.getPropertySources().addLast(ps);
}
}
这样一来,默认的application.yml和fk.yml文件都被加载进来,接下来即可在其他任何Bean组件(如控制器)中通过@Value注解来访问它们。例如如下代码:
@RestController
public class HelloController {
@Value("${fkjava.server.name}")
private String serverName;
@Value("${fkjava.server.post}")
private String serverPost;
@Value("${fkjava.age}")
private String age;
@GetMapping
public String hello(){
return "name: " + serverName + "; post: " + serverPost + "; age: " + age;
}
}
控制台看到如下输出:
通过浏览器访问“http://localhost:8080/”测试上面的hello方法,将看到如下输出:
注意事项
注意yaml文件中对应数字的定义支持进制书写格式,如果使用字符串请使用引号明确标注。
yml示例2
如果仅仅需要加载自定义的YAML文件,在普通组件中使用这些配置属性,并不需要将YAML文件中的属性添加到配置环境中,那么只要在容器中配置一个YamlPropertiesFactoryBean工厂Bean或YamlMapFactoryBean工厂Bean,它们就会自动读取YAML文件,并将其中的配置内容加载为Properties对象或Map对象。
添加如下配置类:
@Configuration
public class MyConfig {
//在配置类在容器中配置了一个YamlPropertiesFactoryBean工厂Bean
@Bean
public YamlPropertiesFactoryBean fkProps(){
//指定该工厂Bean要加载 fk.yml文件
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(new ClassPathResource("fk.yml"));
return factory;
}
}
Spring容器中的工厂Bean(实现FactoryBean接口的Bean)有一个特征:当程序通过Spring容器获取工厂Bean时,Spring容器实际返回的是该工厂Bean的产品(getObject()方法的返回值)。因此,当程序获取上面配置的fkProps时,实际返回的只是一个Properties对象。
控制器类修改如下:
@RestController
public class HelloController {
@Value("${fkjava.server.name}")
private String serverName;
@Value("${fkjava.server.post}")
private String serverPost;
@Resource(name = "fkProps")//name属性必须和工厂Bean的方法名一致
private Properties properties;
@GetMapping
public String hello(){
return "name: " + serverName
+ "; post: " + serverPost + "; age: "
+ properties.getProperty("fkjava.age");
}
上面程序中的粗体字代码指定将容器中的fkProps Bean注入fkProps实例变量,而fkProps就是上面配置类中配置的YamlPropertiesFactoryBean 的产品,也就是它所加载的 YAML 文件转换得到的Properties对象。
改变配置文件的位置
在默认情况下,Spring Boot 会自动按如下顺序加载默认的配置文件,后加载的属性文件可以覆盖先加载的属性文件,因此可认为后加载的属性文件具有更高的优先级:
(1)类加载路径的根路径
(2)类加载路径下的/config子目录
(3)当前路径
(4)当前路径下的/config子目录。
(5)当前路径下/config子目录的任意直接子目录,如/config/abc/、/config/xyz/等。
如果想改变这种默认行为,Spring Boot 可通过如下系统属性(或环境变量,或命令行参数)来改变配置文件的位置:
➢ spring.config.name:改变配置文件的文件名,默认是application。如果用OS环境变量来设置,则该属性对应于SPRING_CONFIG_NAME环境变量名。
➢ spring.config.location:改变配置文件的加载路径。如果用OS 环境变量来设置,则该属性对应于SPRING_CONFIG_LOCATION环境变量名。
➢ spring.config.additional-location:添加配置文件的加载路径,不会覆盖原有的配置文件的加载路径。
@SpringBootApplication(scanBasePackages = "com.bnuz.boot")
public class MainApplication {
static {
//设置配置文件的文件名
System.setProperty("spring.config.name", "application, fk");
//设置配置文件的加载路径
System.setProperty("spring.config.location", "classpath:/, optional:classpath:/fk/");
//添加额外的加载路径
//System.setProperty("spring.config.additional-location", "classpath:/, optional:classpath:/fk/");
}
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
}
}
上面的静态代码块里通过系统属性(System)设置了Spring Boot配置文件的主文件名可以是application和fk;配置文件的加载路径是classpath:/(代表类加载路径的根路径)和optional:classpath:/fk/(代表类加载路径下的/fk/子目录)。如果使用spring.config.additional-location系统属性,则只是添加新的配置文件的加载路径。
需要说明的是:optional:前缀,用于告诉Spring Boot不要检查该路径下是否存在配置文件,如果该路径下存在配置文件就加载,否则就忽略该路径。如果不添加optional:前缀,那么 Spring Boot 会强制检查该路径下是否存在配置文件;若该路径下不存在任何配置文件,则Spring Boot将会抛出ConfigDataLocationNotFoundException异常。
经过上面的设置,Spring Boot就能自动加载application.yml和fk.yml作为配置文件,这样其他Bean组件(如控制器)依然可通过@Value注解来访问这些配置属性。
导入额外的配置文件
在Spring容器刷新之后,Spring Boot还可使用如下方式导入额外的配置文件:
➢ 使用@PropertySource注解导入额外的属性文件。
➢ 使用spring.config.import属性导入额外的配置文件(包括属性文件和YAML文件)。
@SpringBootApplication
// 导入类加载路径下fk/crazyit.properties文件
@PropertySource("classpath:/fk/crazyit.properties")
public class App
{
public static void main(String[] args)
{
// 创建Spring容器、运行Spring Boot应用
SpringApplication.run(App.class, args);
}
}
application.yml:
fkjava:
server:
name: "疯狂软件服务器"
port: 9000
spring:
config:
# 指定导入类加载路径下fk/fk.yml文件
import: optional:classpath:/fk/fk.yml
使用占位符${}
在配置文件中可通过占位符(${})的方式来引用已定义的属性,或者引用其他配置源(如系统属性、环境变量、命令参数等)配置的属性。
fkjava:
server:
name: lyq
post: 9000
description: ${fkjava.server.name}
占位符里即可引用已有的属性,也可以引用配置文件中不存在的属性。
读取构建文件的属性
Spring Boot还允许配置文件读取构建文件(pom.xml或build.gradle)中的属性,可在配置文件中通过“@属性名@”来引用pom.xml文件中的属性。
app:
java:
version: @java.version@
配置随机值RandomValuePropertySource
在配置文件中使用${random.xxx}的形式即可生成各种随机值,不过这个 random 就是Spring Boot的RandomValuePropertySource。
yml文件如下:
fkjava:
secret: "${random.value}"
number: "${random.int}"
bignumber: "${random.long}"
uuid: "${random.uuid}"
number-less-than-ten: "${random.int(10)}"
number-in-range: "${random.int(20,100)}"
// 使用@Value注解访问配置属性
@Value("${fkjava.secret}")
private String secret;
@Value("${fkjava.number}")
private String number;
@Value("${fkjava.bignumber}")
private String bignumber;
@Value("${fkjava.uuid}")
private String uuid;
@Value("${fkjava.number-less-than-ten}")
private String numberLessThanTen;
@Value("${fkjava.number-in-range}")
private String numberInRange;
3.@ConfigurationProperties
基本使用
@Value注解每次只能读取一个配置属性,若需要整体读取多个属性,或者读取具有某种结构关系的一组属性,Spring Boot则提供了@ConfigurationProperties注解来进行处理。
@ConfigurationProperties注解有两种主要用法:
(1)修饰属性处理类:当@ConfigurationProperties注解修饰的类被部署为容器中的Bean时,该注解指定的属性将会被注入该Bean的属性。因此,将@ConfigurationProperties注解修饰的类称为“属性处理类”。
(2)修饰@Bean 注解修饰的方法:使用@Bean 修饰的方法将会配置一个容器中的 Bean,而
@ConfigurationProperties注解指定的属性将会被注入该Bean的属性。
在使用@ConfigurationProperties注解时可指定如下属性:
➢ prefix(或value):指定要加载的属性的前缀。
➢ ignoreInvalidFields():指定是否忽略无效属性值。比如处理类定义了某个字段的类型是Integer,但在配置文件中为该字段配置的值是abc,这就是无效的值。
➢ ignoreUnknownFields():指定是否忽略未知的字段值。如果在配置文件中配置的属性比处理类需要的属性更多,那么多出来的属性就属于未知属性。
application.yml:
servers:
ipaddr: 127.0.0.1
port: 8080
timeout: 100
接下来定义如下带@ConfigurationProperties注解的属性处理类来处理上面的配置信息:
//@ConfigurationProperties注解修饰CrazyitProperties类,因此该类将会被配置成容器中的Bean
//配置文件中以“org.crazyit”开头的属性将会被注入该Bean实例。
@Data
@Component
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {
private String ipAddr;
private int port;
private long timeout;
}
通过Spring容器获取该Bean,控制台输出如下:
@SpringBootApplication
@EnableConfigurationProperties
public class Springboot0101QuickstartApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context =
SpringApplication.run(Springboot0101QuickstartApplication.class, args);
ServerConfig serverConfig = context.getBean(ServerConfig.class);
System.out.println(serverConfig.toString());
}
}
注意的是,Spring Boot并不会自动启用@ConfigurationProperties注解。让Spring Boot启用该注解有如下方式:
(1)为@ConfigurationProperties注解修饰的类添加@Component注解,标注为一个Bean。
(2)将@ConfigurationProperties注解修饰的类显式配置成容器中的Bean。
(3)在启动类使用@EnableConfigurationProperties 注解,该注解可显式指定一个或多个属性处理类,Spring Boot将会启用这些属性处理类上的@ConfigurationProperties注解。
(注意,@EnableConfigurationProperties与@Component不能同时使用)
(4)使用@ConfigurationPropertiesScan 注解,该注解可指定启用一个或多个包及其子包下所有带@ConfigurationProperties注解的类。
解除使用@ConfigurationProperties注解警告
如果出现如上警告,可以在pom.xml里面加载如下坐标:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
宽松绑定
配置的属性为 org.crazyit.remote-address,这与 CrazyitProperties 类中定义的remoteAddress属性并不完全相同,Spring Boot也能成功注入:因为Spring Boot支持所谓的宽松绑定(Relaxed Binding)。
属性 | 说明 |
com.bnuz.remote-address | “烧串”写法。这是*.properties和*.yml配置文件的推荐写法 |
com.bnuz.remoteAddress | 标准驼峰写法 |
com.bnuz.remote_address | 下画线写法。这是*.properties和*.yml配置文件的可选写法 |
COM.BNUZ.REMOTEADDRESS | 大写字母格式。如果使用系统环境变量来配置属性,则这是推荐的写法 |
宽松绑定不支持@Value注解引用单个注解的情况;
第三方Bean属性绑定
如果想为第三方框架里的Bean配置属性,可以用@ConfigurationProperties 注解修饰@Bean 注解修饰的方法,这样Spring Boot将会读取@ConfigurationProperties注解加载的配置属性,并将属性值注入该@Bean方法所配置的Bean组件。如下假设Book就是第三方Bean:
@Configuration
public class MyConfig
{
@Bean
// @ConfigurationProperties注解会驱动Spring自动调用该Bean的setter方法
@ConfigurationProperties("fkjava.book")
public Book book()
{
return new Book();
}
}
从本质上看,@ConfigurationProperties 注解的作用就是驱动被修饰的@Bean 方法所配置的对象调用相应的setter方法,比如@ConfigurationProperties注解读取到title属性,它就会驱动Spring Boot以反射方式执行@Bean方法所配置的对象的setTitle()方法,并将title属性值作为setTitle()方法的参数。
属性转换
Spring Boot内置了常用的类型转换机制,例如,从前面的示例看到,Spring Boot可将配置属性值自动转换为int、double类型。
在默认情况下,如果转换失败,Spring Boot应用启动将会失败,并抛出异常。如果希望 Spring Boot 忽略转换失败的配置属性值,则可将@ConfigurationProperties注解的ignoreInvalidFields属性设置为true(它的默认值为false),这里前面内容也有提到。
此外,Spring Boot还可自动转换如下类型。
➢ Duration:Spring Boot可自动将配置属性值转换为Duration类型,支持为属性值指定单位。
➢ Period:Spring Boot可自动将配置属性值转换为Period类型,支持为属性值指定单位。
➢ DataSize:Spring Boot可自动将配置属性值转换为DataSize类型,支持为属性值指定单位。
属性校验
Spring Boot 还可对属性处理类进行校验,只要为属性处理类添加@Validated 注解,并使用JSR 303的校验注解修饰需要校验的实例变量,Spring Boot就会自动校验配置文件中的属性值。如果某个属性值不能通过校验,Spring Boot应用启动将会失败,并用FailureAnalyzer显示校验错误信息。
@ConfigurationProperties的数据校验是基于JSR 303的,因此在执行数据校验之前,必须先添加 JSR 303 规范的依赖以及 JSR 303实现的依赖。Spring Boot 为数据校验提供了spring-boot-starter-validation依赖库,它已经包含了JSR 303规范和实现的依赖,因此只要在pom.xml文件中添加如下依赖库即可。
<!-- 添加Spring Boot Validation依赖库 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
// 指定读取以org.crazyit.*开头的属性
@ConfigurationProperties(prefix = "org.crazyit", ignoreUnknownFields=false)
@Component
@Validated
@Data
public class CrazyitProperties
{
@NotEmpty
private String name;
@Range(max = 150, min=90, message = "价格必须位于90~150之间")
private double price;
@Pattern(regexp = "[1][3-8][0-9]{9}", message = "必须输入有效的手机号")
private String mobile;
@Valid
private final Item item = new Item();
public static class Item
{
@Length(min=5, max=10, message = "品牌名长度必须在5到10个字符")
private String brand;
@Size(min = 1, message = "comments至少包含一个元素")
private List<String> comments = new ArrayList<>(Collections.singleton("GREAT"));
}
}
运行后发现报如下错误,要你去导入类似Hibernate Validator:
去pom.xml导入如下坐标:
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
@SpringBootTest测试
加载测试专用属性
@SpringBootTest(properties = {"test.prop=testValue1"})
public class PropertiesAndArgsTest {
@Value("${test.prop}")
private String msg;
@Test
void testProperties() {
System.out.println(msg);
}
}
使用 properties = {"test.prop=testValue"} 会覆盖掉配置文件里面的test.prop。
Web环境模拟测试
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc//开启虚拟调用
public class PropertiesAndArgsTest {
/**
* 匹配接口是否正常
* @param mockMvc
* @throws Exception
*/
@Test
void testById(@Autowired MockMvc mockMvc) throws Exception {
MockHttpServletRequestBuilder build = MockMvcRequestBuilders.get("/books");
ResultActions resultActions = mockMvc.perform(build);
//设定预期值,与真实值进行比较,成功测试通过,失败测试失败
StatusResultMatchers statusResultMatchers = MockMvcResultMatchers.status();
ResultMatcher ok = statusResultMatchers.isOk();//预计本次调用时成功的状态200
resultActions.andExpect(ok);
//匹配响应头是否正确
HeaderResultMatchers header = MockMvcResultMatchers.header();
ResultMatcher contentType = header.string("Content-Type", "application/json");
resultActions.andExpect(contentType);
//匹配json数据是否正确
ContentResultMatchers content = MockMvcResultMatchers.content();
ResultMatcher resultMatcher = content.string("这里填返回json结果");//定义本次调用的预期值
resultActions.andExpect(resultMatcher);
}
}
如果没有得到正确响应,控制台会有详细信息:
springBoot is running
MockHttpServletRequest:
HTTP Method = GET
Request URI = /books
Parameters = {}
Headers = []
Body = null
Session Attrs = {}
Handler:
Type = com.bnuz.controller.BookController
Method = com.bnuz.controller.BookController#getById()
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"21"]
Content type = text/plain;charset=UTF-8
Body = springBoot is running
Forwarded URL = null
Redirected URL = null
Cookies = []
java.lang.AssertionError: Response header 'Content-Type' expected:<application/json> but was:<text/plain;charset=UTF-8>
Expected :application/json
Actual :text/plain;charset=UTF-8
<Click to see difference>
数据层测试回滚
@SpringBootTest
@Transactional//增加事务,表示回滚
@Rollback(false) //这个注解表示想在测试里面提交数据,默认为TRUE即回滚
public class DaoTest {
@Autowired
private EmployeeService employeeService;
@Test
void testSaveEmp() {
Employee employee = new Employee();
employee.setName("lyq");
employee.setPassword("123456");
employee.setId(2L);
employee.setIdNumber("2");
employee.setPhone("134211");
employee.setSex("nam");
employee.setStatus(1);
employeeService.save(employee);
}
}
这样可以避免经常在数据库里面留下脏数据。