在学习spring中,发现一段代码 定义接口 public interface BrandDao { @Select("select * from brand where id = #{id}") //配置数据库字段和模型实体类属性映射 @Results({ @Result(column = "brand_name", property = "brandName"), @Result(column = "company_name", property = "companyName") }) Brand findById(Integer id); } 定义一个service类 @Service public class BrandService { @Autowired private BrandDao brandDao; public Brand findById(Integer id) { return brandDao.findById(id); } } 执行方法 public class App { public static void main(String[] args) { ApplicationContext ioc = new AnnotationConfigApplicationContext(SpringConfig.class); BrandService brandService = ioc.getBean(BrandService.class); Brand brand = brandService.findById(2); System.out.println(brand); } } 上面代码可以正常运行,就是BrandDao接口他并没有实现类,为什么可以通过@Autowired进行注入? PS:应该是SpringConfig代码的这段已经有bean了: //定义bean,返回MapperScannerConfigurer对象 @Bean public MapperScannerConfigurer mapperScannerConfigurer(){ MapperScannerConfigurer msc = new MapperScannerConfigurer(); msc.setBasePackage("com.test.dao");//BrandDao接口属于这个包下 return msc; }
这个问题该怎么解决 "image.png" (https://wmprod.oss-cn-shanghai.aliyuncs.com/images/20241225/ade5a0243fb2cdb85f2b68bcb7cb18d4.png)
服务端代码如下: /** * 当有客户端与服务器连接时执行此方法 * 1.打印提示信息 * 2.将客户端ip和连接通道存储到remoteAddressChannleMap */ @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { Channel channel = ctx.channel(); // 客户端建立连接的时候,保存其ip和通道 channel.remoteAddress().toString():/127.0.0.1:12173 InetSocketAddress remoteAddress = (InetSocketAddress) channel.remoteAddress(); System.err.println("有新的客户端与服务器发生连接。客户端地址:" + channel.remoteAddress()); remoteAddressChannelMap.put(remoteAddress.toString().substring(1), channel); System.out.println("remoteAddressChannleMap 的 size:" + remoteAddressChannelMap.size()); // channelGroup.add(channel); } 我们想在客户端连接服务端时保存这个channel,发现客户端的IP地址没有变化,但是端口却一直在变化,本来以为是程序占用的端口,但是cmd查询发现没有任何进程占用。请问是这到底怎么回事?
支付包的alipay-sdk-java有很多漏洞,都是很长时间的问题了,其中有的第三方包估计都放弃维护了,为什么不修复呢?"image.png" (https://wmprod.oss-cn-shanghai.aliyuncs.com/images/20250121/463fba4a6c160baa0580e717b06c557f.png) "image.png" (https://wmprod.oss-cn-shanghai.aliyuncs.com/images/20250121/f2125197985354a568bb4dedb5b3c97d.png)更新的频率也不慢,历史遗留问题吗?按理说第三方sdk应该要处理好这些问题吧!还是支付类的sdk
1. 文档预览2. 开始本节对Spring Boot进行介绍 以及如何安装,我们将引导您构建第一个Spring Boot 应用,同时讨论一些核心准则。2.1 Spring Boot 介绍Spring Boot 帮助您创建可以独立运行的,生产级的Spring 应用程序。您创建的Spring Boot 应用程序,可以通过java -jar 或者 传统的war包方式启动,另外还提供了一个运行spring scripts的命令行工具。2.2 系统要求Spring Boot 2.7.8 需要Java8 ,兼容Java19,Spring 版本5.3.25或更高。构建工具版本Maven3.5+Gradle6.8.x, 6.9.x, and 7.x2.2.1 Servlet 容器Spring Boot 支持如下嵌入式servlet容器:名称Servlet 版本Tomcat 9.04.0Jetty 9.43.1Jetty 10.04.0Undertow 2.04.0您也可以将Spring Boot部署到任何兼容servlet 3.1+的容器中。2.3 Spring Boot 安装安装之前使用java -version检查 Java 版本,Spring Boot 2.7.8 需要Java8 或更高的版本。2.3.1 面向Java开发人员Maven 安装Spring Boot 依赖项使用org.springframework.boot groupId。通常,您的Maven POM文件继承自spring-boot-starter-parent ,并声明一个或多个“Starters”的依赖关系。另外Spring Boot 还提供了可选的Maven 插件来创建可执行的jar,更多信息参考 Spring Boot Maven 插件文档。Gradle 安装同Maven,Spring Boot 也提供了一个 Gradle插件,用于创建可执行的jar,更多信息参考 Spring Boot Gradle 插件文档。2.3.2 安装Spring Boot CLISpring Boot CLI是一个命令行工具,可用于快速创建Spring Boot 初始化应用程序,这在没有IDE的情况下非常有用。手动安装您可以从如下地址下载Spring CLI发行版本:spring-boot-cli-2.7.8-bin.zipspring-boot-cli-2.7.8-bin.tar.gz另外提供了 快照列表下载后,按照解压缩存档中的INSTALL.txt说明进行操作。.zip文件的bin/目录中有一个spring脚本(适用于Windows的spring.bat),或者可以使用jar -jar 运行 jar包。使用SDKMAN安装SDKMAN(软件开发工具包管理器)可用于管理各种二进制SDK版本,包括Groovy和Spring Boot CLI。从sdkman.io获取并使用以下命令安装 Spring Boot:$ sdk install springboot $ spring --version Spring CLI v2.7.8如果您为 CLI 开发功能并希望访问您构建的版本,请使用以下命令:$ sdk install springboot dev /path/to/spring-boot/spring-boot-cli/target/spring-boot-cli-2.7.8-bin/spring-2.7.8/ $ sdk default springboot dev $ spring --version Spring CLI v2.7.8前面的说明安装了一个spring名为instance 的本地dev实例。它指向您的目标构建位置,因此每次您重建 Spring Boot 时,spring它都是最新的。您可以通过运行以下命令来查看它:$ sdk ls springboot ================================================================================ Available Springboot Versions ================================================================================ > + dev * 2.7.8 ================================================================================ + - local version * - installed > - currently in use ================================================================================使用OSX Homebrew安装在Mac上可以使用Homebrew安装。$ brew tap spring-io/tap $ brew install spring-bootHomebrew 安装 spring 到 /usr/local/bin.如果找不到这个命令,尝试使用brew update更新后重试使用MacPorts安装在Mac上使用MacPorts 安装。$ sudo port install spring-boot-cli使用 Windows Scoop安装在Window使用Scoop安装。> scoop bucket add extras > scoop install springbootScoop 安装 spring 到 ~/scoop/apps/springboot/current/bin如果提示命令不存,请使用scoop update更新后再重试Spring CLI 快速启动示例首先创建一个名为app.groovy的文件。@RestController class ThisWillActuallyRun { @RequestMapping("/") String home() { "Hello World!" } }然后使用如下命令运行:$ spring run app.groovy第一次运行需要下载依赖,会比较慢,后面运行会快很多。最后,使用浏览器打开localhost:8080,输出Hello World!2.4 开发第一个Spring Boot 应用程序建议使用start.spring.io 创建Spring Boot 应用程序。3. 升级Spring Boot3.1 从1.x升级从1.x升级,可以查看GitHub wiki上的升级指南3.2 升级到最新的功能版本Spring Boot提供了一种方法来分析应用程序的环境并在启动时打印诊断信息,还可以在运行时临时迁移属性,要启动该功能,在项目中添加以下依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-properties-migrator</artifactId> <scope>runtime</scope> </dependency>完成升级后,删除该依赖。4. Spring Boot开发4.1 构建系统可以使用Maven、Gradle、Ant 构建系统4.1.1 Starters所有官方启动器都遵循类似的命名模式:spring-boot-starter-*,其中*是特定类型的应用程序,如果是自己创建的启动器一般以项目名称开头,如thirdpartyproject-spring-boot-starter。Spring Boot 提供了以下应用启动器org.springframework.boot:名称描述spring-boot-starterCore starter,包括自动配置支持、日志记录和 YAMLspring-boot-starter-activemq使用 Apache ActiveMQ 的 JMS 消息传递启动器spring-boot-starter-amqp使用 Spring AMQP 和 Rabbit MQ 的启动器spring-boot-starter-aop使用 Spring AOP 和 AspectJ 进行面向方面编程的入门spring-boot-starter-artemis使用 Apache Artemis 的 JMS 消息传递启动器spring-boot-starter-batch使用 Spring Batch 的启动器spring-boot-starter-cache使用 Spring Framework 的缓存支持的 Starterspring-boot-starter-data-cassandra使用 Cassandra 分布式数据库和 Spring Data Cassandra 的 Starterspring-boot-starter-data-cassandra-reactive使用 Cassandra 分布式数据库和 Spring Data Cassandra Reactive 的 Starterspring-boot-starter-data-couchbase使用 Couchbase 面向文档的数据库和 Spring Data Couchbase 的启动器spring-boot-starter-data-couchbase-reactive使用 Couchbase 面向文档的数据库和 Spring Data Couchbase Reactive 的 Starterspring-boot-starter-data-elasticsearch使用 Elasticsearch 搜索和分析引擎以及 Spring Data Elasticsearch 的 Starterspring-boot-starter-data-jdbc使用 Spring Data JDBC 的启动器spring-boot-starter-data-jpa将 Spring Data JPA 与 Hibernate 一起使用的启动器spring-boot-starter-data-ldap使用 Spring Data LDAP 的启动器spring-boot-starter-data-mongodb使用 MongoDB 面向文档的数据库和 Spring Data MongoDB 的启动器spring-boot-starter-data-mongodb-reactive使用 MongoDB 文档型数据库和 Spring Data MongoDB Reactive 的 Starterspring-boot-starter-data-neo4j使用 Neo4j 图形数据库和 Spring Data Neo4j 的启动器spring-boot-starter-data-r2dbc使用 Spring Data R2DBC 的启动器spring-boot-starter-data-redis用于将 Redis 键值数据存储与 Spring Data Redis 和 Lettuce 客户端一起使用的 Starterspring-boot-starter-data-redis-reactive将 Redis 键值数据存储与 Spring Data Redis 反应式和 Lettuce 客户端一起使用的启动器spring-boot-starter-data-rest使用 Spring Data REST 通过 REST 公开 Spring Data 存储库的 Starterspring-boot-starter-freemarker使用 FreeMarker 视图构建 MVC Web 应用程序的启动器spring-boot-starter-graphql使用 Spring GraphQL 构建 GraphQL 应用程序的 Starterspring-boot-starter-groovy-templates使用 Groovy 模板视图构建 MVC web 应用程序的启动器spring-boot-starter-hateoas使用 Spring MVC 和 Spring HATEOAS 构建基于超媒体的 RESTful Web 应用程序的启动器spring-boot-starter-integration使用 Spring Integration 的启动器spring-boot-starter-jdbc将 JDBC 与 HikariCP 连接池一起使用的启动器spring-boot-starter-jersey使用 JAX-RS 和 Jersey 构建 RESTful Web 应用程序的启动器。的替代品spring-boot-starter-webspring-boot-starter-jooq使用 jOOQ 通过 JDBC 访问 SQL 数据库的启动器。替代spring-boot-starter-data-jpa或spring-boot-starter-jdbcspring-boot-starter-json读写json的starterspring-boot-starter-jta-atomikos使用 Atomikos 的 JTA 事务启动器spring-boot-starter-mail使用 Java Mail 和 Spring Framework 的电子邮件发送支持的 Starterspring-boot-starter-mustache使用 Mustache 视图构建 Web 应用程序的启动器spring-boot-starter-oauth2-client使用 Spring Security 的 OAuth2/OpenID Connect 客户端功能的 Starterspring-boot-starter-oauth2-resource-server使用 Spring Security 的 OAuth2 资源服务器功能的启动器spring-boot-starter-quartz使用 Quartz 调度器的启动器spring-boot-starter-rsocket用于构建 RSocket 客户端和服务器的启动器spring-boot-starter-security使用 Spring Security 的启动器spring-boot-starter-test用于使用 JUnit Jupiter、Hamcrest 和 Mockito 等库测试 Spring Boot 应用程序的 Starterspring-boot-starter-thymeleaf使用 Thymeleaf 视图构建 MVC Web 应用程序的启动器spring-boot-starter-validation将 Java Bean Validation 与 Hibernate Validator 结合使用的 Starterspring-boot-starter-web用于使用 Spring MVC 构建 Web(包括 RESTful)应用程序的 Starter。使用 Tomcat 作为默认的嵌入式容器spring-boot-starter-web-services使用 Spring Web 服务的启动器spring-boot-starter-webflux用于使用 Spring Framework 的 Reactive Web 支持构建 WebFlux 应用程序的 Starterspring-boot-starter-websocket使用 Spring Framework 的 MVC WebSocket 支持构建 WebSocket 应用程序的 Starter除了应用程序启动器之外,还可以使用以下启动器来添加*生产就绪*功能:名称描述spring-boot-starter-actuator使用 Spring Boot Actuator 的 Starter,它提供生产就绪功能来帮助您监控和管理您的应用程序最后,Spring Boot 还包括以下启动器:名称描述spring-boot-starter-jetty使用 Jetty 作为嵌入式 servlet 容器的启动器的替代品spring-boot-starter-tomcatspring-boot-starter-log4j2使用 Log4j2 进行日志记录的启动器的替代品spring-boot-starter-loggingspring-boot-starter-logging使用 Logback 进行日志记录的启动器。默认日志记录启动器spring-boot-starter-reactor-netty使用 Reactor Netty 作为嵌入式响应式 HTTP 服务器的启动器。spring-boot-starter-tomcat将 Tomcat 用作嵌入式 servlet 容器的启动器。使用的默认 servlet 容器启动器spring-boot-starter-webspring-boot-starter-undertow使用 Undertow 作为嵌入式 servlet 容器的启动器的替代品spring-boot-starter-tomcat其他社区贡献的starter列表,请参阅GitHub 上模块 中的自述文件。spring-boot-starters4.2 构建代码Spring Boot 没有固定的代码布局,但是有些实践提供参考。4.2.1 "default"包当一个类不包含package时,它被认为在“default package”中。通常不建议使用“default package”,它可能会导致@ComponentScan、@ConfigurationPropertiesScan、@EntityScan或@SpringBootApplication 注解出现问题,我们应该遵循推荐的包命名方式,比如com.example.project4.2.2 主程序类通常建议将主程序类放在其他类之上的根包中,@SpringBootApplication通常放在主类中,其隐式的定义了基本的包搜索功能,其内部引入了@EnableAutoConfiguration和@ComponentScan。下面是一个典型的布局:com +- example +- myapplication +- MyApplication.java | +- customer | +- Customer.java | +- CustomerController.java | +- CustomerService.java | +- CustomerRepository.java | +- order +- Order.java +- OrderController.java +- OrderService.java +- OrderRepository.javaMyApplication.java 定义了一个main方法以及@SpringBootApplication,如下所示:import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } }4.3 配置类Spring Boot 支持使用Java进行配置,虽然SpringApplication 跟XML可以一起使用,但还是建议@Configuration是独立的类。4.3.1 导入其他的配置类你不需要把所有的@Configuration 放到一个类中,该@Import注解可用于导入其他的配置类,或者可以使用@ComponentScan自动获取所有的Spring 组件,包括@Configuration类。4.3.2 导入XML配置如果你还是要使用XML配置,依然建议使用@Configuration类,然后使用@ImportResource来加载XML配置。4.4 自动配置Spring Boot会尝试将starter自动配置到应用程序,比如引入了HSQLDB的starter,但是没有手动配置任何数据库连接bean,那么Spring Boot 会自动配置一个内存数据库。开启自动配置,需要添加@EnableAutoConfiguration或者@SpringBootApplication。4.4.1 替换自动配置自动配置是非侵入式的,任何时候都可以使用自定义配置替换自动配置的指定部分,比如,添加了DataSource bean,默认的嵌入式数据库就会被替换。使用--debug启动应用程序,可以打印出当前应用了哪些自动配置。4.4.2 禁用指定的自动配置类如果想要禁用指定的自动配置类,可以使用@SpringBootApplication的exclude属性,如:import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }) public class MyApplication { }如果排除的类不在类路径中,可以使用excludeName指定类的完全限定名,另外如果不用@SpringBootApplication,@EnableAutoConfiguration的exclude和excludeName也是可用的。最后也能用spring.autoconfigure.exclude的配置来排除自动配置类。4.5 Spring Beans和依赖注入通常建议使用构造函数注入依赖项,和@ComponentScan查找bean。如果是按照4.2的方式构建的代码,则可以使用@ComponentScan不带任何参数或者使用@SpringBootApplication其已经包含了@ComponentScan注解,这样所有的组件(@Component、@Service、@Repository、@Controller和其他)都会自动注册为Spring Beans。如下示例表示一个@Service使用构造函数来注入RiskAssessor Bean。import org.springframework.stereotype.Service; @Service public class MyAccountService implements AccountService { private final RiskAssessor riskAssessor; public MyAccountService(RiskAssessor riskAssessor) { this.riskAssessor = riskAssessor; } // ... }如果一个Bean有多个构造函数,需要使用@Autowired标记哪个需要Spring 注入:import java.io.PrintStream; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class MyAccountService implements AccountService { private final RiskAssessor riskAssessor; private final PrintStream out; @Autowired public MyAccountService(RiskAssessor riskAssessor) { this.riskAssessor = riskAssessor; this.out = System.out; } public MyAccountService(RiskAssessor riskAssessor, PrintStream out) { this.riskAssessor = riskAssessor; this.out = out; } // ... }使用构造函数注入应该使用 final标记,表示后面不能再被修改。4.6 使用@SpringBootApplication 注解使用@SpringBootApplication注解可以启用如下三个功能:@EnableAutoConfiguration: 启用Spring Boot 的自动配置机制@ComponentScan @Component:在应用程序所在的包上启用扫描@SpringBootConfiguration: 允许在上下文中注册额外的 beans 或导入额外的配置类。Spring 标准的替代方案@Configuration,有助于在集成测试中进行配置检测。import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; // Same as @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan @SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } }如果不想使用@SpringBootApplication,也可以单独使用注解,如下示例并未使用@ComponentScan 自动扫描功能,而使用显示导入(@Import):import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Import; @SpringBootConfiguration(proxyBeanMethods = false) @EnableAutoConfiguration @Import({ SomeConfiguration.class, AnotherConfiguration.class }) public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } }4.7 运行应用程序4.7.1 从IDE运行4.7.2 作为打包应用程序运行使用java -jar运行:$ java -jar target/myapplication-0.0.1-SNAPSHOT.jar也可以附加远程调式器:$ java -Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=8000,suspend=n \ -jar target/myapplication-0.0.1-SNAPSHOT.jar4.7.3 使用Maven插件Spring Boot Maven 插件包含一个run命令:$ mvn spring-boot:run另外还可以使用MAVEN_OPTS 设置环境变量:$ export MAVEN_OPTS=-Xmx1024m4.7.4 使用Gradle插件Gradle插件包含一个bootRun命令:$ gradle bootRun使用JAVA_OPTS设置环境变量:$ export JAVA_OPTS=-Xmx1024m4.7.5 热插拨Spring Boot 的热插拨基于JVM,JVM在某种程序上受限于可以替换的字节码,对于完整方案可以使用JRebel 。spring-boot-devtools模块还包括对应用程序快速重启的支持,详细信息查看后面的热插拔“操作方法”。4.8 开发者工具Spring Boot 提供spring-boot-devtools 模块提供开发时的额外功能,要支持该功能,需要将依赖添加到项目中:Maven<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> </dependencies>Gradledependencies { developmentOnly("org.springframework.boot:spring-boot-devtools") }默认情况下,打包的应用程序不包含devtools,如果想要使用某个远程devtool特性,在Maven插件中配置,excludeDevtools为false,Gradle插件中配置task任务以包含developmentOnly,如tasks.named("bootWar") { classpath configurations.developmentOnly } 当打包生产应用程序时,开发者工具将被自动禁用。如果您的应用程序是从一个特殊的类加载器启动的或者 使用java -jar,那么会被认为是一个"生产的应用程序"。可以使用spring.devtools.restart.enabled来控制,要开启devtools,使用-Dspring.devtools.restart.enabled=true启动,要禁用devtools,排除依赖项或者使用-Dspring.devtools.restart.enabled=false启动。Maven 中使用optional,Gradle 使用 developmentOnly,表示可以防止devtools被传递到项目的其他模块。4.8.1 诊断类加载问题开发者工具的重启功能是通过使用两个类加载器实现的,对于大不多应用程序效果很好,但是有时候会导致类加载问题,特别是在多模块项目中。要判断是不是由于这个问题,可以尝试禁用重启,使用spring.devtools.restart.enabled=false属性禁用它。另外可以 自定义重启类加载器,自定义由哪个类加载加载,详见[4.8.3自动重启](#4.8.3 自动重启)。4.8.2 属性默认值Spring Boot 的一些库使用缓存来提高性能,比如,模版引擎会缓存编译后的模版,以此避免重复解析,但这样在开发过程中我们就不能即时看到模版的变更。spring-boot-devtools 默认禁用了缓存。下表列出了所有应用的属性:名称默认值server.error.include-binding-errorsalwaysserver.error.include-messagealwaysserver.error.include-stacktracealwaysserver.servlet.jsp.init-parameters.developmenttrueserver.servlet.session.persistenttruespring.freemarker.cachefalsespring.graphql.graphiql.enabledtruespring.groovy.template.cachefalsespring.h2.console.enabledtruespring.mustache.servlet.cachefalsespring.mvc.log-resolved-exceptiontruespring.template.provider.cachefalsespring.thymeleaf.cachefalsespring.web.resources.cache.period0spring.web.resources.chain.cachefalse如果不想应用属性默认值,可以在应用程序配置文件中配置spring.devtools.add-properties=false在开发WEB应用的时候,可以开启DEBUG日志,这样会显示请求、正在处理的程序,响应结果和其他详细信息,如果希望显示所有的详细信息(比如潜在的敏感信息),可以打开spring.mvc.log-request-details或spring.codec.log-request-details。笔者注:开启spring.mvc.log-request-details 后的日志关闭spring.mvc.log-request-details后的日志:4.8.3 自动重启只要类路径上的文件发生变更,使用了spring-boot-devtools的应用程序就会自动重启,但是某些资源(如静态资源和视图模版)不需要重启应用程序。触发重启的方法:由于DevTools 通过监听类路径上的资源来触发重启,所以不管使用哪个IDE都需要重新编译后才能触发重启:Eclipse 中,保存修改后会更新类文件并触发重启IDEA中,通过Build 触发或者编辑项目的Edit Configurations -> On Update action:Update classes and resources也可以触发重启使用构建工具,mvn compile或者gradle build可以触发重启笔者注:官方文档提示:使用Maven或者Gradle时,需要将forking设置为enabled,才能触发重启。实测,新版本的spring-boot-maven-plugin在项目引入spring-boot-devtools后会自动开启fork,如图:并且插件的注释也标记为过期,将在3.0.0中彻底删除:在重启期间 DevTools 依赖应用上下文的 shutdown hook 来关闭,如果设置为SpringApplication.setRegisterShutdownHook(false),就会导致其无法正常工作。笔者注:在笔者按照这样设置后,发现自动重启并无失效public static void main(String[] args) { SpringApplication application = new SpringApplication(SpringBootDemoApplication.class); application.setRegisterShutdownHook(false); application.run(args); } AspectJ 切面不支持自动重启重新启动与重新加载Spring Boot 的重启技术通过使用两个类加载器来工作的,不会更改的类(如:第三方jar的类)被加载到基类加载器中,频繁修改的类被加载到一个重启类加载器中。当应用程序重启时,旧的重启类加载器被丢弃并创建一个新的类加载器,这种方法会被“冷启动”快得多,因为基类加载器已经可用。如果自动重启还是比较慢的,或者遇到类加载问题,可用尝试使用重新加载技术,如JRebel,他们通过加载类时重写类来获得更快的速度。记录条件评估中的变化默认每次自动重启应用程序的时候,都会显示一份对自动配置的变更报告(比如添加或删除bean或者设置配置属性)禁用报告设置:spring.devtools.restart.log-condition-evaluation-delta=false笔者注:开启时候的报告示例:排除资源某些资源在更改时并不会触发自动重启,默认情况下更改 /META-INF/maven, /META-INF/resources, /resources, /static, /public, /templates目录下的资源不会触发重启但是会触发[实时加载](#4.8.4 实时加载),如果要自定义这些排除项,可以使用spring.devtools.restart.exclude属性,比如仅排除/static和/public目录:spring.devtools.restart.exclude=static/**,public/**如果要保留默认的配置,并且添加新的排除项,使用spring.devtools.restart.additional-exclude。监听其他路径文件如果要监听不在类路径中的文件时,使用spring.devtools.restart.additional-paths属性。另外可以配合spring.devtools.restart.exclude来设置其他路径下的文件变更是触发重启还是实时加载。禁用重启使用spring.devtools.restart.enabled禁用重启,如果在application.properties配置,重启类加载器还是会初始化,只是不会监听文件的变更,要完全禁用需要设置系统变量spring.devtools.restart.enabled为false,如下:import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MyApplication { public static void main(String[] args) { System.setProperty("spring.devtools.restart.enabled", "false"); SpringApplication.run(MyApplication.class, args); } }使用触发式文件使用某个指定的文件变更来触发自动重启,使用spring.devtools.restart.trigger-file配置指定文件(不包括路径),该文件必须在类路径下。比如:有这样一个结构的项目:src +- main +- resources +- .reloadtrigger那么trigger-file的配置是spring.devtools.restart.trigger-file=.reloadtrigger自定义重启类加载器默认情况下,IDE中打开的项目都使用重启类加载器,其他.jar文件使用基类加载器。使用mvn spring-boot:run或者gradle bootRun也是这样。可以通过META-INF/spring-devtools.properties文件来自定义,spring-devtools.properties文件包含前缀为restart.exclude和restart.include的属性,include属性被重启类加载器加载,exclude属性被基类加载器排除,该属性适用类路径的正则表达式,如:restart.exclude.companycommonlibs=/mycorp-common-[\\w\\d-\\.]+\\.jar restart.include.projectcommon=/mycorp-myproj-[\\w\\d-\\.]+\\.jar键必须是唯一的,只要是restart.exclude和restart.include开头的属性都会被考虑。META-INF/spring-devtools.properties的内容可以打包中项目中,也可以打包到库中。已知限制对于使用标准ObjectInputStream反序列化的对象,重新启动功能不起作用。如果您需要反序列化数据,则可能需要将Spring的ConfigurableObjectInputStream与Thread.currentThread().getContextClassLoader()结合使用。笔者注:这个点我觉得略过即可4.8.4 实时加载spring-boot-devtools包含一个嵌入式的LiveReload服务器,可用于资源变更时实时触发浏览器刷新。LiveReload 浏览器扩展可从livereload.com免费获得 Chrome、Firefox 和 Safari 。如果您不想在应用程序运行时启动 LiveReload 服务器,您可以将该spring.devtools.livereload.enabled属性设置为false.您一次只能运行一个 LiveReload 服务器。在启动您的应用程序之前,请确保没有其他 LiveReload 服务器正在运行。如果您从 IDE 启动多个应用程序,则只有第一个应用程序支持 LiveReload。笔者注:这个点我觉得略过即可,浏览器手动刷新一下也不费事4.8.5 全局设置可以通过将以下任何文件添加到$HOME/.config/spring-boot目录来配置全局 devtools 设置:spring-boot-devtools.propertiesspring-boot-devtools.yamlspring-boot-devtools.yml添加到该文件的任何配置都适用于该机器上的所有Spring Boot 应用程序,例如,要将自动重启配置为使用触发式文件,可以这样配置:spring.devtools.restart.trigger-file=.reloadtrigger默认情况下,$HOME是用户的主目录。要自定义此位置,请设置SPRING_DEVTOOLS_HOME环境变量或spring.devtools.home系统属性。如果在$HOME/.config/spring-boot中找不到 devtools 配置文件,则会在根$HOME目录中搜索是否存在.spring-boot-devtools.properties文件。这允许您与不支持该$HOME/.config/spring-boot位置的旧版本 Spring Boot 上共享 devtools 全局配置。在.spring-boot-devtools.properties中的配置都不会影响其他的应用配置文件(如application-{profile}之类的文件),并且不支持spring-boot-devtools-.properties和spring.config.activate.on-profile 之类的配置。配置文件监听器FileSystemWatcher通过一定的时间间隔轮询类文件的变更来工作,然后等待预定义的静默期以确保没有更多变更。如果您发现有时候某些更改并没有及时变化,可以尝试修改spring.devtools.restart.poll-interval和spring.devtools.restart.quiet-period参数。spring.devtools.restart.poll-interval=2s spring.devtools.restart.quiet-period=1s受监视的类路径目录现在每 2 秒轮询一次更改,并保持 1 秒的静默期以确保没有其他类更改。4.8.6 远程应用Spring Boot 支持部分远程功能,但有一定安全风险,只能在受信任的网络或SSL保护下运行,并且不能在生产环境上开启该功能。启用该功能,确保如下配置:<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludeDevtools>false</excludeDevtools> </configuration> </plugin> </plugins> </build>然后使用spring.devtools.remote.secret设置一个复杂的密码。Spring WebFlux 不支持该功能运行远程客户端应用程序远程客户端应用程序旨在从IDE中运行。您需要使用与连接到的远程项目相同的类路径运行org.springframework.boot.devtools.RemoteSpringApplication。应用程序的单个必需参数是它连接的远程URL。例如,如果您使用的是Eclipse或STS,并且已经部署到Cloud Foundry的项目名为my-app,则可以执行以下操作:从Run菜单中选择Run Configurations…。创建一个新的Java Application“启动配置”。浏览my-app项目。使用org.springframework.boot.devtools.RemoteSpringApplication作为主类。将https://myapp.cfapps.io添加到Program arguments(或任何远程URL)。正在运行的远程客户端可能类似于以下列表: . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ ___ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | | _ \___ _ __ ___| |_ ___ \ \ \ \ \\/ ___)| |_)| | | | | || (_| []::::::[] / -_) ' \/ _ \ _/ -_) ) ) ) ) ' |____| .__|_| |_|_| |_\__, | |_|_\___|_|_|_\___/\__\___|/ / / / =========|_|==============|___/===================================/_/_/_/ :: Spring Boot Remote :: (v2.7.8) 2023-01-19 14:18:32.205 INFO 16947 --- [ main] o.s.b.devtools.RemoteSpringApplication : Starting RemoteSpringApplication v2.7.8 using Java 1.8.0_362 on myhost with PID 16947 (/Users/myuser/.m2/repository/org/springframework/boot/spring-boot-devtools/2.7.8/spring-boot-devtools-2.7.8.jar started by myuser in /opt/apps/) 2023-01-19 14:18:32.211 INFO 16947 --- [ main] o.s.b.devtools.RemoteSpringApplication : No active profile set, falling back to 1 default profile: "default" 2023-01-19 14:18:32.566 INFO 16947 --- [ main] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729 2023-01-19 14:18:32.584 INFO 16947 --- [ main] o.s.b.devtools.RemoteSpringApplication : Started RemoteSpringApplication in 0.804 seconds (JVM running for 1.204) 因为远程客户端使用与真实应用程序相同的类路径,所以它可以直接读取应用程序属性。这是spring.devtools.remote.secret属性的读取方式并传递给服务器进行身份验证。始终建议使用https://作为连接协议,以便加密连接并且不会截获密码。如果需要使用代理来访问远程应用程序,请配置spring.devtools.remote.proxy.host和spring.devtools.remote.proxy.port属性。远程更新远程客户端以与[本地重新启动](#4.8.3 自动重启)相同的方式监视应用程序类路径以进行更改 。任何更新的资源都会被推送到远程应用程序,并且(如果需要)会触发重新启动。如果您迭代使用本地没有的云服务的功能,这将非常有用。通常,远程更新和重新启动比完全重建和部署周期快得多。仅在远程客户端运行时监视文件。如果在启动远程客户端之前更改文件,则不会将其推送到远程服务器笔者注:对于目前的大型微服务集群来说,并不实用,而且操作繁琐,使用这种更新方式部分类还有可能不生效,如果只是在测试环境使用,还不如Jenkins重新打包部署4.9 打包应用程序使用Maven 或者Gradle 打包应用程序,生成jar包文件。5. 核心功能5.1 SpringApplicationSpringApplication提供了一个main()方法方便引导Spring 应用程序启动,并委托给静态方法SpringApplication.run。import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } }启动后,会看到如下信息: . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.7.8) 2023-01-19 14:18:33.375 INFO 17059 --- [ main] o.s.b.d.f.s.MyApplication : Starting MyApplication using Java 1.8.0_362 on myhost with PID 17059 (/opt/apps/myapp.jar started by myuser in /opt/apps/) 2023-01-19 14:18:33.379 INFO 17059 --- [ main] o.s.b.d.f.s.MyApplication : No active profile set, falling back to 1 default profile: "default" 2023-01-19 14:18:34.288 INFO 17059 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2023-01-19 14:18:34.301 INFO 17059 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2023-01-19 14:18:34.301 INFO 17059 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.71] 2023-01-19 14:18:34.371 INFO 17059 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2023-01-19 14:18:34.371 INFO 17059 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 943 ms 2023-01-19 14:18:34.754 INFO 17059 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2023-01-19 14:18:34.769 INFO 17059 --- [ main] o.s.b.d.f.s.MyApplication : Started MyApplication in 1.789 seconds (JVM running for 2.169)默认情况下日志级别是INFO,如果需要额外的日志级别设置,查看[5.4.5 日志级别](#5.4.5 日志级别)。通过spring.main.log-startup-info设置为false,可以关闭应用程序的日志记录。5.1.1 启动失败如果应用启动失败,能够通过已注册的FailureAnalyzers获取错误信息以便修复问题。比如应用程序启动的8080端口被占用。*************************** APPLICATION FAILED TO START *************************** Description: Embedded servlet container failed to start. Port 8080 was already in use. Action: Identify and stop the process that is listening on port 8080 or configure this application to listen on another port.Spring Boot 支持自定义FailureAnalyzer实现如果没有故障分析器能够处理异常,您需要给org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener[启用debug属性](#5.2 外部配置),或者[开启DEBUG日志](#5.4.5 日志级别)。使用java -jar启动应用程序,使用debug开启日志:$ java -jar myproject-0.0.1-SNAPSHOT.jar --debug5.1.2 惰性初始化SpringApplication 允许延迟初始化应用程序,当启用惰性初始化时,bean 在需要时创建,而不是在启动期间创建。惰性初始化的一个缺点是会延迟发现应用程序的问题,如果配置错误的bean被惰性初始化,则在启动期间不会发生故障,只有在bean 被初始化时才发现问题。另外还要注意确保JVM有足够的内存来容纳所有的bean。因此建议在启用惰性初始化前微调JVM堆大小。使用SpringApplicationBuilder的lazyInitialization或者SpringApplication的setLazyInitialization方法开启惰性初始化,也可以使用spring.main.lazy-initialization开启。spring.main.lazy-initialization=true指定某些bean延迟初始化,使用@Lazy(false)5.1.3 自定义横幅通过将banner.txt添加到类路径中,或者设置spring.banner.location为该类文件的位置,来更改应用启动时打印的横幅。如果文件编码不是UTF-8,可以设置spring.banner.charset。除了使用文本文件外,还可以使用图片,将图片添加到类路径中,或者设置spring.banner.image.location,图形将被转换为ASCII格式。在banner.txt文件中,您可以使用Environment中可用的任何键和以下占位符。占位符描述${application.version}您的应用程序的版本号,如在MANIFEST.MF声明的那样。例如,Implementation-Version: 1.0打印为1.0.${application.formatted-version}您的应用程序的版本号,在MANIFEST.MF中声明并格式化显示(用括号括起来并以 为前缀v)。例如(v1.0)。${spring-boot.version}您正在使用的 Spring Boot 版本。例如2.7.8。${spring-boot.formatted-version}您正在使用的 Spring Boot 版本,经过格式化以供显示(用方括号括起来并以 为前缀v)。例如(v2.7.8)。${Ansi.NAME}(或${AnsiColor.NAME},,${AnsiBackground.NAME})${AnsiStyle.NAME}NAMEANSI 转义代码的名称在哪里。详情请见AnsiPropertySource。${application.title}您的应用程序的标题,如MANIFEST.MF中声明的那样。例如Implementation-Title: MyApp打印为MyApp.使用SpringApplication.setBanner(…)以编程方式设置横幅,使用org.springframework.boot.Banner接口并实现printBanner()方法自定义打印横幅。可以使用spring.main.banner-mode 设置是否在System.out( console)、或者日志文件中打印横幅、或者不打印横幅${application.version}和${application.formatted-version}配置仅仅在使用Spring Boot启动器的时候可用。如果你使用未打包的jar并使用java -cp <classpath> <mainclass>启动,则不会生效。这就是为什么我们建议您始终使用java org.springframework.boot.loader.JarLauncher启动未打包的jar。这将在构建类路径和启动应用程序之前初始化application.*banner变量。5.1.4 自定义SpringApplication如果默认的SpringApplication 不适合您,您可以自己创建一个实例,并进行自定义。例如,要关闭横幅:import org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication application = new SpringApplication(MyApplication.class); application.setBannerMode(Banner.Mode.OFF); application.run(args); } }也可以使用application.properties配置SpringApplication,详细查看[外部配置](#5.2 外部配置)5.1.5 流式API生成器如果您需要构建ApplicationContext层次结构(具有父子关系的多个上下文)或者更喜欢使用流式API构建器,可以使用SpringApplicationBuilder。SpringApplicationBuilder让你将多个方法链式调用,包括parent和child方法创建层级结构,比如:new SpringApplicationBuilder() .sources(Parent.class) .child(Application.class) .bannerMode(Banner.Mode.OFF) .run(args);ApplicationContext创建层次结构 时有一些限制。例如,Web 组件必须包含在子上下文中,并且同样Environment用于父上下文和子上下文。有关详细信息,请参阅SpringApplicationBuilderJavadoc。5.1.6 应用可用性当部署在平台上时,应用程序可以使用Kubernetes Probe等基础设施向平台提供有关其可用性的信息。Spring Boot 对常用的“liveness” 和 “readiness”状态提供开箱即用的支持。如果您使用了Spring Boot 的actuator那么状态将以监控端点的形式提供。另外,您还可以通过ApplicationAvailability接口将可用性状态注入到自己的bean中。Liveness 状态应用程序的"Liveness"状态表示其是否能正常工作,或者当前是失败状态,则自行修复。中断的“Liveness”状态意味着应用程序处于无法恢复的状态,那么基础架构应重启应用程序。“Liveness”状态不应该基于外部检查,比如健康检查。如果这样,一个失败的外部信息(如数据库、外部缓存等)将导致大规模重启和整个平台的连锁故障。Spring Boot 应用程序的内部状态主要根据Spring 的 ApplicationContext。如果应用程序上下文成功启动,则Spring Boot 会认为应用程序处于有效状态,上下文刷新的话,应用程序被认为处于活跃,更多参考[5.1.7 应用程序事件和监听器](#5.1.7 应用程序事件和监听器)Readiness 状态“Readiness”状态表示应用程序是否已经准备好处理请求。失败的“Readiness”状态表示现在不应该接收流量。这通常发生在启动期间,同时处理CommandLineRunner和ApplicationRunner组件,或者在应用程序认为太忙的时候发生。一旦应用程序和使用命令行调用应用程序被调用,就被认为是“Readiness 状态”。预期在启动期间运行的任务应该由组件CommandLineRunner和ApplicationRunner执行,不是使用 Spring 组件生命周期回调,例如@PostConstruct管理应用程序可用性状态应用程序可用随时通过注入ApplicationAvailability 接口并调用其上的方法来获取其可用性状态。还有的情况是,应用程序希望监听状态更新或者更新应用程序的状态。例如,我们可以将应用程序的“Readiness”状态导出到一个文件中,以便Kubernetes“exec Probe”可以查看该文件:import org.springframework.boot.availability.AvailabilityChangeEvent; import org.springframework.boot.availability.ReadinessState; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @Component public class MyReadinessStateExporter { @EventListener public void onStateChange(AvailabilityChangeEvent<ReadinessState> event) { switch (event.getState()) { case ACCEPTING_TRAFFIC: // create file /tmp/healthy break; case REFUSING_TRAFFIC: // remove file /tmp/healthy break; } } }当应用程序中断并且不能恢复的时候,还可以更新这个应用程序的状态。import org.springframework.boot.availability.AvailabilityChangeEvent; import org.springframework.boot.availability.LivenessState; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; @Component public class MyLocalCacheVerifier { private final ApplicationEventPublisher eventPublisher; public MyLocalCacheVerifier(ApplicationEventPublisher eventPublisher) { this.eventPublisher = eventPublisher; } public void checkLocalCache() { try { // ... } catch (CacheCompletelyBrokenException ex) { AvailabilityChangeEvent.publish(this.eventPublisher, ex, LivenessState.BROKEN); } } }Spring Boot 提供[Kubernetes HTTP接口](#11.2.9 Kubernetes Probes),用于Actuator 健康端点的"Liveness" and "Readiness"状态,你能够获取更多指导关于[在Kubernetes上部署应用程序](#12.1.2 Kubernetes)。5.1.7 应用程序事件和监听器除了Spring Framework事件之外,比如ContextRefreshedEvent,SpringApplication 还会发送一些额外的事件。有些事件实际上是在创建ApplicationContext创建之前,因此你不能作为@Bean注册监听器。你能够通过SpringApplication.addListeners(…)方法或者SpringApplicationBuilder.listeners(…)注册。如果希望自动注册这些监听器,可以将监听器添加到META-INF/spring.factories中,使用org.springframework.context.ApplicationListener做为key。运行应用程序的时候,按以下顺序发送事件:ApplicationStartingEvent 在应用程序开始运行时发送(任何处理之前),除了监听器和初始化程序的注册ApplicationEnvironmentPreparedEvent发送,当上下文中要使用的已知Environment时但在创建上下文之前。ApplicationContextInitializedEvent发送,在准备了ApplicationContext并且调用了ApplicationContextInitializers后,但在加载任何bean之前ApplicationPreparedEvent在刷新开始之前,但在加载Bean定义后发送ApplicationStartedEvent在刷新上下文之后,但在任何应用程序和命令行程序被调用之前发送AvailabilityChangeEvent在表示应用程序状态为LivenessState.CORRECT时发送ApplicationReadyEvent在任何应用程序和命令行程序被调用之后发送AvailabilityChangeEvent 在表示应用程序已经做好接收请求准备时发送,状态为ReadinessState.ACCEPTING_TRAFFICApplicationFailedEvent在启动异常时发送上面的列表只包括与SpringApplication相关的SpringApplicationEvent事件。以下事件也在ApplicationPreparedEvent之后和ApplicationStartedEvent之前发送。WebServerInitializedEvent在WebServer准备好后发送,ServletWebServerInitializedEvent和ReactiveWebServerInitializedEvent分别是servlet 和 reactive的变体ContextRefreshedEvent在ApplicationContext刷新后发送事件监听器不应该运行冗长的任务,因为他们默认在同一线程中运行应用程序事件使用Spring Framework的事件发布机制发送。此机制的一部分确保在子上下文中发布给监听器的事件也会在任何祖先上下文中发布给监听器。因此,如果您的应用程序使用SpringApplication实例的层次结构,则监听器可能会收到相同类型的应用程序事件的多个实例。为了允许监听器区分其上下文的事件和后代上下文的事件,它应该请求注入其应用程序上下文,然后将注入的上下文与事件的上下文进行比较。可以通过实现ApplicationContextAware或者如果监听器是bean,使用@Autowired来注入上下文。5.1.8 Web环境SpringApplication会试图创建正确类型的ApplicationContext。用于确定WebApplicationType的算法如下:如果存在 Spring MVC,使用AnnotationConfigServletWebServerApplicationContext如果 Spring MVC 不存在而 Spring WebFlux 存在,使用AnnotationConfigReactiveWebServerApplicationContext否则,使用AnnotationConfigApplicationContext这意味着如果您WebClient在同一应用程序中使用 Spring MVC 和 Spring WebFlux ,则默认情况下将使用 Spring MVC。您可以通过调用setWebApplicationType(WebApplicationType)来覆盖。也可以完全控制ApplicationContext调用所使用的类型setApplicationContextClass(…)。在JUnit测试中使用SpringApplication时,通常需要调用setWebApplicationType(WebApplicationType.NONE)5.1.9 访问应用程序参数如果您需要访问传递给SpringApplication.run(…)的应用程序参数,则可以注入org.springframework.boot.ApplicationArguments bean。ApplicationArguments接口提供对原始String[]参数以及解析的option和non-option参数的访问,如以下示例所示:import java.util.List; import org.springframework.boot.ApplicationArguments; import org.springframework.stereotype.Component; @Component public class MyBean { public MyBean(ApplicationArguments args) { boolean debug = args.containsOption("debug"); List<String> files = args.getNonOptionArgs(); if (debug) { System.out.println(files); } // if run with "--debug logfile.txt" prints ["logfile.txt"] } }Spring Boot还注册CommandLinePropertySource和Spring Environment。这使您还可以使用@Value注释注入单个应用程序参数。5.1.10 使用ApplicationRunner 或 CommandLineRunner如果您需要在启动后运行一些特定的代码SpringApplication,您可以实现ApplicationRunner或CommandLineRunner接口。这两个接口以相同的方式工作,并提供一个run方法,该方法在SpringApplication.run(…)完成之前被调用。非常适合在应用程序启动后但在接受请求之前运行的任务这些CommandLineRunner接口提供字符串数组用于访问对应用程序参数,而ApplicationRunner使用ApplicationArguments。以下示例显示了CommandLineRunner一个run方法:import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @Component public class MyCommandLineRunner implements CommandLineRunner { @Override public void run(String... args) { // Do something... } }如果CommandLineRunner或ApplicationRunner bean必须按顺序调用,可以实现org.springframework.core.Ordered接口或者使用org.springframework.core.annotation.Order注解,5.1.11 应用程序退出每个都SpringApplication向 JVM 注册一个关闭钩子,以确保ApplicationContext在退出时正常关闭。可以使用所有标准的 Spring 生命周期回调(例如DisposableBean接口或@PreDestroy注释)。此外,如果希望在SpringApplication.exit()被调用时返回特定的退出代码,则可以实现该接口org.springframework.boot.ExitCodeGenerator,然后可以将此退出代码传递给System.exit()其将作为状态码返回,如以下示例所示:import org.springframework.boot.ExitCodeGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class MyApplication { @Bean public ExitCodeGenerator exitCodeGenerator() { return () -> 42; } public static void main(String[] args) { System.exit(SpringApplication.exit(SpringApplication.run(MyApplication.class, args))); } }此外,ExitCodeGenerator接口可以通过异常来实现。遇到此类异常时,Spring Boot 会返回已实现getExitCode()方法提供的退出代码。如果存在多个ExitCodeGenerator,则使用生成的第一个非零退出代码。要控制调用生成器的顺序,请另外实现org.springframework.core.Ordered接口或使用org.springfframework.core.annotation.order注解。5.1.12 管理员功能可以通过指定spring.application.admin.enabled属性为应用程序启用与管理相关的功能。这暴露了SpringApplicationAdminMXBean平台上的MBeanServer。您可以使用此功能远程管理您的 Spring Boot 应用程序。此功能也可用于任何服务包装器实现。如果您想知道应用程序在哪个 HTTP 端口上运行,获取local.server.port属性的值。5.1.13 应用程序启动跟踪在应用程序启动期间,SpringApplication执行ApplicationContext许多与应用程序生命周期、bean 生命周期甚至处理应用程序事件相关的任务。有了ApplicationStartupSpring Framework ,您就可以使用StartupStep对象跟踪应用程序启动顺序。可以收集这些数据用于分析目的,或者只是为了更好地了解应用程序启动过程。可以使用setApplicationStartup设置一个实现ApplicationStartup的实例,比如,使用BufferingApplicationStartup的示例:import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; @SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication application = new SpringApplication(MyApplication.class); application.setApplicationStartup(new BufferingApplicationStartup(2048)); application.run(args); } }第一个可用的实现FlightRecorderApplicationStartup由 Spring Framework 提供。它将特定于 Spring 的启动事件添加到 Java Flight Recorder 会话,旨在分析应用程序并将其 Spring 上下文生命周期与 JVM 事件(例如分配、GC、类加载……)相关联。配置完成后,您可以在启用飞行记录器的情况下运行应用程序来记录数据:$ java -XX:StartFlightRecording:filename=recording.jfr,duration=10s -jar demo.jarSpring Boot 附带BufferingApplicationStartup变体;此实现旨在缓冲启动步骤并将它们排入外部指标系统。BufferingApplicationStartup应用程序可以在任何组件中请求类型的 bean 。Spring Boot 还可以配置为公开一个以 JSON 文档形式提供此信息的startup端点。5.2 外部化配置Spring Boot 允许您外部化您的配置,以便您可以在不同的环境中使用相同的应用程序代码。您可以使用各种外部配置源,包括 Java 属性文件、YAML 文件、环境变量和命令行参数。属性值可以通过注解直接注入 bean @Value,通过 Spring 的抽象Environment访问,或者通过@ConfigurationProperties绑定到对象。Spring Boot 使用一种非常特殊的PropertySource顺序,旨在允许合理地覆盖值。后面的属性源可以覆盖前面定义的值。来源按以下顺序考虑:默认properties,由SpringApplication.setDefaultProperties指定在@Configuration上的@PropertySource注解,请注意,在刷新应用程序上下文之前,这些属性源不会添加到Environment中。而logging.* 和 spring.main.* 是在应用程序上下文刷新之前读取。Config 数据,比如application.propertiesRandomValuePropertySource中仅为random.*的配置操作系统环境变量Java系统配置,System.getProperties()来自java:comp/env的JNDI属性ServletContext初始化参数ServletConfig初始化参数来自SPRING_APPLICATION_JSON的属性,嵌入在环境变量(environment variable )或系统属性(system property)中的内联 JSON命令行参数在单元测试中的properties,在@SpringBootTest 和用于测试应用程序特定部分的测试注解上有效。单元测试中的@TestPropertySource在devtools激活下,$HOME/.config/spring-boot目录中的Devtools全局设置配置Config 数据的加载按照以下顺序:打包在jar包中的Application配置,application.properties 和 YAML变体打包在jar包中的application-{profile}.properties和 YAML 变体jar包外的application.properties和 YAML 变体jar包外的application-{profile}.properties和 YAML 变体建议使用一种配置文件格式,如果同时有properties和yaml,properties优先。假设您开发了一个@Component使用name属性的应用程序,如以下示例所示:import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component public class MyBean { @Value("${name}") private String name; // ... }在您的应用程序类路径中(例如,在您的 jar 中),您可以有一个application.properties文件为name. 在新环境中运行时,application.properties可以在 jar 之外提供一个文件来覆盖name. 对于一次性测试,您可以使用特定的命令行开关(例如,java -jar app.jar --name="Spring")启动。env和configprops端点可用于确定属性具有特定值的原因。可以使用这两个端点来诊断预期外的属性值。有关详细信息,请参阅"Production ready features"部分。5.2.1 访问命令行属性默认情况下,SpringApplication将任何命令行选项参数(即以 --开头的参数,例如--server.port=9000)转换为property并将它们添加到 SpringEnvironment中,命令行属性始终优先于基于文件的属性源。如果您不想将命令行属性添加到 中Environment,您可以使用SpringApplication.setAddCommandLineProperties(false)禁用它们。5.2.2 JSON 应用程序属性环境变量和系统属性通常有限制,这意味着某些属性名称不能使用。为了解决这个问题,Spring Boot 允许您将属性块编码为单个 JSON 结构。当您的应用程序启动时,任何spring.application.json或SPRING_APPLICATION_JSON属性将被解析并添加到Environment.例如,SPRING_APPLICATION_JSON可以在 UN*X shell 的命令行中将属性作为环境变量提供:$ SPRING_APPLICATION_JSON='{"my":{"name":"test"}}' java -jar myapp.jar在前面的示例中,您最终在 Spring 的Environment中获取my.name=test。同样的 JSON 也可以作为系统属性提供:$ java -Dspring.application.json='{"my":{"name":"test"}}' -jar myapp.jar或者您可以使用命令行参数提供 JSON:$ java -jar myapp.jar --spring.application.json='{"my":{"name":"test"}}'如果要部署到经典应用程序服务器,您还可以使用名为java:comp/env/spring.application.json的JNDI 变量。虽然JSON中的null将添加到结果属性源中,但PropertySourcesPropertyResolver会将null属性视为缺少的值。这意味着JSON不能用null值覆盖低优先级属性源中的属性。5.2.3 外部应用程序属性当您的应用程序启动时,Spring Boot 将自动从以下位置查找并加载application.properties和application.yaml从classpathclasspath根目录classpath 的 /config 包从当前目录当前目录当前目录的config/子目录config/的直接子目录这个列表按顺序排列(较低的项会覆盖较早的项)。加载的文件会做为PropertySources添加到Spring Environment中。如果您不想用application作为配置文件名,您可以通过指定一个spring.config.name环境属性来切换到另一个文件名。例如,要查找myproject.properties和myproject.yaml文件,您可以按如下方式运行您的应用程序:$ java -jar myproject.jar --spring.config.name=myproject您还可以使用spring.config.location环境属性来引用显式位置。此属性接受一个或多个要检查的位置的逗号分隔列表。以下示例显示如何指定两个不同的文件:$ java -jar myproject.jar --spring.config.location=\ optional:classpath:/default.properties,\ optional:classpath:/override.propertiesoptional前缀表示,位置是可选的,允许不存在spring.config.name, spring.config.location, 和spring.config.additional-location很早就用于确定必须加载哪些文件。它们必须定义为环境属性(通常是操作系统环境变量、系统属性或命令行参数)。如果spring.config.location包含目录(而不是文件),应该以/结尾。在运行时,它们将在加载之前附加从spring.config.name生成的名称目录和文件定位也用于 检查 profile指定文件。例如,如果spring.config.location配置为classpath:myconfig.properties,classpath:myconfig-<profile>.properties的文件也会被加载在大多数情况下,spring.config.location您添加的每个项目都将引用一个文件或目录。位置按照它们被定义的顺序处理,后面的可以覆盖前面的值。如果您有一个复杂的位置要设置,并且您使用profile指定的配置文件,那么您可能需要提供进一步的提示,以便Spring Boot知道它们应该如何分组。位置组是所有被认为处于同一级别的位置的集合。例如,您可能希望对所有类路径位置进行分组,然后对所有外部位置进行分组。位置组中的项目用;分隔,详细查看Profile Specific Files使用spring.config.location替换默认的位置配置。例如,如果spring.config.location设置为optional:classpath:/custom-config/,optional:file:./custom-config/,则完整的位置集是:optional:classpath:custom-config/optional:file:./custom-config/如果您更喜欢添加其他位置,而不是替换它们,您可以使用spring.config.additional-location. 从其他位置加载的属性可以覆盖默认位置中的属性。例如,如果spring.config.additional-location配置了值optional:classpath:/custom-config/,optional:file:./custom-config/,则考虑的完整位置集是:optional:classpath:/;optional:classpath:/config/optional:file:./;optional:file:./config/;optional:file:./config/*/optional:classpath:custom-config/optional:file:./custom-config/此搜索顺序使您可以在一个配置文件中指定默认值,然后在另一个配置文件中有选择地覆盖这些值。您可以在默认位置之一的application.properties(或您选择的任何其他基本名称spring.config.name)中为您的应用程序提供默认值。然后可以在运行时使用位于自定义位置之一的不同文件覆盖这些默认值。如果您使用环境变量而不是系统属性,大多数操作系统不允许使用句点分隔的键名,但您可以使用下划线代替(例如,SPRING_CONFIG_NAME代替spring.config.name)。有关详细信息,请参阅从环境变量绑定。如果您的应用程序在 servlet 容器或应用程序服务器中运行,则可以使用 JNDI 属性(在java:comp/env中)或 servlet 上下文初始化参数来代替或同时使用环境变量或系统属性。可选位置默认情况下,当指定的配置数据位置不存在时,Spring Boot 将抛出ConfigDataLocationNotFoundException,并且应用程序将停止。如果需要指定一个位置,但不是必须存在,使用optional:前缀。可以在spring.config.location和spring.config.additional-location以及spring.config.import中声明。比如,spring.config.import属性,值为optional:file:./myconfig.properties,在文件不存在的情况下,应用程序也能够启动。如果你想要忽略所有的ConfigDataLocationNotFoundExceptions异常,并且始终允许应用程序继续启动,可以使用spring.config.on-not-found配置。或者通过SpringApplication.setDefaultProperties(…)或者使用系统/环境变量设置忽略的值。通配符位置定位如果一个配置文件位置路径最后包含*,则表示其为通配符位置。这在多个配置文件的情况下,非常有用。比如,有一些Redis配置和Mysql配置,可以想要把这两个配置文件分开,但又在application.properties文件中,这样可能会有两个不同的路径,/config/redis/application.properties 和/config/mysql/application.properties,通过config/*/可以将两个配置文件都进行加载。默认情况下,Spring Boot在默认搜索位置包含config/*/,这意味着将搜索jar之外的/config目录的所有子目录。您可以将通配符与spring.config.location和spring.config.additional-location一起使用。通配符位置定位只能包含一个*,对于搜索目录必须以*/结尾,对于搜索文件,则必须以*/<filename>结尾。带有通配符的位置根据文件名的绝对路径按字母顺序排序。通配符位置仅适用于外部目录。不能在classpath:location中使用通配符。Profile特定文件除了application属性文件之外,Spring Boot还将尝试使用命名约定application-{profile}加载profile特定文件。例如,如果应用程序激活名为prod的配置文件并使用YAML文件,那么将同时加载application.yml和application-prod.yml。Profile特定文件的属性加载与标准应用程序属性加载的位置相同,profile特定文件总是覆盖非特定的文件(application.yml)。如果指定了多个配置文件,则采用最后获胜策略。例如,如果配置文件 prod 、live 是由spring.profiles.active属性指定的,那么application-prod.properties中的值可以被application-live.properties中的值覆盖。最后获胜策略适用于位置组级别。spring.config.location的classpath:/cfg/,classpath:/ext/配置和classpath:/cfg/;classpath:/ext/配置的覆盖规则不同。例如,继续上面的prod、live示例,我们可能有以下文件:/cfg application-live.properties /ext application-live.properties application-prod.properties spring.config.location的值为classpath:/cfg/,classpath:/ext/,程序会先处理/cfg下的所有文件,再处理/ext/cfg/application-live.properties/ext/application-prod.properties/ext/application-live.properties如果值为classpath:/cfg/;classpath:/ext/,程序视为同一级别/ext/application-prod.properties/cfg/application-live.properties/ext/application-live.propertiesEnvironment有一组默认配置文件(默认情况下为[default]),如果未设置活动配置文件,则使用这些配置文件。换句话说,如果没有显式激活配置文件,那么将考虑application-default。配置文件只加载一次。如果您已经直接导入了特定配置文件的属性文件,则不会再次导入该文件。导入附加数据应用程序配置可以使用spring.config.import 属性从其他位置导入更多配置数据。例如,classpath application.properties文件中可能包含以下内容:spring.application.name=myapp spring.config.import=optional:file:./dev.properties这将触发当前目录中dev.properties文件的导入(如果存在这样的文件)。导入的dev.properties中的值将优先于触发导入的文件。在上面的示例中,dev.properties可以将spring.application.name重新定义为不同的值。无论声明多少次,都只能导入一次。在导入properties/yaml的文件中定义的单个文档顺序是无关紧要的,比如,下面的两个例子产生相同的结果。spring.config.import=my.properties my.property=valuemy.property=value spring.config.import=my.properties在上述两个示例中,my.properties文件中的值将优先于触发其导入的文件。可以在一个spring.config.import下指定多个位置,位置将按照定义的顺序进行处理,以后导入的配置优先。适当时,特定配置文件的变体还会导入,上面的示例将导入my.properties以及任何my-<profile>.properties变体。Spring Boot包括可插拔API,允许支持各种不同的位置地址。默认情况下,您可以导入Java配置、YAML和“配置树”。第三方jar可以提供对其他技术的支持(不要求文件是本地的)。例如,您可以想象配置数据来自Consul、Apache ZooKeeper或Netflix Archaius等外部存储。如果要支持自定义位置,请参阅org.springframework.boot.context.config包中的ConfigDataLocationResolver和ConfigDataLoader类。导入无扩展名文件某些云平台无法向卷装载的文件添加文件扩展名。要导入这些无扩展名文件,您需要给Spring Boot一个提示,以便它知道如何加载它们。您可以通过在方括号中放置扩展提示来完成此操作。例如,假设您有一个/etc/config/myconfig文件,希望将其作为yaml导入。您可以使用以下命令从application.properties导入它:spring.config.import=file:/etc/config/myconfig[.yaml]使用配置树在云平台(如Kubernetes)上运行应用程序时,通常需要读取平台提供的配置值。出于这种目的使用环境变量并不罕见,但这可能会有缺点,特别是如果值应该保密的话。作为环境变量的替代方案,许多云平台现在允许您将配置映射到装载的数据卷中。例如,Kubernetes可以卷装载ConfigMaps和Secrets。可以使用两种常见的卷装载模式:单个文件包含一组完整的属性(通常写为 YAML)多个文件被写入目录树,文件名成为“key”,内容成为“value”对于第一种情况,可以上述配置使用spring.config.import导入YAML或Properties文件。对于第二种情况,您需要使用configtree:前缀,以便Spring Boot知道它需要将所有文件公开为Properties。例如,让我们假设Kubernetes安装了以下卷:etc/ config/ myapp/ username passwordusername 是一个配置的值,password是一个加密字符串要导入这些配置,你可以将如下内容导入application.properties或者application.yamlspring.config.import=optional:configtree:/etc/config/然后,您可以用通常的方式从Environment中访问或注入myapp.username和myapp.password属性。 配置树下的文件夹构成属性名称。在上面的示例中,要以username和password的形式访问属性,可以将spring.config.import设置为optional:configtree:/etc/config/myapp。带有点符号的文件名也正确映射。例如,在上面的示例中,/etc/config中名为myapp.username的文件将在Environment中生成myapp.username属性。配置树值可以绑定到字符串String和byte[]类型,具体取决于预期的内容。如果要从同一父文件夹导入多个配置树,则可以使用通配符快捷方式。任何以/*/结尾的configtree:location都会将所有直接子级作为配置树导入。etc/ config/ dbconfig/ db/ username password mqconfig/ mq/ username password您可以使用configtree:/etc/config/*/作为导入位置:spring.config.import=optional:configtree:/etc/config/*/如上配置将导入 db.username, db.password, mq.username 和 mq.password 属性。使用通配符加载的目录按字母顺序排序。如果您需要不同的排序,则应将每个位置列为单独的导入配置树也可以用于Docker 保密数据。当Docker群服务被授权访问一个保密数据时,该保密数据被装入容器中。例如,如果名为db.password的保密数据安装在位置/run/secrets/,则可以使用以下变量db.passwords在Spring环境中:spring.config.import=optional:configtree:/run/secrets/属性占位符application.properties和application.yml中的值在使用时会通过现有的Environment进行过滤,因此您可以引用以前定义的值(例如,从系统属性或环境变量)。标准的${name}属性占位符语法可以在值的任何位置使用,属性占位符还可以使用:指定默认值,将默认值与属性名称分开,例如${name:default}。以下示例显示了带默认值和不带默认值的占位符的使用:app.name=MyApp app.description=${app.name} is a Spring Boot application written by ${username:Unknown}您应该始终使用占位符中的规范形式(kebab-case仅使用小写字母)引用占位符中的属性名称。这将允许Spring Boot使用与@ConfigurationProperties相同的宽松绑定逻辑。例如,${demo.item-price}将从application.properties文件中获取demo.iterm-price和demo.itemPrice,并从系统环境中获取DEMO_ITEMPRICE。如果改用${demo.itemPrice},则不会考虑demo.item-price和DEMO_ITEMPRICE。您还可以使用此技术创建现有SpringBoot属性的“短”变体。有关详细信息,请参阅使用“短”命令行参数的方法。使用多文档文件Spring Boot允许您将单个物理文件拆分为多个逻辑文档,每个逻辑文档都是独立添加的。文档按照从上到下的顺序进行处理。后续文档可以覆盖早期文档中定义的配置。对于application.yml文件,使用标准的YAML多文档语法。三个连续的连字符表示一个文档的结尾和下一个文档开始。例如,以下包含两个逻辑文档:spring: application: name: "MyApp" --- spring: application: name: "MyCloudApp" config: activate: on-cloud-platform: "kubernetes"application.properties文件使用#---或者!--- 来分割文档spring.application.name=MyApp #--- spring.application.name=MyCloudApp spring.config.activate.on-cloud-platform=kubernetes 配置文件分隔符不能有任何前导空格,必须正好有三个连字符。多文档属性文件通常与激活配置(如spring.config.activate.on-profile)结合使用。有关详细信息,请参阅下一节。无法使用@PropertySource或@TestPropertySource注解加载多文档属性文件。激活属性您可能具有仅在特定配置文件处于激活状态时才关联配置。您可以使用spring.config.activate.*有条件地激活配置属性。下面的激活配置可用:PropertyNoteon-profile必须匹配才能激活文档的配置文件表达式on-cloud-platform要使文档处于活动状态,必须检测到“CloudPlatform”例如,下面指定第二个文档仅在Kubernetes上运行时有效,并且仅在“prod”或“staging”配置文件处于激活状态时有效:myprop=always-set #--- spring.config.activate.on-cloud-platform=kubernetes spring.config.activate.on-profile=prod | staging myotherprop=sometimes-set5.2.4 加密属性Spring Boot不提供任何加密属性的内置支持,但它提供了修改Spring环境中包含值所需的钩子点。EnvironmentPostProcessor允许你在应用程序启动的时候控制Environment,详细查看[启动时自定义环境变量](#15. “How-to” 指南)。如果您需要一种安全的方式来存储凭据和密码,Spring Cloud Vault项目将支持在HashiCorp Vault中存储外部化配置。5.2.5 使用YAML文件YAML是JSON的超集,是指定分层配置数据的便捷格式。只要类路径上有SnakeYAML库,SpringApplication类就会自动支持YAML作为properties的替代。YAML 映射到PropertiesYAML文档需要从其分层格式转换为可用于Spring Environment的平面结构。例如,如下的YAML文档:environments: dev: url: "https://dev.example.com" name: "Developer Setup" prod: url: "https://another.example.com" name: "My Cool App"为了从Environment中访问这些属性,它们将按以下方式展平:environments.dev.url=https://dev.example.com environments.dev.name=Developer Setup environments.prod.url=https://another.example.com environments.prod.name=My Cool App同样,YAML列表也需要扁平化,使用[index]做为键,比如下面的YAML。my: servers: - "dev.example.com" - "another.example.com"上述示例转为properties后:my.servers[0]=dev.example.com my.servers[1]=another.example.com使用[index]表示的properties能够绑定到Java的List或Set对象。有关更多详细信息,请参阅下面的“[类型安全配置属性](#5.2.8 类型安全配置属性)”部分。无法使用@PropertySource或@TestPropertySource注解加载YAML文件。因此,如果需要以这种方式加载值,则需要使用properties文件。直接加载YAMLSpring Framework提供了两个方便类,可用于加载YAML文档。YamlPropertiesFactoryBean将YAML作为Properties加载,YamlMapFactoryBean将YAML作为Map加载。如果要将YAML作为Spring PropertySource加载,也可以使用YamlPropertySourceLoader类。5.2.6 配置随机值RandomValuePropertySource用于注入随机值(例如,注入加密字符或测试用例)。它可以生成integer、long、uuid或string,如下例所示:my.secret=${random.value} my.number=${random.int} my.bignumber=${random.long} my.uuid=${random.uuid} my.number-less-than-ten=${random.int(10)} my.number-in-range=${random.int[1024,65536]}random.int*语法是OPEN value (,max) CLOSE,其中OPEN,CLOSE是任何字符,value、max是整数,如果提供了max,则value是最小值,max是最大值(不包括)。5.2.7 配置系统环境属性Spring Boot支持为环境属性设置前缀。如果系统环境由具有不同配置要求的多个Spring Boot应用程序共享,这将非常有用。系统环境属性的前缀可以直接在SpringApplication上设置。例如,如果将前缀设置为input,则诸如remote.timeout之类的属性也将在系统环境中解析为input.remote.timeout。5.2.8 类型安全的配置属性使用@Value("${property}")注入配置属性有时会很麻烦,特别是当您有多个属性或数据本质上是分层的时候。SpringBoot提供了另一种使用properties的方法,该方法允许强类型bean管理和验证应用程序的配置。也可以查看@Value 和 type-safe configuration properties的不同点JavaBean 属性绑定可以绑定到一个标准的JavaBean,如下例所示:import java.net.InetAddress; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties("my.service") public class MyProperties { private boolean enabled; private InetAddress remoteAddress; private final Security security = new Security(); public boolean isEnabled() { return this.enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public InetAddress getRemoteAddress() { return this.remoteAddress; } public void setRemoteAddress(InetAddress remoteAddress) { this.remoteAddress = remoteAddress; } public Security getSecurity() { return this.security; } public static class Security { private String username; private String password; private List<String> roles = new ArrayList<>(Collections.singleton("USER")); public String getUsername() { return this.username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return this.password; } public void setPassword(String password) { this.password = password; } public List<String> getRoles() { return this.roles; } public void setRoles(List<String> roles) { this.roles = roles; } } }前面的POJO定义了以下属性:my.service.enabled,默认为falsemy.service.remote-address,能够强制转成Stringmy.service.security.usernamemy.service.security.passwordmy.service.security.roles String类型的列表,默认是USER映射到Spring Boot中可用的@ConfigurationProperties类的properties是公共API,这些类是通过properties文件、YAML文件、环境变量和其他机制配置的,但类本身的访问器(getters/setters)并不打算直接使用。这种安排依赖于默认的空构造函数,getter和setter通常是强制性的,因为绑定是通过标准的JavaBeans属性描述符进行的,就像在SpringMVC中一样。在下列情况下,可以省略setter:Maps,只要它们被初始化,就需要getter,但不一定需要setter,因为它们可以被绑定器改变。可以通过索引(通常使用 YAML)或使用单个逗号分隔值(属性)来访问集合和数组。在后一种情况下,setter 是强制性的。我们建议始终为此类类型添加一个 setter。如果您初始化一个集合,请确保它不是不可变的(如前例所示)。如果嵌套的 POJO 属性被初始化(如Security前面示例中的字段),则不需要 setter。如果希望绑定器使用其默认构造函数动态创建实例,则需要setter。有些人使用 Project Lombok 来自动添加 getter 和 setter。确保 Lombok 不会为此类类型生成任何特殊的构造函数,因为容器会自动使用它来实例化对象。最后,只考虑标准 Java Bean 属性,不支持绑定静态属性。构造函数绑定上一节中的示例可以以不可变的方式重写,如以下示例所示:import java.net.InetAddress; import java.util.List; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConstructorBinding; import org.springframework.boot.context.properties.bind.DefaultValue; @ConstructorBinding @ConfigurationProperties("my.service") public class MyProperties { private final boolean enabled; private final InetAddress remoteAddress; private final Security security; public MyProperties(boolean enabled, InetAddress remoteAddress, Security security) { this.enabled = enabled; this.remoteAddress = remoteAddress; this.security = security; } public boolean isEnabled() { return this.enabled; } public InetAddress getRemoteAddress() { return this.remoteAddress; } public Security getSecurity() { return this.security; } public static class Security { private final String username; private final String password; private final List<String> roles; public Security(String username, String password, @DefaultValue("USER") List<String> roles) { this.username = username; this.password = password; this.roles = roles; } public String getUsername() { return this.username; } public String getPassword() { return this.password; } public List<String> getRoles() { return this.roles; } } }在此设置中,@ConstructorBinding注解用于指示应使用构造函数绑定。这意味着绑定器将期望找到一个带有您希望绑定的参数的构造函数。如果您使用的是 Java 16 或更高版本,构造函数绑定可以与记录一起使用。在这种情况下,除非您的记录有多个构造函数,否则没有必要使用@ConstructorBinding.类的嵌套成员@ConstructorBinding(如上Security例)也将通过其构造函数进行绑定。可以使用@DefaultValue构造函数参数指定默认值,或者在使用 Java 16 或更高版本时使用记录组件指定默认值。转换服务将用于将String值强制转换为缺失属性的目标类型。参考前面的示例,如果没有属性绑定到Security,则该MyProperties实例将包含 一个null值的security。要使它包含一个非空的实例,Security即使没有属性绑定到它(使用 Kotlin 时,这将需要将 的username和password参数Security声明为可空的,因为它们没有默认值),使用空@DefaultValue注解:public MyProperties(boolean enabled, InetAddress remoteAddress, @DefaultValue Security security) { this.enabled = enabled; this.remoteAddress = remoteAddress; this.security = security; }要使用构造函数绑定,必须使用@EnableConfigurationProperties或@ConfigurationProperties来启用类。您不能对由常规 Spring 机制创建的 bean 使用构造函数绑定(例如@Componentbean、使用@Bean方法创建的 bean 或使用 @Import加载的 bean)如果您的类有多个构造函数,您也可以在应该绑定的构造函数上直接使用@ConstructorBinding不建议将java.util.Optional与@ConfigurationProperties一起使用,因为它主要用作返回类型。因此,它不太适合配置属性注入。为了与其他类型的属性保持一致,如果您确实声明了一个Optional属性并且它没有值,那么将绑定 null一个空值。启用@ConfigurationProperties注解类型Spring Boot 提供基础设施来绑定@ConfigurationProperties类型并将它们注册为 beans。您可以逐个类地启用配置属性,也可以启用以类似于组件扫描的方式工作的配置属性扫描。有时,带有注解的类@ConfigurationProperties可能不适合扫描,例如,如果您正在开发自己的自动配置或您希望有条件地启用它们。在这些情况下,使用@EnableConfigurationProperties注解指定要处理的类型列表。这可以在任何@Configuration类上完成,如以下示例所示:import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(SomeProperties.class) public class MyConfiguration { }要使用配置属性扫描,请将@ConfigurationPropertiesScan注解添加到您的应用程序。通常,它被添加到带有@SpringBootApplication的类中,但它可以添加到任何@Configuration类中。默认情况下,将从声明注解的类的包中进行扫描。如果要定义要扫描的指定包,可以按照以下示例所示进行操作:import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; @SpringBootApplication @ConfigurationPropertiesScan({ "com.example.app", "com.example.another" }) public class MyApplication { }当@ConfigurationProperties使用配置属性扫描或通过 @EnableConfigurationProperties注册 bean时,bean 有一个约定名称:<prefix>-<fqn>,其中<prefix>是@ConfigurationProperties中指定的环境键前缀,<fqn>是 bean 的完全限定名称。如果不提供任何前缀,则仅使用 bean 的完全限定名称。上面示例中的 bean 名称是com.example.app-com.example.app.SomeProperties.我们建议@ConfigurationProperties只处理环境,特别是不要从上下文中注入其他 beans。对于极端情况,可以使用 setter 注入或*Aware框架提供的任何接口(例如,EnvironmentAware如果您需要访问Environment)。如果您仍想使用构造函数注入其他 bean,则配置属性 bean 必须注释@Component并使用基于 JavaBean 的属性绑定。使用@ConfigurationProperties 注解类型这种类型的配置在SpringApplication外部YAML配置中尤其适用,如下例所示:my: service: remote-address: 192.168.1.1 security: username: "admin" roles: - "USER" - "ADMIN"要使用@ConfigurationProperties bean,可以与任何其他bean相同的方式注入,如下例所示:import org.springframework.stereotype.Service; @Service public class MyService { private final MyProperties properties; public MyService(MyProperties properties) { this.properties = properties; } public void openConnection() { Server server = new Server(this.properties.getRemoteAddress()); server.start(); // ... } // ... }使用@ConfigurationProperties还可以生成元数据文件,IDE可以使用这些文件自动完成自己的密钥。三方配置除了使用@ConfigurationProperties注解类之外,还可以在公共@Bean方法上使用它。当您想将属性绑定到不在您控制范围内的第三方组件时,这样做特别有用。要从Environment中配置bean,请将@ConfigurationProperties添加到其bean注册中,如以下示例所示:import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration(proxyBeanMethods = false) public class ThirdPartyConfiguration { @Bean @ConfigurationProperties(prefix = "another") public AnotherComponent anotherComponent() { return new AnotherComponent(); } }使用another前缀定义的属性都以类似于前面的SomeProperties示例的方式映射到该AnotherComponent bean上。宽松绑定Spring Boot使用一些宽松的规则将Environment属性绑定到@ConfigurationProperties bean,因此,Environment属性名称和bean属性名称之间不需要完全匹配。这很有用的常见示例包括以破折号分隔的环境属性(例如,context-path绑定到contextPath),和大写的环境属性(例如,PORT绑定到port)。例如,以下@ConfigurationProperties类:import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "my.main-project.person") public class MyPersonProperties { private String firstName; public String getFirstName() { return this.firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } }使用上述代码,可以使用以下属性名称:PropertyNotemy.main-project.person.first-name建议在.properties和.yml文件中使用my.main-project.person.firstName标准的驼峰写法my.main-project.person.first_name下划线表示法,建议在.properties和.yml文件中使用MY_MAINPROJECT_PERSON_FIRSTNAME大写格式,建议在系统环境变量中使用注解的前缀值必须是kebab大小写(小写并用-分隔,例如my.main-project.person)。Property SourceSimpleListProperties Files驼峰大小写、kebab小写或下划线符号标准的使用[ ]或者逗号分割值YAML Files驼峰大小写、kebab小写或下划线符号标准的YAML列表或者逗号分割值Environment Variables以下划线作为分隔符的大写格式( Binding From Environment Variables).用下划线包围的数值 (see Binding From Environment Variables)System properties驼峰大小写、kebab小写或下划线符号标准的使用[ ]或者逗号分割值我们建议在可能的情况下,将属性存储为小写的kebab格式,例如my.person.first-name=Rod。绑定 Maps绑定到Map配置时,可能需要使用特殊的括号表示法,以便保留原始键值。如果键未被[]包围,则为非字母数字、-或.任何字符将被移除。例如,以下示例绑定到Map<String,String>:my.map.[/key1]=value1 my.map.[/key2]=value2 my.map./key3=value3 对于YAML文件,括号需要用引号括起来,以便正确解析键。上面的配置将以/key1、/key2和key3作为映射中的键绑定到Map。斜线已从key3中删除,因为它没有被方括号包围。当绑定到标量值时,使用键.其中不需要被[]包围。标量值包括枚举和java.lang包中除Object之外的所有类型。将a.b=c绑定到Map<String, String>将会保留.,并返回包含{"a.b"="c"}项的map。对于任何其他类型,如果键包含.,则需要使用括号表示法。比如,将a.b=c绑定到Map<String, Object>,将返回{"a"={"b"="c"}}项的map,而[a.b]=c将返回{"a.b"="c"}项的map。绑定环境变量大多数操作系统对可用于环境变量的名称施加严格的规则。例如,Linux shell变量只能包含字母(a到z或a到z)、数字(0到9)或下划线字符(_)。按照惯例,Unix shell变量的名称也将以大写字母表示。Spring Boot的宽松绑定规则尽可能与这些命名限制兼容。要将规范形式的属性名称转换为环境变量名称,可以遵循以下规则:将.替换为_移除-转换为大写例如,一个spring.main.log-startup-info属性转换为环境变量后为SPRING_MAIN_LOGSTARTUPINFO。绑定到对象列表时也可以使用环境变量。要绑定到List,元素编号应在变量名称中用下划线包围。例如,一个my.service[0].other转换为环境变量后是MY_SERVICE_0_OTHER。合并复杂类型当在多个位置配置列表时,覆盖通过替换整个列表来工作。例如,假设MyPojo对象的名称和描述属性默认为null。以下示例显示MyProperties中的MyPojo对象列表:import java.util.ArrayList; import java.util.List; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties("my") public class MyProperties { private final List<MyPojo> list = new ArrayList<>(); public List<MyPojo> getList() { return this.list; } }可以如下配置:my.list[0].name=my name my.list[0].description=my description #--- spring.config.activate.on-profile=dev my.list[0].name=my another name如果dev未激活,MyProperties.list包含一个MyPojo项,如果dev激活,然而,列表仍然只包含一个条目(名称为my another name,description为null)。此配置不会向列表中添加第二个MyPojo实例,也不会合并项目。当在多个配置文件中指定列表时,将使用优先级最高的配置文件(并且仅使用该配置文件)。my.list[0].name=my name my.list[0].description=my description my.list[1].name=another name my.list[1].description=another description #--- spring.config.activate.on-profile=dev my.list[0].name=my another name在前面的示例中,dev激活,MyProperties.list包含一个MyPojo项,name为my another name和description为null。对于YAML,逗号分隔列表和YAML列表都可以用于完全覆盖列表的内容。对于Map属性,可以使用从多个源绘制的属性值进行绑定。但是,对于多个源中的相同属性,将使用具有最高优先级的属性。以下示例,MyProperties公开了一个Map<String, MyPojo>属性。import java.util.LinkedHashMap; import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties("my") public class MyProperties { private final Map<String, MyPojo> map = new LinkedHashMap<>(); public Map<String, MyPojo> getMap() { return this.map; } }以下示例配置:my.map.key1.name=my name 1 my.map.key1.description=my description 1 #--- spring.config.activate.on-profile=dev my.map.key1.name=dev name 1 my.map.key2.name=dev name 2 my.map.key2.description=dev description 2如果dev未激活,MyProperties.map仅包含一个key为key1的项(name是my name 1,description是my description 1)。如果dev激活,将包含两个项目键为key1(name是dev name 1,description是my description 1),键为key2(name是dev name 2,description是my description 2)上述合并规则适用于所有属性源的配置,而不仅仅是文件。属性转换当绑定到@ConfigurationProperties bean时,SpringBoot会尝试将外部应用程序属性强制为正确的类型。如果需要自定义类型转换,可以提供ConversionService bean(带有名为conversionService的bean)或自定义属性编辑器(通过CustomEditorConfigurer bean)或定制Converter(带有@ConfigurationPropertiesBinding注解的bean定义)。由于此bean在应用程序生命周期的早期被请求,请确保限制ConversionService正在使用的依赖关系。通常,您需要的任何依赖项在创建时都可能无法完全初始化。如果配置键不强制需要,并且仅依赖于用@ConfigurationPropertiesBinding限定的自定义转换器,则可能需要重命名自定义ConversionService。转换 DurationsSpring Boot 支持Durations,如果你公开java.time.Duration,应用程序中可以用以下格式:通常使用long描述,如果没有指定@DurationUnit,默认是毫秒java.time.Duration使用的标准ISO-8601格式一种更可读的格式,其中值和单位是耦合的(10s表示10秒)import java.time.Duration; import java.time.temporal.ChronoUnit; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.convert.DurationUnit; @ConfigurationProperties("my") public class MyProperties { @DurationUnit(ChronoUnit.SECONDS) private Duration sessionTimeout = Duration.ofSeconds(30); private Duration readTimeout = Duration.ofMillis(1000); public Duration getSessionTimeout() { return this.sessionTimeout; } public void setSessionTimeout(Duration sessionTimeout) { this.sessionTimeout = sessionTimeout; } public Duration getReadTimeout() { return this.readTimeout; } public void setReadTimeout(Duration readTimeout) { this.readTimeout = readTimeout; } } 指定会话超时时间30s,PT30S和30s等效,读取超时500ms可以以下列任意形式指定:500、PT0.5S和500ms。您也可以使用任何支持的单位:ns 纳秒us 微秒ms 毫秒s 秒m 分h 小时d 天默认单位为毫秒,可以使用@DurationUnit重写,如上面的示例所示。如果您喜欢使用构造函数绑定,可以公开相同的属性,如以下示例所示:import java.time.Duration; import java.time.temporal.ChronoUnit; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConstructorBinding; import org.springframework.boot.context.properties.bind.DefaultValue; import org.springframework.boot.convert.DurationUnit; @ConfigurationProperties("my") @ConstructorBinding public class MyProperties { private final Duration sessionTimeout; private final Duration readTimeout; public MyProperties(@DurationUnit(ChronoUnit.SECONDS) @DefaultValue("30s") Duration sessionTimeout, @DefaultValue("1000ms") Duration readTimeout) { this.sessionTimeout = sessionTimeout; this.readTimeout = readTimeout; } public Duration getSessionTimeout() { return this.sessionTimeout; } public Duration getReadTimeout() { return this.readTimeout; } }如果要升级Long属性,请确保定义单位(使用@DurationUnit)(如果不是毫秒)。这样做可以提供透明的升级路径,同时支持更丰富的格式。转换 Periods除了持续时间,Spring Boot还可以使用java.time.Period类型。应用程序配置中可以使用以下格式:通常使用int描述,默认使用天,除非指定了@PeriodUnitjava.time.Period使用标准的ISO-8601一种更简单的格式,其中值和单位对是耦合的(1y3d表示1年3天)简单格式支持以下单位:y 年m 月w 周d 天java.time.Period类型实际上从未存储周数,它是一个表示“7天”的快捷方式。转换 Data SizesSpring Framework具有以字节表示大小的DataSize值类型,如果你要公开DataSize,以下格式可以使用:通常是long格式,默认使用bytes,除非指定了@DataSizeUnit一种更可读的格式,其中值和单位是耦合的(10MB表示10兆字节)如下示例:import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.convert.DataSizeUnit; import org.springframework.util.unit.DataSize; import org.springframework.util.unit.DataUnit; @ConfigurationProperties("my") public class MyProperties { @DataSizeUnit(DataUnit.MEGABYTES) private DataSize bufferSize = DataSize.ofMegabytes(2); private DataSize sizeThreshold = DataSize.ofBytes(512); public DataSize getBufferSize() { return this.bufferSize; } public void setBufferSize(DataSize bufferSize) { this.bufferSize = bufferSize; } public DataSize getSizeThreshold() { return this.sizeThreshold; } public void setSizeThreshold(DataSize sizeThreshold) { this.sizeThreshold = sizeThreshold; } } 要指定10兆字节的缓冲区大小,10和10MB同等。256字节的大小阈值可以指定为256或256B。您也可以使用任何支持的单位。这些是:B bytesKB kilobytesMB megabytesGB gigabytesTB terabytes默认单位是字节,可以使用@DataSizeUnit重写,如上面的示例所示。如果您喜欢使用构造函数绑定,可以公开相同的属性,如以下示例所示:import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConstructorBinding; import org.springframework.boot.context.properties.bind.DefaultValue; import org.springframework.boot.convert.DataSizeUnit; import org.springframework.util.unit.DataSize; import org.springframework.util.unit.DataUnit; @ConfigurationProperties("my") @ConstructorBinding public class MyProperties { private final DataSize bufferSize; private final DataSize sizeThreshold; public MyProperties(@DataSizeUnit(DataUnit.MEGABYTES) @DefaultValue("2MB") DataSize bufferSize, @DefaultValue("512B") DataSize sizeThreshold) { this.bufferSize = bufferSize; this.sizeThreshold = sizeThreshold; } public DataSize getBufferSize() { return this.bufferSize; } public DataSize getSizeThreshold() { return this.sizeThreshold; } }如果要升级Long属性,请确保定义单位(使用@DataSizeUnit)(如果不是字节)。这样做可以提供透明的升级路径,同时支持更丰富的格式。@ConfigurationProperties 验证当@ConfigurationProperties类被Spring的@Validated注解注释时,Spring Boot会尝试验证它们。您可以直接在配置类上使用JSR-303 javax.validation约束注释。要做到这一点,请确保类路径上有一个兼容的JSR-303实现,然后向字段添加约束注解,如下例所示:import java.net.InetAddress; import javax.validation.constraints.NotNull; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; @ConfigurationProperties("my.service") @Validated public class MyProperties { @NotNull private InetAddress remoteAddress; public InetAddress getRemoteAddress() { return this.remoteAddress; } public void setRemoteAddress(InetAddress remoteAddress) { this.remoteAddress = remoteAddress; } } 您还可以通过注释@Bean方法来触发验证,该方法使用@Validated创建配置属性。为了确保始终为嵌套属性触发验证,即使找不到,也必须用@Valid注解相关字段。以下示例基于前面的MyProperties示例:import java.net.InetAddress; import javax.validation.Valid; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; @ConfigurationProperties("my.service") @Validated public class MyProperties { @NotNull private InetAddress remoteAddress; @Valid private final Security security = new Security(); public InetAddress getRemoteAddress() { return this.remoteAddress; } public void setRemoteAddress(InetAddress remoteAddress) { this.remoteAddress = remoteAddress; } public Security getSecurity() { return this.security; } public static class Security { @NotEmpty private String username; public String getUsername() { return this.username; } public void setUsername(String username) { this.username = username; } } }您还可以通过创建名为configurationPropertiesValidator的bean定义来添加自定义Spring Validator。@Bean方法应声明为静态。配置属性验证器是在应用程序生命周期的早期创建的,将@Bean方法声明为static创建Bean,而无需实例化@configuration类。这样做可以避免早期实例化可能导致的任何问题。spring-boot-actuator包括一个端点,它公开所有@ConfigurationProperties bean。将web浏览器指向/actuator/configprops或使用等效的JMX端点。有关详细信息,请参阅“[生产就绪功能](#11. 生产就绪功能)”部分。@ConfigurationProperties vs. @Value@Value注解是一个核心容器功能,它提供的功能与类型安全配置属性不同。下表总结了@ConfigurationProperties和@Value支持的功能:Feature@ConfigurationProperties@Value宽松绑定YesLimited (see note below)元数据支持YesNoSpEL 表达式NoYes如果您确实想使用@Value,我们建议您使用规范形式引用属性名称(kebab-case仅使用小写字母)。这将允许Spring Boot与使用宽松绑定的@ConfigurationProperties相同的逻辑。例如,@Value(“${demo.item-price}”)将从application.properties文件中获取demo.iitem-price和demo.itermPrice表单,并从系统环境中获取DEMO_ITEMPRICE。如果改用@Value(“${demo.itemPrice}”),则不会考虑demo.item-price和DEMO_ITEMPRICE。如果您为自己的组件定义了一组配置键,我们建议您将它们分组到带有@ConfigurationProperties注释的POJO中。这样做将为您提供结构化的类型安全对象,您可以将其注入到自己的bean中。在解析这些文件并填充环境时,不会处理应用程序属性文件中的SpEL表达式。但是,可以在@Value中编写SpEL表达式。如果应用程序属性文件中的属性值是SpEL表达式,则在通过@value使用时将对其求值。5.3 ProfilesSpring profiels 提供了一种隔离应用程序配置部分的方法,使其仅在特定环境中可用。任何@Component、@Configuration或@ConfigurationProperties都可以用@Profile标记加以限制,如下例所示:import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @Configuration(proxyBeanMethods = false) @Profile("production") public class ProductionConfiguration { // ... }如果@ConfigurationProperties bean是通过@EnableConfigurationProperties而不是自动扫描注册的,则需要在具有@EnableConfigurationProperty注解的@Configuration类上指定@Profile注解。在扫描@ConfigurationProperties的情况下,可以在@ConfigurationProperties类本身上指定@Profile。可以使用spring.profiles.active Environment属性来指定哪些配置文件处于活动状态。您可以用本章前面描述的任何方式指定属性,例如,您可以将其包含在application.properties中,如下例所示:spring.profiles.active=dev,hsqldb也可以使用以下开关在命令行上指定它:--spring.profiles.active=dev,hsqldb。如果没有激活配置文件,则启用默认配置文件。默认配置文件的名称是默认的,可以使用spring.profile.default Environment属性对其进行调整,如下例所示:spring.profiles.default=nonespring.profiles.active和spring.profiles.default只能在非配置文件特定的文档中使用。这意味着它们不能包含在spring.config.activate.on-profile激活的特定配置文件的文件或激活属性中。例如,如下配置无效:# this document is valid spring.profiles.active=prod #--- # this document is invalid spring.config.activate.on-profile=prod spring.profiles.active=metrics5.3.1 添加活动配置文件spring.profiles.active属性遵循与其他属性相同的排序规则。PropertySource优先级最高,这意味着您可以在application.properties中指定活动配置文件,然后使用命令行开关替换它们。有时,将配置添加到活动配置文件而不是替换它们是很有用的。spring.profiles.include属性可用于在spring.profiles.active属性激活的配置文件之上添加活动配置文件。SpringApplication入口点还具有用于设置其他配置文件的Java API,请参阅SpringApplication中的setAdditionalProfiles()方法。例如,当运行以下配置的应用程序时,即使使用-spring.profiles.active 开关运行,也会激活common和local配置文件:spring.profiles.include[0]=common spring.profiles.include[1]=local与spring.profile.active类似,spring.profile.include只能用于非配置文件特定的文档。如果给定的配置文件处于活动状态,则也可以使用配置文件组(在下一节中介绍)添加活动的配置文件。5.3.2 配置文件组有时,您在应用程序中定义和使用的配置文件过于细粒度,使用起来很麻烦。例如,您可以使用proddb和prodmq配置文件来独立启用数据库和消息传递功能。为了帮助实现这一点,Spring Boot允许您定义配置文件组。配置文件组允许您定义相关配置文件组的逻辑名称。例如,我们可以创建一个由prodb和prodmq配置文件组成的生产组。spring.profiles.group.production[0]=proddb spring.profiles.group.production[1]=prodmq我们的应用程序现在可以使用--spring.profiles.active=production启动,一次激活production、proddb和prodmq配置文件。5.3.3 以编程方式设置配置文件您可以在应用程序运行之前通过调用SpringApplication.setAdditionalProfiles(...),还可以使用Spring的ConfigurationEnvironment接口激活配置文件。5.3.4 指定配置文件application.properties(或application.yml)和通过@ConfigurationProperties引用的文件的特定配置文件的变体都被视为文件并被加载。有关详细信息,请参阅“Profile特定文件”。5.4 日志Spring Boot使用Commons Logging进行所有内部日志记录,但底层日志实现保持打开状态。默认配置提供了Java Util Logging, Log4J2, 和 Logback。在每种情况下,记录器都预先配置为使用控制台输出,也可以使用可选的文件输出。默认情况下,如果使用“Starters”,则使用Logback进行日志记录。还包括适当的Logback路由,以确保使用Java Util Logging、Commons Logging、Log4J或SLF4J的依赖库都能正常工作。Java有很多可用的日志框架。如果上面的列表看起来令人困惑,请不要担心。通常,您不需要更改日志依赖关系,Spring Boot默认值也可以正常工作。当您将应用程序部署到servlet容器或应用程序服务器时,使用JavaUtil Logging API执行的日志记录不会路由到应用程序的日志中。这将防止容器或已部署到容器的其他应用程序执行的日志记录出现在应用程序的日志中。5.4.1 日志格式化Spring Boot的默认日志输出类似于以下示例:2023-01-19 14:18:28.678 INFO 16676 --- [ main] o.s.b.d.f.s.MyApplication : Starting MyApplication using Java 1.8.0_362 on myhost with PID 16676 (/opt/apps/myapp.jar started by myuser in /opt/apps/) 2023-01-19 14:18:28.686 INFO 16676 --- [ main] o.s.b.d.f.s.MyApplication : No active profile set, falling back to 1 default profile: "default" 2023-01-19 14:18:30.656 INFO 16676 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2023-01-19 14:18:30.672 INFO 16676 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2023-01-19 14:18:30.672 INFO 16676 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.71] 2023-01-19 14:18:30.756 INFO 16676 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2023-01-19 14:18:30.757 INFO 16676 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1977 ms 2023-01-19 14:18:31.328 INFO 16676 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2023-01-19 14:18:31.339 INFO 16676 --- [ main] o.s.b.d.f.s.MyApplication : Started MyApplication in 3.552 seconds (JVM running for 4.101)输出以下项目:日期和时间:毫秒级精度和易于排序的Log级别:ERROR, WARN, INFO, DEBUG, 或者 TRACE进程ID— 分隔符,用于区分实际的日志消息开头线程名称:用方括号括起来(可能会被控制台输出截断)Logger 名称:通常是源类名(通常是缩写)日志消息Logback没有FATAL级别。它被映射到ERROR。5.4.2 Console 输出默认日志配置在写入消息时将消息回显到控制台。默认情况下,记录ERROR级别、WARN级别和INFO级别消息。您还可以通过使用--debug标志启动应用程序来启用“调试”模式。$ java -jar myapp.jar --debug您还可以在application.properties中指定debug=true。启用调试模式后,将配置一组核心记录器(嵌入式容器、Hibernate和Spring Boot)以输出更多信息。启用调试模式使用debug级别不会将应用程序配置为记录所有消息。或者,您可以通过使用--trace标志(或应用程序配置中的trace=true)来启动应用程序“trace”模式,这样做可以为一些核心记录器(嵌入式容器、Hibernate模式生成和整个Spring组合)启用跟踪日志记录。彩色输出如果您的终端支持ANSI,则使用颜色输出来提高可读性。您可以将spring.output.ansi.enabled设置为支持的值,以覆盖自动检测。通过使用%clr转换字配置颜色编码。在最简单的形式中,转换器根据日志级别为输出着色,如下例所示:%clr(%5p)下表描述了日志级别到颜色的映射:LevelColorFATALRedERRORRedWARNYellowINFOGreenDEBUGGreenTRACEGreen或者,您可以通过将其作为转换选项来指定应使用的颜色或样式。例如,要使文本变为黄色,请使用以下设置:%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){yellow}支持以下颜色和样式:bluecyanfaintgreenmagentaredyellow5.4.3 文件输出默认情况下,Spring Boot只记录到控制台,不写入日志文件。如果要在控制台输出之外写入日志文件,你需要设置logging.file.name或者logging.file.path。下表显示了logging.*如何一起使用:logging.file.namelogging.file.pathExampleDescription(none)(none)仅仅在控制台输出Specific file(none)my.log写入指定的日志文件。名称可以是确切的位置或相对于当前目录。(none)Specific directory/var/log将spring.log写入指定目录。名称可以是确切的位置或相对于当前目录。日志文件在达到10MB时会重新开始写入,与控制台输出一样,默认情况下会记录ERROR级别、WARN级别和INFO级别的消息。日志记录配置独立于实际的日志记录基础结构。因此,特定的配置键(如logback.configurationFile for logback)不会由springBoot管理。5.4.4 文件周期如果使用Logback,则可以使用application.properties或application.yaml文件微调日志周期设置。对于所有其他日志记录系统,您需要自己直接配置周期设置(例如,如果使用Log4j2,则可以添加Log4j2.xml或Log4j2-pring.xml文件)。支持以下周期性配置:NameDescriptionlogging.logback.rollingpolicy.file-name-pattern用于创建日志存档的文件名模式。logging.logback.rollingpolicy.clean-history-on-start应用程序启动时应进行日志存档清理。logging.logback.rollingpolicy.max-file-size存档前日志文件的最大大小。logging.logback.rollingpolicy.total-size-cap删除日志存档文件之前可以使用的最大大小。logging.logback.rollingpolicy.max-history要保留的存档日志文件的最大数量(默认为7)。5.4.5 日志级别所有支持的日志记录系统都可以在Spring环境中设置日志记录程序级别(例如,在application.properties中),通过使用logging.level.<logger-name>=<level>,其中级别是TRACE、DEBUG、INFO、WARN、ERROR、FATAL或OFF之一。root日志记录程序可以通过使用logging.level.root进行配置。如下是application.properties中的日志配置:logging.level.root=warn logging.level.org.springframework.web=debug logging.level.org.hibernate=error还可以使用环境变量设置日志记录级别。例如,LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_WEB=DEBUG将设置org.springframework.web为DEBUG。上述方法仅适用于包级日志记录。由于宽松绑定总是将环境变量转换为小写,因此不可能以这种方式为单个类配置日志记录。如果需要给类配置日志,你可以使用SPRING_APPLICATION_JSON 。5.4.6 日志组能够将相关的记录器分组在一起,以便可以同时配置它们,这通常很有用。例如,您可能通常会更改所有Tomcat相关记录器的日志记录级别,但您不容易记住顶级包。为了帮助实现这一点,Spring Boot允许您在Spring环境中定义日志组。例如,以下是如何通过将“tomcat”组添加到application.properties:logging.group.tomcat=org.apache.catalina,org.apache.coyote,org.apache.tomcat定义后,您可以使用单行更改组中所有记录器的级别:logging.level.tomcat=traceSpring Boot包括以下预定义的日志记录组,可以立即使用:NameLoggersweborg.springframework.core.codec, org.springframework.http, org.springframework.web, org.springframework.boot.actuate.endpoint.web, org.springframework.boot.web.servlet.ServletContextInitializerBeanssqlorg.springframework.jdbc.core, org.hibernate.SQL, org.jooq.tools.LoggerListener5.4.7 使用日志 ShutDown 钩子为了在应用程序终止时释放日志记录资源,提供了在JVM退出时触发日志系统清理的关闭挂钩。除非将应用程序部署为war文件,否则会自动注册此关闭挂钩。如果应用程序具有复杂的上下文层次结构,则关闭挂钩可能无法满足您的需要。如果没有,请禁用关闭挂钩并调查底层日志系统直接提供的选项。例如,Logback提供了上下文选择器,允许在自己的上下文中创建每个Logger。你可以使用logging.register-shutdown-hook关闭钩子,设置false关闭注册。logging.register-shutdown-hook=false5.4.8 可以通过在类路径中包含适当的库来激活各种日志记录系统,并且可以通过在路径的根目录中或在以下Spring Environment属性指定的位置提供适当的配置文件来进一步定制:logging.config。您可以强制Spring Boot使用特定的日志记录系统,使用org.springframework.boot.logging.LoggingSystem。该值应该是LoggingSystem实现的完全限定类名。您还可以使用值none完全禁用Spring Boot的日志记录配置。由于日志记录是在创建ApplicationContext之前初始化的,因此无法从Spring@Configuration文件中的@PropertySources控制日志记录。更改日志记录系统或完全禁用日志记录系统的唯一方法是通过系统配置。根据您的日志记录系统,将加载以下文件:Logging SystemCustomizationLogbacklogback-spring.xml, logback-spring.groovy, logback.xml, or logback.groovyLog4j2log4j2-spring.xml or log4j2.xmlJDK (Java Util Logging)logging.properties如果可能,我们建议您在日志配置中使用-spring变量(例如,logback-spring.xml而不是logback.xml)。如果您使用标准配置位置,spring无法完全控制日志初始化。Java Util Logging存在已知的类加载问题,在从“可执行jar”运行时会导致问题。我们建议您在从“可执行jar”运行时尽可能避免使用它。为了帮助定制,提供了一些其他配置从Spring环境传输到系统配置,如下表所述:Spring EnvironmentSystem PropertyCommentslogging.exception-conversion-wordLOG_EXCEPTION_CONVERSION_WORD记录异常时使用的转换字。logging.file.nameLOG_FILE如果已定义,则在默认日志配置中使用。logging.file.pathLOG_PATH如果已定义,则在默认日志配置中使用。logging.pattern.consoleCONSOLE_LOG_PATTERN要在控制台上使用的日志模式(stdout)。logging.pattern.dateformatLOG_DATEFORMAT_PATTERN日志日期格式的追加模式。logging.charset.consoleCONSOLE_LOG_CHARSET用于控制台日志记录的字符集。logging.pattern.fileFILE_LOG_PATTERN要在文件中使用的日志模式(如果启用了“LOG_FILE”)。logging.charset.fileFILE_LOG_CHARSET用于文件日志记录的字符集(如果启用了“LOG_FILE”)。logging.pattern.levelLOG_LEVEL_PATTERN呈现日志级别时使用的格式(默认为“%5p”)。PIDPID当前进程ID(如果可能,并且尚未定义为操作系统环境变量时发现)。如果使用Logback,还将传输以下配置:Spring EnvironmentSystem PropertyCommentslogging.logback.rollingpolicy.file-name-patternLOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN滚动的日志文件名模式 (默认${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz).logging.logback.rollingpolicy.clean-history-on-startLOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START是否在启动时清除存档日志文件。logging.logback.rollingpolicy.max-file-sizeLOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE最大日志文件大小。logging.logback.rollingpolicy.total-size-capLOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP要保留的日志备份的总大小。logging.logback.rollingpolicy.max-historyLOGBACK_ROLLINGPOLICY_MAX_HISTORY要保留的存档日志文件的最大数量。所有受支持的日志记录系统在解析其配置文件时都可以参考系统配置。有关示例,请参见spring-bot.jar中的默认配置:LogbackLog4j 2Java Util logging如果要在日志属性中使用占位符,则应使用Spring Boot的语法,而不是底层框架的语法。值得注意的是,如果使用Logback,则应使用:作为属性名称与其默认值之间的分隔符,而不要使用:-。通过仅覆盖LOG_LEVEL_PATTERN(或Logback 的 logging.pattern.level),可以将MDC和其他特殊内容添加到日志行。如你使用logging.pattern.level=user:%X{user} %5p,那么默认日志格式包含“user”的MDC条目(如果存在),如下例所示。2019-08-30 12:30:04.031 user:someone INFO 22174 --- [ nio-8080-exec-0] demo.Controller Handling authenticated request 5.4.9 Logback扩展Spring Boot包括许多Logback扩展,可以帮助进行高级配置。您可以在logback-spring.xml配置文件中使用这些扩展名。因为标准logback.xml配置文件很早就加载,您需要使用logback-spring.xml或定义logging.config属性扩展不能用于Logback的配置扫描。如果尝试这样做,则对配置文件进行更改会导致类似以下错误会被记录:ERROR in ch.qos.logback.core.joran.spi.Interpreter@4:71 - no applicable action for [springProperty], current ElementPath is [[configuration][springProperty]] ERROR in ch.qos.logback.core.joran.spi.Interpreter@4:71 - no applicable action for [springProfile], current ElementPath is [[configuration][springProfile]]Profile指定配置<springProfile>标记允许您根据激活的Spring配置文件选择性地包括或排除配置部分。<configuration>元素中的任何位置都支持配置文件部分。使用name属性指定哪个配置文件接受配置。<springProfile>标记可以包含profile文件名称(例如staging)或profile文件表达式。profile表达式允许表达更复杂的逻辑,例如,production & (eu-central | eu-west),查看 Spring Framework指南查看详细信息。以下列表显示了三个示例配置文件:<springProfile name="staging"> <!-- configuration to be enabled when the "staging" profile is active --> </springProfile> <springProfile name="dev | staging"> <!-- configuration to be enabled when the "dev" or "staging" profiles are active --> </springProfile> <springProfile name="!production"> <!-- configuration to be enabled when the "production" profile is not active --> </springProfile>环境属性配置<springProperty>标记允许您从Spring环境中公开配置,以便在Logback中使用。如果您想从Logback配置中的访问application.properties文件的值,那么这样做很有用。该标记的工作方式与Logback的标准<property>标记类似。可以指定属性的source(从环境中),而不是指定直接值。如果您需要将属性存储在local范围以外的其他位置,你可以使用scope属性。如果需要回退值(为在Environment环境中设置),你可以使用defaultValue属性,以下示例显示如何公开配置以在Logback中使用:<springProperty scope="context" name="fluentHost" source="myapp.fluentd.host" defaultValue="localhost"/> <appender name="FLUENT" class="ch.qos.logback.more.appenders.DataFluentAppender"> <remoteHost>${fluentHost}</remoteHost> ... </appender>source必须使用kebab格式指定(例如my.property-name)。然而,可以使用宽松的规则将属性添加到Environment环境中。5.4.10 Log4j2 扩展Spring Boot包括对Log4j2的许多扩展,可以帮助进行高级配置,您可以在任何log4j2-spring.xml配置文件中使用这些扩展。由于标准log4j2.xml配置文件加载得太早,因此不能在其中使用扩展。您需要使用log4j2-spring.xml或定义logging.config属性。这些扩展取代了Log4J提供的Spring Boot支持。您应该确保在构建中不包含org.apache.logging.log4j:log4j-spring-boot模块。Profile 指定配置<springProfile>标记允许您根据激活的Spring配置文件选择性地包括或排除配置部分。<configuration>元素中的任何位置都支持配置文件部分。使用name属性指定哪个配置文件接受配置。<springProfile>标记可以包含profile文件名称(例如staging)或profile文件表达式。profile表达式允许表达更复杂的逻辑,例如,production & (eu-central | eu-west),查看 Spring Framework指南查看详细信息。以下列表显示了三个示例配置文件:<SpringProfile name="staging"> <!-- configuration to be enabled when the "staging" profile is active --> </SpringProfile> <SpringProfile name="dev | staging"> <!-- configuration to be enabled when the "dev" or "staging" profiles are active --> </SpringProfile> <SpringProfile name="!production"> <!-- configuration to be enabled when the "production" profile is not active --> </SpringProfile>环境属性查找如果您想在Log4j2配置中引用Spring环境中的属性,可以使用spring:前缀查找。如果您想访问Log4j2配置中application.properties文件中的值,那么这样做很有用。以下示例显示如何设置名为applicationName的Log4j2属性,该属性从spring环境中读取spring.application.name:<Properties> <Property name="applicationName">${spring:spring.application.name}</Property> </Properties>查找关键字应该以kebab 格式(例如 my.property-name)。Log4j2 系统配置Log4j2支持许多可用于配置各种项目的系统配置。log4j2.skipJansi系统属性可用于配置ConsoleAppender是否将尝试在Windows上使用Jansi输出流。Log4j2初始化后加载的所有系统配置都可以从Spring环境中获得。例如,可以将log4j2.skipJansi=false添加到application.properties文件中,以便ConsoleAppender在Windows上使用Jansi。只有当系统配置和操作系统环境变量不包含加载的值时,才考虑Spring Environment环境。5.5 国际化Spring Boot支持本地化消息,以便您的应用程序能够迎合不同语言需求的用户。默认情况下,Spring Boot会在类路径的根位置查找messages资源包。当配置的资源束的默认配置文件可用时(默认情况下为messages.properties),将应用自动配置。如果资源包只包含特定于语言的配置文件,则需要添加默认值。如果没有找到与任何配置的基本名称匹配的配置文件,则不会有自动配置的MessageSource。可以使用spring.messages命名空间配置资源包的基本名称以及其他几个属性,如下例所示:spring.messages.basename=messages,config.i18n.messages spring.messages.fallback-to-system-locale=falsespring.messages.basename支持逗号分隔的位置列表,可以是包限定符,也可以是从类路径根解析的资源。有关更多支持的选项,请参阅MessageSourceProperties。5.6 JSONSpring Boot提供了与三个JSON映射库的集成:GsonJacksonJSON-BJackson是首选和默认库。5.6.1 Jackson提供了Jackson的自动配置,Jackson是spring-boot-starter-json的一部分。当Jackson在类路径上时,会自动配置ObjectMapper bean。提供了几个配置,用于自定义ObjectMapper的配置。自定义序列化和反序列化如果使用Jackson序列化和反序列化JSON数据,您可能需要编写自己的JsonSerializer和JsonDeserializer类。自定义序列化,通常使用模块化注册。Spring Boot提供了另一种@JsonComponent注解,使直接注册Spring Beans变得更容易。你能使用@JsonComponent注解在JsonSerializer, JsonDeserializer 或KeyDeserializer 实现上。您还可以在包含序列化程序/反序列化程序作为内部类的类上使用它,如下例所示:import java.io.IOException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.ObjectCodec; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import org.springframework.boot.jackson.JsonComponent; @JsonComponent public class MyJsonComponent { public static class Serializer extends JsonSerializer<MyObject> { @Override public void serialize(MyObject value, JsonGenerator jgen, SerializerProvider serializers) throws IOException { jgen.writeStartObject(); jgen.writeStringField("name", value.getName()); jgen.writeNumberField("age", value.getAge()); jgen.writeEndObject(); } } public static class Deserializer extends JsonDeserializer<MyObject> { @Override public MyObject deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException { ObjectCodec codec = jsonParser.getCodec(); JsonNode tree = codec.readTree(jsonParser); String name = tree.get("name").textValue(); int age = tree.get("age").intValue(); return new MyObject(name, age); } } }ApplicationContext中的所有@JsonComponent bean都会自动向Jackson注册。因为@JsonComponent是用@Component元注释的,所以通常的组件扫描规则适用。Spring Boot还提供了JsonObjectSerializer和JsonObjectDeserializer基类,这些基类在序列化对象时为标准Jackson版本提供了有用的替代方案。有关详细信息,请参阅Javadoc中的JsonObjectSerializer和JsonObjectDeserializer。上面的示例可以重写为使用JsonObjectSerializer/JsonObjectDeserializer,如下所示:import java.io.IOException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.ObjectCodec; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.SerializerProvider; import org.springframework.boot.jackson.JsonComponent; import org.springframework.boot.jackson.JsonObjectDeserializer; import org.springframework.boot.jackson.JsonObjectSerializer; @JsonComponent public class MyJsonComponent { public static class Serializer extends JsonObjectSerializer<MyObject> { @Override protected void serializeObject(MyObject value, JsonGenerator jgen, SerializerProvider provider) throws IOException { jgen.writeStringField("name", value.getName()); jgen.writeNumberField("age", value.getAge()); } } public static class Deserializer extends JsonObjectDeserializer<MyObject> { @Override protected MyObject deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec, JsonNode tree) throws IOException { String name = nullSafeValue(tree.get("name"), String.class); int age = nullSafeValue(tree.get("age"), Integer.class); return new MyObject(name, age); } } }混合Jackson支持混合可以用于将附加注释混合到目标类上已经声明的注释中。Spring Boot的Jackson自动配置将扫描应用程序的包,查找用@JsonMixin注释的类,并将它们注册到自动配置的ObjectMapper中。注册由Spring Boot的JsonMixinModule执行。5.6.2 Gson提供了Gson的自动配置。当Gson在类路径上时,会自动配置Gson bean。提供了几个spring.gson.*配置来定制配置。要获得更多控制,可以使用一个或多个GsonBuilderCustomizer bean。5.6.3 JSON-B提供JSON-B的自动配置。当JSON-B API和实现位于类路径上时,将自动配置Jsonb bean。首选的JSON-B实现是Eclipse Yasson,它提供了依赖性管理。5.7 任务执行和调度在上下文中缺少Executor bean的情况下,Spring Boot 自动配置ThreadPoolTaskExecutor,并使用可自动关联到异步任务执行(@EnableAsync)和Spring MVC异步请求处理的合理默认值。常规任务执行(即@EnableAsync)将透明地使用它,但不会配置Spring MVC支持,因为它需要AsyncTaskExecutor实现(名为applicationTaskExecutor)。根据您的目标安排,您可以将Executor更改为ThreadPoolTaskExecutor,或者同时定义ThreadPoolTaskExecutor和AsyncConfigurer来包装自定义Executor。自动配置的TaskExecutorBuilder允许您轻松创建复制默认情况下自动配置的实例。线程池使用8个核心线程,可以根据负载增长和收缩。可以使用spring.task.execution命名空间对这些默认设置进行微调,如下例所示:spring.task.execution.pool.max-size=16 spring.task.execution.pool.queue-capacity=100 spring.task.execution.pool.keep-alive=10s这将更改线程池以使用有界队列,这样当队列已满(100个任务)时,线程池将增加到最多16个线程。当线程空闲10秒(而不是默认情况下的60秒)时,会回收线程,因此池的收缩更为积极。如果需要将ThreadPoolTaskScheduler与计划的任务执行相关联(例如使用@EnableScheduling),也可以自动配置ThreadPoolTaskScheduler。线程池默认使用一个线程,其设置可以使用spring.task.scheduling进行微调,如下例所示:spring.task.scheduling.thread-name-prefix=scheduling- spring.task.scheduling.pool.size=2如果需要创建自定义执行器或调度程序,则在上下文中可以使用TaskExecutorBuilder bean和TaskSchedulerBuilder bean。笔者注:TaskExecutorBuilder 和 TaskSchedulerBuilder 能通过Bean 注入,最终创建的Bean 为 ThreadPoolTaskExecutor5.8 测试Spring Boot提供了许多实用程序和注解,以帮助测试应用程序。测试支持由两个模块提供:spring-boot-test包含核心项目,spring-boot-test-autoconfigure支持自动配置。大多数开发者使用spring-boot-starter-test开始,它导入了两个Spring Boot测试模块以及JUnit Jupiter、AssertJ、Hamcrest和许多其他有用的库。如果您有使用JUnit4的测试,可以使用JUnit5引擎来运行它们,按以下示例添加junit-vintage-engine依赖<dependency> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-core</artifactId> </exclusion> </exclusions> </dependency>5.8.1 测试范围依赖spring-boot-starter-test 的Starter(test scope)包含如下库:JUnit 5: Java单元测试的实际标准Spring Test & Spring Boot Test: Spring Boot应用程序的实用程序和集成测试支持AssertJ: 断言库Hamcrest: 匹配器对象库(也称为约束或谓词Mockito: Java模拟框架.JSONassert: JSON断言库JsonPath: JSON适用的XPath我们通常发现这些公共库在编写测试时很有用。如果这些库不符合您的需要,您可以添加自己的附加测试依赖项。5.8.2 测试Spring 应用程序依赖注入的一个主要优点是它应该使代码更容易进行单元测试。您可以使用new操作符实例化对象,而不需要使用Spring,也可是使用mock对象。通常需要集成测试而不是单元测试(在Spring ApplicationContext中)。Spring 框架包括这样的集成测试模块,你可以直接依赖org.springframework:spring-test或者使用spring-boot-starter-test。如果您以前没有使用过spring测试模块,那么应该首先阅读spring Framework参考文档的相关部分。5.8.3 测试Spring Boot 应用程序Spring Boot应用程序是一个Spring ApplicationContext,因此除了使用普通的Spring上下文进行测试外,无需进行任何特殊的测试。Spring Boot 提供 @SpringBootTest注解,当您需要Spring Boot特性时,它可以作为标准spring test @ContextConfiguration注释的替代。注释的工作原理是通过SpringApplication创建测试中使用的ApplicationContext。除了@SpringBootTest之外,还提供了许多其他注解来测试应用程序的更具体的切片。检测Web应用程序类型默认的,@SpringBootTest不会启动一个服务,您可以使用@SpringBootTest的webEnvironment属性来进一步优化测试的运行方式:MOCK(Default) : 加载一个web ApplicationContext并提供一个模拟的web环境。使用该注解时不会启动一个容器。如果在类路径上web 环境不可用,将创建一个非web环境的ApplicationContext。它可以与 @AutoConfigureMockMvc 或者 @AutoConfigureWebTestClient 一起使用,用来对web应用程序进行模拟测试。RANDOM_PORT:加载WebServerApplicationContext并提供真实的web环境。嵌入服务器将启动和监听基于随机端口。DEFINED_PORT: 加载WebServerApplicationContext并提供真实的web环境。嵌入式服务器启动并在定义的端口(基于application.properties)或默认端口8080上侦听。NONE:使用SpringApplication加载ApplicationContext,但不提供任何web环境(模拟或其他)。如果您的测试是@Transactional,默认情况下,它会在每个测试方法结束时回滚事务。然而,当RANDOM_PORT或DEFINED_PORT一起使用时,实际会提供了一个真实的servlet环境,HTTP客户端和服务器在单独的线程中运行,在这种情况下,在服务器上启动的任何事务都不会回滚。检测测试配置如果使用Spring 框架,你可以使用@ContextConfiguration(classes=…)指定要加载的@Configuration,或者在测试用使用的嵌套@Configuration。使用Spring Boot测试,这些不是必须的,Spring Boot 的@*Test注解会自动搜索primary 配置,只要没有显示指定配置。从当前的测试包开始搜索,直到搜索到@SpringBootApplication或@SpringBootConfiguration为止,只要以合理的方式构造代码,总是可以找到。如果需要自定义主配置,可以使用嵌套的@TestConfiguration类,不同于嵌套的@Configuration类,嵌套的@TestConfiguration使用在应用程序的主配置之外。Spring 测试框架会缓存上下文,因此只要你的测试共享配置,这些耗时操作都只会加载一次。笔者注:@TestConfiguration 用来对@Configuration做补充,用来指定专门用来测试的bean排除测试配置如果你的应用使用了组件扫描(比如,使用了@SpringBootApplication 或 @ComponentScan),你可能会发现对于一些特定测试创建的配置类也被加载。@TestConfiguration能够使用在测试的内部类中,以自定义配置。如果你定义在顶级类,则表示src/test/java中的类不通过扫描获取,这个时候可以通过Import显式导入。import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; @SpringBootTest @Import(MyTestsConfiguration.class) class MyTests { @Test void exampleTest() { // ... } }如果不使用@SpringBootApplication,而是使用的@ComponentScan,则应该注册TypeExcludeFilter,用来排除配置使用应用程序参数如果你的应用需要参数,那么可以使用@SpringBootTest的args属性。import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.test.context.SpringBootTest; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(args = "--app.test=one") class MyApplicationArgumentTests { @Test void applicationArgumentsPopulated(@Autowired ApplicationArguments args) { assertThat(args.getOptionNames()).containsOnly("app.test"); assertThat(args.getOptionValues("app.test")).containsOnly("one"); } }使用Mock环境测试使用Spring MVC,我们可以使用MockMvc或WebTestClient查询web端点,如下例所示:import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @AutoConfigureMockMvc class MyMockMvcTests { @Test void testWithMockMvc(@Autowired MockMvc mvc) throws Exception { mvc.perform(get("/")).andExpect(status().isOk()).andExpect(content().string("Hello World")); } // 如果WebFlux存在,也可以使用WebTestClient测试 @Test void testWithWebTestClient(@Autowired WebTestClient webClient) { webClient .get().uri("/") .exchange() .expectStatus().isOk() .expectBody(String.class).isEqualTo("Hello World"); } }如果希望只关注web层而不启动完整的ApplicationContext,请考虑使用@WebMvcTest。对于Spring WebFlux,可以使用WebTestClient,如下示例:import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.reactive.server.WebTestClient; @SpringBootTest @AutoConfigureWebTestClient class MyMockWebTestClientTests { @Test void exampleTest(@Autowired WebTestClient webClient) { webClient .get().uri("/") .exchange() .expectStatus().isOk() .expectBody(String.class).isEqualTo("Hello World"); } }在模拟环境中进行测试通常比使用完整的servlet容器运行更快。然而,由于模拟发生在SpringMVC层,依赖于较低级别servlet容器行为的代码不能直接使用MockMvc进行测试。例如,Spring Boot的错误处理基于servlet容器提供的“错误页”支持。这意味着,虽然可以按预期测试MVC层抛出和处理异常,但不能直接测试是否呈现了特定的自定义错误页面。如果需要测试这些较低级别的问题,可以启动一个完全运行的服务器,如下一节所述。使用运行服务器测试如果需要启动一个完整的运行服务器,建议使用随机端口。使用@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT),将在每次运行测试的时候获取一个随机的可用端口。@LocalServerPort注解能在测试中注入实际使用的端口。为了方便,在参数中使用@Autowired注入WebTestClient。import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.test.web.reactive.server.WebTestClient; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class MyRandomPortWebTestClientTests { @Test void exampleTest(@Autowired WebTestClient webClient) { webClient .get().uri("/") .exchange() .expectStatus().isOk() .expectBody(String.class).isEqualTo("Hello World"); } }WebTestClient 能使用在实时服务和模拟环境中这种方式要求类路径中存在spring-webflux,如果你不添加webflux,Spring Boot提供了TestRestTemplate。import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class MyRandomPortTestRestTemplateTests { @Test void exampleTest(@Autowired TestRestTemplate restTemplate) { String body = restTemplate.getForObject("/", String.class); assertThat(body).isEqualTo("Hello World"); } }自定义WebTestClient需要自定义WebTestClient,配置一个WebTestClientBuilderCustomizer bean。使用WebTestClient.Builder会调用此类创建的bean。笔者注:源码如下: @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ WebClient.class, WebTestClient.class }) static class WebTestClientMockMvcConfiguration { @Bean @ConditionalOnMissingBean WebTestClient webTestClient(MockMvc mockMvc, List<WebTestClientBuilderCustomizer> customizers) { WebTestClient.Builder builder = MockMvcWebTestClient.bindTo(mockMvc); for (WebTestClientBuilderCustomizer customizer : customizers) { customizer.customize(builder); } return builder.build(); } } 使用JMX因为测试上下文框架缓存上下文的缘故,默认情况下禁用JMX以防止相同的组件注册在同一域上。如果此类需要访问MBeanServer,请标记dirty。import javax.management.MBeanServer; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(properties = "spring.jmx.enabled=true") @DirtiesContext class MyJmxTests { @Autowired private MBeanServer mBeanServer; @Test void exampleTest() { assertThat(this.mBeanServer.getDomains()).contains("java.lang"); // ... } }查看更多介绍,@DirtiesContext使用Metrics不论类路径是怎么样的,使用@SpringBootTest时都不会自动配置,除了内存中支持的注册表。如果需要将度量导出到其他后端作为集成测试的一部分,请使用@AutoConfigureMetrics对其进行注释。Mocking and Spying Beans运行测试时,有时需要模拟应用程序上下文中的某些组件。例如,您可能有一个在开发期间不可用的远程服务的facade。当您想要模拟在真实环境中很难触发的故障时,模拟也很有用。Spring Boot包含一个@MockBean注解,可用于为ApplicationContext中的bean定义Mockito mock。您可以使用注解添加新bean或替换单个现有bean定义。注解可以直接用于测试类、测试中的字段或@Configuration类和字段。当在字段上使用时,创建的mock的实例也会被注入。模拟bean在每个测试方法之后都会自动重置。如果您的测试使用SpringBoot的一个测试注释(例如@SpringBootTest),则会自动启用此功能。要将此功能用于不同的排列,必须显式添加监听器,如下例所示:import org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener; import org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; @ContextConfiguration(classes = MyConfig.class) @TestExecutionListeners({ MockitoTestExecutionListener.class, ResetMocksTestExecutionListener.class }) class MyTests { // ... } 以下示例使用模拟实现替换现有的RemoteService bean:import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @SpringBootTest class MyTests { @Autowired private Reverser reverser; @MockBean private RemoteService remoteService; @Test void exampleTest() { given(this.remoteService.getValue()).willReturn("spring"); String reverse = this.reverser.getReverseValue(); // Calls injected RemoteService assertThat(reverse).isEqualTo("gnirps"); } }@MockBean不能用于模拟在应用程序上下文刷新期间执行的bean的行为。执行测试时,应用程序上下文刷新已完成,此时配置模拟行为已经晚了。建议在这种情况下使用@Bean方法来创建和配置模拟。此外,您可以使用@SpyBean用Mockito spy包装任何现有的bean。有关详细信息,请参阅Javadoc。笔者注:@SpyBean 可以用在类上,或者@Configuration类型、测试类和@RunWith类中的字段上。@SpyBean 示例:@RunWith(SpringRunner.class) public class ExampleTests { @SpyBean private ExampleService service; @Autowired private UserOfService userOfService; @Test public void testUserOfService() { String actual = this.userOfService.makeUse(); assertEquals("Was: Hello", actual); verify(this.service).greet(); } @Configuration @Import(UserOfService.class) // A @Component injected with ExampleService static class Config { } } CGLib代理,例如为作用域bean创建的代理,将代理的方法声明为final。这会阻止Mockito正常运行,因为它无法在默认配置中模拟或监视最终方法。如果您想模拟或监视这样的bean,请通过将org.Mockito:Mockito-inline添加到应用程序的测试依赖项中,将Mockito配置为使用其内联模拟生成器。这允许Mockito模拟和监视最终方法。虽然Spring的测试框架在测试之间缓存应用程序上下文,并为共享相同配置的测试重用上下文,但使用@MockBean或@SpyBean会影响缓存键,这很可能会增加上下文的数量。如果您使用@SpyBean监视带有@Cacheable方法的bean,这些方法按名称引用参数,则必须使用-parameters编译应用程序。这确保一旦监视到bean,缓存基础结构就可以使用参数名称。当您使用@SpyBean监视由Spring代理的bean时,在某些情况下,您可能需要删除Spring的代理,例如,在使用given或when设置期望值时。使用AopTestUtils.getTargetObject(yourProxiedSpy)执行此操作。Auto-configured 测试Spring Boot 自动配置系统对于应用程序能够运行的很好,但有时对测试来讲还是太多了。在测试的时候只加载测试“片段”是非常有帮助的。比如,您可能希望测试Spring MVC控制器是否正确映射URL,并且不希望在这些测试中涉及数据库调用,或者您可能希望对JPA实体进行测试,并且当这些测试运行时,您不关心web层。spring-boot-test-autoconfigure模块有许多注解,能够用来配置这些"片段"。他们中的每个都以相似的方式工作,提供了@…Test注解用来加载ApplicationContext,一个或多个@AutoConfigure…用来自定义自动化配置。每个片段将组件扫描限制到适当的组件,并加载一组非常有限的自动配置类。如果您需要排除其中一个,大多数@…Test批注提供excludeAutoConfiguration属性。或者,您可以使用@ImportAutoConfiguration#exclude。不支持一个测试中使用多个@...Test来包含多个“片段”。如果您需要多个“片段”,请选择一个@…Test注解并包括其他片段的@AutoConfiguration… 注解。如果你对应用的测试片段不关心,但需要一些自动配置的测试bean,可以使用@AutoConfigure…和@SpringBootTest注解的组合。Auto-configured JSON 测试要测试对象JSON序列化和反序列化是否按预期工作,可以使用@JsonTest注解。@JsonTest自动配置可用的受支持的JSON映射器,它可以是以下库之一:Jackson ObjectMapper, 任何 @JsonComponent bean 和任何 Jackson ModuleGsonJsonb@JsonTest 启用的自动配置列表可在附录中找到。如果需要自动配置的元素,你可以使用@AutoConfigureJsonTesters注解。Spring Boot 包括基于AssertJ的辅助程序,他们与JSONAssert 和 JsonPath库一起工作,以检查JSON是否按预期工作。JacksonTester、GsonTester、JsonbTester和BasicJsonTester类能够分别用于Jackson, Gson, Jsonb和字符串。使用@JsonTest时,测试类上的任何辅助字段都可以使用@Autowired。以下示例显示了Jackson的测试类。import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.json.JsonTest; import org.springframework.boot.test.json.JacksonTester; import static org.assertj.core.api.Assertions.assertThat; @JsonTest class MyJsonTests { @Autowired private JacksonTester<VehicleDetails> json; @Test void serialize() throws Exception { VehicleDetails details = new VehicleDetails("Honda", "Civic"); // Assert against a `.json` file in the same package as the test assertThat(this.json.write(details)).isEqualToJson("expected.json"); // Or use JSON path based assertions assertThat(this.json.write(details)).hasJsonPathStringValue("@.make"); assertThat(this.json.write(details)).extractingJsonPathStringValue("@.make").isEqualTo("Honda"); } @Test void deserialize() throws Exception { String content = "{\"make\":\"Ford\",\"model\":\"Focus\"}"; assertThat(this.json.parse(content)).isEqualTo(new VehicleDetails("Ford", "Focus")); assertThat(this.json.parseObject(content).getMake()).isEqualTo("Ford"); } }JSON 辅助类也可以直接用于单元测试。如果不使用@JsonTest,请在@Before中调用helper的initFields方法如果是用Spring Boot 基于AssertJ的辅助程序来断言给定JSON路径上的数值,则可能没法根据类型使用isEqualTo。相反,可使用AssertJ的satisfies来断言该值与给定条件是否匹配。例如,下面示例断言实际数字是一个接近0.15的浮点值,偏移量为0.01@Test void someTest() throws Exception { SomeObject value = new SomeObject(0.152f); assertThat(this.json.write(value)).extractingJsonPathNumberValue("@.test.numberValue") .satisfies((number) -> assertThat(number.floatValue()).isCloseTo(0.15f, within(0.01f))); }Auto-configured Spring MVC 测试要测试Spring MVC 控制器是否按预期工作,使用@WebMvcTest注解。@WebMvcTest自动配置Spring MVC 基础结构,并将扫描到的bean限制为@Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor, WebMvcConfigurer, WebMvcRegistrations, 和 HandlerMethodArgumentResolver。使用@WebMvcTest注解时,不会扫描常规@Component和@ConfigurationProperties bean。@EnableConfigurationProperties可用于包含@ConfigurationProperties bean。可以在附录中查看@WebMvcTest启用的自动配置列表如果需要注册额外的组件,比如Jackson Module,你能使用@Import导入额外的配置类。一般的@WebMvcTest只能用于一个控制器,并与@MockBean结合使用,提供模拟实现。@WebMvcTest也自动配置MockMvc,Mock MVC 提供一个强大快速测试Mvc控制器的方法,而不需要启动整个HTTP 服务器。还能够在非@WebMvcTest(如@SpringBootTest)中使用@AutoConfigureMockMvc对MockMVC 进行自动配置。如下是MockMvc的示例:import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(UserVehicleController.class) class MyControllerTests { @Autowired private MockMvc mvc; @MockBean private UserVehicleService userVehicleService; @Test void testExample() throws Exception { given(this.userVehicleService.getVehicleDetails("sboot")) .willReturn(new VehicleDetails("Honda", "Civic")); this.mvc.perform(get("/sboot/vehicle").accept(MediaType.TEXT_PLAIN)) .andExpect(status().isOk()) .andExpect(content().string("Honda Civic")); } }如果需要配置自动配置的元素(例如,当应用servlet过滤器时),可以使用@AutoConfigureMockMvc注解中的属性。如果使用HtmlUnit和Selenium,自动配置还提供HtmlUnit WebClient bean或Selenium WebDriver bean。以下示例使用HtmlUnit:import com.gargoylesoftware.htmlunit.WebClient; import com.gargoylesoftware.htmlunit.html.HtmlPage; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @WebMvcTest(UserVehicleController.class) class MyHtmlUnitTests { @Autowired private WebClient webClient; @MockBean private UserVehicleService userVehicleService; @Test void testExample() throws Exception { given(this.userVehicleService.getVehicleDetails("sboot")).willReturn(new VehicleDetails("Honda", "Civic")); HtmlPage page = this.webClient.getPage("/sboot/vehicle.html"); assertThat(page.getBody().getTextContent()).isEqualTo("Honda Civic"); } }默认情况下,Spring Boot 将WebDriver bean 放置在一个特殊的 scope中,以确保每次测试以后驱动程序退出,并注入新的实例。Spring Boot 创建的 webDriver 作用域将替换任何用户定义的同名作用域。若是定义了本身的webDriver作用域,则在使用@WebMvcTest时可能会发现他停止工作。如果类路径上有Spring Security,@WebMvcTest还将扫描WebSecurityConfigurer bean。您可以使用SpringSecurity的测试支持,而不是完全禁用此类测试的安全性。有关如何使用Spring Security的MockMvc支持的更多详细信息,请参阅本节的Testing With Spring Security操作指南。有时编写SpringMVC测试是不够的;Spring Boot可以帮助您在实际服务器上运行完整的端到端测试。Auto-configured Spring WebFlux 测试要测试Spring WebFlux 控制器是否按预期工作,你能够使用@WebFluxTest注解。@WebFluxTest自动配置Spring WebFlux 基础设施,并将扫描的bean限制为@Controller、@ControllerAdvice、@JsonComponent、Converter、GenericConverter、WebFilter和WebFluxConfigurer。使用@WebFluxTest 注解时,不会扫描常规的@Component和@ConfigurationProperties。@WebFluxTest启用的自动配置列表可在附录中找到如果您需要注册额外的组件,例如Jackson Module,您可以在测试中使用@import导入其他配置类。通常,@WebFluxTest仅限于一个控制器,并与@MockBean注解结合使用,为所需的合作者提供模拟实现。@WebFluxTest还自动配置WebTestClient,它提供了一种快速测试WebFlux控制器的强大方法,无需启动完整的HTTP服务器。您还可以在非@WebFluxTest(例如@SpringBootTest)使用@AutoConfigureWebTestClient注解,以自动配置web测试客户端。以下示例显示了一个同时使用@WebFluxTest和WebTestClient的类:import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.reactive.server.WebTestClient; import static org.mockito.BDDMockito.given; @WebFluxTest(UserVehicleController.class) class MyControllerTests { @Autowired private WebTestClient webClient; @MockBean private UserVehicleService userVehicleService; @Test void testExample() { given(this.userVehicleService.getVehicleDetails("sboot")) .willReturn(new VehicleDetails("Honda", "Civic")); this.webClient.get().uri("/sboot/vehicle").accept(MediaType.TEXT_PLAIN).exchange() .expectStatus().isOk() .expectBody(String.class).isEqualTo("Honda Civic"); } }此设置仅受WebFlux应用程序支持,因为在模拟的web应用程序中使用WebTestClient目前仅适用于WebFlux。@WebFluxTest无法检测通过功能web框架注册的路由。要在上下文中测试RouterFunction bean,请考虑使用@Import或@SpringBootTest自己导入RouterFunction。@WebFluxTest无法检测注册为SecurityWebFilterChain类型的@Bean的自定义安全配置。要将其包含在测试中,您需要使用@import或@SpringBootTest导入注册bean的配置。有时编写SpringWebFlux测试是不够的;Spring Boot可以帮助您在实际服务器上运行完整的端到端测试。Auto-configured Spring GraphQL 测试略Auto-configured Data Cassandra 测试略Auto-configured Data Couchbase 测试略Auto-configured Data Elasticsearch 测试您可以使用@DataElasticsearchTest测试Elasticsearch应用程序。默认情况下,它配置ElasticsearchRestTemplate,扫描@Document类,并配置SpringDataElasticSearch存储库。使用@DataElasticsearchTest注解时,不会扫描常规@Component和@ConfigurationProperties bean。@EnableConfigurationProperties可用于包含@ConfigurationProperties bean。(有关在Spring Boot中使用Elasticsearch的更多信息,请参阅本章前面的“Elasticsearch”。)更多@DataElasticsearchTest列表在这里查看以下示例显示了在Spring Boot中使用Elasticsearch测试的典型示例:import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.data.elasticsearch.DataElasticsearchTest; @DataElasticsearchTest class MyDataElasticsearchTests { @Autowired private SomeRepository repository; // ... }Auto-configured Data JPA 测试略Auto-configured JDBC 测试略Auto-configured Data JDBC 测试略Auto-configured jOOQ 测试略Auto-configured Data MongoDB 测试您可以使用@DataMongoTest测试MongoDB应用程序。默认情况下,它配置内存中嵌入的MongoDB(如果可用),配置MongoTemplate,扫描@Document类,并配置Spring Data MongoDB存储库。使用@DataMongoTest注解时,不会扫描常规@Component和@ConfigurationProperties bean。@EnableConfigurationProperties可用于包含@ConfigurationProperties bean。(有关在Spring Boot中使用MongoDB的更多信息,请参阅“MongoDB”。)@DataMongoTest 自动配置列表查看。下面是典型的使用@DataMongoTest的示例:import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; import org.springframework.data.mongodb.core.MongoTemplate; @DataMongoTest class MyDataMongoDbTests { @Autowired private MongoTemplate mongoTemplate; // ... }内存嵌入式MongoDB通常很适合测试,因为它速度快,不需要任何开发人员安装。但是,如果您希望对真实的MongoDB服务器运行测试,则应排除嵌入式MongoDB自动配置,如下例所示:import org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration; import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; @DataMongoTest(excludeAutoConfiguration = EmbeddedMongoAutoConfiguration.class) class MyDataMongoDbTests { // ... }Auto-configured Data Neo4j 测试略Auto-configured Data Redis 测试您可以使用@DataRedisTest测试Redis应用程序。默认情况下,它扫描@RedisHash类并配置Spring Data Redis存储库。使用@DataRedisTest注解时,不会扫描常规@Component和@ConfigurationProperties bean。@EnableConfigurationProperties可用于包含@ConfigurationProperties bean。(有关在Spring Boot中使用Redis的更多信息,请参阅“Redis”。)@DataRedisTest注解使用示例:import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.data.redis.DataRedisTest; @DataRedisTest class MyDataRedisTests { @Autowired private SomeRepository repository; // ... }Auto-configured Data LDAP 测试Auto-configured REST Clients您可以使用@RestClientTest注解来测试REST客户端。默认情况下,它自动配置Jackson、GSON和Jsonb支持,配置RestTemplateBuilder,并添加对MockRestServiceServer的支持。使用@RestClientTest注解时,不会扫描常规@Component和@ConfigurationProperties bean。@EnableConfigurationProperties可用于包含@ConfigurationProperties bean。应使用@RestClientTest的value或components属性指定要测试的特定bean,如下例所示:import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; import org.springframework.http.MediaType; import org.springframework.test.web.client.MockRestServiceServer; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; @RestClientTest(RemoteVehicleDetailsService.class) class MyRestClientTests { @Autowired private RemoteVehicleDetailsService service; @Autowired private MockRestServiceServer server; @Test void getVehicleDetailsWhenResultIsSuccessShouldReturnDetails() { this.server.expect(requestTo("/greet/details")).andRespond(withSuccess("hello", MediaType.TEXT_PLAIN)); String greeting = this.service.callRestService(); assertThat(greeting).isEqualTo("hello"); } }Auto-configured Spring REST Docs Tests你可以使用@AutoConfigureRestDocs注解在Mock MVC,REST Assured,或者 WebTestClient的测试中来使用Spring REST Docs。@AutoConfigureRestDocs可以覆盖默认的输出目录(如果使用Maven,则为target/generated-snippets,如果是Gradle,则为build/generated-snippets)。使用Mock MVC 测试 Auto-configured Spring REST Docs基于servlet的Web应用程序@AutoConfigureRestDocs支持自定义MockMvc bean,以便在测试中使用Spring REST Docs,在单元测试中使用@Autowired注入。import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(UserController.class) @AutoConfigureRestDocs class MyUserDocumentationTests { @Autowired private MockMvc mvc; @Test void listUsers() throws Exception { this.mvc.perform(get("/users").accept(MediaType.TEXT_PLAIN)) .andExpect(status().isOk()) .andDo(document("list-users")); } }如果要比@AutoConfigureRestDocs更多的控制Spring REST Docs的配置,可以使用RestDocsMockMvcConfigurationCustomizer。import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentationConfigurer; import org.springframework.restdocs.templates.TemplateFormats; @TestConfiguration(proxyBeanMethods = false) public class MyRestDocsConfiguration implements RestDocsMockMvcConfigurationCustomizer { @Override public void customize(MockMvcRestDocumentationConfigurer configurer) { configurer.snippets().withTemplateFormat(TemplateFormats.markdown()); } }如果要让Spring REST Docs支持参数化输出目录,可以创建一个RestDocumentationResultHandler bean。自动配置使用此结果处理程序调用alwaysDo,从而使每个MockMvc调用自动生成默认代码段。以下示例显示了正在定义的RestDocumentationResultHandler:import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; @TestConfiguration(proxyBeanMethods = false) public class MyResultHandlerConfiguration { @Bean public RestDocumentationResultHandler restDocumentation() { return MockMvcRestDocumentation.document("{method-name}"); } }使用WebTestClient测试Auto-configured Spring REST Docs在reactive环境的Web应用程序,@AutoConfigureRestDocs可以使用WebTestClient进行测试。可以使用@Autowired注入,并在测试中使用@WebFluxTest和Spring REST Docs。import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; import org.springframework.test.web.reactive.server.WebTestClient; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; @WebFluxTest @AutoConfigureRestDocs class MyUsersDocumentationTests { @Autowired private WebTestClient webTestClient; @Test void listUsers() { this.webTestClient .get().uri("/") .exchange() .expectStatus() .isOk() .expectBody() .consumeWith(document("list-users")); } }同样,可以自定义RestDocsWebTestClientConfigurationCustomizer bean,提供更多的Spring REST Docs配置控制。import org.springframework.boot.test.autoconfigure.restdocs.RestDocsWebTestClientConfigurationCustomizer; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentationConfigurer; @TestConfiguration(proxyBeanMethods = false) public class MyRestDocsConfiguration implements RestDocsWebTestClientConfigurationCustomizer { @Override public void customize(WebTestClientRestDocumentationConfigurer configurer) { configurer.snippets().withEncoding("UTF-8"); } }使用WebTestClientBuilderCustomizer配置让Spring REST Docs提供对参数化输出目录的支持。import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.web.reactive.server.WebTestClientBuilderCustomizer; import org.springframework.context.annotation.Bean; import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; @TestConfiguration(proxyBeanMethods = false) public class MyWebTestClientBuilderCustomizerConfiguration { @Bean public WebTestClientBuilderCustomizer restDocumentation() { return (builder) -> builder.entityExchangeResultConsumer(document("{method-name}")); } }使用REST Assured 测试Auto-configured Spring REST Docs@AutoConfigureRestDocs使用一个RequestSpecification bean(预配置为使用Spring REST Docs)在单元测试中,使用@Autowired注入。import io.restassured.specification.RequestSpecification; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalServerPort; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.is; import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @AutoConfigureRestDocs class MyUserDocumentationTests { @Test void listUsers(@Autowired RequestSpecification documentationSpec, @LocalServerPort int port) { given(documentationSpec) .filter(document("list-users")) .when() .port(port) .get("/") .then().assertThat() .statusCode(is(200)); } }使用RestDocsRestAssuredConfigurationCustomizer自定义配置提供更多的配置控制。import org.springframework.boot.test.autoconfigure.restdocs.RestDocsRestAssuredConfigurationCustomizer; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.restdocs.restassured3.RestAssuredRestDocumentationConfigurer; import org.springframework.restdocs.templates.TemplateFormats; @TestConfiguration(proxyBeanMethods = false) public class MyRestDocsConfiguration implements RestDocsRestAssuredConfigurationCustomizer { @Override public void customize(RestAssuredRestDocumentationConfigurer configurer) { configurer.snippets().withTemplateFormat(TemplateFormats.markdown()); } }Auto-configured Spring Web Services测试使用@WebServiceClientTest测试使用了Spring Web Services的项目。默认情况下,它配置一个模拟WebServiceServerbean 并自动定义WebServiceTemplateBuilder。(更多Spring Boot使用 Web Service 查看 “Web Services”)import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.webservices.client.WebServiceClientTest; import org.springframework.ws.test.client.MockWebServiceServer; import org.springframework.xml.transform.StringSource; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.ws.test.client.RequestMatchers.payload; import static org.springframework.ws.test.client.ResponseCreators.withPayload; @WebServiceClientTest(SomeWebService.class) class MyWebServiceClientTests { @Autowired private MockWebServiceServer server; @Autowired private SomeWebService someWebService; @Test void mockServerCall() { this.server .expect(payload(new StringSource("<request/>"))) .andRespond(withPayload(new StringSource("<response><status>200</status></response>"))); assertThat(this.someWebService.test()) .extracting(Response::getStatus) .isEqualTo(200); } }Auto-configured Spring Web Services Client 测试使用@WebServiceClientTest测试使用了Spring Web Services 项目的应用程序。默认的,它会配置一个模拟的WebServiceServer bean 和自动自定义WebServiceTemplateBuilder。import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.webservices.client.WebServiceClientTest; import org.springframework.ws.test.client.MockWebServiceServer; import org.springframework.xml.transform.StringSource; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.ws.test.client.RequestMatchers.payload; import static org.springframework.ws.test.client.ResponseCreators.withPayload; @WebServiceClientTest(SomeWebService.class) class MyWebServiceClientTests { @Autowired private MockWebServiceServer server; @Autowired private SomeWebService someWebService; @Test void mockServerCall() { this.server .expect(payload(new StringSource("<request/>"))) .andRespond(withPayload(new StringSource("<response><status>200</status></response>"))); assertThat(this.someWebService.test()) .extracting(Response::getStatus) .isEqualTo(200); } }Auto-configured Spring Web Services Server 测试使用@WebServiceServerTest测试Spring Web Services 的项目。默认它配置一个MockWebServiceClient bean ,用于调用 web Service 端点。import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.webservices.server.WebServiceServerTest; import org.springframework.ws.test.server.MockWebServiceClient; import org.springframework.ws.test.server.RequestCreators; import org.springframework.ws.test.server.ResponseMatchers; import org.springframework.xml.transform.StringSource; @WebServiceServerTest(ExampleEndpoint.class) class MyWebServiceServerTests { @Autowired private MockWebServiceClient client; @Test void mockServerCall() { this.client .sendRequest(RequestCreators.withPayload(new StringSource("<ExampleRequest/>"))) .andExpect(ResponseMatchers.payload(new StringSource("<ExampleResponse>42</ExampleResponse>"))); } }其他 自动配置 和片段每个片段提供一个或多个@AutoConfigure…注解,即定义一部分包含自动配置。可以通过创建自定义@AutoConfigure…逐个添加自动化配置或者使用@ImportAutoConfiguration添加到测试中。import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration; import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; @JdbcTest @ImportAutoConfiguration(IntegrationAutoConfiguration.class) class MyJdbcTests { }不要使用@Import注解来导入自动配置,由于他们由Spring Boot以特定方式处理。笔者注:@Import和@ImportAutoConfiguration的区别:https://www.cnblogs.com/imyjy/p/16092825.html另外,可以通过META-INF/spring中添加自动配置文件META-INF/spring/org.springframework.boot.test.autoconfigure.jdbc.JdbcTest.importscom.example.IntegrationAutoConfiguration在这个例子中,com.example.IntegrationAutoConfiguration会在每个@JdbcTest注解中开启。可以在文件中使用#注释使用 Configuration 和 片段略使用Spock 测试 Spring Boot 应用程序Spock 2.x 能用于测试Spring Boot 应用程序,只要添加 spock-spring模块依赖,详细查看Spock 的文档。5.8.4 测试实用程序ConfigDataApplicationContextInitializerConfigDataApplicationContextInitializer是一个ApplicationContextInitializer,可以用于测试加载Spring Boot application.properties文件。当不需要@SpringBootTest提供的全部功能时,可以使用。import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer; import org.springframework.test.context.ContextConfiguration; @ContextConfiguration(classes = Config.class, initializers = ConfigDataApplicationContextInitializer.class) class MyConfigFileTests { // ... }ConfigDataApplicationContextInitializer不提供@Value("${…}")注入支持,它仅用于将application.properties文件加载到Spring 的Environment中。要支持@Value,你需要另外配置PropertySourcesPlaceholderConfigurer或者使用@SpringBootTest。TestPropertyValuesTestPropertyValues允许你可以快速添加配置到ConfigurableEnvironment或者ConfigurableApplicationContext中,使用key=value形式。import org.junit.jupiter.api.Test; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.mock.env.MockEnvironment; import static org.assertj.core.api.Assertions.assertThat; class MyEnvironmentTests { @Test void testPropertySources() { MockEnvironment environment = new MockEnvironment(); TestPropertyValues.of("org=Spring", "name=Boot").applyTo(environment); assertThat(environment.getProperty("name")).isEqualTo("Boot"); } }OutputCaptureOutputCapture是一个Junit 扩展,用于捕获System.out 和 System.err输出。添加@ExtendWith(OutputCaptureExtension.class),并将CapturedOutput作为参数注入测试类或构造函数中。import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(OutputCaptureExtension.class) class MyOutputCaptureTests { @Test void testName(CapturedOutput output) { System.out.println("Hello World!"); assertThat(output).contains("World"); } }TestRestTemplateTestRestTemplate是 Spring RestTemplate的有用替代方式。它以测试友好的方式运行,可以通过返回的ResponseEntity检测出错误。Spring Framework 5.0提供了一个新的WebTestClient,用于WebFlux集成测试。建议使用高于4.3.2 的Apache HTTP Client 版本,但不是强制的。如果你的类路径中存在该类,TestRestTemplate将配置合适的客户端来响应,如果没有,将使用其他的友好方式:不遵循重定向规则(因此可以断言响应的位置)Cookies被忽略(因此模板是无状态的)TestRestTemplate可以在集成测试中实例化,如下示例:import org.junit.jupiter.api.Test; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.ResponseEntity; import static org.assertj.core.api.Assertions.assertThat; class MyTests { private final TestRestTemplate template = new TestRestTemplate(); @Test void testRequest() { ResponseEntity<String> headers = this.template.getForEntity("https://myhost.example.com/example", String.class); assertThat(headers.getHeaders().getLocation()).hasHost("other.example.com"); } }或者,如果将 WebEnvironment.RANDOM_PORT 或者 WebEnvironment.DEFINED_PORT与@SpringBootTest注解一起使用,你能注入一个TestRestTemplate并可以开始使用。如果有需要可以使用RestTemplateBuilder bean 自定义配置。host 和端口 将自动配置连接到容器。import java.time.Duration; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpHeaders; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class MySpringBootTests { @Autowired private TestRestTemplate template; @Test void testRequest() { HttpHeaders headers = this.template.getForEntity("/example", String.class).getHeaders(); assertThat(headers.getLocation()).hasHost("other.example.com"); } @TestConfiguration(proxyBeanMethods = false) static class RestTemplateBuilderConfiguration { @Bean RestTemplateBuilder restTemplateBuilder() { return new RestTemplateBuilder().setConnectTimeout(Duration.ofSeconds(1)) .setReadTimeout(Duration.ofSeconds(1)); } } }5.9 创建自己的自动化配置自动化配置可以跟"starter"相关联,该启动器提供自动化配置代码以及使用的库。5.9.1 了解自动配置的Bean实现自动配置的类使用@AutoConfiguration注解,这个注解使用@Configuration标注,使的自动配置称为标注的@Configuration类。@Conditional注解用于约束何时应用自动配置。通常自动配置类使用@ConditionalOnClass和@ConditionalOnMissingBean注解。这确保自动配置仅在找到相关类且尚未声明你自己的@Configuration时适用。可以查看Spring Boot源码浏览提供的自动配置类,或者查看文件META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports5.9.2 查找候选的自动配置Spring Boot检查发布的jar中是否存在META-INF/Spring/org.springframework.Boot.autoconfig.AutoConfiguration.imports文件,该文件每行列出你的配置类。com.mycorp.libx.autoconfigure.LibXAutoConfiguration com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration可以在文件中使用#字符注释如果需要按指定顺序应用配置,则可以使用 @AutoConfiguration 注解上的 before, beforeName, after 和 afterName 属性,或者使用专用的 @AutoConfigureBefore 和 @AutoConfigureAfter 注解。比如,如果你提供特定于web的配置,你的类可能需要在WebMvcAutoConfiguration后应用。如果你想要对一些不互相了解的类进行排序,也可以使用@AutoConfigureOrder。该注解跟@Order有相同的语义,但专门用于自动配置类。与标准@Configuration类一样,自动配置类的应用顺序只影响其bean的定义顺序。随后创建的这些bean的顺序不受影响,并由每个bean的依赖关系和任何@DependsOn关系决定。5.9.3 条件注解如果想要在自动配置类上,始终配置一个或多个@Conditional,那么最常用的是@ConditionalOnMissingBean注解。Spring Boot 包含许多的@Conditional注解,包括如下注解:Class ConditionsBean ConditionsProperty ConditionsResource ConditionsWeb Application ConditionsSpEL Expression ConditionsClass Conditions@ConditionalOnClass 和 @ConditionOnMissingClass 注解允许根据指定类是否存在来加载@Configuration类。该机制不适用于@Bean方法,其中返回类型通常是condition 的target:在方法的condition应用之前,JVM将加载类和可能处理的方法引用,如果类不存在,这些方法引用将失败。为了处理这种情况,可以使用单独的@Configuration类来隔离condition,如下所示:import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @AutoConfiguration // Some conditions ... public class MyAutoConfiguration { // Auto-configured beans ... @Configuration(proxyBeanMethods = false) @ConditionalOnClass(SomeService.class) public static class SomeServiceConfiguration { @Bean @ConditionalOnMissingBean public SomeService someService() { return new SomeService(); } } }如果使用@ConditionalOnClass或者@ConditionalOnMissingClass作为元注解的一部分来编写自己的组合注解,则必须使用name引用类。Bean Conditions@ConditionalOnBean 和 @ConditionalOnMissingBean 注解会根据是否存在指定bean来判断是否加载 bean,使用value指定bean 的类型或者name指定bean 的名称,search属性允许你限制搜索bean时要考虑的ApplicationContext层次结构。当放置于@Bean上事,目标类型默认为方法的返回类型,如下所示:import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; @AutoConfiguration public class MyAutoConfiguration { @Bean @ConditionalOnMissingBean public SomeService someService() { return new SomeService(); } }这个例子中,someService将在SomeService 类型的Bean 不在ApplicationContext中时创建。建议在自动配置类上只使用@ConditionalOnBean 和 @ConditionalOnMissingBean注解,因为这些注解能够保证是在 任何用户自定义bean 后加载的。在声明@Bean方法时,在方法的返回类型中提供尽可能多的类型信息,例如,如果您的bean的具体类实现了接口,那么bean 方法返回的类型应该是具体类而不是接口。Property Conditions@ConditionalOnProperty 注解根据Spring Environment 是否包含指定配置进行加载。使用prefix和name指定要检查的属性,默认情况下匹配任何存在且不等于false的属性。另外可以使用havingValue和matchIfMissing属性创建高级检查。笔者注:havingValue 配置预期值(字符串形式),如果未指定,则属性不能等于falsematchIfMissing 表示如果未设置属性,条件是否匹配,默认为falseResource Conditions@ConditionalOnResource注解仅在指定资源存在时才加载配置。可以使用常用的Spring 约定指定资源,比如file:/home/user/test.dat。Web Application Conditions@ConditionalOnWebApplication 和 @ConditionalOnNotWebApplication注解根据应用是否是一个"web应用"来判断是否加载配置。基于servlet 的 web 应用使用了Spring WebApplicationContext,定义了一个session生命周期或者有一个ConfigurableWebEnvironment,反应式的 web 应用程序使用ReactiveWebApplicationContext或者存在ConfigurableReactiveWebEnvironment。@ConditionalOnWarDeployment注解根据应用是否是传统的WAR应用来判断是否加载配置。对于使用嵌入式应用程序,该条件不会匹配。SpEL Expression Conditions@ConditionalOnWarDeployment注解根据SpEL表达式结果来判断是否加载配置。在表达式中引用bean将导致bean在上下文刷新处理中过早初始化,这将导致bean无法进行post-processing处理(比如配置绑定)并且状态可能是不完整的。5.9.4 测试自动配置自动配置可能会受到许多因素的影响:用户配置(@Bean定义和自定义的Environment)、评估条件和其他的。每个测试都应该创建一个良好的ApplicationContext,表示这些自定义的组合,ApplicationContextRunner提供了实现这一点的好方法。ApplicationContextRunner通常定义为测试类的一个字段,用来收集基础的、公共配置。以下示例确保了MyServiceAutoConfiguration始终被调用。private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(MyServiceAutoConfiguration.class));如果定义了多个自动配置,则不需要对其声明排序,因为它们的调用顺序与应用程序的顺序完全相同。每个测试都可以使用runner来表示特定的用例。例如,下面的示例调用用户配置(UserConfiguration)并检查自动配置是否正确退出。run提供的回调上下文可以在AssertJ中使用。@Test void defaultServiceBacksOff() { this.contextRunner.withUserConfiguration(UserConfiguration.class).run((context) -> { assertThat(context).hasSingleBean(MyService.class); assertThat(context).getBean("myCustomService").isSameAs(context.getBean(MyService.class)); }); } @Configuration(proxyBeanMethods = false) static class UserConfiguration { @Bean MyService myCustomService() { return new MyService("mine"); } }还可以轻松自定义Environment,如下所示:@Test void serviceNameCanBeConfigured() { this.contextRunner.withPropertyValues("user.name=test123").run((context) -> { assertThat(context).hasSingleBean(MyService.class); assertThat(context.getBean(MyService.class).getName()).isEqualTo("test123"); }); }runner 也能够用于显示ConditionEvaluationReport,这个报告可以在INFO 或 DEBUG级别打印。如下示例展示如何使用ConditionEvaluationReportLoggingListener在自动化测试中打印报告。import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; import org.springframework.boot.logging.LogLevel; import org.springframework.boot.test.context.runner.ApplicationContextRunner; class MyConditionEvaluationReportingTests { @Test void autoConfigTest() { new ApplicationContextRunner() .withInitializer(new ConditionEvaluationReportLoggingListener(LogLevel.INFO)) .run((context) -> { // Test something... }); } }模拟Web上下文如果你仅需要在servlet 或者 reactive 上下文测试自动化配置,你可以使用WebApplicationContextRunner或者ReactiveWebApplicationContextRunner。覆盖类路径还可以在运行时测试特定的包或类是否存在,Spring Boot 附带了一个FilteredClassLoader,以下示例断言MyService不存在时,自动配置将禁用。@Test void serviceIsIgnoredIfLibraryIsNotPresent() { this.contextRunner.withClassLoader(new FilteredClassLoader(MyService.class)) .run((context) -> assertThat(context).doesNotHaveBean("myService")); }5.9.5 创建自己的Starter命名给starter提供一个合适的命名空间,不要以spring-boot开头,即使使用不同的maven groupId。配置键如果你的启动器提供配置键,请使用唯一的命名空间。不要使用Spring Boot 使用的键(如server、management、spring等),根据经验,应该在所有键上加上独有的命名空间,比如acme。import java.time.Duration; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties("acme") public class AcmeProperties { /** * Whether to check the location of acme resources. */ private boolean checkLocation = true; /** * Timeout for establishing a connection to the acme server. */ private Duration loginTimeout = Duration.ofSeconds(3); public boolean isCheckLocation() { return this.checkLocation; } public void setCheckLocation(boolean checkLocation) { this.checkLocation = checkLocation; } public Duration getLoginTimeout() { return this.loginTimeout; } public void setLoginTimeout(Duration loginTimeout) { this.loginTimeout = loginTimeout; } }这里是一些遵循的规则:不要以The或A开始boolean类型,以Whether或者Enable开始列表类型,以“逗号分隔列表”开始(原文 Comma-separated list)使用java.time.Duration而不是long除非在运行时确认,否则不要提供默认值确保你生成了Annotation Processor以便IDE的提示可用,可以在META-INF/spring-configuration-metadata.json查看生成的metadata,确保键已经被正确生成,然后在IDE中进行验证。自动配置模块autoconfigure模块包含任何必要的启动库,可能也包含配置键定义(比如ConfigurationProperties)。你应该将对库的依赖标记为可选,这样就可以更容易地将自动配置模块包含在你的项目中。Spring Boot 使用注释处理器从META-INF/spring-autoconfigure-metadata.properties中收集自动配置条件。如果存在该文件,则可以在早期过滤不匹配的自动配置,这有助于提高启动时间。当用maven构建的时候,推荐添加如下的依赖到模块中:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure-processor</artifactId> <optional>true</optional> </dependency> 如果已经在应用程序中直接定义了自动配置,确保spring-boot-maven-plugin已经配置,防止repackage 将依赖重新打包到fat jar中。<project> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure-processor</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>使用Gradle,应该添加annotationProcessor配置,如下所示:dependencies { annotationProcessor "org.springframework.boot:spring-boot-autoconfigure-processor" }Starter模块starter确实是一个空jar,它只提供一些必要的依赖项。
前言Spring Boot 3.0.0 GA版已经发布,好多人也开始尝试升级,有人测试升级后,启动速度确实快了不少,如下为网络截图,于是我也按捺不住的想尝试下。历程首先就是要把Spring Boot、Spring Cloud 相关的依赖升一下Spring Boot:3.0.0Spring Cloud:2022.0.0-RC2统一依赖版本管理:<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2022.0.0-RC2</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>现在还不能下载Spring 相关依赖包,需要加入Spring 仓库。在你的maven仓库中加入如下配置,我是加在了pom.xml中<repository> <id>netflix-candidates</id> <name>Netflix Candidates</name> <url>https://artifactory-oss.prod.netflix.net/artifactory/maven-oss-candidates</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository>另外Spring Boot 3.X 开始使用了Java 17,将java版本调整到>17,为了不必要的麻烦,就选17IDEA选择17,并在pom.xml文件中指定版本:<java.version>17</java.version>到这里我们的common 包是能正常编译了。接下来是服务的配置同样调整Spring Boot、Spring Cloud、Java的版本,同common的配置。碰到如下的几个问题:找不到hystrix的依赖问题:升级后找不到hystrix的版本,官网也找不到,这里我显式指定了版本<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> <version>2.2.9.RELEASE</version> </dependency>rabbitmq问题:相关的配置丢失,比如如下图,这边进行适当调整或者直接注释解决。TypeVariableImpl丢失问题:原来服务中引入了sun.reflect.generics.reflectiveObjects.TypeVariableImpl,现在17中已经被隐藏无法直接使用,这边为了能够先启动,暂时注释,后面再想办法。Log 异常问题:由于之前我们项目中历史原因,既有用log4j,也有用logback,升级后已经不行,提示冲突,报错如下Exception in thread "main" java.lang.IllegalArgumentException: LoggerFactory is not a Logback LoggerContext but Logback is on the classpath. Either remove Logback or the competing implementation (class org.slf4j.helpers.NOPLoggerFactory loaded from file:/Users/chenjujun/.m2/repository/org/slf4j/slf4j-api/1.7.0/slf4j-api-1.7.0.jar). If you are using WebLogic you will need to add 'org.slf4j' to prefer-application-packages in WEB-INF/weblogic.xml: org.slf4j.helpers.NOPLoggerFactory at org.springframework.util.Assert.instanceCheckFailed(Assert.java:713) at org.springframework.util.Assert.isInstanceOf(Assert.java:632)意思是,要么移除Logback,要么解决slf4j-api的冲突依赖,这里两种方式都尝试了,slf4j-api依赖的地方太多,后面移除了Logback。要排除依赖一个好办法:使用Maven Helper插件logback依赖:<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.8</version> </dependency>Apollo问题:使用Apollo会提示该错误,需要在启动中加入--add-opens java.base/java.lang=ALL-UNNAMEDCaused by: com.ctrip.framework.apollo.exceptions.ApolloConfigException: Unable to load instance for com.ctrip.framework.apollo.spring.config.ConfigPropertySourceFactory! at com.ctrip.framework.apollo.spring.util.SpringInjector.getInstance(SpringInjector.java:40) at com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer.<init>(ApolloApplicationContextInitializer.java:66) ... 16 more Caused by: com.ctrip.framework.apollo.exceptions.ApolloConfigException: Unable to initialize Apollo Spring Injector! at com.ctrip.framework.apollo.spring.util.SpringInjector.getInjector(SpringInjector.java:24) at com.ctrip.framework.apollo.spring.util.SpringInjector.getInstance(SpringInjector.java:37) ... 17 more Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @16612a51 at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354) at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297) at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199) at java.base/java.lang.reflect.Method.setAccessible(Method.java:193) at com.google.inject.internal.cglib.core.$ReflectUtils$1.run(ReflectUtils.java:52) at java.base/java.security.AccessController.doPrivileged(AccessController.java:318) at com.google.inject.internal.cglib.core.$ReflectUtils.<clinit>(ReflectUtils.java:42)通过上述配置调整后,能编译成功,但是无法启动,控制没有任何日志,初步怀疑还是log依赖问题,由于时间关系,没有再继续,问题留到以后再弄,后面有新进展,会持续更新该文。javax 的依赖都变成jakarta:比如原来基于javax.validation包中的验证,javax.validation.constraints.NotNull此类的都需要调整Spring Boot 3.0后,很多starter不能用:Spring Boot 3.0后,以前的spring.factories 不能用了,只能使用META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ,对于一些还没改造的starter都无法使用,目前mybatisplus 已经有支持3.0 的 SNAPSHOT版本,其他的druid、nacos 等还适配3.0,要等等了。但我怎么会坐以待毙,我尝试自己改造中间件的starter,可还是报错Failed to instantiate [org.springframework.boot.env.EnvironmentPostProcessor]: Specified class is an interface
属性配置介绍Spring Boot 3.1.0 支持的属性配置方式与2.x版本没有什么变动,按照以下的顺序处理,后面的配置将覆盖前面的配置:1、SpringApplication.setDefaultProperties 指定的默认属性2、@PropertySource注解配置3、Jar包内部的application.properties 和 YAML 变量4、Jar包内部的application-{profile}.properties 和 YAML 变量5、Jar包外部的application.properties 和 YAML 变量6、Jar包外部的application-{profile}.properties 和 YAML 变量7、RandomValuePropertySource的随机值属性8、操作系统环境变量9、Java System属性 (System.getProperties())10、JNDI属性11、ServletContext 初始化参数12、ServletConfig 初始化参数13、嵌入在环境变量或系统属性中的SPRING_APPLICATION_JSON 的属性14、命令行参数15、测试环境properties 属性16、测试环境的@TestPropertySource 注解17、Devtools 全局配置属性配置实验使用前面的MyApplicationRunListener来读取Spring Boot 启动完成后的自定义配置,如下: public void started(ConfigurableApplicationContext context, Duration timeTaken) { System.out.println("上下文已刷新,应用程序已启动,但尚未调用CommandLineRunners和ApplicationRunners"); System.out.println(context.getEnvironment().getProperty("me")); }默认属性Properties properties = new Properties(); properties.setProperty("me", "123456"); springApplication.setDefaultProperties(properties); springApplication.run(args);@PropertySource注解配置创建一个app.yml文件,放置于resource目录下:me: 333333在SpringBootDemoApplication中标注,@PropertySource("classpath:app.yml")运行后,此配置覆盖了“SpringApplication.setDefaultProperties 指定的默认属性”。基于 @PropertySource注解的配置,需要刷新上下文后才能读取,因此需要在刷新之前就加载的配置如 logging.* and spring.main.* ,不适用。Jar包内部的application.properties 和 YAML 变量在resources内部的application.yml中定义me: 4444运行后覆盖之前的配置值Jar包内部的application-{profile}.properties 和 YAML 变量在resources内部的application-test.yml中定义me: 55555并在application.yml中定义spring: profiles: active: - test运行后覆盖之前的配置值Jar包外部的application.properties 和 YAML 变量在jar包所在目录,创建一个application.yml文件:me: 666666运行后覆盖之前的配置值Jar包外部的application-{profile}.properties 和 YAML 变量在jar 所在目录,创建一个application-test.yml文件:me: 777777运行后覆盖之前的配置值RandomValuePropertySource的随机值属性RandomValuePropertySource 会解析random.*开头的属性,返回一个随机值,如${random.int}返回一个随机整数同样在前面的application-test.yml文件中配置:me: ${random.int}启动后,打印一个随机整数操作系统环境变量在操作系统中配置一个me变量,值为888888,启动后,即可读取到me的环境变量:注意:操作系统环境变量要全局生效,否则会读取不到Java System属性 (System.getProperties())在这里,我们不再往JVM中设置新的属性,而是读取其原有的属性,如java.version在MyApplicationRunListener中,输出java.version@Override public void started(ConfigurableApplicationContext context, Duration timeTaken) { System.out.println("上下文已刷新,应用程序已启动,但尚未调用CommandLineRunners和ApplicationRunners"); System.out.println(context.getEnvironment().getProperty("me")); System.out.println(context.getEnvironment().getProperty("java.version")); }为了能够体现后面的配置覆盖前面的配置,在application-test.yml中手动配置java.versionjava: version: 1.8运行后,打印的结果:JNDI属性这块用的很少,就忽略了,如果是同样的配置,该配置会覆盖前面的配置。ServletContext 初始化参数ServletConfig 初始化参数如上两个都是servlet的配置,如server.port嵌入在环境变量或系统属性中的SPRING_APPLICATION_JSON 的属性在IDEA中配置启动时候的环境变量,SPRING_APPLICATION_JSON是一个JSON格式,如:启动后,将打印:命令行参数同样的在IDEA中配置命令行参数,--me=10000启动后打印结果如下,覆盖以前配置的值:测试环境properties 属性该配置是在单元测试中使用,如:@SpringBootTest(properties = {"me=2000"}) class GatewayApplicationTests { @Autowired private Environment environment; @Test void contextLoads() { System.out.println(environment.getProperty("me")); } }启动后,将打印2000测试环境的@TestPropertySource 注解该配置是在单元测试中使用,如:@TestPropertySource(properties = {"me=3000"}) @SpringBootTest(properties = {"me=2000"}) class SpringBootDemoTests { @Autowired private Environment environment; @Test void contextLoads() { System.out.println(environment.getProperty("me")); } }启动后打印3000Devtools 全局配置Devtools 是Spring Boot 提供的一套开发工具,启用需要依赖如下依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency>默认读取$HOME/.config/spring-boot目录下的spring-boot-devtools.properties、spring-boot-devtools.yaml、spring-boot-devtools.yml文件,如果不存在,会从 $HOME 目录的根目录中搜索是否存在 .spring-bootdevtools.properties如在.spring-bootdevtools.properties中配置:启动后打印的结果如下,已为最新值:
GraalVM 介绍既然是VM,那肯定也是一个虚拟机,那它跟JVM有关系吗?有一定关系,GraalVM 可以完全取代上面提到的那几种虚拟机,比如 HotSpot。把你之前运行在 HotSpot 上的代码直接平移到 GraalVM 上,不用做任何的改变,甚至都感知不到,项目可以完美的运行。但是 GraalVM 还有更广泛的用途,不仅支持 Java 语言,还支持其他语言。这些其他语言不仅包括嫡系的 JVM 系语言,例如 Kotlin、Scala,还包括例如 JavaScript、Nodejs、Ruby、Python 等,如图。GraalVM Native Image 介绍GraalVM Native Image 是GraalVM 提供的一种能够将Spring Boot 程序打包成云原生可执行文件的技术,并且比JVM 占用更少的内存和更快的启动速度,非常适合使用容器部署和在Faas平台使用。与在JVM运行的应用程序不同,GraalVM Native Image需要提前对代码进行编译处理才能创建可执行文件,GraalVM Native Image 的运行不需要提供JVM虚拟机。GraalVM 文档地址:https://www.graalvm.org/latest/docs/getting-started/GraalVM Native Image 文档地址:https://www.graalvm.org/latest/reference-manual/native-image/创建第一个GraalVM云原生应用程序有两种办法创建原生应用程序:使用Cloud Native Buildpacks 来生成一个包含可执行应用程序的轻量级容器使用GraalVM Native 构建工具生成一个可执行文件下面示例使用GraalVM Native来构建。环境准备安装GraalVM SDK压缩包安装下载对应版本软件:https://github.com/graalvm/graalvm-ce-builds/releasesWindows解压ZIP包到安装目录配置path路径到GraalVM 的bin目录setx /M PATH “C:\Progra~1\Java<graalvm>\bin;%PATH%”配置JAVA_HOME到GraalVM 的安装目录setx /M JAVA_HOME “C:\Progra~1\Java<graalvm>”重启,测试Linux解压ZIP包到指定目录tar -xzf graalvm-ce-java-linux--.tar.gz2.配置PATH路径export PATH=/path/to//bin:$PATH3.配置JAVA_HOME路径export JAVA_HOME=/path/to/4.测试MAC解压ZIP包tar -xzf graalvm-ce-java-darwin-amd64-.tar.gz如果使用的是macOS Catalina更高版本,可能需要执行如下命令:sudo xattr -r -d com.apple.quarantine /path/to/graalvm2.移动解压的包到/Library/Java/JavaVirtualMachinessudo mv graalvm-ce-java- /Library/Java/JavaVirtualMachines验证是否成功:/usr/libexec/java_home -V 将会得到一个安装的JDK目录3.配置PATH路径export PATH=/Library/Java/JavaVirtualMachines//Contents/Home/bin:$PATH4.配置JAVA_HOME路径export JAVA_HOME=/Library/Java/JavaVirtualMachines//Contents/HomeIDEA 安装使用IDEA内置功能即可,下载有点慢,这边IDEA只有基于Java 19 的版本使用IDEA 下载后,只能在IDEA内部运行应用程序,如果要使用maven 打包,还需要配置PATH和JAVA_HOME路径,同压缩包安装方式安装Native Image 工具如果没有安装该工具,maven 在打包的时候会自动下载,但建议提前安装打包工具gu install native-image安装Native Image依赖的本地环境因为要编译成指定本地可执行文件,比如exe,需要Windows安装了Microsoft Visual C++ (MSVC),MAC 需要安装xcode,通过xcode-select --install,Linux sudo yum install gcc glibc-devel zlib-develUbuntu sudo apt-get install build-essential libz-dev zlib1g-dev其他Linux sudo dnf install gcc glibc-devel zlib-devel libstdc++-static这里以Windows为例,安装 Visual Studio 2017 或更高版本的 构建工具和 Windows 10 SDK使用start.spring.io创建一个Spring Boot 3.0应用1、选择Java 17 版本2、选择GraalVM Native Support、Spring Web创建后的pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.0.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>graalvm-native-application</artifactId> <version>0.0.1-SNAPSHOT</version> <name>graalvm-native-application</name> <description>graalvm-native-application</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <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> <build> <plugins> <plugin> <groupId>org.graalvm.buildtools</groupId> <artifactId>native-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> 3、写一个简单的接口package com.example.graalvmnativeapplication; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @SpringBootApplication public class GraalvmNativeApplication { public static void main(String[] args) { SpringApplication.run(GraalvmNativeApplication.class, args); } @RequestMapping("/") String home() { return "Hello World!"; } }4、打包可执行文件在 安装VS 中找到 x64 Native Tools Command Prompt 执行如下命令mvn -Pnative native:compile一共7个步骤,花费了差不多2分钟打包完,生成的可执行文件在target目录5、运行可执行文件双击exe文件,Spring Boot 应用程序几乎瞬间启动完毕,文件大小有68M,对于一个没什么业务代码的demo来说,确实太大了。访问地址http://localhost:8080/,能正常访问。6、与Java 17 比较VM包大小启动时间GraalVM Native Image68M0.15sJava 1718M2.15sJava 816.5M3.5s从这个DEMO看出,使用GraalVM Spring Boot 的启动时间确实快乐很多,但同时包也大了很多 ,有点空间换时间的意思。如果要打包原生可执行文件的话,环境配置也比较繁琐。不过使用GraalVM 来替代JVM 跑Java 程序还是很值得尝试的。参考资料:https://www.graalvm.org/latest/docs/getting-started/windows/ (GraalVM在 Windows的使用)https://blog.csdn.net/q412086027/article/details/113878426 (给了我启发)https://medium.com/graalvm/using-graalvm-and-native-image-on-windows-10-9954dc071311 (比较清楚的Windows 配置步骤)
在开始源码阅读之前,我们要有一个统一的Spring Boot 版本,不同的版本源码会略有差别,先搭建一个简易的SSM环境用于测试,这边简单的记录一下,阅读我专栏的读者可以下载我使用的Demo环境:demo环境地址:https://github.com/jujunchen/Spring-Boot-Demo.git版本统一:工具:IDEA JDK:jdk11 Spring Boot 版本:3.1.0 Mybatis Plus 版本:3.5.2使用 Spring Initializr 创建一个项目项目pom.xml文件如下<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.1.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.springboot</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>Spring-Boot-Demo</name> <description>Spring-Boot-Demo</description> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> <repository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> <releases> <enabled>false</enabled> </releases> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </pluginRepository> <pluginRepository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> <releases> <enabled>false</enabled> </releases> </pluginRepository> </pluginRepositories> </project> application.yml文件配置如下# Mysql数据库 spring: datasource: driver-class-name: org.h2.Driver url: jdbc:h2:./spring-boot-demo username: root password: 123456启动项目,打印出如下日志2023-06-16T13:29:22.124+08:00 INFO 23093 --- [ restartedMain] o.s.boot.SpringApplication : /$$$$$$ /$$$$$$ /$$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$$$ /$$$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$ /$$ /$$ /$$__ $$ /$$__ $$| $$__ $$| $$$ | $$ |_ $$_/|__ $$__//$$__ $$ /$$__ $$| $$ /$$//$$__ $$ /$$__ $$| $$ /$$/ | $$ \__/| $$ \__/| $$ \ $$| $$$$| $$ | $$ | $$ | $$ \__/| $$ \ $$ \ $$ /$$/| $$ \__/| $$ \ $$ \ $$ /$$/ | $$ | $$$$$$ | $$ | $$| $$ $$ $$ /$$$$$$| $$ | $$ | $$$$$$ | $$$$$$$$ \ $$$$/ | $$$$$$ | $$$$$$$$ \ $$$$/ | $$ \____ $$| $$ | $$| $$ $$$$|______/| $$ | $$ \____ $$| $$__ $$ \ $$/ \____ $$| $$__ $$ \ $$/ | $$ $$ /$$ \ $$| $$ | $$| $$\ $$$ | $$ | $$ /$$ \ $$| $$ | $$ | $$ /$$ \ $$| $$ | $$ | $$ | $$$$$$/| $$$$$$/| $$$$$$$/| $$ \ $$ /$$$$$$ | $$ | $$$$$$/| $$ | $$ | $$ | $$$$$$/| $$ | $$ | $$ \______/ \______/ |_______/ |__/ \__/ |______/ |__/ \______/ |__/ |__/ |__/ \______/ |__/ |__/ |__/ ::Spring Boot Version: (v3.1.0) 环境能正常启动后,就可以开始阅读源码了。有时候只是看最新的源码我们只能知道现在是怎么样的,但不知道为什么会演变成这样,比如有的类Spring Boot 2.X版本中有,3.X没有了,项目作者是怎么考虑的,这些就需要下载Spring Boot 的项目看Git提交记录。Spring Boot 项目地址:https://github.com/spring-projects/spring-boot
Spring Boot :3.1Java: 17前言Spring Boot 3.x 中的自动配置使用META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ,而不是META-INF/spring.factories,这个变动其实在2.7的时候已经改变2.6.9版本文档介绍2.7.0版本介绍文档中有创建自己的Starter的详细介绍,《Spring Boot 中文参考指南-创建自己的自动配置》加载原理Spring Boot 3.x的自动配置加载入口是META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ,Spring Boot会读取该文件中的自动配置类,并实例化,我们以该文件为入口。如果你是新手,并且没有资料可查,可以使用IDE的全局文件搜索功能,搜索关键词,如我搜索org.springframework.boot.autoconfigure.AutoConfiguration.imports 的结果如下,再通过打断点的方式就能判断加载该文件的入口。可知,该文件的加载是由AutoConfigurationImportSelector类进行处理,但AutoConfigurationImportSelector类又是如何加载的。通过断点的堆栈可知加载使用到了Spring 框架refresh()中的invokeBeanFactoryPostProcessors,其作用是在实例化Bean之前加载额外定义的Bean到上下文中,我们从头开始梳理,能力强的可以掌握方法自行阅读。堆栈信息AbstractApplicationContext-invokeBeanFactoryPostProcessors protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) { PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors()); // 如果beanFactory中是否包含LoadTimeWeaver,如果包含则使用临时ClassLoader进行处理,LoadTimeWeaver是一种类加载器的动态织入技术 if (!NativeDetector.inNativeImage() && beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) { beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory)); beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader())); } }该方法的作用是:实例化并调用注册的BeanFactoryPostProcessorgetBeanFactoryPostProcessors() 用来获取所有的BeanFactoryPostProcessor实例,BeanFactoryPostProcessor实例通过ApplicationContextInitializer来加载到上下文中,ApplicationContextInitializer的加载原理可以通过前面的文章了解《Spring Boot 系统初始化器详解》。PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors()); 委托处理BeanFactoryPostProcessor。PostProcessorRegistrationDelegate-invokeBeanFactoryPostProcessorspublic static void invokeBeanFactoryPostProcessors( ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) { Set<String> processedBeans = new HashSet<>(); //如果beanFactory 是BeanDefinitionRegistry的实现,先处理BeanDefinitionRegistryPostProcessors if (beanFactory instanceof BeanDefinitionRegistry registry) { List<BeanFactoryPostProcessor> regularPostProcessors = new ArrayList<>(); List<BeanDefinitionRegistryPostProcessor> registryProcessors = new ArrayList<>(); for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) { if (postProcessor instanceof BeanDefinitionRegistryPostProcessor registryProcessor) { registryProcessor.postProcessBeanDefinitionRegistry(registry); registryProcessors.add(registryProcessor); } else { regularPostProcessors.add(postProcessor); } } List<BeanDefinitionRegistryPostProcessor> currentRegistryProcessors = new ArrayList<>(); // 先处理 PriorityOrdered 的 BeanDefinitionRegistryPostProcessor String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); for (String ppName : postProcessorNames) { if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); processedBeans.add(ppName); } } sortPostProcessors(currentRegistryProcessors, beanFactory); registryProcessors.addAll(currentRegistryProcessors); //处理BeanDefinitionRegistryPostProcessors invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup()); currentRegistryProcessors.clear(); // 再处理 Ordered 的 BeanDefinitionRegistryPostProcessor postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); for (String ppName : postProcessorNames) { if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) { currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); processedBeans.add(ppName); } } sortPostProcessors(currentRegistryProcessors, beanFactory); registryProcessors.addAll(currentRegistryProcessors); invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup()); currentRegistryProcessors.clear(); // 最后处理剩余的 BeanDefinitionRegistryPostProcessor,直到没有新的 BeanDefinitionRegistryPostProcessor 添加为止 boolean reiterate = true; while (reiterate) { reiterate = false; postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); for (String ppName : postProcessorNames) { if (!processedBeans.contains(ppName)) { currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); processedBeans.add(ppName); reiterate = true; } } sortPostProcessors(currentRegistryProcessors, beanFactory); registryProcessors.addAll(currentRegistryProcessors); invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup()); currentRegistryProcessors.clear(); } // 处理所有 BeanFactoryPostProcessor invokeBeanFactoryPostProcessors(registryProcessors, beanFactory); invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory); } else { // 处理普通的上下文中的BeanFactoryPostProcessor invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory); } //获取所有常规化Bean,但不初始化,保留让后期的post-processors处理 String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false); //分离实现PriorityOrdered、Ordered和其他的BeanFactoryPostProcessors List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new ArrayList<>(); List<String> orderedPostProcessorNames = new ArrayList<>(); List<String> nonOrderedPostProcessorNames = new ArrayList<>(); for (String ppName : postProcessorNames) { if (processedBeans.contains(ppName)) { // 跳过已经处理的 } else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class)); } else if (beanFactory.isTypeMatch(ppName, Ordered.class)) { orderedPostProcessorNames.add(ppName); } else { nonOrderedPostProcessorNames.add(ppName); } } //首先处理实现了PriorityOrdered的BeanFactoryPostProcessors sortPostProcessors(priorityOrderedPostProcessors, beanFactory); invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory); //处理实现了Ordered的BeanFactoryPostProcessors List<BeanFactoryPostProcessor> orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size()); for (String postProcessorName : orderedPostProcessorNames) { orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class)); } sortPostProcessors(orderedPostProcessors, beanFactory); invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory); // 最后处理其他的BeanFactoryPostProcessors List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size()); for (String postProcessorName : nonOrderedPostProcessorNames) { nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class)); } invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory); // 清除缓存 beanFactory.clearMetadataCache(); }invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup()); 是处理自动配置的关键点PostProcessorRegistrationDelegate-invokeBeanDefinitionRegistryPostProcessorsprivate static void invokeBeanDefinitionRegistryPostProcessors( Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry, ApplicationStartup applicationStartup) { //循环处理BeanDefinitionRegistryPostProcessor实现 for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) { //步骤日志记录 StartupStep postProcessBeanDefRegistry = applicationStartup.start("spring.context.beandef-registry.post-process") .tag("postProcessor", postProcessor::toString); postProcessor.postProcessBeanDefinitionRegistry(registry); postProcessBeanDefRegistry.end(); } }BeanDefinitionRegistryPostProcessor接口继承了BeanFactoryPostProcessor接口,允许在初始化Bean实例之前修改、添加、删除容器中注册的Bean定义信息在该示例的SpringBoot-Demo中,只有一个BeanDefinitionRegistryPostProcessor实现,即ConfigurationClassPostProcessorConfigurationClassPostProcessor-postProcessBeanDefinitionRegistrypublic void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { //计算hashcode int registryId = System.identityHashCode(registry); //判断是否处理过 if (this.registriesPostProcessed.contains(registryId)) { throw new IllegalStateException( "postProcessBeanDefinitionRegistry already called on this post-processor against " + registry); } if (this.factoriesPostProcessed.contains(registryId)) { throw new IllegalStateException( "postProcessBeanFactory already called on this post-processor against " + registry); } this.registriesPostProcessed.add(registryId); //处理配置Bean定义信息 processConfigBeanDefinitions(registry); }ConfigurationClassPostProcessor-processConfigBeanDefinitionspublic void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { List<BeanDefinitionHolder> configCandidates = new ArrayList<>(); //获取容器中所有的bean定义名称 String[] candidateNames = registry.getBeanDefinitionNames(); for (String beanName : candidateNames) { // 获取 bean 的定义 BeanDefinition beanDef = registry.getBeanDefinition(beanName); // 检查该 bean 是否已经被处理成配置类 if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) { if (logger.isDebugEnabled()) { logger.debug("Bean definition has already been processed as a configuration class: " + beanDef); } } // 检查该 bean 是否是配置类候选者 else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) { configCandidates.add(new BeanDefinitionHolder(beanDef, beanName)); } } // 如果没有需要处理的配置类,则直接返回 if (configCandidates.isEmpty()) { return; } // 对配置类候选者进行排序,按照先前确定的 @Order 值排序 configCandidates.sort((bd1, bd2) -> { int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition()); int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition()); return Integer.compare(i1, i2); }); // 检查是否有自定义的 bean 名称生成策略 SingletonBeanRegistry sbr = null; if (registry instanceof SingletonBeanRegistry _sbr) { sbr = _sbr; if (!this.localBeanNameGeneratorSet) { BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton( AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR); if (generator != null) { this.componentScanBeanNameGenerator = generator; this.importBeanNameGenerator = generator; } } } if (this.environment == null) { this.environment = new StandardEnvironment(); } // 解析每个 @Configuration 类 ConfigurationClassParser parser = new ConfigurationClassParser( this.metadataReaderFactory, this.problemReporter, this.environment, this.resourceLoader, this.componentScanBeanNameGenerator, registry); Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates); Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size()); do { StartupStep processConfig = this.applicationStartup.start("spring.context.config-classes.parse"); //解析 parser.parse(candidates); //验证 parser.validate(); Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses()); //去除已经解析的类 configClasses.removeAll(alreadyParsed); // Read the model and create bean definitions based on its content if (this.reader == null) { this.reader = new ConfigurationClassBeanDefinitionReader( registry, this.sourceExtractor, this.resourceLoader, this.environment, this.importBeanNameGenerator, parser.getImportRegistry()); } this.reader.loadBeanDefinitions(configClasses); alreadyParsed.addAll(configClasses); processConfig.tag("classCount", () -> String.valueOf(configClasses.size())).end(); // 检查新的 BeanDefinition 是否包含新的配置类候选者 candidates.clear(); if (registry.getBeanDefinitionCount() > candidateNames.length) { String[] newCandidateNames = registry.getBeanDefinitionNames(); Set<String> oldCandidateNames = Set.of(candidateNames); Set<String> alreadyParsedClasses = new HashSet<>(); for (ConfigurationClass configurationClass : alreadyParsed) { alreadyParsedClasses.add(configurationClass.getMetadata().getClassName()); } for (String candidateName : newCandidateNames) { if (!oldCandidateNames.contains(candidateName)) { BeanDefinition bd = registry.getBeanDefinition(candidateName); if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) && !alreadyParsedClasses.contains(bd.getBeanClassName())) { candidates.add(new BeanDefinitionHolder(bd, candidateName)); } } } candidateNames = newCandidateNames; } } while (!candidates.isEmpty()); // 注册 ImportRegistry 为一个 bean,以支持 @ImportAware ConfigurationClass if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) { sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry()); } // 存储 PropertySourceDescriptors,便于在AOT中使用 this.propertySourceDescriptors = parser.getPropertySourceDescriptors(); if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory cachingMetadataReaderFactory) { // 清除缓存,防止由于缓存无法更新导致的出错问题 cachingMetadataReaderFactory.clearCache(); } }此处代码很长,对于配置类的解析,在parser.parse(candidates)中完成。parser变量的实例为:ConfigurationClassParserConfigurationClassParser-parsepublic void parse(Set<BeanDefinitionHolder> configCandidates) { for (BeanDefinitionHolder holder : configCandidates) { BeanDefinition bd = holder.getBeanDefinition(); try { // 如果 BeanDefinition 是一个 AnnotatedBeanDefinition,则需要解析该 BeanDefinition 中的注解信息 if (bd instanceof AnnotatedBeanDefinition annotatedBeanDef) { parse(annotatedBeanDef.getMetadata(), holder.getBeanName()); } // 如果 BeanDefinition 不是 AnnotatedBeanDefinition,并且具有 beanClass 属性,则解析该 beanClass 中的注解信息 else if (bd instanceof AbstractBeanDefinition abstractBeanDef && abstractBeanDef.hasBeanClass()) { parse(abstractBeanDef.getBeanClass(), holder.getBeanName()); } else { // 如果 BeanDefinition 没有 beanClass 属性,则解析该 BeanDefinition 中的 beanClassName 所指定的类中的注解信息 parse(bd.getBeanClassName(), holder.getBeanName()); } } catch (BeanDefinitionStoreException ex) { throw ex; } catch (Throwable ex) { throw new BeanDefinitionStoreException( "Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex); } } //处理所有的DeferredImportSelector实例 this.deferredImportSelectorHandler.process(); }在开始的时候,我们已经知道实际解析自动配置类是AutoConfigurationImportSelector,AutoConfigurationImportSelector 实现了DeferredImportSelector接口,而这里正好有deferredImportSelectorHandler来处理所有的deferredImportSelectorHandler实例。看下DeferredImportSelectorHandler中的process。DeferredImportSelectorHandler-processpublic void process() { List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors; this.deferredImportSelectors = null; try { if (deferredImports != null) { DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler(); deferredImports.sort(DEFERRED_IMPORT_COMPARATOR); deferredImports.forEach(handler::register); handler.processGroupImports(); } } finally { this.deferredImportSelectors = new ArrayList<>(); } }DeferredImportSelectorGroupingHandler 类是在 Spring Boot 中处理 DeferredImportSelector 接口的辅助类,主要用于按照分组将DeferredImportSelector分组进行处理。此处,我们要关注handler.processGroupImports(),调用到DeferredImportSelectorGroupingHandler类的processGroupImports方法。DeferredImportSelectorGroupingHandler-processGroupImportspublic void processGroupImports() { //遍历所有分组 for (DeferredImportSelectorGrouping grouping : this.groupings.values()) { //获取候选过滤器 Predicate<String> exclusionFilter = grouping.getCandidateFilter(); //遍历分组中的每个元素 grouping.getImports().forEach(entry -> { ConfigurationClass configurationClass = this.configurationClasses.get(entry.getMetadata()); try { //处理当前元素 processImports(configurationClass, asSourceClass(configurationClass, exclusionFilter), Collections.singleton(asSourceClass(entry.getImportClassName(), exclusionFilter)), exclusionFilter, false); } catch (BeanDefinitionStoreException ex) { throw ex; } catch (Throwable ex) { throw new BeanDefinitionStoreException( "Failed to process import candidates for configuration class [" + configurationClass.getMetadata().getClassName() + "]", ex); } }); } }这里首先要关注grouping.getImports(),在这里对自动配置类进行了加载DeferredImportSelectorGrouping-getImportspublic Iterable<Group.Entry> getImports() { for (DeferredImportSelectorHolder deferredImport : this.deferredImports) { //处理每个分组的DeferredImportSelectorHolder this.group.process(deferredImport.getConfigurationClass().getMetadata(), deferredImport.getImportSelector()); } //返回需要导入的类 return this.group.selectImports(); }this.group.process中进入到AutoConfigurationImportSelector的内部类AutoConfigurationGroup中AutoConfigurationImportSelector-AutoConfigurationGroup-processpublic void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) { Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector, () -> String.format("Only %s implementations are supported, got %s", AutoConfigurationImportSelector.class.getSimpleName(), deferredImportSelector.getClass().getName())); //转为为AutoConfigurationImportSelector,获取AutoConfigurationEntry AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector) .getAutoConfigurationEntry(annotationMetadata); //添加到autoConfigurationEntries变量中,供后期使用 this.autoConfigurationEntries.add(autoConfigurationEntry); //设置entries变量,导入的类名和元注解映射关系 for (String importClassName : autoConfigurationEntry.getConfigurations()) { this.entries.putIfAbsent(importClassName, annotationMetadata); } }此处deferredImportSelector强转为AutoConfigurationImportSelector后,再次调用了getAutoConfigurationEntry方法。AutoConfigurationImportSelector-getAutoConfigurationEntryprotected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { // 如果自动配置不可用,则返回 EMPTY_ENTRY if (!isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } // 获取注解的属性 AnnotationAttributes attributes = getAttributes(annotationMetadata); // 获取候选自动配置类,此文关键 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); // 去除重复的自动配置类 configurations = removeDuplicates(configurations); // 获取需要排除的自动配置类,spring.autoconfigure.exclude配置 Set<String> exclusions = getExclusions(annotationMetadata, attributes); // 检查是否有被排除的自动配置类 checkExcludedClasses(configurations, exclusions); // 去除被排除的自动配置类 configurations.removeAll(exclusions); // 使用 ConfigurationClassFilter 过滤自动配置类 configurations = getConfigurationClassFilter().filter(configurations); // 发送自动配置事件 fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationEntry(configurations, exclusions); }该方法的作用是根据给定的注解元数据获取自动配置项,对于自动配置的加载,关键在getCandidateConfigurations(annotationMetadata, attributes)。AutoConfigurationImportSelector-getCandidateConfigurationsprotected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { //从类路径的META-INF/spring中载入名为AutoConfiguration全限定名的类 List<String> configurations = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()) .getCandidates(); Assert.notEmpty(configurations, "No auto configuration classes found in " + "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you " + "are using a custom packaging, make sure that file is correct."); return configurations; }该方法的ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader())会拼凑出META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports路径,用于加载定义的AutoConfiguration类。ImportCandidates-ImportCandidatespublic static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) { Assert.notNull(annotation, "'annotation' must not be null"); ClassLoader classLoaderToUse = decideClassloader(classLoader); //将META-INF/spring/%s.imports字符串格式化为指定的路径 String location = String.format(LOCATION, annotation.getName()); Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location); List<String> importCandidates = new ArrayList<>(); while (urls.hasMoreElements()) { URL url = urls.nextElement(); importCandidates.addAll(readCandidateConfigurations(url)); } return new ImportCandidates(importCandidates); }在grouping.getImports()获取到所有要导入的AutoConfigurationEntry后,通过processImports进行处理ConfigurationClassParser-processImportsprivate void processImports(ConfigurationClass configClass, SourceClass currentSourceClass, Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter, boolean checkForCircularImports) { // 如果没有可导入的类,则直接返回 if (importCandidates.isEmpty()) { return; } // 检查循环导入 if (checkForCircularImports && isChainedImportOnStack(configClass)) { this.problemReporter.error(new CircularImportProblem(configClass, this.importStack)); } else { // 将当前配置类推入导入栈中 this.importStack.push(configClass); try { // 处理每个导入的类 for (SourceClass candidate : importCandidates) { if (candidate.isAssignable(ImportSelector.class)) { // Candidate class is an ImportSelector -> delegate to it to determine imports // 如果候选类是 ImportSelector 的子类,则交由它来决定导入哪些类 Class<?> candidateClass = candidate.loadClass(); ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class, this.environment, this.resourceLoader, this.registry); Predicate<String> selectorFilter = selector.getExclusionFilter(); if (selectorFilter != null) { exclusionFilter = exclusionFilter.or(selectorFilter); } if (selector instanceof DeferredImportSelector deferredImportSelector) { this.deferredImportSelectorHandler.handle(configClass, deferredImportSelector); } else { String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata()); Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter); processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false); } } else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) { // Candidate class is an ImportBeanDefinitionRegistrar -> // delegate to it to register additional bean definitions // 如果候选类是 ImportBeanDefinitionRegistrar 的子类,则交由它来注册更多的 Bean 定义 Class<?> candidateClass = candidate.loadClass(); ImportBeanDefinitionRegistrar registrar = ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class, this.environment, this.resourceLoader, this.registry); configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata()); } else { // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar -> // process it as an @Configuration class // 如果候选类不是 ImportSelector 或 ImportBeanDefinitionRegistrar 的子类,则将其作为 @Configuration 类来处理 this.importStack.registerImport( currentSourceClass.getMetadata(), candidate.getMetadata().getClassName()); processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter); } } } catch (BeanDefinitionStoreException ex) { throw ex; } catch (Throwable ex) { throw new BeanDefinitionStoreException( "Failed to process import candidates for configuration class [" + configClass.getMetadata().getClassName() + "]: " + ex.getMessage(), ex); } finally { this.importStack.pop(); } } }以MybatisPlusLanguageDriverAutoConfiguration自动配置为例,会通过processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);进行处理。ConfigurationClassParser-processConfigurationClassprotected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) throws IOException { // 如果该配置类被标记为跳过,则直接返回,不需要再处理 if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) { return; } // 如果已经存在同名的配置类,则判断两者的导入情况并合并 ConfigurationClass existingClass = this.configurationClasses.get(configClass); if (existingClass != null) { if (configClass.isImported()) { if (existingClass.isImported()) { existingClass.mergeImportedBy(configClass); } // Otherwise ignore new imported config class; existing non-imported class overrides it. return; } else { // Explicit bean definition found, probably replacing an import. // Let's remove the old one and go with the new one. this.configurationClasses.remove(configClass); this.knownSuperclasses.values().removeIf(configClass::equals); } } // 递归地处理该配置类及其父类的继承关系,解析 Bean 定义 SourceClass sourceClass = asSourceClass(configClass, filter); do { sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter); } while (sourceClass != null); // 将该配置类加入到 configurationClasses 中 this.configurationClasses.put(configClass, configClass); } ConfigurationClassParser-doProcessConfigurationClassprotected final SourceClass doProcessConfigurationClass( ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter) throws IOException { if (configClass.getMetadata().isAnnotated(Component.class.getName())) { // 如果该配置类被@Component注解标记,则递归处理任何嵌套类。 processMemberClasses(configClass, sourceClass, filter); } // 处理 @PropertySource for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), PropertySources.class, org.springframework.context.annotation.PropertySource.class)) { if (this.propertySourceRegistry != null) { this.propertySourceRegistry.processPropertySource(propertySource); } else { logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() + "]. Reason: Environment must implement ConfigurableEnvironment"); } } // 处理@ComponentScan注解 Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { for (AnnotationAttributes componentScan : componentScans) { // The config class is annotated with @ComponentScan -> perform the scan immediately Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); // 检查扫描的定义集是否有任何进一步的配置类,并在需要时递归解析 for (BeanDefinitionHolder holder : scannedBeanDefinitions) { BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition(); if (bdCand == null) { bdCand = holder.getBeanDefinition(); } if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) { parse(bdCand.getBeanClassName(), holder.getBeanName()); } } } } // 处理@Import processImports(configClass, sourceClass, getImports(sourceClass), filter, true); // 处理@ImportResource AnnotationAttributes importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class); if (importResource != null) { String[] resources = importResource.getStringArray("locations"); Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader"); for (String resource : resources) { String resolvedResource = this.environment.resolveRequiredPlaceholders(resource); configClass.addImportedResource(resolvedResource, readerClass); } } // 处理单个的@Bean方法 Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass); for (MethodMetadata methodMetadata : beanMethods) { configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass)); } // 处理接口上的默认方法 processInterfaces(configClass, sourceClass); // 处理超类 if (sourceClass.getMetadata().hasSuperClass()) { String superclass = sourceClass.getMetadata().getSuperClassName(); if (superclass != null && !superclass.startsWith("java") && !this.knownSuperclasses.containsKey(superclass)) { this.knownSuperclasses.put(superclass, configClass); // 找到超类,返回其注释元数据并递归 return sourceClass.getSuperClass(); } } // 没有超类,完成 return null; }总结一张图说明整个自动配置加载解析流程