文章目录
- SpringBoot2整篇
- 单元测试
- JUnit5 的变化
- 常用注解
- 断言
- 前置条件
- 嵌套测试
- 参数化设置
- 指标监控
- SpringBoot Actuator
- 2.x与1.x
- Actuator Endpoint
- 如何使用
- Health Endpoint
- Metrics Endpoint
- 开启与关闭
- 定制Health
- 定制info(应用详细信息)
- 定制Metric
- 自定义端点
- 可视化界面
- 高级特性
- 环境切换
- 外部化配置
- 自定义starter
- Springboot运行原理(2.4.1)
- 自定义事件监听组件
SpringBoot2整篇
- SpringBoot2(上篇) 爆干三万字,只为博你一赞
- SpringBoot2 (中篇) Web访问和数据访问,简单上手,通俗底层,深刻理解
- SpringBoot2 (终极篇里的高级特性) 你觉得你玩过单元测试?你知道什么指标监控?还是运行原理?怎么去自定义事件监听?
单元测试
JUnit5 的变化
Spring Boot 2.2.0 版本开始引入 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 Engine Removed from ****spring-boot-starter-test,如果需要继续兼容junit4需要自行引入vintage**
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
现在版本:
@SpringBootTest
class Boot05WebAdminApplicationTests {
@Test
void contextLoads() {
}
}
以前:
@SpringBootTest + @RunWith(SpringTest.class)
SpringBoot整合Junit以后。
- 编写测试方法:@Test标注(注意需要使用junit5版本的注解)
- Junit类具有Spring的功能,@Autowired、比如 @Transactional 标注测试方法,测试完成后自动回滚
常用注解
- JUnit5的注解与JUnit4的注解有所变化
https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations
- **@Test 😗*表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
- **@ParameterizedTest 😗*表示方法是参数化测试,下方会有详细介绍
- **@RepeatedTest 😗*表示方法可重复执行,下方会有详细介绍
- **@DisplayName 😗*为测试类或者测试方法设置展示名称
- **@BeforeEach 😗*表示在每个单元测试之前执行
- **@AfterEach 😗*表示在每个单元测试之后执行
- **@BeforeAll 😗*表示在所有单元测试之前执行
- **@AfterAll 😗*表示在所有单元测试之后执行
- **@Tag 😗*表示单元测试类别,类似于JUnit4中的@Categories
- **@Disabled 😗*表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
- **@Timeout 😗*表示测试方法运行如果超过了指定时间将会返回错误
- **@ExtendWith 😗*为测试类或测试方法提供扩展类引用
@DisplayName("Junit5Test")
public class Junit5Test {
//● @Test :表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
@Test
public void TestTest(){
System.out.println("这是测试@Test");
}
//● @ParameterizedTest :表示方法是参数化测试,下方会有详细介绍
@ParameterizedTest
@ValueSource(strings = {"a","b","c"})
public void ParameterTest(String s){
System.out.println(s);
}
//● @RepeatedTest :表示方法可重复执行,下方会有详细介绍
@RepeatedTest(value = 5)
public void RepeatedTest(){
System.out.println(1);
}
//● @DisplayName :为测试类或者测试方法设置展示名称
//● @BeforeEach :表示在每个单元测试之前执行
@BeforeEach
@Test
public void BeforeEach(){
System.out.println("BeforeEach...");
}
//● @AfterEach :表示在每个单元测试之后执行
//● @BeforeAll :表示在所有单元测试之前执行,自己本身不能执行
@BeforeAll
@Test
static void beforeAllTest(){
System.out.println("beforeAll");
}
//● @AfterAll :表示在所有单元测试之后执行
//● @Tag :表示单元测试类别,类似于JUnit4中的@Categories
//● @Disabled :表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
//● @Timeout :表示测试方法运行如果超过了指定时间将会返回错误
//● @ExtendWith :为测试类或测试方法提供扩展类引用 @SpringBootTest 便引用了该注解
}
断言
断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成如下几个类别:
检查业务逻辑返回的数据是否合理。
所有的测试运行结束以后,会有一个详细的测试报告:
然后观看控制台便会出现一个单元测试报告
用来对单个值进行简单的验证。如:
方法 | 说明 |
assertEquals | 判断两个对象或两个原始类型是否相等 |
assertNotEquals | 判断两个对象或两个原始类型是否不相等 |
assertSame | 判断两个对象引用是否指向同一个对象 |
assertNotSame | 判断两个对象引用是否指向不同的对象 |
assertTrue | 判断给定的布尔值是否为 true |
assertFalse | 判断给定的布尔值是否为 false |
assertNull | 判断给定的对象引用是否为 null |
assertNotNull | 判断给定的对象引用是否不为 null |
package com.hyb.springboot2springweb;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("AssertTest")
public class AssertTest {
/*
* 断言机制:前面断言失败,后面代码都不执行
* */
@Test
public void t1(){
// int count = 6;
// assertEquals(5,count,"断言失败,count不等于5");
// int [] a=new int[]{1, 2, 3};
// int [] b=new int[]{1,2,4};
// assertArrayEquals(a,b,"断言失败,两数组不相等");
// Object o = new Object();
// Object o1 = new Object();
// assertSame(o,o1,"断言失败,对象不相等");
}
/*
* 断言机制:只有全部断言成功,代码才能往下执行
* */
@Test
@DisplayName("组合断言")
public void t2(){
assertAll(
()->assertTrue(2==1,"断言失败,not true"),
()->assertEquals(1,1,"断言失败,值不相等")
);
}
/*
*
* 注意:是业务逻辑执行成功了,这个断言才会失败,业务逻辑出现了异常,该断言成功,因为断言的是业务逻辑一定会出现异常
* */
@DisplayName("异常断言")
@Test
public void t3(){
assertThrows(ArithmeticException.class,()->{int a=10/0;},"该方法没有异常");
}
/*
* 不超时断言,该业务逻辑若超出时间,断言失败
* 断言业务逻辑一定不超时
* */
@DisplayName("不超时断言")
@Test
public void t4(){
assertTimeout(Duration.ofMillis(1000),()->{Thread.sleep(1001);},"业务逻辑超时");
}
/*
* 快速失败,直接让测试失效
* */
@DisplayName("快速失败")
@Test
public void t5(){
if (1==1){
fail("快速失败");
}
System.out.println(222);
}
}
前置条件
- 前置条件和断言差不多,只不过断言要是不满足条件的话,会使得这个测试方法执行是不败,不是不执行,
- 而前置条件就相当于这个方法执行的条件,一旦条件失效,这个方法就直接不执行了.
@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(1, 1),
() -> System.out.println("In DEV")
);
}
}
assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;但当条件不满足时,测试执行并不会终止,只是Executable 对象不执行了,这是个特例。
嵌套测试
@Nested测试给了测试作者更多的能力来表达几组测试之间的关系。这种嵌套测试利用了 Java 的嵌套类,并促进了对测试结构的分层思考。这是一个详细的示例,既作为源代码,也作为 IDE 中的执行屏幕截图。
用于测试堆栈的嵌套测试套件
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.EmptyStackException;
import java.util.Stack;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@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());
}
}
}
}
外部测试的设置代码在内部测试执行之前运行,这一事实使您能够独立运行所有测试。你甚至可以单独运行内部测试而不运行外部测试,因为外部测试的设置代码总是被执行。(外部不能驱动内部,内部可以驱动外部)
参数化设置
- 参数化测试可以使用不同的参数多次运行测试。它们像常规@Test方法一样被声明,但使用 @ParameterizedTest注解代替。此外,您必须声明至少一个 将为每次调用提供参数的_源_,然后在测试方法中_使用这些参数。_
- 前面的常用注解举例:
//● @ParameterizedTest :表示方法是参数化测试,下方会有详细介绍
@ParameterizedTest
@ValueSource(strings = {"a","b","c"})
public void ParameterTest(String s){
System.out.println(s);
}
@ParameterizedTest 标注该方法为参数化测试方法,@ValueSource指定参数来源,注意,这里的参数来源不止这个注解,还有很多,详见:https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests
指标监控
SpringBoot Actuator
未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2.x与1.x
![]([object Object]&originHeight=430&originWidth=1068&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
Actuator Endpoint
ID | 描述 |
| 暴露当前应用程序的审核事件信息。需要一个 |
| 显示应用程序中所有Spring Bean的完整列表。 |
| 暴露可用的缓存。 |
| 显示自动配置的所有条件信息,包括匹配或不匹配的原因。 |
| 显示所有 |
| 暴露Spring的属性 |
| 显示已应用的所有Flyway数据库迁移。 |
需要一个或多个 | |
| 显示应用程序运行状况信息。 |
| 显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个 |
| 显示应用程序信息。 |
| 显示Spring |
| 显示和修改应用程序中日志的配置。 |
| 显示已应用的所有Liquibase数据库迁移。需要一个或多个 |
| 显示当前应用程序的“指标”信息。 |
| 显示所有 |
| 显示应用程序中的计划任务。 |
| 允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。 |
| 使应用程序正常关闭。默认禁用。 |
| 显示由 |
| 执行线程转储。 |
如果您的应用程序是Web应用程序(Spring MVC,Spring WebFlux或Jersey),则可以使用以下附加端点:
ID | 描述 |
| 返回 |
| 通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖 |
| 返回日志文件的内容(如果已设置 |
| 以Prometheus服务器可以抓取的格式公开指标。需要依赖 |
最常用的Endpoint
- Health:监控状况
- Metrics:运行时指标
- Loggers:日志记录
如何使用
- 但注意的是,Actuator的监控端口有两个,一个JMX,一个是HTTP,这两个端口对端点的默认支持是不一样的:
| 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 | No |
| 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 |
| quartz | Yes | No |
| scheduledtasks | Yes | No |
| sessions | Yes | No |
| shutdown | Yes | No |
| startup | Yes | No |
| threaddump | Yes | No |
注意:不同版本的springboot默认暴露端点是不一样的.
- 对于JMX,并非是发送地址的方式进行监控的:
注意: 如果使用该管理平台,记住要导入监控依赖后,启动项目才能够连接.
- 而要想HTTP端口的监控,则是浏览http://localhost:8080/actuator/端点/(具体某一项),但因为我们咋web端口监控的时候,端点很多都被禁用了,所以我们要进行开启:
management:
endpoints:
enabled-by-default: true #开启所有端点信息,默认就是开启的,通过JMX全部暴露,不过对web禁用了大部分 web:
exposure:
include: '*' #以web方式暴露
测试: http://localhost:8080/actuator/beans
Health Endpoint
- 测试http://localhost:8080/actuator/health 可以查看应用的健康状态,如果正常,返回UP,非正常显示宕机状态.
- 若有返回详细的健康信息,可以对health端点进行配置:
management:
endpoint: # 对某个端点的具体配置
health:
show-details: always
Metrics Endpoint
提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到;
- 通过Metrics对接多种监控系统
- 简化核心Metrics开发
- 添加自定义Metrics或者扩展已有Metrics’’
http://localhost:8080/actuator/metrics
开启与关闭
- 在开发中,最好不要全局开启全部端点,而是选择单个开启:
management:
endpoints:
enabled-by-default: false #不暴露所有端点信息
web:
exposure:
include: '*' #如果有暴露,以web方式暴露
endpoint: # 对某个端点的具体配置
health:
show-details: always
enabled: true # 暴露该端点
定制Health
@Component
public class ProjectHealthIndicator extends AbstractHealthIndicator {
/*
* 健康状态检查,可以利用一个Map,保存健康状态
* */
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
Map<String,Object> map=new HashMap<>();
if (1==1){
builder.status(Status.UP);
map.put("healthCode",200);
}else {
builder.status(Status.DOWN);
map.put("healthCode",100);
}
if (map.get("healthCode")==(Object) 100){
builder.withDetail("code",100).withDetails(map);
}else {
builder.withDetail("code",200).withDetails(map);
}
}
}
定制info(应用详细信息)
- 在yaml文件中直接定制
info:
appName: Web1
appVersion: 1.0
mavenProjectVersion: '@project.version@' # 表示取project标签中的<version>标签
- 代码编写:
@Component
public class InfoIndicator implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
Map<String,Object > map=new HashMap<>();
map.put("WebName","Web1");
map.put("version","1.0.0");
builder.withDetail("Web","info").withDetails(map);
}
}
- 注意:如果是两个方式组合在一起,信息时一起输出的,不能覆盖.
定制Metric
Counter counter;
// 构造器的时候加载MeterRegistry
public ThymeleafController(MeterRegistry meterRegistry){
counter=meterRegistry.counter("count");
}
@GetMapping("/map")
public String map(){
// int i=10/0;
counter.increment();
log.info("/map的访问次数是:{}",(int)counter.count());
return "helloThymeleaf";
}
//也可以使用下面的方式
@Bean
MeterBinder queueSize(Queue queue) {
return (registry) -> Gauge.builder("queueSize", queue::size).register(registry);
}
自定义端点
@Component
@Endpoint(id = "myWeb")
@Slf4j
public class MyWebEndPoint {
/*
* 读操作,浏览器会显示出来
* */
@ReadOperation
public Map<String,Object> getInfo(){
Map<String,Object> map=new HashMap<>();
map.put("WebName","Web1");
map.put("version","1.0.0");
return map;
}
/*
* 写操作,浏览器显示不出来,一般用于jconsole查看
* */
@WriteOperation
public void writerInfo(){
log.info("info:{}",200);
}
}
可视化界面
- 网络上有一个开源项目提供了监控项目的可视化界面.https://codecentric.github.io/spring-boot-admin/2.3.1/
- 第一步,新建一个springboot工程,然后导入以下依赖:
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- 在启动类上加上@EnableAdminServer 注解.
- 启动该springboot项目,然后访问,注意,如果端口号冲突,记得修改端口号:
- 在需要被监控的项目中
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.3.0</version>
</dependency>
spring:
boot:
admin:
client:
url: http://localhost:8081 #监控你的项目地址
instance:
prefer-ip: true #使用ip地址识别你的项目
application:
name: SpringBoot2-SpringWeb
- 启动后,会发现在监控页面显示:
- 注意:我这里是以2.3.1版本为例,如果是其他版本,配置可能有些不同,而且监控和客户端的依赖版本要大致一致.
GitHub地址: https://github.com/codecentric/spring-boot-admin
高级特性
环境切换
- springboot提供了更加简单的环境切换功能,方便开发者在不同的环境下进行项目的开发.
- yaml文件加载:
在resources目录下,application.yaml文件被默认加载,但除了这个文件外,我们还可以定义更多的根据不同环境匹配的yaml文件,比如我们另外创建两个yaml文件,一个是生产环境application-prod,一个是测试环境application-test:
application-xxx表示不同环境的配置文件,这些文件只要我们在默认的配置文件中指定环境,便可被springboot识别,比如,我们的默认配置文件是application.yaml,所以我们要在这个文件内指定当前启动加载哪个配置文件:
spring:
profiles:
active: prod #这个名字是application-xxx后的xxx
这里我们指定了启动类加载的配置文件是prod,所以是生产环境,这个时候,获取的yaml文件的值便是application-prod.
@Value("${person.name:默认值}") // 虽然有默认值和默认配置文件,但指定了生产环境,所以加载生产的配置yaml
private String name;
注意: 如果不同环境下的yaml文件和默认加载的yaml文件有相同的配置项,以更精确的为准,也就是不同环境下的配置文件为准.
拓展1):在Maven打包的过程中,在默认配置文件中可以不指定spring.profiles.active,或者已经指定了,但打包的时候可以修改打包的环境,和配置文件中的值,比如:
java -jar xxx.jar --**spring.profiles.active=prod --person.name=haha **表示我们打这个jar,然后指定prod环境,将该prod环境中的配置文件中的person.name配置项的值修改.
拓展2):在默认配置中,可以使用组的形式指定多个环境:
spring:
application:
name: Actuator
profiles:
active: prod
include:
- prod
- prod1
如果包含了include项,表示该配置项里的环境都被指定了,这个时候,这两个环境的配置文件都会被加载进来.注意:active中的prod虽然和include中的prod相同,但是active指定的是prod和prod1,是根据前缀进行匹配的,如果active指定单个环境,不写include就可以了.
测试:这里我们将在prod设置person.name,然后在prod1设置person.age,之后新建一个类:
@Component
@ConfigurationProperties(prefix = "person")
@Data
public class Person {
private String name;
private Integer age;
}
然后测试:
@Autowired
Person person;
@Test
void contextLoads() {
System.out.println(person);
}
这个时候,person这个对象是的属性是都有值的,虽然我们将不同属性配置在了不同的文件中,但是因为组配置的关系,所以都能加载进来.
- 在配置文件指定环境后,可以用@Profile(环境名) 标注在一个类或方法上,表示当环境是该环境的时候才生效.
外部化配置
- 在编写代码的时候,我们可以从外部文件中加载值,比如,使用@Value的时候我们必须结合@ConfigurationProperties注解,该注解便是引入了外部的properties配置文件.
- 其默认是从resource目录下加载的,但在springboot中,其默认加载的路径不止如此.
配置文件路径寻找的位置:
(1) classpath 根路径
(2) classpath 根路径下config目录
(3) jar包当前目录
(4) jar包当前目录的config目录
(5) /config子目录的直接子目录 (在Linux环境下)
- 配置文件加载顺序:
当前jar包内部的application.properties和application.yml
当前jar包内部的application-{profile}.properties 和 application-{profile}.yml
引用的外部jar包的application.properties和application.yml
引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml
- 无论是路径寻找还是文件加载,后者的规则总会覆盖前者,所以,如果我们对一个旧的程序进行拓展,可以在文件夹下进行文件的创建,利用其加载的顺序规则,将我们自己的配置文件覆盖原来的配置文件.比如:我们要修改项目中的某个类路径下的yaml文件,不用打开编辑器,直接在文件夹下的类路径下的config中新建一个,这个时候,该config中的文件便会覆盖原来的文件.或者:如果我们将项目打包了之后,我们不想要原来的yaml文件,但又不想重新打包一次,这个时候就可以在jar包当前目录下,新建一个文件,这个文件便会覆盖jar文件,就不用重新打包jar了.
自定义starter
- 新建Maven工程,名为hello-(spring-boot-)starter.
- 新建springboot工程,名为hello-autoconfigure.
- 然后在starter里导入configure的坐标.
- 在configure里编写逻辑代码.
- 新建一个业务类:
public class HelloService {
@Autowired
HelloProperties helloProperties;
public String sayHello(String strings){
return helloProperties.getPrefix()+strings+helloProperties.getSuffix();
}
}
- 业务类的数据来自xxxproperties文件中,此处映射成一个类
@ConfigurationProperties(prefix = "com.hyb")
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;
}
}
- 将该业务类和properties类注入到容器中:
@Configuration
@ConditionalOnMissingBean(HelloService.class)
@EnableConfigurationProperties(HelloProperties.class)
public class HelloConfiguration {
@Bean
public HelloService helloService(){
return new HelloService();
}
}
- 将HelloConfiguration配置类的全类名,导入spring.factories文件中,没有在类路径下新建一个:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hyb.helloautoconfigure.configuration.HelloConfiguration
因为该文件是项目启动文件.
- 将这两个项目打包成jat并导入本地maven仓库中:
注意:先导入xxxconfigure,然后导入starter.
- 之后,新建一个demo,导入starter就可以了.
<dependency>
<groupId>com.hyb</groupId>
<artifactId>hello-starter</artifactId>
<version>2.0-SNAPSHOT</version>
</dependency>
- 然后测试:
@Autowired
HelloService helloService;
@Test
public void t1(){
String s = helloService.sayHello("张三");
System.out.println(s);
}
- 如果命名已经导入了jar,但还是提示找不到该包或者类,可以在starter的pom中加入mave插件:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!--安装到仓库后,引用时不出现 BOOT-INF文件夹(会导致找不到相关类)-->
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
- 如果还是不成功,查看mave仓库是否存在该jar,如果不存在,可能是maven仓库地址设置的不对.
Springboot运行原理(2.4.1)
注意:在这章节之前的springboot都是2.3.7,这里讲原理用2.4.1
- 在run方法打一个端点,可接连介入如下方法:
从这里我们可以看到,springboot先new了一个springboot应用,然后才调用run方法.
- new 一个应用,其实就是一个有参构造器:
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
//加载资源
this.sources = new LinkedHashSet();
this.bannerMode = Mode.CONSOLE;
this.logStartupInfo = true;
this.addCommandLineProperties = true;
this.addConversionService = true;
this.headless = true;
this.registerShutdownHook = true;
this.additionalProfiles = Collections.emptySet();
this.isCustomEnvironment = false;
this.lazyInitialization = false;
this.applicationContextFactory = ApplicationContextFactory.DEFAULT;
this.applicationStartup = ApplicationStartup.DEFAULT;
//资源加载器
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
//判断web应用类型,一般是Servlet
this.webApplicationType = WebApplicationType.deduceFromClasspath();
//初始启动引导器,去spring.factories文件中找 org.springframework.boot.Bootstrapper
this.bootstrappers = new ArrayList(this.getSpringFactoriesInstances(Bootstrapper.class));
//去spring.factories找 ApplicationContextInitializer
this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
//去spring.factories找监听器
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
//找启动类
this.mainApplicationClass = this.deduceMainApplicationClass();
}
->setInitializers(初始化器):getSpringFactoriesInstances:
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = this.getClassLoader();
Set<String> names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = this.createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
SpringFactoriesLoader 会加载spring.factories文件,去该文件里找组件.
同样的下一行代码->setListeners 为了寻找监听器组件,也是从spring.factories文件里找.
- 返回后,进入run方法,这里的args参数传入,虽然平时我们代码中不用,但是前面说过,命令行也是可以传参的,这个时候args就起作用了:
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
// 执行该start方法,记录应用启动的时间
stopWatch.start();
// 创建引导上下文(Context环境)createBootstrapContext()
// 获取到所有之前的 bootstrappers 挨个执行 intitialize() 来完成对引导启动器上下文环境设置
DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();
ConfigurableApplicationContext context = null;
// 让当前应用进入headless模式。java.awt.headless
this.configureHeadlessProperty();
// 创建运行监听器 利用getSpringFactoriesInstances获取SpringApplicationRunListener
SpringApplicationRunListeners listeners = this.getRunListeners(args);
// 开启所有监听器,开启监听项目运行,其底层是遍历操作,所以该机制可以让开发者拓展自己的监听器,因为自己的
// 监听器也可以被遍历
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 保存args参数,相当于保存命令行参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
/*
* 创建环境,调用getOrCreateEnvironment初始化一些基础环境,保存基本的环境信息
* configureEnvironment 配置环境信息
* attach 绑定环境信息
* environmentPrepared 通知所有的监听器当前环境准备完成
*
*
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
this.configureIgnoreBeanInfo(environment);
Banner printedBanner = this.printBanner(environment);
// !! 创建IOC容器
context = this.createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
this.refreshContext(context);
this.afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
(new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
}
listeners.started(context);
this.callRunners(context, applicationArguments);
} catch (Throwable var10) {
this.handleRunFailure(context, var10, listeners);
throw new IllegalStateException(var10);
}
try {
listeners.running(context);
return context;
} catch (Throwable var9) {
this.handleRunFailure(context, var9, (SpringApplicationRunListeners)null);
throw new IllegalStateException(var9);
}
}
- createApplicationContext->create->(webApplicationType)创建IOC容器:
ApplicationContextFactory DEFAULT = (webApplicationType) -> {
try {
switch(webApplicationType) {
case SERVLET:
return new AnnotationConfigServletWebServerApplicationContext();
case REACTIVE:
return new AnnotationConfigReactiveWebServerApplicationContext();
default:
return new AnnotationConfigApplicationContext();
}
} catch (Exception var2) {
throw new IllegalStateException("Unable create a default ApplicationContext instance, you may need a custom ApplicationContextFactory", var2);
}
};
根据项目类型创建IOC容器,因为当前项目是Servlet项目,所以会创建AnnotationConfigServletWebServerApplicationContext.
**2) **prepareContext 准备IOC容器环境:
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
// 保存环境信息
context.setEnvironment(environment);
// 后置处理流程(注册组件,加载资源,类型转换)
this.postProcessApplicationContext(context);
// 应用初始化器: 遍历所有的ApplicationContextInitializer,每个都调用initialize进行IOC的初始化
this.applyInitializers(context);
// 所有监听器监听IOC容器准备完成
listeners.contextPrepared(context);
bootstrapContext.close(context);
if (this.logStartupInfo) {
this.logStartupInfo(context.getParent() == null);
this.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 = this.getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
this.load(context, sources.toArray(new Object[0]));
// 所有监听器监听IOC容器加载完成
listeners.contextLoaded(context);
}
- refreshContext 刷新IOC容器
- started 监听器监听器项目启动完成.
- callRunners
- 如果IOC 启动出现异常,handleRunFailure->failed 处理异常,如果没有异常,监听器调用running(context),监听项目正在启动.running如果有问题,继续用同样的方法处理异常.
- 总结:
- 创建 SpringApplication
- 保存一些信息。
- 判定当前应用的类型。ClassUtils。Servlet
- **bootstrappers:初始启动引导器(List):去spring.factories文件中找 **org.springframework.boot.Bootstrapper
- 找 ApplicationContextInitializer;去spring.factories找 ApplicationContextInitializer
- List<ApplicationContextInitializer<?>> initializers
- 找 ApplicationListener ;应用监听器。去spring.factories找 ApplicationListener
- List<ApplicationListener<?>> listeners
- 运行 SpringApplication
- StopWatch
- 记录应用的启动时间
- 创建引导上下文(Context环境)createBootstrapContext()
- 获取到所有之前的 **bootstrappers 挨个执行 **intitialize() 来完成对引导启动器上下文环境设置
- 让当前应用进入headless模式。java.awt.headless
- 获取所有 RunListener(运行监听器)【为了方便所有Listener进行事件感知】
- getSpringFactoriesInstances 去spring.factories找 SpringApplicationRunListener.
- 遍历 SpringApplicationRunListener 调用 starting 方法;
- **
- 保存命令行参数;ApplicationArguments
- 准备环境 prepareEnvironment();
- 返回或者创建基础环境信息对象。StandardServletEnvironment
- 配置环境信息对象。
- 读取所有的配置源的配置属性值。
- 绑定环境信息
- 监听器调用 listener.environmentPrepared();通知所有的监听器当前环境准备完成
- 创建IOC容器(createApplicationContext())
- 根据项目类型(Servlet)创建容器,
- 当前会创建 AnnotationConfigServletWebServerApplicationContext
- **准备ApplicationContext IOC容器的基本信息 ** prepareContext()
- 保存环境信息
- IOC容器的后置处理流程。
- 应用初始化器;applyInitializers;
- 遍历所有的 ApplicationContextInitializer 。调用 initialize.。来对ioc容器进行初始化扩展功能
- 遍历所有的 listener 调用 contextPrepared。EventPublishRunListenr;通知所有的监听器contextPrepared
- 所有的监听器 调用 contextLoaded。通知所有的监听器 contextLoaded;
- **刷新IOC容器。**refreshContext
- 创建容器中的所有组件(Spring注解)
- 容器刷新完成后工作?afterRefresh
- 所有监听 器 调用 listeners.started(context); 通知所有的监听器 started
- **调用所有runners;**callRunners()
- 获取容器中的 **ApplicationRunner **
- 获取容器中的 CommandLineRunner
- 合并所有runner并且按照@Order进行排序
- 遍历所有的runner。调用 run 方法
- 如果以上有异常,
- 调用Listener 的 failed
- **调用所有监听器的 running 方法 **listeners.running(context); **通知所有的监听器 running **
- running如果有问题。继续通知 failed 。调用所有 Listener 的 failed;通知所有的监听器 failed
自定义事件监听组件
- 监听初始化器
public class MyInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
System.out.println("Initializer....");
}
}
因为从spring.factories找 ApplicationContextInitializer,所以要在类路径下创建META-INF 包,然后创建spring.factories文件.文件的写法可以参照spring-boot-2.3.7.RELEASE.jar下同样路径的文件写法,这里我们找到初始化器的写法:
org.springframework.context.ApplicationContextInitializer=\
com.hyb.springboot2springweb.listener.MyInitializer
- 自定义监听器:
public class MyLisener implements ApplicationListener {
@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
System.out.println("监听器....");
}
}
监听器也得从同样的factories文件中查找,所以:
org.springframework.context.ApplicationListener=\
com.hyb.springboot2springweb.listener.MyLisener
- 自定义运行监听器,其也要从文件中查找:
public class MyRunLisenter implements SpringApplicationRunListener {
private SpringApplication springApplication;
public MyRunLisenter(SpringApplication springApplication,String [] args) {
this.springApplication=springApplication;
}
@Override
public void starting() {
System.out.println("starting...");
}
@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
System.out.println("environmentPrepared..");
}
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
System.out.println("contextPrepared...");
}
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
System.out.println("contextLoaded...");
}
@Override
public void started(ConfigurableApplicationContext context) {
System.out.println("started....");
}
@Override
public void running(ConfigurableApplicationContext context) {
System.out.println("running...");
}
@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
System.out.println("failed...");
}
}
org.springframework.boot.SpringApplicationRunListener=\
com.hyb.springboot2springweb.listener.MyRunLisenter
- 自定义runner,该runner都要从容器中拿,所以:
@Component
public class MyRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("ApplicationRunner..run...");
}
}
@Component
public class MyComandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("CommandLineRunner..run...");
}
}