文章目录

  • 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监控数据修改 springboot自定义监控指标_springboot监控数据修改


注意:

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>

springboot监控数据修改 springboot自定义监控指标_sprint_02

<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 标注测试方法,测试完成后自动回滚

常用注解

  1. 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 内置的断言可以分成如下几个类别:

检查业务逻辑返回的数据是否合理。

所有的测试运行结束以后,会有一个详细的测试报告:

springboot监控数据修改 springboot自定义监控指标_sprint_03


然后观看控制台便会出现一个单元测试报告

用来对单个值进行简单的验证。如:

方法

说明

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);
    }
}

前置条件

  1. 前置条件和断言差不多,只不过断言要是不满足条件的话,会使得这个测试方法执行是不败,不是不执行,
  2. 而前置条件就相当于这个方法执行的条件,一旦条件失效,这个方法就直接不执行了.
@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());
            }
        }
    }
}

外部测试的设置代码在内部测试执行之前运行,这一事实使您能够独立运行所有测试。你甚至可以单独运行内部测试而不运行外部测试,因为外部测试的设置代码总是被执行。(外部不能驱动内部,内部可以驱动外部)

参数化设置

  1. 参数化测试可以使用不同的参数多次运行测试。它们像常规@Test方法一样被声明,但使用 @ParameterizedTest注解代替。此外,您必须声明至少一个 将为每次调用提供参数的_源_,然后在测试方法中_使用这些参数。_
  2. 前面的常用注解举例:
//● @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

描述

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.namelogging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容。

prometheus

以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus

最常用的Endpoint

  • Health:监控状况
  • Metrics:运行时指标
  • Loggers:日志记录

如何使用

  1. 但注意的是,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默认暴露端点是不一样的.

  1. 对于JMX,并非是发送地址的方式进行监控的:

springboot监控数据修改 springboot自定义监控指标_sprint_04


注意: 如果使用该管理平台,记住要导入监控依赖后,启动项目才能够连接.

  1. 而要想HTTP端口的监控,则是浏览http://localhost:8080/actuator/端点/(具体某一项),但因为我们咋web端口监控的时候,端点很多都被禁用了,所以我们要进行开启:

springboot监控数据修改 springboot自定义监控指标_springboot监控数据修改_05

management:
  endpoints:
    enabled-by-default: true #开启所有端点信息,默认就是开启的,通过JMX全部暴露,不过对web禁用了大部分    web:
      exposure:
        include: '*'  #以web方式暴露

测试: http://localhost:8080/actuator/beans

Health Endpoint

  1. 测试http://localhost:8080/actuator/health 可以查看应用的健康状态,如果正常,返回UP,非正常显示宕机状态.
  2. 若有返回详细的健康信息,可以对health端点进行配置:
management:
  endpoint: # 对某个端点的具体配置
    health:
      show-details: always

Metrics Endpoint

提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到;

  • 通过Metrics对接多种监控系统
  • 简化核心Metrics开发
  • 添加自定义Metrics或者扩展已有Metrics’’

http://localhost:8080/actuator/metrics

开启与关闭

  1. 在开发中,最好不要全局开启全部端点,而是选择单个开启:
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);
        }

    }
}

springboot监控数据修改 springboot自定义监控指标_sprint_06

定制info(应用详细信息)

  1. 在yaml文件中直接定制
info:
  appName: Web1
  appVersion: 1.0
  mavenProjectVersion: '@project.version@' # 表示取project标签中的<version>标签

springboot监控数据修改 springboot自定义监控指标_测试方法_07

  1. 代码编写:
@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);
    }
}

springboot监控数据修改 springboot自定义监控指标_测试方法_08

  1. 注意:如果是两个方式组合在一起,信息时一起输出的,不能覆盖.

定制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);
    }
}

springboot监控数据修改 springboot自定义监控指标_spring boot_09


springboot监控数据修改 springboot自定义监控指标_测试方法_10

可视化界面

  1. 网络上有一个开源项目提供了监控项目的可视化界面.https://codecentric.github.io/spring-boot-admin/2.3.1/
  2. 第一步,新建一个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>
  1. 在启动类上加上@EnableAdminServer 注解.
  2. 启动该springboot项目,然后访问,注意,如果端口号冲突,记得修改端口号:

springboot监控数据修改 springboot自定义监控指标_springboot监控数据修改_11

  1. 在需要被监控的项目中
<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
  1. 启动后,会发现在监控页面显示:

springboot监控数据修改 springboot自定义监控指标_测试方法_12

  1. 注意:我这里是以2.3.1版本为例,如果是其他版本,配置可能有些不同,而且监控和客户端的依赖版本要大致一致.

GitHub地址: https://github.com/codecentric/spring-boot-admin

高级特性

