数据库场景的自动配置分析与整合测试 导入JDBC场景 1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-jdbc</artifactId > </dependency >
接着导入数据库驱动包(MySQL为例)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <mysql.version > 8.0.22</mysql.version > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency > <properties > <java.version > 1.8</java.version > <mysql.version > 5.1.49</mysql.version > </properties >
相关数据源配置类
DataSourceAutoConfiguration
: 数据源的自动配置。
修改数据源相关的配置:spring.datasource
。
数据库连接池的配置,是自己容器中没有DataSource才自动配置的 。
底层配置好的连接池是:HikariDataSource
。
DataSourceTransactionManagerAutoConfiguration
: 事务管理器的自动配置。
JdbcTemplateAutoConfiguration
: JdbcTemplate
的自动配置,可以来对数据库进行CRUD。
可以修改前缀为spring.jdbc
的配置项来修改JdbcTemplate
。
@Bean @Primary JdbcTemplate
:Spring容器中有这个JdbcTemplate
组件,使用@Autowired
。
JndiDataSourceAutoConfiguration
: JNDI的自动配置。
XADataSourceAutoConfiguration
: 分布式事务相关的。
修改配置项 1 2 3 4 5 6 spring: datasource: url: jdbc:mysql://localhost:3306/db_account username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver
单元测试数据源 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.jdbc.core.JdbcTemplate;@SpringBootTest class Boot05WebAdminApplicationTests { @Autowired JdbcTemplate jdbcTemplate; @Test void contextLoads () { Long aLong = jdbcTemplate.queryForObject("select count(*) from account_tbl" , Long.class); log.info("记录总数:{}" ,aLong); } }
自定义方式整合druid数据源 Druid官网
Druid是什么? 它是数据库连接池,它能够提供强大的监控和扩展功能。
官方文档 - Druid连接池介绍
Spring Boot整合第三方技术的两种方式:
自定义方式 添加依赖 :
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > druid</artifactId > <version > 1.1.17</version > </dependency >
配置Druid数据源 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Configuration public class MyConfig { @Bean @ConfigurationProperties("spring.datasource") public DataSource dataSource () throws SQLException { DruidDataSource druidDataSource = new DruidDataSource (); return druidDataSource; } }
更多配置项
配置Druid的监控页功能 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @Configuration public class MyConfig { @Bean @ConfigurationProperties("spring.datasource") public DataSource dataSource () throws SQLException { DruidDataSource druidDataSource = new DruidDataSource (); druidDataSource.setFilters("stat,wall" ); return druidDataSource; } @Bean public ServletRegistrationBean statViewServlet () { StatViewServlet statViewServlet = new StatViewServlet (); ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean <>(statViewServlet, "/druid/*" ); registrationBean.addInitParameter("loginUsername" ,"admin" ); registrationBean.addInitParameter("loginPassword" ,"123456" ); return registrationBean; } @Bean public FilterRegistrationBean webStatFilter () { WebStatFilter webStatFilter = new WebStatFilter (); FilterRegistrationBean<WebStatFilter> filterRegistrationBean = new FilterRegistrationBean <>(webStatFilter); filterRegistrationBean.setUrlPatterns(Arrays.asList("/*" )); filterRegistrationBean.addInitParameter("exclusions" ,"*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" ); return filterRegistrationBean; } }
druid数据源starter整合方式 官方文档 - Druid Spring Boot Starter
引入依赖 :
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > <version > 1.1.17</version > </dependency >
分析自动配置 :
扩展配置项 spring.datasource.druid
自动配置类DruidDataSourceAutoConfigure
DruidSpringAopConfiguration.class
, 监控SpringBean的;配置项:spring.datasource.druid.aop-patterns
DruidStatViewServletConfiguration.class
, 监控页的配置。spring.datasource.druid.stat-view-servlet
默认开启。
DruidWebStatFilterConfiguration.class
,web监控配置。spring.datasource.druid.web-stat-filter
默认开启。
DruidFilterConfiguration.class
所有Druid的filter的配置:
1 2 3 4 5 6 7 8 9 private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat" ;private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config" ;private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding" ;private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j" ;private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j" ;private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2" ;private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log" ;private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall" ;12345678
配置示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 spring: datasource: url: jdbc:mysql://localhost:3306/db_account username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver druid: aop-patterns: com.atguigu.admin.* filters: stat,wall stat-view-servlet: enabled: true login-username: admin login-password: admin resetEnable: false web-stat-filter: enabled: true urlPattern: /* exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' filter: stat: slow-sql-millis: 1000 logSlowSql: true enabled: true wall: enabled: true config: drop-table-allow: false
整合MyBatis-配置版 MyBatis的GitHub仓库
MyBatis官方
starter的命名方式 :
SpringBoot官方的Starter:spring-boot-starter-*
第三方的: *-spring-boot-starter
引入依赖 :
1 2 3 4 5 <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > <version > 2.1.4</version > </dependency >
配置模式 :
全局配置文件
SqlSessionFactory
:自动配置好了
SqlSession
:自动配置了SqlSessionTemplate
组合了SqlSession
@Import(AutoConfiguredMapperScannerRegistrar.class)
Mapper
: 只要我们写的操作MyBatis的接口标准了@Mapper
就会被自动扫描进来
1 2 3 4 5 6 7 8 9 10 @EnableConfigurationProperties(MybatisProperties.class) : MyBatis配置项绑定类。@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class }) public class MybatisAutoConfiguration { ... }@ConfigurationProperties(prefix = "mybatis") public class MybatisProperties { ... }
配置文件 :
1 2 3 4 5 6 7 8 9 10 11 spring: datasource: username: root password: 1234 url: jdbc:mysql://localhost:3306/my driver-class-name: com.mysql.jdbc.Driver mybatis: config-location: classpath:mybatis/mybatis-config.xml mapper-locations: classpath:mybatis/*.xml
mybatis-config.xml :
1 2 3 4 5 6 7 8 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd" > <configuration > </configuration >
Mapper接口 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.lun.boot.mapper.UserMapper" > <select id ="getUser" resultType ="com.lun.boot.bean.User" > select * from user where id=#{id} </select > </mapper > 12345678910 import com.lun.boot.bean.User; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserMapper { public User getUser(Integer id); }
POJO :
1 2 3 4 5 6 public class User { private Integer id; private String name; }
DB :
1 2 3 4 5 CREATE TABLE `user ` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `name` varchar (45 ) DEFAULT NULL , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 3 DEFAULT CHARSET= utf8mb4;
Controller and Service :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Controller public class UserController { @Autowired private UserService userService; @ResponseBody @GetMapping("/user/{id}") public User getUser (@PathVariable("id") Integer id) { return userService.getUser(id); } }@Service public class UserService { @Autowired private UserMapper userMapper; public User getUser (Integer id) { return userMapper.getUser(id); } }
配置private Configuration configuration;
也就是配置mybatis.configuration
相关的,就是相当于改mybatis全局配置文件中的值。(也就是说配置了mybatis.configuration
,就不需配置mybatis全局配置文件了)
1 2 3 4 5 6 7 8 mybatis: mapper-locations: classpath:mybatis/mapper/*.xml configuration: map-underscore-to-camel-case: true 1234567
小结
导入MyBatis官方Starter。
编写Mapper接口,需@Mapper
注解。
编写SQL映射文件并绑定Mapper接口。
在application.yaml
中指定Mapper配置文件的所处位置,以及指定全局配置文件的信息 (建议:**配置在mybatis.configuration
**)。
整合MyBatis-注解配置混合版 你可以通过Spring Initializr添加MyBatis的Starer。
注解与配置混合搭配,干活不累 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Mapper public interface UserMapper { public User getUser (Integer id) ; @Select("select * from user where id=#{id}") public User getUser2 (Integer id) ; public void saveUser (User user) ; @Insert("insert into user(`name`) values(#{name})") @Options(useGeneratedKeys = true, keyProperty = "id") public void saveUser2 (User user) ; } <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.lun.boot.mapper.UserMapper" > <select id="getUser" resultType="com.lun.boot.bean.User" > select * from user where id=#{id} </select> <insert id="saveUser" useGeneratedKeys="true" keyProperty="id" > insert into user (`name`) values(#{name}) </insert> </mapper>
简单DAO方法就写在注解上。复杂的就写在配置文件里。
使用@MapperScan("com.lun.boot.mapper")
简化,Mapper接口就可以不用标注@Mapper
注解。
1 2 3 4 5 6 7 8 9 @MapperScan("com.lun.boot.mapper") @SpringBootApplication public class MainApplication { public static void main (String[] args) { SpringApplication.run(MainApplication.class, args); } }
整合MyBatisPlus操作数据库 IDEA的MyBatis的插件 - MyBatisX
MyBatisPlus官网
MyBatisPlus官方文档
MyBatisPlus是什么 MyBatis-Plus (简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
添加依赖:
1 2 3 4 5 <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.4.1</version > </dependency >
MybatisPlusAutoConfiguration
配置类,MybatisPlusProperties
配置项绑定。
SqlSessionFactory
自动配置好,底层是容器中默认的数据源。
mapperLocations
自动配置好的,有默认值classpath*:/mapper/**/*.xml
,这表示任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。 建议以后sql映射文件放在 mapper下。
容器中也自动配置好了SqlSessionTemplate
。
@Mapper
标注的接口也会被自动扫描,建议直接 @MapperScan("com.lun.boot.mapper")
批量扫描。
MyBatisPlus优点 之一:只需要我们的Mapper继承MyBatisPlus的BaseMapper
就可以拥有CRUD能力,减轻开发工作。
1 2 3 4 import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.lun.hellomybatisplus.model.User;public interface UserMapper extends BaseMapper <User> {}
CRUD实验-数据列表展示 官方文档 - CRUD接口
使用MyBatis Plus提供的IService
,ServiceImpl
,减轻Service层开发工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import com.lun.hellomybatisplus.model.User;import com.baomidou.mybatisplus.extension.service.IService;import java.util.List;public interface UserService extends IService <User> { }import com.lun.hellomybatisplus.model.User;import com.lun.hellomybatisplus.mapper.UserMapper;import com.lun.hellomybatisplus.service.UserService;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.util.List;@Service public class UserServiceImpl extends ServiceImpl <UserMapper,User> implements UserService { }
与下一节联合在一起
分页数据展示 与下一节联合在一起
删除用户完成 添加分页插件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 @Configuration public class MyBatisConfig { @Bean public MybatisPlusInterceptor paginationInterceptor () { MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor (); PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor (); paginationInnerInterceptor.setOverflow(true ); paginationInnerInterceptor.setMaxLimit(500L ); mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor); return mybatisPlusInterceptor; } } <table class="display table table-bordered table-striped" id="dynamic-table" > <thead> <tr> <th>#</th> <th>name</th> <th>age</th> <th>email</th> <th>操作</th> </tr> </thead> <tbody> <tr class="gradeX" th:each="user: ${users.records}" > <td th:text="${user.id}" ></td> <td>[[${user.name}]]</td> <td th:text="${user.age}" >Win 95 +</td> <td th:text="${user.email}" >4 </td> <td> <a th:href="@{/user/delete/{id}(id=${user.id},pn=${users.current})}" class="btn btn-danger btn-sm" type="button" >删除</a> </td> </tr> </tfoot> </table> <div class="row-fluid" > <div class="span6" > <div class="dataTables_info" id="dynamic-table_info" > 当前第[[${users.current}]]页 总计 [[${users.pages}]]页 共[[${users.total}]]条记录 </div> </div> <div class="span6" > <div class="dataTables_paginate paging_bootstrap pagination" > <ul> <li class="prev disabled" ><a href="#" >← 前一页</a></li> <li th:class="${num == users.current?'active':''}" th:each="num:${#numbers.sequence(1,users.pages)}" > <a th:href="@{/dynamic_table(pn=${num})}" >[[${num}]]</a> </li> <li class="next disabled" ><a href="#" >下一页 → </a></li> </ul> </div> </div> </div>
#numbers
表示methods for formatting numeric objects.link
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @GetMapping("/user/delete/{id}") public String deleteUser (@PathVariable("id") Long id, @RequestParam(value = "pn",defaultValue = "1") Integer pn, RedirectAttributes ra) { userService.removeById(id); ra.addAttribute("pn" ,pn); return "redirect:/dynamic_table" ; }@GetMapping("/dynamic_table") public String dynamic_table (@RequestParam(value="pn",defaultValue = "1") Integer pn,Model model) { Page<User> page = new Page <>(pn, 2 ); Page<User> userPage = userService.page(page, null ); model.addAttribute("users" ,userPage); return "table/dynamic_table" ; }
准备阿里云Redis环境 添加依赖 :
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency > <dependency > <groupId > redis.clients</groupId > <artifactId > jedis</artifactId > </dependency >
RedisAutoConfiguration
自动配置类,RedisProperties 属性类 –> spring.redis.xxx是对redis的配置。
连接工厂LettuceConnectionConfiguration
、JedisConnectionConfiguration
是准备好的。
自动注入了RedisTemplate<Object, Object>
,xxxTemplate
。
自动注入了StringRedisTemplate
,key,value都是String
底层只要我们使用StringRedisTemplate
、RedisTemplate
就可以操作Redis。
外网Redis环境搭建 :
阿里云按量付费Redis,其中选择经典网络 。
申请Redis的公网连接地址。
修改白名单,允许0.0.0.0/0
访问。
Redis操作与统计小实验 相关Redis配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 spring: redis: # url: redis: host: r-bp1nc7reqesxisgxpipd.redis.rds.aliyuncs.com port: 6379 password: lfy:Lfy123456 client-type: jedis jedis: pool: max-active: 10 # lettuce:# 另一个用来连接redis的java框架 # pool: # max-active: 10 # min-idle: 5
测试Redis连接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @SpringBootTest public class Boot05WebAdminApplicationTests { @Autowired StringRedisTemplate redisTemplate; @Autowired RedisConnectionFactory redisConnectionFactory; @Test void testRedis () { ValueOperations<String, String> operations = redisTemplate.opsForValue(); operations.set("hello" ,"world" ); String hello = operations.get("hello" ); System.out.println(hello); System.out.println(redisConnectionFactory.getClass()); } }
Redis Desktop Manager:可视化Redis管理软件。
URL统计拦截器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Component public class RedisUrlCountInterceptor implements HandlerInterceptor { @Autowired StringRedisTemplate redisTemplate; @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String uri = request.getRequestURI(); redisTemplate.opsForValue().increment(uri); return true ; } }
注册URL统计拦截器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Configuration public class AdminWebConfig implements WebMvcConfigurer { @Autowired RedisUrlCountInterceptor redisUrlCountInterceptor; @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(redisUrlCountInterceptor) .addPathPatterns("/**" ) .excludePathPatterns("/" ,"/login" ,"/css/**" ,"/fonts/**" ,"/images/**" , "/js/**" ,"/aa/**" ); } }
Filter、Interceptor 几乎拥有相同的功能?
Filter是Servlet定义的原生组件,它的好处是脱离Spring应用也能使用。
Interceptor是Spring定义的接口,可以使用Spring的自动装配等功能。
调用Redis内的统计数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Slf4j @Controller public class IndexController { @Autowired StringRedisTemplate redisTemplate; @GetMapping("/main.html") public String mainPage (HttpSession session,Model model) { log.info("当前方法是:{}" ,"mainPage" ); ValueOperations<String, String> opsForValue = redisTemplate.opsForValue(); String s = opsForValue.get("/main.html" ); String s1 = opsForValue.get("/sql" ); model.addAttribute("mainCount" ,s); model.addAttribute("sqlCount" ,s1); return "main" ; } }
JUnit5简介 Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库
JUnit 5官方文档
作为最新版本的JUnit框架,JUnit5与之前版本的JUnit框架有很大的不同。由三个不同子项目的几个不同模块组成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform : Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。
JUnit Jupiter : JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部包含了一个测试引擎 ,用于在Junit Platform上运行。
JUnit Vintage : 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,JUnit3.x的测试引擎。
注意 :
SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容JUnit4需要自行引入(不能使用JUnit4的功能 @Test)
JUnit 5’s Vintage已经从spring-boot-starter-test
从移除。如果需要继续兼容Junit4需要自行引入Vintage依赖:
1 2 3 4 5 6 7 8 9 10 11 <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 >
使用添加JUnit 5,添加对应的starter:
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency >
Spring的JUnit 5的基本单元测试模板(Spring的JUnit4的是@SpringBootTest
+@RunWith(SpringRunner.class)
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import org.junit.jupiter.api.Assertions;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest class SpringBootApplicationTests { @Autowired private Component component; @Test public void contextLoads () { Assertions.assertEquals(5 , component.getFive()); } }
常用测试注解 官方文档 - Annotations
@Test :表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
@ParameterizedTest :表示方法是参数化测试。
@RepeatedTest :表示方法可重复执行。
@DisplayName :为测试类或者测试方法设置展示名称。
@BeforeEach :表示在每个 单元测试之前 执行。
@AfterEach :表示在每个 单元测试之后 执行。
@BeforeAll :表示在所有 单元测试之前 执行。
@AfterAll :表示在所有 单元测试之后 执行。
@Tag :表示单元测试类别,类似于JUnit4中的@Categories。
@Disabled :表示测试类或测试方法不执行,类似于JUnit4中的@Ignore。
@Timeout :表示测试方法运行如果超过了指定时间将会返回错误。
@ExtendWith :为测试类或测试方法提供扩展类引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 import org.junit.jupiter.api.*;@DisplayName("junit5功能测试类") public class Junit5Test { @DisplayName("测试displayname注解") @Test void testDisplayName () { System.out.println(1 ); System.out.println(jdbcTemplate); } @ParameterizedTest @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" }) void palindromes (String candidate) { assertTrue(StringUtils.isPalindrome(candidate)); } @Disabled @DisplayName("测试方法2") @Test void test2 () { System.out.println(2 ); } @RepeatedTest(5) @Test void test3 () { System.out.println(5 ); } @Timeout(value = 500, unit = TimeUnit.MILLISECONDS) @Test void testTimeout () throws InterruptedException { Thread.sleep(600 ); } @BeforeEach void testBeforeEach () { System.out.println("测试就要开始了..." ); } @AfterEach void testAfterEach () { System.out.println("测试结束了..." ); } @BeforeAll static void testBeforeAll () { System.out.println("所有测试就要开始了..." ); } @AfterAll static void testAfterAll () { System.out.println("所有测试以及结束了..." ); } }
断言机制 断言Assertion是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是org.junit.jupiter.api.Assertions的静态方法。检查业务逻辑返回的数据是否合理。所有的测试运行结束以后,会有一个详细的测试报告。
JUnit 5 内置的断言可以分成如下几个类别:
简单断言 用来对单个值进行简单的验证。如:
方法
说明
assertEquals
判断两个对象或两个原始类型是否相等
assertNotEquals
判断两个对象或两个原始类型是否不相等
assertSame
判断两个对象引用是否指向同一个对象
assertNotSame
判断两个对象引用是否指向不同的对象
assertTrue
判断给定的布尔值是否为 true
assertFalse
判断给定的布尔值是否为 false
assertNull
判断给定的对象引用是否为 null
assertNotNull
判断给定的对象引用是否不为 null
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test @DisplayName("simple assertion") public void simple () { assertEquals(3 , 1 + 2 , "simple math" ); assertNotEquals(3 , 1 + 1 ); assertNotSame(new Object (), new Object ()); Object obj = new Object (); assertSame(obj, obj); assertFalse(1 > 2 ); assertTrue(1 < 2 ); assertNull(null ); assertNotNull(new Object ()); }
数组断言 通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等。
1 2 3 4 5 @Test @DisplayName("array assertion") public void array () { assertArrayEquals(new int []{1 , 2 }, new int [] {1 , 2 }); }
组合断言 assertAll()
方法接受多个 org.junit.jupiter.api.Executable
函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言。
1 2 3 4 5 6 7 8 @Test @DisplayName("assert all") public void all () { assertAll("Math" , () -> assertEquals(2 , 1 + 1 ), () -> assertTrue(1 > 0 ) ); }
异常断言 在JUnit4时期,想要测试方法的异常情况时,需要用@Rule
注解的ExpectedException
变量还是比较麻烦的。而JUnit5提供了一种新的断言方式Assertions.assertThrows()
,配合函数式编程就可以进行使用。
1 2 3 4 5 6 7 @Test @DisplayName("异常测试") public void exceptionTest () { ArithmeticException exception = Assertions.assertThrows( ArithmeticException.class, () -> System.out.println(1 % 0 )); }
超时断言 JUnit5还提供了Assertions.assertTimeout()为测试方法设置了超时时间。
1 2 3 4 5 6 @Test @DisplayName("超时测试") public void timeoutTest () { Assertions.assertTimeout(Duration.ofMillis(1000 ), () -> Thread.sleep(500 )); }
快速失败 通过 fail 方法直接使得测试失败。
1 2 3 4 5 @Test @DisplayName("fail") public void shouldFail () { fail("This should fail" ); }
前置条件 JUnit 5 中的前置条件(assumptions【假设】)类似于断言,不同之处在于不满足的断言assertions 会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止 。
前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @DisplayName("前置条件") public class AssumptionsTest { private final String environment = "DEV" ; @Test @DisplayName("simple") public void simpleAssume () { assumeTrue(Objects.equals(this .environment, "DEV" )); assumeFalse(() -> Objects.equals(this .environment, "PROD" )); } @Test @DisplayName("assume then do") public void assumeThenDo () { assumingThat( Objects.equals(this .environment, "DEV" ), () -> System.out.println("In DEV" ) ); } }
assumeTrue
和 assumFalse
确保给定的条件为 true
或 false
,不满足条件会使得测试执行终止。
assumingThat
的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable
对象才会被执行;当条件不满足时,测试执行并不会终止。
嵌套测试 官方文档 - Nested Tests
JUnit 5 可以通过 Java 中的内部类和@Nested
注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach
和@AfterEach
注解,而且嵌套的层次没有限制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 @DisplayName("A stack") class TestingAStackDemo { Stack<Object> stack; @Test @DisplayName("is instantiated with new Stack()") void isInstantiatedWithNew () { new Stack <>(); } @Nested @DisplayName("when new") class WhenNew { @BeforeEach void createNewStack () { stack = new Stack <>(); } @Test @DisplayName("is empty") void isEmpty () { assertTrue(stack.isEmpty()); } @Test @DisplayName("throws EmptyStackException when popped") void throwsExceptionWhenPopped () { assertThrows(EmptyStackException.class, stack::pop); } @Test @DisplayName("throws EmptyStackException when peeked") void throwsExceptionWhenPeeked () { assertThrows(EmptyStackException.class, stack::peek); } @Nested @DisplayName("after pushing an element") class AfterPushing { String anElement = "an element" ; @BeforeEach void pushAnElement () { stack.push(anElement); } @Test @DisplayName("it is no longer empty") void isNotEmpty () { assertFalse(stack.isEmpty()); } @Test @DisplayName("returns the element when popped and is empty") void returnElementWhenPopped () { assertEquals(anElement, stack.pop()); assertTrue(stack.isEmpty()); } @Test @DisplayName("returns the element when peeked but remains not empty") void returnElementWhenPeeked () { assertEquals(anElement, stack.peek()); assertFalse(stack.isEmpty()); } } } }
参数化测试 官方文档 - Parameterized Tests
参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。
利用@ValueSource等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
利用**@ValueSource**等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
@ValueSource : 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
@NullSource : 表示为参数化测试提供一个null的入参
@EnumSource : 表示为参数化测试提供一个枚举入参
@CsvFileSource :表示读取指定CSV文件内容作为参数化测试入参
@MethodSource :表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现**ArgumentsProvider
**接口,任何外部文件都可以作为它的入参。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @ParameterizedTest @ValueSource(strings = {"one", "two", "three"}) @DisplayName("参数化测试1") public void parameterizedTest1 (String string) { System.out.println(string); Assertions.assertTrue(StringUtils.isNotBlank(string)); }@ParameterizedTest @MethodSource("method") @DisplayName("方法来源参数") public void testWithExplicitLocalMethodSource (String name) { System.out.println(name); Assertions.assertNotNull(name); }static Stream<String> method () { return Stream.of("apple" , "banana" ); }
迁移指南 官方文档 - Migrating from JUnit 4
在进行迁移的时候需要注意如下的变化:
注解在 org.junit.jupiter.api
包中,断言在 org.junit.jupiter.api.Assertions
类中,前置条件在 org.junit.jupiter.api.Assumptions
类中。
把@Before
和@After
替换成@BeforeEach
和@AfterEach
。
把@BeforeClass
和@AfterClass
替换成@BeforeAll
和@AfterAll。
把@Ignore
替换成@Disabled
。
把@Category
替换成@Tag
。
把@RunWith
、@Rule
和@ClassRule
替换成@ExtendWith
。
SpringBoot Actuator与Endpoint 未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
官方文档 - Spring Boot Actuator: Production-ready Features
1.x与2.x的不同 :
SpringBoot Actuator 1.x
支持SpringMVC
基于继承方式进行扩展
层级Metrics配置
自定义Metrics收集
默认较少的安全策略
SpringBoot Actuator 2.x
支持SpringMVC、JAX-RS以及Webflux
注解驱动进行扩展
层级&名称空间Metrics
底层使用MicroMeter,强大、便捷默认丰富的安全策略
如何使用
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency >
访问http://localhost:8080/actuator/**
。
暴露所有监控信息为HTTP。
1 2 3 4 5 6 management: endpoints: enabled-by-default: true web: exposure: include: '*'
actuator
英 [ˈæktjʊeɪtə] 美 [ˈæktjuˌeɪtər]
n. 致动(促动,激励,调节)器;传动(装置,机构);拖动装置;马达;操作机构;执行机构(元件);(电磁铁)螺线管;操纵装置(阀门);调速控制器;往复运动油(气)缸;作动筒
metric
英 [ˈmetrɪk] 美 [ˈmetrɪk]
adj. 米制的;公制的;按公制制作的;用公制测量的
n. 度量标准;[数学]度量;诗体;韵文;诗韵
常使用的端点及开启与禁用 常使用的端点
ID
描述
auditevents
暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件
。
beans
显示应用程序中所有Spring Bean的完整列表。
caches
暴露可用的缓存。
conditions
显示自动配置的所有条件信息,包括匹配或不匹配的原因。
configprops
显示所有@ConfigurationProperties
。
env
暴露Spring的属性ConfigurableEnvironment
flyway
显示已应用的所有Flyway数据库迁移。 需要一个或多个Flyway
组件。
health
显示应用程序运行状况信息。
httptrace
显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository
组件。
info
显示应用程序信息。
integrationgraph
显示Spring integrationgraph
。需要依赖spring-integration-core
。
loggers
显示和修改应用程序中日志的配置。
liquibase
显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase
组件。
metrics
显示当前应用程序的“指标”信息。
mappings
显示所有@RequestMapping
路径列表。
scheduledtasks
显示应用程序中的计划任务。
sessions
允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。
shutdown
使应用程序正常关闭。默认禁用。
startup
显示由ApplicationStartup
收集的启动步骤数据。需要使用SpringApplication
进行配置BufferingApplicationStartup
。
threaddump
执行线程转储。
如果您的应用程序是Web应用程序(Spring MVC,Spring WebFlux或Jersey),则可以使用以下附加端点:
ID
描述
heapdump
返回hprof
堆转储文件。
jolokia
通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core
。
logfile
返回日志文件的内容(如果已设置logging.file.name
或logging.file.path
属性)。支持使用HTTPRange
标头来检索部分日志文件的内容。
prometheus
以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus
。
其中最常用的Endpoint:
Health:监控状况
Metrics:运行时指标
Loggers:日志记录
Health Endpoint 健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。
重要的几点:
health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告。
很多的健康检查默认已经自动配置好了,比如:数据库、redis等。
可以很容易的添加自定义的健康检查机制。
Metrics Endpoint 提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到:
通过Metrics对接多种监控系统。
简化核心Metrics开发。
添加自定义Metrics或者扩展已有Metrics。
开启与禁用Endpoints
默认所有的Endpoint除过shutdown都是开启的。
需要开启或者禁用某个Endpoint。配置模式为management.endpoint.<endpointName>.enabled = true
1 2 3 4 management: endpoint: beans: enabled: true
或者禁用所有的Endpoint然后手动开启指定的Endpoint。
1 2 3 4 5 6 7 8 management: endpoints: enabled-by-default: false endpoint: beans: enabled: true health: enabled: true
暴露Endpoints 支持的暴露方式
HTTP:默认只暴露health和info。
JMX:默认暴露所有Endpoint。
除过health和info,剩下的Endpoint都应该进行保护访问。如果引入Spring Security,则会默认配置安全访问规则。
ID
JMX
Web
auditevents
Yes
No
beans
Yes
No
caches
Yes
No
conditions
Yes
No
configprops
Yes
No
env
Yes
No
flyway
Yes
No
health
Yes
Yes
heapdump
N/A
No
httptrace
Yes
No
info
Yes
Yes
integrationgraph
Yes
No
jolokia
N/A
No
logfile
N/A
No
loggers
Yes
No
liquibase
Yes
No
metrics
Yes
No
mappings
Yes
No
prometheus
N/A
No
scheduledtasks
Yes
No
sessions
Yes
No
shutdown
Yes
No
startup
Yes
No
threaddump
Yes
No
若要更改公开的Endpoint,请配置以下的包含和排除属性:
Property
Default
management.endpoints.jmx.exposure.exclude
management.endpoints.jmx.exposure.include
*
management.endpoints.web.exposure.exclude
management.endpoints.web.exposure.include
info, health
官方文档 - Exposing Endpoints
定制Endpoint 定制 Health 信息 1 2 3 4 management: health: enabled: true show-details: always
通过实现HealthIndicator
接口,或继承MyComHealthIndicator
类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import org.springframework.boot.actuate.health.Health;import org.springframework.boot.actuate.health.HealthIndicator;import org.springframework.stereotype.Component;@Component public class MyHealthIndicator implements HealthIndicator { @Override public Health health () { int errorCode = check(); if (errorCode != 0 ) { return Health.down().withDetail("Error Code" , errorCode).build(); } return Health.up().build(); } }@Component public class MyComHealthIndicator extends AbstractHealthIndicator { @Override protected void doHealthCheck (Health.Builder builder) throws Exception { Map<String,Object> map = new HashMap <>(); if (1 == 2 ){ builder.status(Status.UP); map.put("count" ,1 ); map.put("ms" ,100 ); }else { builder.status(Status.OUT_OF_SERVICE); map.put("err" ,"连接超时" ); map.put("ms" ,3000 ); } builder.withDetail("code" ,100 ) .withDetails(map); } }
定制info信息 常用两种方式:
1 2 3 4 5 info: appName: boot-admin version: 2.0 .1 mavenProjectName: @project.artifactId@ mavenProjectVersion: @project.version@
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import java.util.Collections;import org.springframework.boot.actuate.info.Info;import org.springframework.boot.actuate.info.InfoContributor;import org.springframework.stereotype.Component;@Component public class ExampleInfoContributor implements InfoContributor { @Override public void contribute (Info.Builder builder) { builder.withDetail("example" , Collections.singletonMap("key" , "value" )); } }
http://localhost:8080/actuator/info 会输出以上方式返回的所有info信息
定制Metrics信息 Spring Boot支持的metrics
增加定制Metrics:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class MyService { Counter counter; public MyService (MeterRegistry meterRegistry) { counter = meterRegistry.counter("myservice.method.running.counter" ); } public void hello () { counter.increment(); } }12345678910 @Bean MeterBinder queueSize (Queue queue) { return (registry) -> Gauge.builder("queueSize" , queue::size).register(registry); }
定制Endpoint 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Component @Endpoint(id = "container") public class DockerEndpoint { @ReadOperation public Map getDockerInfo () { return Collections.singletonMap("info" ,"docker started..." ); } @WriteOperation private void restartDocker () { System.out.println("docker restarted...." ); } }
场景:
开发ReadinessEndpoint来管理程序是否就绪。
开发LivenessEndpoint来管理程序是否存活。
Boot Admin Server 官方Github
官方文档
开始使用方法
Profile环境切换 为了方便多环境适配,Spring Boot简化了profile功能。
默认配置文件application.yaml
任何时候都会加载。
指定环境配置文件application-{env}.yaml
,env
通常替代为test
,
激活指定环境
配置文件激活:spring.profiles.active=prod
命令行激活:java -jar xxx.jar --spring.profiles.active=prod --person.name=haha
(修改配置文件的任意值,命令行优先 )
默认配置与环境配置同时生效
同名配置项,profile配置优先
@Profile条件装配功能 1 2 3 4 5 6 7 @Data @Component @ConfigurationProperties("person") public class Person { private String name; private Integer age; }
application.properties
1 2 3 person: name: lun age: 8
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public interface Person { String getName () ; Integer getAge () ; }@Profile("test") @Component @ConfigurationProperties("person") @Data public class Worker implements Person { private String name; private Integer age; }@Profile(value = {"prod","default"}) @Component @ConfigurationProperties("person") @Data public class Boss implements Person { private String name; private Integer age; }
application-test.yaml
1 2 3 4 5 person: name: test-张三 server: port: 7000
application-prod.yaml
1 2 3 4 5 person: name: prod-张三 server: port: 8000
application.properties
1 2 3 4 5 6 7 8 9 10 11 spring.profiles.active =prod 12 @Autowired private Person person; @GetMapping("/") public String hello(){ //激活了prod,则返回Boss;激活了test,则返回Worker return person.getClass().toString(); }
@Profile还可以修饰在方法上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Color { }@Configuration public class MyConfig { @Profile("prod") @Bean public Color red () { return new Color (); } @Profile("test") @Bean public Color green () { return new Color (); } }
可以激活一组:
1 2 3 4 5 spring.profiles.active =production spring.profiles.group.production[0] =proddb spring.profiles.group.production[1] =prodmq 1234
配置加载优先级 外部化配置 官方文档 - Externalized Configuration
Spring Boot uses a very particular PropertySource
order that is designed to allow sensible overriding of values. Properties are considered in the following order (with values from lower items overriding earlier ones)(1优先级最低,14优先级最高):
Default properties (specified by setting SpringApplication.setDefaultProperties
).
@PropertySource
annotations on your @Configuration
classes. Please note that such property sources are not added to the Environment
until the application context is being refreshed. This is too late to configure certain properties such as logging.*
and spring.main.*
which are read before refresh begins.
Config data (such as application.properties
files)
A RandomValuePropertySource
that has properties only in random.*
.
OS environment variables.
Java System properties (System.getProperties()
).
JNDI attributes from java:comp/env
.
ServletContext
init parameters.
ServletConfig
init parameters.
Properties from SPRING_APPLICATION_JSON
(inline JSON embedded in an environment variable or system property).
Command line arguments.
properties
attribute on your tests. Available on @SpringBootTest
and the test annotations for testing a particular slice of your application .
@TestPropertySource
annotations on your tests.
Devtools global settings properties in the $HOME/.config/spring-boot
directory when devtools is active.
1 2 3 4 5 6 7 8 9 10 11 12 import org.springframework.stereotype.*;import org.springframework.beans.factory.annotation.*;@Component public class MyBean { @Value("${name}") private String name; }
外部配置源
Java属性文件。
YAML文件。
环境变量。
命令行参数。
配置文件查找位置
classpath 根路径。
classpath 根路径下config目录。
jar包当前目录。
jar包当前目录的config目录。
/config子目录的直接子目录。
配置文件加载顺序:
当前jar包内部的application.properties
和application.yml
。
当前jar包内部的application-{profile}.properties
和 application-{profile}.yml
。
引用的外部jar包的application.properties
和application.yml
。
引用的外部jar包的application-{profile}.properties
和application-{profile}.yml
。
指定环境优先,外部优先,后面的可以覆盖前面的同名配置项。
自定义starter细节 starter启动原理
starter的pom.xml引入autoconfigure依赖
starterautoconfigurespring-boot-starter
autoconfigure包中配置使用META-INF/spring.factories
中EnableAutoConfiguration
的值,使得项目启动加载指定的自动配置类
编写自动配置类 xxxAutoConfiguration
-> xxxxProperties
@Configuration
@Conditional
@EnableConfigurationProperties
@Bean
…
引入starter — xxxAutoConfiguration
— 容器中放入组件 —- 绑定xxxProperties
—- 配置项
自定义starter
目标:创建HelloService
的自定义starter。
创建两个工程,分别命名为hello-spring-boot-starter
(普通Maven工程),hello-spring-boot-starter-autoconfigure
(需用用到Spring Initializr创建的Maven工程)。
hello-spring-boot-starter
无需编写什么代码,只需让该工程引入hello-spring-boot-starter-autoconfigure
依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?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 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > com.lun</groupId > <artifactId > hello-spring-boot-starter</artifactId > <version > 1.0.0-SNAPSHOT</version > <dependencies > <dependency > <groupId > com.lun</groupId > <artifactId > hello-spring-boot-starter-autoconfigure</artifactId > <version > 1.0.0-SNAPSHOT</version > </dependency > </dependencies > </project >
hello-spring-boot-starter-autoconfigure
的pom.xml如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?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 > 2.4.2</version > <relativePath /> </parent > <groupId > com.lun</groupId > <artifactId > hello-spring-boot-starter-autoconfigure</artifactId > <version > 1.0.0-SNAPSHOT</version > <name > hello-spring-boot-starter-autoconfigure</name > <description > Demo project for Spring Boot</description > <properties > <java.version > 1.8</java.version > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter</artifactId > </dependency > </dependencies > </project >
创建4个文件:
com/lun/hello/auto/HelloServiceAutoConfiguration
com/lun/hello/bean/HelloProperties
com/lun/hello/service/HelloService
src/main/resources/META-INF/spring.factories
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 import com.lun.hello.bean.HelloProperties;import com.lun.hello.service.HelloService;import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;import org.springframework.boot.context.properties.EnableConfigurationProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configuration @ConditionalOnMissingBean(HelloService.class) @EnableConfigurationProperties(HelloProperties.class) public class HelloServiceAutoConfiguration { @Bean public HelloService helloService () { return new HelloService (); } }import org.springframework.boot.context.properties.ConfigurationProperties;@ConfigurationProperties("hello") public class HelloProperties { private String prefix; private String suffix; public String getPrefix () { return prefix; } public void setPrefix (String prefix) { this .prefix = prefix; } public String getSuffix () { return suffix; } public void setSuffix (String suffix) { this .suffix = suffix; } }import com.lun.hello.bean.HelloProperties;import org.springframework.beans.factory.annotation.Autowired;public class HelloService { @Autowired private HelloProperties helloProperties; public String sayHello (String userName) { return helloProperties.getPrefix() + ": " + userName + " > " + helloProperties.getSuffix(); } } # Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.lun.hello.auto.HelloServiceAutoConfiguration123
用maven插件,将两工程install到本地。
接下来,测试使用自定义starter,用Spring Initializr创建名为hello-spring-boot-starter-test
工程,引入hello-spring-boot-starter
依赖,其pom.xml如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 <?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 > 2.4.2</version > <relativePath /> </parent > <groupId > com.lun</groupId > <artifactId > hello-spring-boot-starter-test</artifactId > <version > 1.0.0-SNAPSHOT</version > <name > hello-spring-boot-starter-test</name > <description > Demo project for Spring Boot</description > <properties > <java.version > 1.8</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-test</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > com.lun</groupId > <artifactId > hello-spring-boot-starter</artifactId > <version > 1.0.0-SNAPSHOT</version > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build > </project >
添加配置文件application.properties
:
1 2 hello.prefix =hello hello.suffix =666
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import com.lun.hello.service.HelloService;import org.junit.jupiter.api.Assertions;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest class HelloSpringBootStarterTestApplicationTests { @Autowired private HelloService helloService; @Test void contextLoads () { Assertions.assertEquals("hello: lun > 666" , helloService.sayHello("lun" )); } }
SpringApplication创建初始化流程 SpringBoot启动过程 Spring Boot应用的启动类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication public class HelloSpringBootStarterTestApplication { public static void main (String[] args) { SpringApplication.run(HelloSpringBootStarterTestApplication.class, args); } }public class SpringApplication { ... public static ConfigurableApplicationContext run (Class<?> primarySource, String... args) { return run(new Class <?>[] { primarySource }, args); } public static ConfigurableApplicationContext run (Class<?>[] primarySources, String[] args) { return new SpringApplication (primarySources).run(args); } public SpringApplication (Class<?>... primarySources) { this (null , primarySources); } public SpringApplication (ResourceLoader resourceLoader, Class<?>... primarySources) { this .resourceLoader = resourceLoader; Assert.notNull(primarySources, "PrimarySources must not be null" ); this .primarySources = new LinkedHashSet <>(Arrays.asList(primarySources)); this .webApplicationType = WebApplicationType.deduceFromClasspath(); this .bootstrappers = new ArrayList <>(getSpringFactoriesInstances(Bootstrapper.class)); setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); this .mainApplicationClass = deduceMainApplicationClass(); } private Class<?> deduceMainApplicationClass() { try { StackTraceElement[] stackTrace = new RuntimeException ().getStackTrace(); for (StackTraceElement stackTraceElement : stackTrace) { if ("main" .equals(stackTraceElement.getMethodName())) { return Class.forName(stackTraceElement.getClassName()); } } } catch (ClassNotFoundException ex) { } return null ; } ... }
spring.factories:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ... org.springframework.context.ApplicationContextInitializer =\ org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\ org.springframework.boot.context.ContextIdApplicationContextInitializer,\ org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\ org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,\ org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer org.springframework.context.ApplicationListener =\ org.springframework.boot.ClearCachesApplicationListener,\ org.springframework.boot.builder.ParentContextCloserApplicationListener,\ org.springframework.boot.context.FileEncodingApplicationListener,\ org.springframework.boot.context.config.AnsiOutputApplicationListener,\ org.springframework.boot.context.config.DelegatingApplicationListener,\ org.springframework.boot.context.logging.LoggingApplicationListener,\ org.springframework.boot.env.EnvironmentPostProcessorApplicationListener,\ org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener ...
SpringBoot完整启动过程 继续上一节,接着讨论return new SpringApplication(primarySources).run(args)
的run
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 public class SpringApplication { ... public ConfigurableApplicationContext run (String... args) { StopWatch stopWatch = new StopWatch (); stopWatch.start(); DefaultBootstrapContext bootstrapContext = createBootstrapContext(); ConfigurableApplicationContext context = null ; configureHeadlessProperty(); SpringApplicationRunListeners listeners = getRunListeners(args); listeners.starting(bootstrapContext, this .mainApplicationClass); try { ApplicationArguments applicationArguments = new DefaultApplicationArguments (args); ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments); configureIgnoreBeanInfo(environment); Banner printedBanner = printBanner(environment); context = createApplicationContext(); context.setApplicationStartup(this .applicationStartup); prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner); refreshContext(context); afterRefresh(context, applicationArguments); stopWatch.stop(); if (this .logStartupInfo) { new StartupInfoLogger (this .mainApplicationClass).logStarted(getApplicationLog(), stopWatch); } listeners.started(context); callRunners(context, applicationArguments); } catch (Throwable ex) { handleRunFailure(context, ex, listeners); throw new IllegalStateException (ex); } try { listeners.running(context); } catch (Throwable ex) { handleRunFailure(context, ex, null ); throw new IllegalStateException (ex); } return context; } private DefaultBootstrapContext createBootstrapContext () { DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext (); this .bootstrappers.forEach((initializer) -> initializer.intitialize(bootstrapContext)); return bootstrapContext; } private void configureHeadlessProperty () { System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, System.getProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this .headless))); } private static final String SYSTEM_PROPERTY_JAVA_AWT_HEADLESS = "java.awt.headless" ; private SpringApplicationRunListeners getRunListeners (String[] args) { Class<?>[] types = new Class <?>[] { SpringApplication.class, String[].class }; return new SpringApplicationRunListeners (logger, getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this , args), this .applicationStartup); } private ConfigurableEnvironment prepareEnvironment (SpringApplicationRunListeners listeners, DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) { ConfigurableEnvironment environment = getOrCreateEnvironment(); configureEnvironment(environment, applicationArguments.getSourceArgs()); ConfigurationPropertySources.attach(environment); listeners.environmentPrepared(bootstrapContext, environment); DefaultPropertiesPropertySource.moveToEnd(environment); configureAdditionalProfiles(environment); bindToSpringApplication(environment); if (!this .isCustomEnvironment) { environment = new EnvironmentConverter (getClassLoader()).convertEnvironmentIfNecessary(environment, deduceEnvironmentClass()); } ConfigurationPropertySources.attach(environment); return environment; } private void prepareContext (DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) { context.setEnvironment(environment); postProcessApplicationContext(context); applyInitializers(context); listeners.contextPrepared(context); bootstrapContext.close(context); if (this .logStartupInfo) { logStartupInfo(context.getParent() == null ); logStartupProfileInfo(context); } ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); beanFactory.registerSingleton("springApplicationArguments" , applicationArguments); if (printedBanner != null ) { beanFactory.registerSingleton("springBootBanner" , printedBanner); } if (beanFactory instanceof DefaultListableBeanFactory) { ((DefaultListableBeanFactory) beanFactory) .setAllowBeanDefinitionOverriding(this .allowBeanDefinitionOverriding); } if (this .lazyInitialization) { context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor ()); } Set<Object> sources = getAllSources(); Assert.notEmpty(sources, "Sources must not be empty" ); load(context, sources.toArray(new Object [0 ])); listeners.contextLoaded(context); } private void callRunners (ApplicationContext context, ApplicationArguments args) { List<Object> runners = new ArrayList <>(); runners.addAll(context.getBeansOfType(ApplicationRunner.class).values()); runners.addAll(context.getBeansOfType(CommandLineRunner.class).values()); AnnotationAwareOrderComparator.sort(runners); for (Object runner : new LinkedHashSet <>(runners)) { if (runner instanceof ApplicationRunner) { callRunner((ApplicationRunner) runner, args); } if (runner instanceof CommandLineRunner) { callRunner((CommandLineRunner) runner, args); } } } private void handleRunFailure (ConfigurableApplicationContext context, Throwable exception, SpringApplicationRunListeners listeners) { try { try { handleExitCode(context, exception); if (listeners != null ) { listeners.failed(context, exception); } } finally { reportFailure(getExceptionReporters(context), exception); if (context != null ) { context.close(); } } } catch (Exception ex) { logger.warn("Unable to close ApplicationContext" , ex); } ReflectionUtils.rethrowRuntimeException(exception); } ... }public interface ConfigurableApplicationContext extends ApplicationContext , Lifecycle, Closeable { String CONFIG_LOCATION_DELIMITERS = ",; \t\n" ; String CONVERSION_SERVICE_BEAN_NAME = "conversionService" ; String LOAD_TIME_WEAVER_BEAN_NAME = "loadTimeWeaver" ; String ENVIRONMENT_BEAN_NAME = "environment" ; String SYSTEM_PROPERTIES_BEAN_NAME = "systemProperties" ; String SYSTEM_ENVIRONMENT_BEAN_NAME = "systemEnvironment" ; String APPLICATION_STARTUP_BEAN_NAME = "applicationStartup" ; String SHUTDOWN_HOOK_THREAD_NAME = "SpringContextShutdownHook" ; void setId (String var1) ; void setParent (@Nullable ApplicationContext var1) ; void setEnvironment (ConfigurableEnvironment var1) ; ConfigurableEnvironment getEnvironment () ; void setApplicationStartup (ApplicationStartup var1) ; ApplicationStartup getApplicationStartup () ; void addBeanFactoryPostProcessor (BeanFactoryPostProcessor var1) ; void addApplicationListener (ApplicationListener<?> var1) ; void setClassLoader (ClassLoader var1) ; void addProtocolResolver (ProtocolResolver var1) ; void refresh () throws BeansException, IllegalStateException; void registerShutdownHook () ; void close () ; boolean isActive () ; ConfigurableListableBeanFactory getBeanFactory () throws IllegalStateException; } #4. #spring.factories # Run Listeners org.springframework.boot.SpringApplicationRunListener=\ org.springframework.boot.context.event.EventPublishingRunListener12345 class SpringApplicationRunListeners { private final Log log; private final List<SpringApplicationRunListener> listeners; private final ApplicationStartup applicationStartup; SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener > listeners, ApplicationStartup applicationStartup) { this .log = log; this .listeners = new ArrayList <>(listeners); this .applicationStartup = applicationStartup; } void starting (ConfigurableBootstrapContext bootstrapContext, Class<?> mainApplicationClass) { doWithListeners("spring.boot.application.starting" , (listener) -> listener.starting(bootstrapContext), (step) -> { if (mainApplicationClass != null ) { step.tag("mainApplicationClass" , mainApplicationClass.getName()); } }); } void environmentPrepared (ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) { doWithListeners("spring.boot.application.environment-prepared" , (listener) -> listener.environmentPrepared(bootstrapContext, environment)); } void contextPrepared (ConfigurableApplicationContext context) { doWithListeners("spring.boot.application.context-prepared" , (listener) -> listener.contextPrepared(context)); } void contextLoaded (ConfigurableApplicationContext context) { doWithListeners("spring.boot.application.context-loaded" , (listener) -> listener.contextLoaded(context)); } void started (ConfigurableApplicationContext context) { doWithListeners("spring.boot.application.started" , (listener) -> listener.started(context)); } void running (ConfigurableApplicationContext context) { doWithListeners("spring.boot.application.running" , (listener) -> listener.running(context)); } void failed (ConfigurableApplicationContext context, Throwable exception) { doWithListeners("spring.boot.application.failed" , (listener) -> callFailedListener(listener, context, exception), (step) -> { step.tag("exception" , exception.getClass().toString()); step.tag("message" , exception.getMessage()); }); } private void doWithListeners (String stepName, Consumer<SpringApplicationRunListener> listenerAction, Consumer<StartupStep> stepAction) { StartupStep step = this .applicationStartup.start(stepName); this .listeners.forEach(listenerAction); if (stepAction != null ) { stepAction.accept(step); } step.end(); } ... }
自定义事件监听组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 MyApplicationContextInitializer .java import org.springframework .context .ApplicationContextInitializer ;import org.springframework .context .ConfigurableApplicationContext ;public class MyApplicationContextInitializer implements ApplicationContextInitializer { @Override public void initialize (ConfigurableApplicationContext applicationContext ) { System .out .println ("MyApplicationContextInitializer ....initialize.... " ); } }MyApplicationListener .java import org.springframework .context .ApplicationEvent ;import org.springframework .context .ApplicationListener ;public class MyApplicationListener implements ApplicationListener { @Override public void onApplicationEvent (ApplicationEvent event ) { System .out .println ("MyApplicationListener.....onApplicationEvent..." ); } }MyApplicationRunner .java import org.springframework .boot .ApplicationArguments ;import org.springframework .boot .ApplicationRunner ;import org.springframework .core .annotation .Order ;import org.springframework .stereotype .Component ;@Order (1 )@Component public class MyApplicationRunner implements ApplicationRunner { @Override public void run (ApplicationArguments args) throws Exception { System .out .println ("MyApplicationRunner...run..." ); } }MyCommandLineRunner .java import org.springframework .boot .CommandLineRunner ;import org.springframework .core .annotation .Order ;import org.springframework .stereotype .Component ;@Order (2 )@Component public class MyCommandLineRunner implements CommandLineRunner { @Override public void run (String ... args) throws Exception { System .out .println ("MyCommandLineRunner....run...." ); } }MySpringApplicationRunListener .java import org.springframework .boot .ConfigurableBootstrapContext ;import org.springframework .boot .SpringApplication ;import org.springframework .boot .SpringApplicationRunListener ;import org.springframework .context .ConfigurableApplicationContext ;import org.springframework .core .env .ConfigurableEnvironment ;public class MySpringApplicationRunListener implements SpringApplicationRunListener { private SpringApplication application; public MySpringApplicationRunListener (SpringApplication application, String [] args){ this .application = application; } @Override public void starting (ConfigurableBootstrapContext bootstrapContext ) { System .out .println ("MySpringApplicationRunListener....starting...." ); } @Override public void environmentPrepared (ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment ) { System .out .println ("MySpringApplicationRunListener....environmentPrepared...." ); } @Override public void contextPrepared (ConfigurableApplicationContext context ) { System .out .println ("MySpringApplicationRunListener....contextPrepared...." ); } @Override public void contextLoaded (ConfigurableApplicationContext context ) { System .out .println ("MySpringApplicationRunListener....contextLoaded...." ); } @Override public void started (ConfigurableApplicationContext context ) { System .out .println ("MySpringApplicationRunListener....started...." ); } @Override public void running (ConfigurableApplicationContext context ) { System .out .println ("MySpringApplicationRunListener....running...." ); } @Override public void failed (ConfigurableApplicationContext context, Throwable exception ) { System .out .println ("MySpringApplicationRunListener....failed...." ); } }
注册MyApplicationContextInitializer
,MyApplicationListener
,MySpringApplicationRunListener
:
resources / META-INF / spring.factories
:
1 2 3 4 5 6 7 8 org.springframework.context.ApplicationContextInitializer =\ com.lun.boot.listener.MyApplicationContextInitializer org.springframework.context.ApplicationListener =\ com.lun.boot.listener.MyApplicationListener org.springframework.boot.SpringApplicationRunListener =\ com.lun.boot.listener.MySpringApplicationRunListener