环境切换

  1. springboot提供了更加简单的环境切换功能,方便开发者在不同的环境下进行项目的开发.
  2. 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这个对象是的属性是都有值的,虽然我们将不同属性配置在了不同的文件中,但是因为组配置的关系,所以都能加载进来.

  1. 在配置文件指定环境后,可以用@Profile(环境名) 标注在一个类或方法上,表示当环境是该环境的时候才生效.

外部化配置

  1. 在编写代码的时候,我们可以从外部文件中加载值,比如,使用@Value的时候我们必须结合@ConfigurationProperties注解,该注解便是引入了外部的properties配置文件.
  2. 其默认是从resource目录下加载的,但在springboot中,其默认加载的路径不止如此.

配置文件路径寻找的位置:
(1) classpath 根路径
(2) classpath 根路径下config目录
(3) jar包当前目录
(4) jar包当前目录的config目录
(5) /config子目录的直接子目录 (在Linux环境下)

  1. 配置文件加载顺序:

当前jar包内部的application.properties和application.yml
当前jar包内部的application-{profile}.properties 和 application-{profile}.yml
引用的外部jar包的application.properties和application.yml
引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml

  1. 无论是路径寻找还是文件加载,后者的规则总会覆盖前者,所以,如果我们对一个旧的程序进行拓展,可以在文件夹下进行文件的创建,利用其加载的顺序规则,将我们自己的配置文件覆盖原来的配置文件.比如:我们要修改项目中的某个类路径下的yaml文件,不用打开编辑器,直接在文件夹下的类路径下的config中新建一个,这个时候,该config中的文件便会覆盖原来的文件.或者:如果我们将项目打包了之后,我们不想要原来的yaml文件,但又不想重新打包一次,这个时候就可以在jar包当前目录下,新建一个文件,这个文件便会覆盖jar文件,就不用重新打包jar了.

自定义starter

  1. 新建Maven工程,名为hello-(spring-boot-)starter.
  2. 新建springboot工程,名为hello-autoconfigure.
  3. 然后在starter里导入configure的坐标.
  4. 在configure里编写逻辑代码.
  5. 新建一个业务类:
public class HelloService {

    @Autowired
    HelloProperties helloProperties;

    public String sayHello(String strings){
        return helloProperties.getPrefix()+strings+helloProperties.getSuffix();
    }
}
  1. 业务类的数据来自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;
    }
}
  1. 将该业务类和properties类注入到容器中:
@Configuration
@ConditionalOnMissingBean(HelloService.class)
@EnableConfigurationProperties(HelloProperties.class)
public class HelloConfiguration {

    @Bean
    public HelloService helloService(){
        return new HelloService();
    }
}
  1. 将HelloConfiguration配置类的全类名,导入spring.factories文件中,没有在类路径下新建一个:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hyb.helloautoconfigure.configuration.HelloConfiguration

因为该文件是项目启动文件.

  1. 将这两个项目打包成jat并导入本地maven仓库中:

springboot监控数据修改 springboot自定义监控指标_springboot监控数据修改_13


注意:先导入xxxconfigure,然后导入starter.

  1. 之后,新建一个demo,导入starter就可以了.
<dependency>
  <groupId>com.hyb</groupId>
  <artifactId>hello-starter</artifactId>
  <version>2.0-SNAPSHOT</version>
</dependency>
  1. 然后测试:
@Autowired
    HelloService helloService;

    @Test
    public void t1(){
        String s = helloService.sayHello("张三");
        System.out.println(s);
    }
  1. 如果命名已经导入了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>
  1. 如果还是不成功,查看mave仓库是否存在该jar,如果不存在,可能是maven仓库地址设置的不对.

Springboot运行原理(2.4.1)

注意:在这章节之前的springboot都是2.3.7,这里讲原理用2.4.1

  1. 在run方法打一个端点,可接连介入如下方法:

springboot监控数据修改 springboot自定义监控指标_spring boot_14


从这里我们可以看到,springboot先new了一个springboot应用,然后才调用run方法.

  1. 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文件里找.

  1. 返回后,进入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);
    }
}
  1. 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);
}
  1. refreshContext 刷新IOC容器
  2. started 监听器监听器项目启动完成.
  3. callRunners
  4. 如果IOC 启动出现异常,handleRunFailure->failed 处理异常,如果没有异常,监听器调用running(context),监听项目正在启动.running如果有问题,继续用同样的方法处理异常.
  5. 总结:
  • 创建 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

自定义事件监听组件

  1. 监听初始化器
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
  1. 自定义监听器:
public class MyLisener implements ApplicationListener {
    @Override
    public void onApplicationEvent(ApplicationEvent applicationEvent) {
        System.out.println("监听器....");
    }
}

监听器也得从同样的factories文件中查找,所以:

org.springframework.context.ApplicationListener=\
com.hyb.springboot2springweb.listener.MyLisener
  1. 自定义运行监听器,其也要从文件中查找:
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
  1. 自定义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...");
    }
}