# SpringBoot教程-黑马

https://www.bilibili.com/video/BV15b4y1a7yG

课程设计:

  1. 基础篇
    • 会用
  2. 应用篇
    • 补全知识树
    • 加强应用
    • 解决能力
  3. 原理篇
    • 提升理解层次
    • 能够自定义
  4. 番外篇
    • 丰富视野
    • 提升方案能力

课程学习目标:

  1. 基础篇
    • 能够创建 SpringBoot 工程
    • 基于 SpringBoot 实现 ssm 整合
  2. 实用篇
    • 运维实用篇
      • 掌握 SpringBoot 程序多环境开发
      • 基于 Linux 系统发布 SpringBoot 工程
      • 解决线上灵活配置 SpringBoot 工程的需求
    • 开发实用篇
      • 基于 SpringBoot 整合任意第三方技术
  3. 原理篇
    • 掌握 SpringBoot 内部工作流程
    • 理解 SpringBoot 整合第三方技术的原理
    • 实现自定义开发整合第三方技术的组件

# 1. 基础篇-入门案例

目录:

  • 快速上手 SpringBoot
  • SpringBoot 基础设置
  • 基于 SpringBoot 实现 SSM 整合

# 1.1. 创建项目

SpringBoot:

  • 设计目的: 简化 Spring 应用的 初始搭建 以及 开发过程

创建项目 - 基于 SpringBoot initializer:

  • 基于阿里云创建项目,地址: https://start.aliyun.com (可以使用 JDK8)
  • 基于官网创建项目,地址: https://start.spring.io (不可以使用 JDK8)

创建项目 - 手动:

<!-- POM.xml -->

<groupId>org.example</groupId>
<artifactId>springboot_01_01_quickstart</artifactId>
<version>0.0.1-SNAPSHOT</version>

<parent>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-parent</artifactId>
   <version>2.5.4</version>
</parent>
<dependencies>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
</dependencies>

# 1.2. 隐藏文件(夹)

IDEA 中隐藏文件(夹):

  • Settings -> Editor -> File Types: Ignored Files and Folder
  • 比如隐藏: .idea 目录

# 1.3. 入门案例解析

Spring 程序缺点:

  • 依赖设置繁琐
  • 配置繁琐

SpringBoot 程序优点:

  • 起步依赖(简化依赖配置)
  • 自动配置(简化常用工程相关配置)
  • 辅助功能(内置服务器, ......)

SpringBoot 通过如下四件事做到的:

  • parent
  • starter
  • 引导类
  • 内嵌 Tomcat

# 1.4. 入门案例解析 - parent

定义所有的依赖和版本,但未使用

通过官网初始化器创建的项目: (pom.xml)

继承 spring-boot-starter-parent

      继承 spring-boot-dependencies

spring-boot-dependencies: (pom.xml)

<!-- 自定义属性名 -->
<properties>
   定义 所有的可能用到的依赖 的版本信息
</properties>

<!-- 集中管理所有依赖 (仅定义未使用) -->
<dependencyManagement>
   定义 所有的可能用到的依赖
</dependencyManagement>

项目中引入依赖:

<!-- 如果在 dependencyManagement 中存在,则不需要写版本号 -->
<dependency>
   <groupId>com.google.code.gson</groupId>
   <artifactId>gson</artifactId>
</dependency>

阿里云初始化器创建的项目: (pom.xml)

<properties>
   <spring-boot.version>2.7.6</spring-boot.version>
</properties>

<!-- 
   通过 引入依赖的方式 使用 spring-boot-dependencies
   好处: 继承 只能有一次,但 依赖 可以有多个
 --> 
<dependencyManagement>
   <dependencies>
      <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
      </dependency>
   </dependencies>
</dependencyManagement>

小结:

  1. 开发 SpringBoot 程序要继承 spring-boot-starter-parent
  2. spring-boot-starter-parent 中定义了若干个依赖管理
  3. 继承 parent 模块可以避免多个依赖使用相同技术时出现依赖版本冲突
  4. 继承 parent 的形式也可以采用引入依赖的形式实现效果

parent:

  • 定义了很多依赖坐标版本(依赖管理,而非依赖),以达到减少依赖冲突的目的
  • spring-boot-starter-parent 不同版本之间,涉及的依赖的版本“大部分”不一样

# 1.5. 入门案例解析 - starter

包含若干个 依赖 的 pom 文件

starter:

  • SpringBoot 中常见项目名称,定义了 该项目 使用的所有依赖 的坐标,以达到减少依赖配置的目的
  • xyz-starter: 包含 xyz 技术涉及的所有依赖,由于依赖的传递性,我们的项目相当于也导入了这些必须的依赖

实际开发:

  • 使用坐标时,GAV 中,只写 GA
  • 刷新 Maven 后
    • 不报错,说明 SpringBoot 提供了该坐标的版本
    • 报错,则需要手动指定 V

小结:

  • 开发 SpringBoot 应用时,导坐标时,通常导入对应的 starter
  • starter 通常包含多个依赖坐标
  • 使用 starter 可以实现快速配置的效果,达到简化配置的目的

parent 和 starter 都是解决配置问题,但运行程序仅有配置是不行的

# 1.6. 入门案例解析 - 引导类

启动方式:

/**
 * 包含如下注解:
 *
 *  @Configuration 配置类
 *
 *  @ComponentScan 默认扫描 当前类 所在的包及其子包
 */
@SpringBootApplication
public class App {
    public static void main(String[] args) {
        // 初始化 IoC 容器
        ConfigurableApplicationContext ctx = SpringApplication.run(App.class, args);

        BookController bean = ctx.getBean(BookController.class);
        System.out.println("bean = " + bean);
        //=> org.example.controller.BookController@58065f0c
    }
}

SpringBoot 的引导类是 Boot 工程的执行入口,运行 main() 方法就可以启动项目。

SpringBoot 工程运行后初始 Spring 容器,扫描引导类所在的包加载 bean

小结:

  • SpringBoot 工程提供引导类用来启动程序
  • SpringBoot 工程启动后创建并初始化 Spring 容器

# 1.7. 入门案例解析 - 辅助功能 - 内嵌 tomcat

内嵌 Tomcat:

spring-boot-starter-web
   spring-boot-starter-tomcat
      tomcat-embed-core

切换为 Jetty:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
   <exclusions>
         <!-- 排除 Tomcat -->
         <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
         </exclusion>
   </exclusions>
</dependency>

<!-- 
   使用 Jetty
   Jetty 比 Tomcat 更轻量级,可扩展性更强(相较于Tomcat), 谷歌应用引擎(GAE)已经全面切换为Jetty 。
   如果是小型应用,建议使用 Jetty
-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

内置服务器:

  • tomcat (默认)
    • Apache 出品,粉丝多,应用面广,负载了若干较重的组件
  • jetty
    • 更轻量级,负载性能(并发性能)远不及 tomcat
  • undertow
    • 负载性能勉强跑赢 tomcat

小结:

  • 内嵌 tomcat 服务器是 SpringBoot 辅助功能之一
  • 内嵌 tomcat 工作原理是将 tomcat 服务器作为对象运行,并将该对象交给 Spring 容器管理
  • 变更内嵌服务器: 排除现有服务器的 starter,添加新的服务器 starter

# 2. 知识加油站 - REST

REST:

  • 新的访问资源的格式
  • 旧的: (传统风格资源描述形式)
    • 增: POST /user/saveUser
    • 删: POST /user/removeById
    • 改: POST /user/updateUser
    • 查: POST /user/getById (单个)
    • 查: POST /user/getList (多个)
  • 新的: (REST 风格描述形式)
    • 增: POST /users
    • 删: DELETE /users/{id}
    • 改: PUT /users
    • 查: GET /users/{id} (单个)
    • 查: GET /users (多个)

优点:

  • 隐藏资源访问行为,无法通过 URI 得知对资源是何种操作
  • 简化书写

风格:

  • 风格是约定、建议,可以不遵守
  • 虽然不是规范,但是大家都这么做,就变成了事实上的规范了

RESTful:

  • 使用 REST 风格访问资源(的行为)称为 RESTful

# 3. 基础篇 - 基础配置

目录:

  • 属性配置
  • 配置文件分类
  • yaml 文件
  • yaml 数据读取

# 3.1. 教你一招:复制模块

原则:

  • 保留工程基础结构
  • 抹掉原始工程痕迹

步骤 1. 拷贝原始工程的目录并重命名

拷贝 
   SpringBootDemo/springboot_01_01_quickstart

重命名
   SpringBootDemo/springboot_0x_abc

步骤 2. 删除无用的目录和文件,仅保留 src 、 pom.xml

步骤 3. 修改 pom.xml,改 artifactId,删 name、description

<!-- 修改 artifactId 为目录名称 -->
<artifactId>springboot_0x_abc</artifactId>

<!-- 
   删除 <name> <description> , 没有实际的作用
   Maven 面板中的项目名称会优先显示 name,如果没有 name 则显示 artifactId
--> 
<name>springboot_01_01_quickstart</name>
<description>desc</description>

步骤 4. IDEA 中导入模块后刷新 Maven

1. Project Structure

2. Import Module
      Import Module from external model
         Maven

小结:

  1. 在工作空间中复制对应工程,并修改工程名称
  2. 删除 IDEA 相关配置目录与文件,仅保留 src 目录和 pom.xml 文件
  3. 修改 pom.xml 文件中的 artifactId (与模块名一致)
  4. 删除 name 标签
  5. 保留备份工程供后期使用

# 3.2. 基础配置

配置文件位置:

  • resources/application.properties

配置的官方文档:

  • https://docs.spring.io/spring-boot/docs/2.5.4/reference/html/application-properties.html

配置文件中的配置由来:

  • 配置项是由依赖包提供的
  • 也就是说引入了对应技术的依赖,在配置文件里才有对应配置(及推荐)

# 3.3. 三种配置文件类型

application.properties (传统格式/默认格式)

server.port=80

application.yml (主流格式)

server:
   port: 81

application.yaml

server:
   port: 82

# 3.4. 配置文件加载优先级

当三个配置文件都存在时

加载的顺序: (优先级 从高到低)

  1. application.properties
  2. application.yml
  3. application.yaml

配置项:

  • 配置项都保留: 三个文件的配置项会合并
  • 冲突的配置项: 取 优先级高的文件 的配置项

# 3.5. 教你一招:属性提示消失解决方案

问题:

  • 在 application.yaml 书写配置时,没有提示(推荐)

原因:

  • IDEA 未能将 application.yaml 识别为 SpringBoot 的配置文件

解决: (IDEA 2025.3)

1. 打开 Project Structure
   
2. 切换到 Modules 页签

3. 选中(有 application.yaml 的)项目/工程

4. 点击 “+”,下拉菜单里选择 “Spring” 即可

# 3.6. yaml 数据格式

YAML:

  • 一种数据序列化格式

优点:

  • 容易阅读
  • 容易与脚本语言交互
  • 以数据为核心,重数据 轻格式

扩展名:

  • .yml (主流)
  • .yaml

语法规则:

  • 大小写敏感
  • 多层属性名 用多行描述,每行属性名结尾跟冒号
  • 使用(空格)缩进表示层级关系,同层级左侧对齐
  • 属性值前加空格(属性名 与 属性值 之间用空格和冒号隔开)
  • # 表示注释

字面值:

  • boolean
    • 真: true / True / TRUE
    • 假: false / False / FALSE
  • float
    • eg: 3.14, 1.2e+5
  • int
    • eg: 123
  • null
    • ~ 表示
  • string
    • 普通字符串: 直接书写
    • 特殊字符串: 用双引号包裹
    • eg: "Zhang San" (包含空格,使用双引号包裹)
  • date
    • 必须使用 yyy-MM-dd 格式
    • eg: 2025-02-13
  • datetime
    • 格式: 日期T时间+时区
    • eg: 2025-02-13T12:07:25+08:00

数组:

# 数组 - 基本类型元素
hobbies:
  - game
  - read
  - sleep

# 数组 - 基本类型元素 - 缩略
hobbies2: [game, read, sleep]

# 数组 - 对象类型元素 - 格式 1
persons:
  - name: wangwu
    age: 21
  - name: zhanliu
    age: 29

# 数组 - 对象类型元素 - 格式 2
persons2:
  -
    name: wangwu
    age: 21
  -
    name: zhanliu
    age: 29

# 数组 - 对象类型元素 - 缩略
persons3: [{name:wangwu, age:21}, {name:zhanliu, age:29}]

# 3.7. 读取 yaml 单一属性数据

使用 @Value 配合 SpEL 读取单个数据

如果数据存在多层级,依次书写层级名称即可

示例:

@Value("${flag}")
private Boolean flag;

@Value("${person.name}")
private String name;

@Value("${persons2[1].age}") // 数组索引从 0 开始
private Integer age;

# 3.8. yaml 文件中的引用其它属性

在配置文件中使用 ${属性名} 引用其它属性的值

如果属性中有特殊字符(比如转义字符 \n),需要使用双引号包裹

baseDir: C:\window

tempDir: ${baseDir}\tmp

# 双引号包裹的字符串,其中的转义字符会生效
tempDir2: "${baseDir}:\n 1.xxx \n 2.yyy"

# 3.9. 读取 yaml 全部属性数据

Environment 对象封装了全部配置信息,通过 @Autowire 注入并使用:

// 注入封装了全部属性的 Environment 对象
@Autowired
private Environment env;

// 读取属性
env.getProperty("person.name")

# 3.10. 读取 yaml 引用类型属性数据

使用 @ConfigurationProperties(prefix = "1级属性名") 注解绑定配置信息到封装类

封装类需要交给 Spring 管理

/*
person:
  name: lisi
  age: 20
*/

@Component
@ConfigurationProperties(prefix = "person")
@Data
public class Person {
    private String name;
    private Integer age;
}

# 4. 基础篇 - 整合第三方技术

  • 整合 JUnit
  • 整合 MyBatis
  • 整合 MyBatis-Plus
  • 整合 Druid

# 4.1. 整合 JUnit

步骤:

  1. 导入测试对应的 starter
  2. 测试类使用 @SpringBootTest 修饰
  3. 使用自动装配的形式添加要测试的对象

示例:

@Repository
public class BookDaoImpl implements BookDao {
    @Override
    public void save() {
        System.out.println("Book Dao is running...");
    }
}


@SpringBootTest
class Springboot03JunitApplicationTests {
    @Autowired
    private BookDao bookDao;

    @Test
    void contextLoads() {
        bookDao.save();
    }
}

# 4.2. 整合 JUnit - classes 属性

测试类如果不在 引导类所在的包或子包,则需要通过 @SpringBootTest(classes=引导类) 来显式指定

# 4.3. 整合 MyBatis

配置:

  • 核心配置: 数据库连接相关信息
  • 映射配置: SQL 映射(XML/注解)

starter 的 artifactId 名称惯例:

  • Spring 官方的 starter: spring-boot-starter-技术名
  • 第三方技术的 starter: 技术名-spring-boot-starter

步骤 1. 创建项目时勾选 “ SQL -> MyBatis Framework ” 、“ SQL -> MySQL Driver ”

<dependency>
   <groupId>org.mybatis.spring.boot</groupId>
   <artifactId>mybatis-spring-boot-starter</artifactId>
   <version>2.2.2</version>
</dependency>
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <scope>runtime</scope>
</dependency>

步骤 2. 数据连接相关信息 (application.yml)

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springboot_db
    username: root
    password: 123456

步骤 3. 在 dao 接口上添加 @Mapper 才能被容器识别

@Mapper
public interface BookDao {
    @Select("select * from tbl_book where id = #{id}")
    public Book getById(Integer id);
}

步骤 4. 测试

@SpringBootTest
class Springboot04MybatisApplicationTests {
    @Autowired
    private BookDao bookDao;

    @Test
    void contextLoads() {
        System.out.println(bookDao.getById(1));
    }
}

# 4.4. 整合 MyBatis - 常见问题处理

MySQL 8.x 要求设置时区:

  • 方式 1: 修改 URL, 添加 serverTimezone=UTC
  • 方式 2: 修改 MySQL 配置文件(mysql.ini)

驱动过时警告:

  • 使用 com.mysql.cj.jdbc.Driver 驱动类

# 4.5. 整合 MyBatisPlus

步骤 1. 添加 MP 的坐标

<dependency>
   <groupId>com.baomidou</groupId>
   <artifactId>mybatis-plus-boot-starter</artifactId>
   <!-- 
      PaginationInnerInterceptor 已分离出来。
      如需使用,则需单独引入 mybatis-plus-jsqlparser 依赖  
    -->
   <version>3.5.7</version>
</dependency>
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <scope>runtime</scope>
</dependency>

步骤 2. 数据连接相关信息

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springboot_db
    username: root
    password: 123456
mybatis-plus:
  global-config:
    db-config:
      table-prefix: tbl_

步骤 3. 给 dao 接口继承 BaseMapper

@Mapper
public interface BookDao extends BaseMapper<Book> {
}

步骤 4. 测试

@SpringBootTest
class Springboot05MybatisplusApplicationTests {
    @Autowired
    private BookDao bookDao;

    @Test
    void contextLoads() {
        System.out.println(bookDao.selectById(1));
    }
}

# 4.6. 整合 Druid

步骤 1. 导入 Druid 的 starter (中央仓库上搜 “druid”)

<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>druid-spring-boot-starter</artifactId>
   <version>1.2.27</version>
</dependency>

<dependency>
   <groupId>com.baomidou</groupId>
   <artifactId>mybatis-plus-boot-starter</artifactId>
   <version>3.5.7</version>
</dependency>
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <scope>runtime</scope>
</dependency>

步骤 2. 配置 Druid

# 方式一: 指定 druid 数据源即可
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springboot_db
    username: root
    password: 123456
    type: com.alibaba.druid.pool.DruidDataSource

# 方式二: 在 spring.datasource.druid 下配置 【推荐】
spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/springboot_db
      username: root
      password: 123456

步骤 3. 测试

整合第三方技术的通用方式:

  • 导入对应的 starter
  • 根据提供的配置格式,配置非默认值对应的配置项

# 5. 基础篇 - SSMP 整合案例

# 5.1. 案例实现方案分析

案例实现方案分析:

  • POJO: 使用 Lombok 快速制作实体类
  • Dao: MyBatisPlus,开发与测试
  • Service: MyBatisPlus,开发与测试
  • Controller: Restful,PostMan 测试接口
  • 页面: Vue + ElementUI
  • 项目异常处理
  • 按条件查询

# 5.2. 模块创建

  1. SpringBoot initializer: 勾选 Spring Web 和 MySQL Driver
  2. 修改配置文件的后缀名为 yml
  3. 设置服务器的端口为 80

# 5.3. 实体类快速开发 - lombok

Lombok,提供一组注解,简化 POJO 实体类的开发

<!-- lombok 的版本由 SpringBoot 提供 -->
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
</dependency>

示例:

// 为当前实体类在 编译期 设置如下方法: 
// getter / setter / toString / hashCode / equals
@Data
public class Book {
    private Integer id;
    private String name;
    private String type;
    private String description;
}

# 5.4. 数据层开发 - 基础 CRUD

技术实现方案:

  • Druid
  • MyBatisPlus

小结:

  1. 手动导入 starter
  2. 配置 数据源 和 MyBatisPlus
  3. 开发 Dao 接口(继承 BaseMapper)
  4. Dao 功能的测试

# 5.5. 开启 MP 运行日志

mybatis-plus:
  configuration:
    # 输出到控制台
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# 5.6. MP 分页

分页操作是 MP 的增强功能,内部是动态拼写 SQL 语句,使用 MP 的拦截器实现:

package org.example.config;

@Configuration
public class MpConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        // 分页插件: MP v3.5.8 之后的版本需要单独引入依赖
        // 使用 3.5.8 的版本,启动会报错,原因位置,使用 3.5.7 不报错
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return mybatisPlusInterceptor;
    }
}

分页操作需要设置 分页对象 IPage,其中封装了分页操作中的所有数据:

@Test
void testGetPage() {
    IPage<Book> page = Page.of(1, 5);
    bookDao.selectPage(page, null);
    // 页码
    long pageIndex = page.getCurrent();
    // 页大小
    long pageSize = page.getSize();
    // 总页数
    long pages = page.getPages();
    // 总记录数
    long total = page.getTotal();
    // 该页的记录
    List<Book> records = page.getRecords();
}

# 5.7. 数据层标准开发 - 条件查询

小结:

  1. 使用 QueryWrapper / LambdaQueryWrapper 封装查询条件
  2. 所有查询操作封装成方法调用
  3. 查询条件支持动态条件拼装

# 5.8. 业务层标准开发 - 基础 CRUD

service 层接口:

  • 业务相关,使用 业务名称 作为接口名称
  • 比如: login(String username, String password)

dao 层接口:

  • 数据操作,使用 操作数据的行为 作为接口名称
  • 比如: selectByUserNameAndPassword(String username, String password)

# 5.9. 业务层快速开发 - 基于 MP 构建

快速开发方案:

  • 使用 MP 提供的 业务层接口(IService<PO>) 与实现类(ServiceImpl<DAO, PO>)

# 5.10. 表现层标准开发

基于 Restful 制作表现层接口:

  • 新增: POST
  • 删除: DELETE
  • 修改: PUT
  • 查询: GET

接收参数:

  • 实体数据: @RequestBody
  • 路径变量: @PathVariable

# 5.11. 表现层数据一致性处理 - R 对象

表现层结果的模型类,统一前后端数据格式,也称为 前后端数据协议

@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class R {
    private Boolean flag;
    private Object data;
    private String msg;

    public static R ok() {
        return R.of(true, null, "ok");
    }

    public static R ok(Object data) {
        return R.of(true, data, "ok");
    }

    public static R fail(String msg) {
        return R.of(false, null, msg);
    }
}

# 5.12. 前后端调用

单体项目中页面放置在 resources/static 目录下,默认可以直接访问

# 5.13. 异常消息处理

package org.example.controller.utils;

// @RestControllerAdvice = @ControllerAdvice + @ResponseBody
@RestControllerAdvice
public class ProjectExceptionAdvice {
    // 拦截指定类型的异常
    // @ExceptionHandler(RuntimeException.class)
    
    // 默认拦截处理所有异常
    @ExceptionHandler
    public R doException(Exception e) {
        e.printStackTrace();
        return R.fail("服务器故障,请稍后重试");
    }
}

# 5.14. 分页功能维护 - 删除 BUG

问题:

  • 如果最后一页,只有一条数据,此时删除该条数据,会导致 分页查询当前页 时数据为空

解决:

  • 后台处理: 分页查询,如果当前页比总页数还大,则将总页数作为当前页再次查询并返回
  • 前台处理: 删除之后,返回第一页

# 6. 运维实用篇 - 打包与运行

程序打包与运行(Windows版)

程序运行(Linux版)

# 6.1. 打包与运行 - Windows

打包:

# 会执行测试代码
mvn package

# ‌跳过测试但不跳过单元测试编译
mvn package -DskipTests

# 完全跳过测试阶段‌
mvn package -DskipTests -Dmaven.test.skip=true

运行:

java -jar springboot.jar

maven 插件:

<!-- 
   jar 包 如果想在命令行运行,需要配置如下 maven 插件
-->
<build>
   <plugins>
      <plugin>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
   </plugins>
</build>

# 6.2. 打包插件

使用 压缩软件 打开 springboot.jar:

springboot.jar/
   BOOT-INF/      # 项目资源
      classes/       # 类
      lib/           # 依赖
   META-INF/      # 清单
      MANIFEST.MF
   org/           # SpringBoot 启动器

MANIFEST.MF: (基于 spring-boot-maven-plugin 打包的工程)

# 启动类(引导类)
Start-Class: org.example.Springboot07SsmpApplication
# Jar 启动器
Main-Class: org.springframework.boot.loader.JarLauncher

windows 上端口被占用的处理,相关命令:

# 查询指定端口的进程 PID
netstat -ano | findstr ":80"
# 协议   本地地址       外部地址     状态        PID
# TCP    0.0.0.0:80    0.0.0.0:0   LISTENING   9144


# 根据进程 PID 查询进程名称
tasklist | findstr "9144"
# 映像名称   PID      会话名   会话#   内存使用
# java.exe  9144     Console  1      17,256 K


# 根据 PID 杀死进程
taskkill /f /pid "9144"
# 成功: 已终止 PID 为 9144 的进程。


# 根据进程名称杀死任务
taskkill -f -t -im "进程名称"

# 6.3. 运行 - Linux

安装包(jar)存放目录:

  • /usr/local/自定义目录
  • $HOME/自定义目录

后台运行:

nohup java -jar springboot.jar > server.log 2>&1 &

终止服务:

# 查 PID
ps -ef | grep "java -jar"

# 杀 进程
kill -9 进程ID

# 7. 运维实用篇 - 配置高级

临时属性设置

配置文件分类

自定义配置文件

# 7.1. 临时属性 - 命令行参数

启动时,通过临时属性替换配置文件的属性:

# 格式: 
# java -jar xyz.jar --属性1=值1 --属性2=值2

# 示例:
java -jar springboot.jar --server.port=8080

# 7.2. 临时属性 - 开发环境

在 IDEA 中设置临时属性:

  • Run/Debug Configuration -> Build And Run -> Modify Options
  • 勾选 Java -> Program Arguments,输入 --server.port=8081 即可

运行 IDEA 中的该运行配置,相当于如下命令:

java -jar springboot.jar --server.port=8081

原理: 通过 main() 方法接收的命令行参数会传入 SpringBoot 启动器

@SpringBootApplication
public class Springboot07SsmpApplication {

    // main() 方法的 args 参数接收命令行参数
    public static void main(String[] args) {
        System.out.println(Arrays.toString(args));
        //=> [--server.port=8081]

        // 命令行参数 传入 SpringBoot 后生效
        SpringApplication.run(Springboot07SsmpApplication.class, args);

        // 启动时,不接收命令参数
        // SpringApplication.run(Springboot07SsmpApplication.class);
    }
}

# 7.3. 配置文件 4 级分类

SpringBoot 中 4 级配置文件

  • 1 级: jar包所在目录/config/application.yml 【最高】
  • 2 级: jar包所在目录/application.yml
  • 3 级: resources/config/application.yml
  • 4 级: resources/application.yml 【最低】

作用:

  • 1级、2级,运维阶段,运维人员设置属性
  • 3级、4级,开发阶段,开发人员设置属性
  • 3级 常用于项目经理进行整体项目属性调控
  • 1级 常用于运维经理进行线上整体项目部署方案调控

存在多个级别的配置文件时:

  • 合并: 各个配置文件的属性会合并(叠加)
  • 覆盖: 冲突的配置,高级别的配置文件的属性优先

注意:

  • SpringBoot 启动时会一直往上找 application.yml 或 config,找到根路径为止
  • 也就是说,1级、2级 的配置文件也可以放在 jar 包所在目录的祖先目录

# 7.4. 自定义配置文件

通过启动参数指定 配置文件的名称:

# 类路径下的 myApp.properties / myApp.yml / myApp.yaml 会生效
--spring.config.name=myApp

通过启动参数指定 配置文件的路径:

# 类路径
--spring.config.name=classpath:myApp.yml

# 文件路径
--spring.config.name=D:\dev\myApp.yml

# 多个文件, 后面的生效
--spring.config.name=classpath:myApp.yml,classpath:myApp2.yml

# 8. 运维实用篇 - 多环境开发

多环境开发(YAML 版)

多环境开发(Properties 版)

多环境开发控制

# 8.1. 多环境开发 - 一个配置文件

在一个配置文件里配置多个环境

# 应用环境
spring:
  profiles:
    active: dev

# 公共配置

---
# 开发
spring:
  profiles: dev
server:
  port: 8080
---
# 测试
spring:
  profiles: test
server:
  port: 80
---
# 生产
spring:
  profiles: prod
server:
  port: 443

# 8.2. 多环境开发 - 多个配置文件

说明:

  • 主配置文件中设置公共配置(全局)
  • 环境分类配置文件中常用于设置冲突属性(局部)
  • properties 格式的配置文件也类似

主启动配置文件 application.yml

# 应用环境
spring:
  profiles:
    active: prod

# 公共配置

环境分类配置文件 application-dev.yml

server:
  port: 8080

环境分类配置文件 application-test.yml

server:
  port: 80

环境分类配置文件 application-prod.yml

server:
  port: 443

# 8.3. 多环境分组管理

拆分配置文件中信息:

application.yml

application-dev.yml
application-devDB.yml
application-devMVC.yml

application-test.yml
application-testDB.yml
application-testMVC.yml

激活指定环境的配置:

spring:
  profiles:
    active: dev
    # SpringBoot 2.4 之后引入 group 属性替代 include 属性
    # group 属性定义多种主环境与子环境的包含关系
    group:
      "dev": devDB, devMVC
      "test": testDB, testMVC

控制台打印:

The following profiles are active: dev,devDB,devMVC

说明:

  • 属性冲突时,后加载的配置文件的属性优先级高

# 8.4. 多环境开发控制 - maven

maven 配置多环境: (pom.xml)

<profiles>
    <profile>
        <id>dev_env</id>
        <properties>
            <profile.active>dev</profile.active>
        </properties>
    </profile>
    <profile>
        <id>test_env</id>
        <properties>
            <profile.active>test</profile.active>
        </properties>
        <!-- 通过 activation 激活配置 -->
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
    </profile>
</profiles>

application.yml 中使用 maven 的自定义属性:

spring:
  profiles:
    active: @profile.active@

说明:

  • pom.xml 中修改完 profile 后,需要执行 compile 以使其生效
  • 可以通过 mvn package -P dev_env 来激活配置

# 9. 运维实用篇 - 日志

日志基础

日志输出格式控制

日志文件

# 9.1. 日志基础操作

日志作用:

  • 编程期 调试代码
  • 运营期 记录信息

运营期记录信息:

  • 日常运营重要信息,比如 峰值流量、平均响应时长
  • 应用报错信息,比如 错误堆栈
  • 运维过程数据,比如 扩容、宕机、报警

写日志:

@RestController
public class BookController {
    private static final Logger log = LoggerFactory.getLogger(BookController.class);

    @GetMapping
    public String getById() {
        log.debug("debug ...");
        log.info("info ...");
        log.warn("warn ...");
        log.error("error ...");
        // ...
    }
}

设置日志级别:

logging:
  # 将 多个包 组织到 一个组
  group:
    app: org.example.controller, org.example.service
    aliyun: com.aliyum

  level:
    # root 表示根节点,即整体应用的日志级别(所有包的日志级别),默认为 info
    root: info

    # 设置 包 的日志级别
    org.example.controller: debug

    # 设置 组 的日志级别
    app: war

# 9.2. 教你一招 - 快速创建日志对象

使用 lombok 提供 @Slf4j 注解简化日志对象的创建:

// 自动给该类加上如下语句:
// private static final org.slf4j.Logger log
//     = org.slf4j.LoggerFactory.getLogger(BookController.class)
@Slf4j
@RestController
public class BookController {

    @GetMapping
    public String getById() {
        log.debug("debug ...");
        log.info("info ...");
        log.warn("warn ...");
        log.error("error ...");
        // ...
    }
}

# 9.3. 日志输出格式控制

自定义控制台日志格式:

logging:
  pattern:
    # 控制台日志格式

    # %d : 时间
    # %clr() : 彩色
    # %clr(){cyan} : 彩色,并设置指定颜色
    # %p : 级别
    # %5p : 占 5 个字符的宽度
    # %t : 线程名称
    # %c : 类名
    # %-40c: 40位的宽,左对齐
    # %-40.40c: 40位的宽,左对齐,超过40则截断
    # %m : 消息
    # %n : 换行
    console: "%d %clr(%5p) --- [%16t] %clr(%-40.40c){cyan} : %m %n"

# 9.4. 文件记录日志

LOG_FILE_NAME: server-log/application

logging:
  file:
    # jar 包所在目录下 / 项目所在目录
    name: ${LOG_FILE_NAME}.log
  logback:
    # 滚动策略
    rollingpolicy:
      # 单个文件最大尺寸,默认 10MB
      max-file-size: 3KB
      # 拆分的日志名称, %i 从0开始, 如果以 .gz 结尾则会压缩
      file-name-pattern: ${LOG_FILE_NAME}.%d{yyyy-MM-dd}.%i.log

# 10. 开发实用篇

热部署

配置高级

测试

数据层解决方案

整合第三方技术

监控

# 10.1. 手工启动热部署

重启(Restart):

  • 使用 restart 类加载器
  • 涉及 自定义开发代码,包含 类、页面、配置文件 等
  • 场景: 热部署

重载(Reload):

  • 使用 base 类加载器
  • 涉及 项目的所有的 jar 包,包含 第三方 jar
  • 场景: 启动项目

热部署: (热启动)

  • 仅涉及当前开发者 自定义开发的资源,不加载 jar
  • 对文件进行监控,一旦变化则重启

步骤 1. 引入 开发工具 包

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-devtools</artifactId>
   <optional>true</optional>
</dependency>

步骤 2. 修改完代码后 手动构建项目

  • Build -> Build Project (Ctrl + F9)

# 10.2. 自动启动热部署(自动构建)

开启自动构建 即可达到自动热部署的目的

步骤 1. 设置

  • Settings -> Build, Execution, Deployment -> Compiler
  • 勾选: Build Project Automatically

步骤 2. 打开 Maintenance 面板

  • 在 设置的 Keymap 里搜索 "Maintenance",添加快捷键 Alt + Shift + ~

步骤 3. 选择 Registry ...

  • 搜索 " compiler.automake " 相关选项,并勾选

# 10.3. 热部署范围配置 (自定义重启排除项)

默认不触发重启的目录列表: (类路径下)

  • /META-INFO/maven
  • /META-INFO/resources
  • /resources
  • /static
  • /public
  • /templates

可以手动设置:

spring:
  devtools:
    restart:
      exclude: public/**, config/application.yml

# 10.4. 关闭热部署功能

通过 配置文件 关闭:

spring:
  devtools:
    restart:
      enabled: false

通过 系统变量 配置:

System.setProperty("spring.devtools.restart.enabled", "false");

SpringApplication.run(Application.class, args);

# 11. 配置高级

@ConfigurationProperties

宽松绑定 / 松散绑定

常用计量单位绑定

数据校验

# 11.1. @ConfigurationProperties

# 11.1.1. 配置属性绑定到 自定义 bean

配置属性: (application.yml)

servers:
  ipAddress: 192.168.1.123
  port: 1234
  timeout: -1

绑定配置属性:

@Data
@Component
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {
    private String ipAddress;
    private int port;
    private long timeout;
}

测试:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        ConfigurableApplicationContext ctx = SpringApplication.run(Application.class, args);

        System.out.println("ServerConfig = " + ctx.getBean(ServerConfig.class));
        //=> ServerConfig(ipAddress=192.168.1.123, port=1234, timeout=-1)
    }
}

# 11.1.2. 集中管理 绑定配置属性 的 bean

@Data
// 此时不能再设置 @Component 及衍生注解
// @Component
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {
    private String ipAddress;
    private int port;
    private long timeout;
}
@SpringBootApplication
@EnableConfigurationProperties({ ServerConfig.class })
public class Application {

    public static void main(String[] args) {
        ConfigurableApplicationContext ctx = SpringApplication.run(Application.class, args);

        System.out.println("ServerConfig = " + ctx.getBean(ServerConfig.class));
        //=> ServerConfig(ipAddress=192.168.1.123, port=1234, timeout=-1)
    }
}

说明:

  • @EnableConfigurationProperties 指定 绑定配置属性的 POJO,会自动将其纳入 IoC 容器

# 11.1.3. 配置属性绑定到 第三方 bean

配置属性: (application.yml)

datasource:
  driverClassName: com.mysql.jdbc.Driver123

引导类:

@SpringBootApplication
public class Application {

    @Bean
    @ConfigurationProperties(prefix = "datasource")
    public DruidDataSource datasource() {
        return new DruidDataSource();
    }

    public static void main(String[] args) {
        ConfigurableApplicationContext ctx = SpringApplication.run(Application.class, args);

        System.out.println("druidDataSource.driverClassName = " + ctx.getBean(DruidDataSource.class).getDriverClassName());
        //=> com.mysql.jdbc.Driver123
    }
}

# 11.2. 松散绑定 - @ConfigurationProperties

@ConfigurationProperties 绑定属性 支持 属性名宽松绑定:

servers:
  # 驼峰
  ipAddress: 192.168.1.111

  # 下划线
  ip_address: 192.168.1.222

  # 中划线 (烤肉串模式)【推荐】【主流】
  ip-address: 192.168.1.333

  # 常量
  IP_ADDRESS: 192.168.1.444

  # 小写
  ipaddress: 192.168.1.555

  # 大写
  IPADDRESS: 192.168.1.666
@Data
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {
    private String ipAddress;
}

也就是说,属性绑定时,忽略属性名的大小写与分隔符

配置文件中 前缀、属性名 可以随便写,但 @ConfigurationProperties(prefix = "") 中的 prefix 必须使用 中划线模式(小写字母、数字、中划线 组成,以字母打头)

Canonical names should be kebab-case ('-' separated), lowercase alpha-numeric characters and must start with a letter

注意: @Value 不支持松散绑定,要与配置文件完全一致

# 11.3. 常用计量单位应用

SpringBoot 支持 JDK8 提供的 时间和空间 计量单位

# 11.3.1. 时间 Duration

@ConfigurationProperties(prefix = "servers")
public class ServerConfig {
    @DurationUnit(ChronoUnit.SECONDS)
    private Duration time;
}
servers:
  # 不带单位,则使用 @DurationUnit 设置的单位
  time: 1

  time: 2H

# 11.3.2. 空间 DataSize

@ConfigurationProperties(prefix = "servers")
public class ServerConfig {
    @DataSizeUnit(DataUnit.MEGABYTES)
    private DataSize size;
}
servers:
  # 不带单位,则使用 @DataSizeUnit 设置的单位
  size: 1
  
  size: 2MB

# 11.4. bean 属性校验 - Bean 校验框架

说明:

  • 开启数据校验有助于系统安全性
  • J2EE 中的 JSR303 规范 定义了一组有关数据校验的 API

步骤 1. 导入 JSR303 规范和实现

<!-- JSR303 规范: Bean Validation API -->
<dependency>
   <groupId>javax.validation</groupId>
   <artifactId>validation-api</artifactId>
</dependency>

<!-- JSR303 规范的实现: Hibernate Validator Engine -->
<dependency>
   <groupId>org.hibernate.validator</groupId>
   <artifactId>hibernate-validator</artifactId>
</dependency>

步骤 2. 给 Bean 开启校验功能

@Validated
public class ServerConfig {
}

步骤 3. 给 Bean 的属性使用校验规则

@Validated
public class ServerConfig {
    @Max(value = 10000, message = "最大 10000")
    @Min(value = 100, message = "最小 100")
    private int port;
}

查看有哪些校验规则:

import javax.validation.constraints.Max;
import org.hibernate.validator.constraints.Length;

// 点击 import 语句中的 “constraints” 即可跳转到对应包下
// 可以看到 规范中定义的规则 以及 hibernate 额外实现的规则

# 11.5. 进制数据转换规则

# “0” 打头的数字被识别为 八进制,会自动转 十进制
# 在 bean 里用字符串属性接收时,拿到的会是 "83"
# 使用 "0123" 可以避免这个问题
password: 0123
@SpringBootTest
class ApplicationTests {
    @Value("${password}")
    private String password;
    @Test
    void contextLoads() {
        System.out.println("password = " + password);
        //=> 83
    }
}

# 12. 测试

加载测试专用属性

加载测试专用配置

Web 环境模拟测试

数据层测试回滚

测试用例数据设定

# 12.1. 加载测试专用属性 - 设置临时配置属性

application.yml:

test:
  prop: value1
@SpringBootTest // 默认读取 application.yml 中的属性
class PropertiesAndArgsTest {
    @Value("${test.prop}") //=> value1
    private String msg;
}

启动测试类时,通过 @SpringBootTest(properties = {}) 设置该测试用例专有属性

@SpringBootTest(properties = { "test.prop=value2" }) // 会覆盖 application.yml 中的属性
class PropertiesAndArgsTest {
    @Value("${test.prop}") //=> value2
    private String msg;
}

启动测试类时,通过 @SpringBootTest(args = {}) 设置该测试用例专有命令行参数

@SpringBootTest(args = { "--test.prop=value3" }) // 会覆盖 application.yml 中的属性
class PropertiesAndArgsTest {
    @Value("${test.prop}") //=> value3
    private String msg;
}

注意:

  • args 的优先级比 properties

# 12.2. 加载测试专用配置 - 临时加载配置类

测试环境专用配置类:

// src/test/java
package org.example.config;

@Configuration
public class JdbcConfig {
    @Bean
    public DataSource dataSource() {
        return MyDataSource.of("root", "123");
    }
}

自定义的数据源:

// src/test/java
package org.example.config;

@Data
@AllArgsConstructor(staticName = "of")
public class MyDataSource implements DataSource {
    private String username;
    private String password;
}

测试:

@SpringBootTest
@Import({JdbcConfig.class}) // 为 当前测试用例 加载专用配置类
public class ConfigTest {
    @Autowired
    private DataSource dataSource;

    @Test
    void test() {
        System.out.println(dataSource);
        //=> MyDataSource(username=root, password=123)
    }
}

# 12.3. 测试类中启动 web 环境

// 启动 Web 服务器,使用 application.yml 中定义好的端口
// @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)

// 启动 Web 服务器,使用 随机 端口
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class WebTest {
    @Test
    void test() {
    }
}

# 12.4. 发送虚拟请求

业务:

@RestController
@RequestMapping("/books")
public class BookController {
    @GetMapping("{id}")
    public String getById(@PathVariable Integer id) {
        String result = String.format("getById(%s) is executed", id);
        System.out.printf(result);
        return result;
    }
}

测试:

// 1. 启动 Web 服务器: 使用 随机 端口
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
// 2. 开启虚拟 MVC
@AutoConfigureMockMvc
public class WebTest {
    @Test
    // 3. 注入虚拟 MVC 对象
    void test(@Autowired MockMvc mockMvc) throws Exception {
        // 4. 创建虚拟请求
        MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/books/1");

        // 5. 发送请求
        ResultActions action = mockMvc.perform(requestBuilder);
    }
}

# 12.5. 请求执行结果的匹配 - HTTP 状态

@Test
void testStatus(@Autowired MockMvc mockMvc) throws Exception {
    MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/books/1");
    ResultActions action = mockMvc.perform(requestBuilder);

    // 定义匹配器: HTTP 响应的状态
    StatusResultMatchers statusResultMatchers = MockMvcResultMatchers.status();

    // 定义预期结果: 200
    ResultMatcher okStaus = statusResultMatchers.isOk();

    // 匹配
    action.andExpect(okStaus);
}

# 12.6. 请求执行结果的匹配 - 响应体 - 字符串

@Test
void testResBodyString(@Autowired MockMvc mockMvc) throws Exception {
    MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/books/1");
    ResultActions action = mockMvc.perform(requestBuilder);

    // 定义匹配器: 响应体
    ContentResultMatchers contentResultMatchers = MockMvcResultMatchers.content();

    // 定义预期结果: 响应体字符串内容
    ResultMatcher responseBody = contentResultMatchers.string("getById(1) is executed");

    // 匹配
    action.andExpect(responseBody);
}

# 12.7. 请求执行结果的匹配 - 响应体 - JSON

@Test
void testResBodyJson(@Autowired MockMvc mockMvc) throws Exception {
    MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/books");
    ResultActions action = mockMvc.perform(requestBuilder);

    // 定义匹配器: 响应体
    ContentResultMatchers contentResultMatchers = MockMvcResultMatchers.content();

    List<Book> list = Collections.singletonList(Book.of(1, "SpringBoot"));
    String listJsonStr = JSONUtil.toJsonStr(list);

    // 定义预期结果: 响应体 JSON 字符串
    ResultMatcher responseBody = contentResultMatchers.string(listJsonStr);

    // 匹配
    action.andExpect(responseBody);
}

# 12.8. 请求执行结果的匹配 - 响应头 - Content-Type

@Test
void testResHead(@Autowired MockMvc mockMvc) throws Exception {
    MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/books");
    ResultActions action = mockMvc.perform(requestBuilder);

    // 定义匹配器: 响应头
    HeaderResultMatchers headerResultMatchers = MockMvcResultMatchers.header();

    // 定义预期结果: 响应头 - Content-Type
    ResultMatcher headerResultMatcher = headerResultMatchers.string("Content-Type", "application/json");

    // 匹配
    action.andExpect(headerResultMatcher);
}

# 12.9. 业务层测试 事务回滚

// @SpringBootTest + @Transactional 组合使用会使事务直接回滚
@SpringBootTest
@Transactional

// 在有 @Transactional 的情况下,如果不想回滚,则设置 @Rollback(value = false)
@Rollback(value = false)
class BookServiceTest {
    @Autowired
    private IBookService bookService;

    @Test
    void testAdd() {
        String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        bookService.save(Book.of( null, "name-" + timestamp, "type-" + timestamp, "desc-" + timestamp));
    }
}

# 12.10. 测试用例 设置随机数据

SpringBoot 配置文件里可以设置随机数

配置:

testcase:
  random-value:
    # 随机整数
    intValue: ${random.int}

    # 随机整数  <= 10
    int10: ${random.int(10)}

    # 随机整数 1~10
    int-1-to-10: ${random.int(1,10)}

    # 随机 uuid
    uuid: ${random.uuid}

    # 随机 MD5 字符串,32位
    str: ${random.value}

    longValue: ${random.long}

绑定:

@Data
@Component
@ConfigurationProperties(prefix = "testcase.random-value")
public class RandomValue {
    private int intValue;
    private int int10;
    private int int1To10;
    private String uuid;
    private String str;
    private long longValue;
}

读取:

@SpringBootTest
public class RandomTest {
    @Autowired
    private RandomValue randomValue;

    @Test
    void test() {
        System.out.println("randomValue = " + randomValue);
        /*
        RandomValue(
            intValue=-1132366547,
            int10=4,
            int1To10=6,
            uuid=3ea3a3f4-9f72-4c9f-a06f-a2788786cadf,
            str=8dcd2560d328be20b490c7f5e90cb0c6,
            longValue=7782114441838784860
        )
         */
    }
}

# 13. 数据层解决方案

SQL

NoSQL

# 13.1. 内置数据源 - HikariCP

目前使用的数据层解决方案选型:

  • 数据源: Druid
  • 持久化技术: MyBatisPlus / MyBatis
  • 数据库: MySQL

Druid:

  • 引入 druid-spring-boot-starter 依赖后会自动使用该数据源

SpringBoot 提供了 3 种内嵌的数据源对象供开发者选项:

  • HikariCP (默认)
  • Tomcat 提供的 DataSource
  • Commons DBCP

HikariCP:

  • 默认内置数据源对象

Tomcat 提供的 DataSource:

  • HikariCP 不可用时,web 环境时且使用 tomcat 时,则使用 tomcat 数据源

Commons DBCP:

  • HikariCP 和 Tomcat 数据源都不可用时,则使用 dbcp

示例:

spring:
  datasource:
    # 基础配置 / 通用配置
    url: jdbc:mysql://localhost:3306/springboot_db
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456

    # 内置数据源 - 默认
    hikari:
      # hikari 不能直接设置 url, 需要读取 spring.datasource.url 的值
      maximum-pool-size: 50

    # 内置数据源 - tomcat 提供的
    tomcat:
      max-active: 50

    # 内置数据源 - dbcp
    dbcp2:
      max-total: 50

# 13.2. 内置持久化技术 - JdbcTemplate

依赖:

<!-- jdbc-starter 依赖 hikari -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

配置:

spring:
  jdbc:
    template:
      query-timeout: -1 # 查询超时时间
      max-rows: 500     # 最大行数
      fetch-size: -1    # 缓存行数

示例:

@Test
void testJdbcTemplateSave(@Autowired JdbcTemplate jdbcTemplate) {
    String sql = "insert into tbl_book values(null, 'name-1', 'type-2', 'desc-3')";
    jdbcTemplate.update(sql);
}

@Test
void testJdbcTemplateQuery(@Autowired JdbcTemplate jdbcTemplate) {
    String sql = "select * from tbl_book";

    RowMapper<Book> rowMapper = new RowMapper<Book>() {
        @Override
        public Book mapRow(ResultSet resultSet, int i) throws SQLException {
            return Book.of(
                    resultSet.getInt("id"),
                    resultSet.getString("name"),
                    resultSet.getString("type"),
                    resultSet.getString("description")
            );
        }
    };

    List<Book> list = jdbcTemplate.query(sql, rowMapper);

    System.out.println("list = " + list);
}

# 13.3. 内置数据库 - H2

SpringBoot 提供了 3 种内嵌数据库:

  • H2
  • HSQL
  • Derby

特点:

  • 这几款数据库都是 Java 编写的
  • 都可以在内存中启动,跟 Tomcat 一样
  • 小巧,内存级数据库

导入 H2 的依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

配置:

server:
  port: 80
spring:
  h2:
    console:
      # 开启 h2 的客户端
      # 仅用于 开发阶段,生产环境务必关闭
      enabled: true
      # 通过 如下路径访问,默认账号: sa/123456
      path: /h2-console

  # 第一次连 h2 内存数据库时,需要配置 datasource 进行初始化
  # 数据库相关文件 %HOME%/test.mv.db 、%HOME%/test.trace.db
  datasource:
    url: jdbc:h2:~/test
    hikari:
      driver-class-name: org.h2.Driver
      username: sa
      password: 123456

建表:

create table tbl_book(id int, type varchar, name varchar, description varchar)

insert into tbl_book values(1, 'type-1', 'name-1', 'desc-1')
insert into tbl_book values(2, 'type-2', 'name-2', 'desc-2')

select * from tbl_book

使用:

  • MP、JdbcTemplate 都可以连接该数据库

补充:

  • SpringBoot 可以根据 URL 自动识别数据库驱动类,并在类路径里找到并装载最合适的驱动类

# 13.4. redis - 下载、安装、基本使用

NoSQL 解决方案:

  • Redis
  • Mongo
  • ES

Redis:

  • key-value 存储结构的 内存级 NoSQL 数据库
  • 支持多种数据存储格式
  • 支持持久化
  • 支持集群

下载:

  • windows 版: https://github.com/tporadowski/redis/releases

启动:

# 服务
redis-server.exe redis.windows.conf

# 客户端
redis-cli.exe

启动问题:

问题:

  第一次执行 redis-server.exe redis.windows.conf 可能会失败

解决方案:

  1. 执行 redis-cli 进入客户端

  2. 输入 shutdown

  3. 输入 exit

# 13.5. Redis - SpringBoot 整合 - RedisTemplate

依赖:

<!-- 起步依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置:

# 可省略
spring:
  redis:
    host: localhost
    port: 6379

使用:

@Test
void testSet(@Autowired RedisTemplate redisTemplate) {
    ValueOperations ops = redisTemplate.opsForValue();
    ops.set("age", 18);
}

@Test
void testGet(@Autowired RedisTemplate redisTemplate) {
    ValueOperations ops = redisTemplate.opsForValue();
    Object age = ops.get("age");

    System.out.println("age = " + age);
}

# 13.6. Redis - SpringBoot 整合 - StringRedisTemplate

RedisTemplate:

  • key 、value 都是对象类型,内部会进行(自动)序列化

StringRedisTemplate:

  • key 、value 都是字符串类型,直接可以使用,不需要序列化

示例:

@Test
void getGet(@Autowired StringRedisTemplate stringRedisTemplate) {
    ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
    String age = ops.get("age");
    System.out.println("age = " + age);
}

# 13.7. Redis - 更换客户端 - Jedis

依赖:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

配置:

spring:
  redis:
    # 默认是 lettuce
    client-type: jedis

# 13.8. MongoDB - 简介

说明:

  • MongoDB 是一个开源、高性能、无模式的 文档型 数据库
  • 最像关系型数据库的 NoSQL 数据库

淘宝用户 数据:

  • 存储: 数据库
  • 特征: 永久性存储,修改频率极低

游戏装备、道具 数据:

  • 存储: 数据库、MongoDB
  • 特征: 永久性存储 与 临时存储 结合,修改频率较高

直播、打赏、粉丝 数据:

  • 存储: 数据库、MongoDB
  • 特征: 永久性存储 与 临时存储 结合,修改频率极高

物联网 数据:

  • 存储: MongoDB
  • 特征: 临时存储,修改频率飞速

# 13.9. MongoDB - 下载与安装

下载:

  • windows 环境,社区版,5.0.32
  • https://www.mongodb.com/try/download/community-edition

安装: (解压)

D:/soft/mongodb
  bin/  # 可执行文件的目录
  data/ # 新建的 数据 目录
    db/   # 新建的 数据库 目录

启动:

cd D:/soft/mongodb/bin

# 服务器
.\mongod.exe --dbpath=..\data\db

处理 windows 系统缺失 dll 的问题:

1. 在网上搜索并下载缺失的 dll 文件 (比如 1.dll)

2. 将 1.dll 拷贝到 C:\Windows\System32

3. 执行命令注册 1.dll

   regsvr32 1.dll

可视化客户端:

  • Robo 3T (注意版本要兼容 MongoDB)

# 13.10. MongoDB - 基础操作

// book 为集合名称

// 新增
db.book.save({ name: "name-1", type: "type-1" })

// 查询: 查所有
db.book.find();

// 查询: 精确查询
db.book.find({ name: "name-1" });

// 删除
db.book.remove({ name: "name-1" }, {})

// 修改: 修改满足条件的第一条
db.book.update({ name: "name-1" }, { $set: { name: "name-2" } })

# 13.11. MongoDB - SpringBoot 整合

依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

配置:

spring:
  data:
    mongodb:
      uri: mongodb://localhost/springboot-mongo-db

使用:

@Test
void testSave(@Autowired MongoTemplate mongoTemplate) {
    mongoTemplate.save(Book.of(1, "name-1", "type-1", "desc-1"));
}
@Test
void testFindAll(@Autowired MongoTemplate mongoTemplate) {
    List<Book> list = mongoTemplate.findAll(Book.class);
    System.out.println("list = " + list);
}

# 13.12. ElasticSearch (ES) - 简介

说明:

  • ES 是一个分布式全文搜索引擎

倒排索引:

  • 根据关键词查找文档 id 列表
  • 先把数据进行分词,每个关键词对应若干 id,每个 id 有对应的简化数据

创建文档:

  • 文档: 关键词 -> id 列表 -> id 对应的简化数据

使用文档:

  • 根据关键字找到对应的文档

# 13.13. ES - 下载、安装

下载:

  • windows 环境,7.17.29
  • 地址: https://www.elastic.co/downloads/past-releases/elasticsearch-7-17-29

解压:

  • 目录: D:\soft\elasticsearch

运行:

  • 双击 D:\soft\elasticsearch\bin\elasticsearch.bat 即可

端口:

  • 9300
  • 9200 : 对外提供服务的端口

访问:

  • http://localhost:9200/

# 13.14. ES - 索引操作

# 13.14.1. 基础

创建 books 索引(类比 MySQL 中创建 books 库)

PUT http://localhost:9200/books

返回结果:

{
  "acknowledged": true, // 相当于 "success": true
  "shards_acknowledged": true,
  "index": "books"
}

查看 books 索引:

GET http://localhost:9200/books

删除 books 索引:

DELETE http://localhost:9200/books

# 13.14.2. 分词

GitHub:

  • https://github.com/infinilabs/analysis-ik

安装 IK 分词器:

# 指定 ES 的版本
bin\elasticsearch-plugin install https://get.infini.cloud/elasticsearch/analysis-ik/7.17.29

创建 books 索引时进行配置:

PUT http://localhost:9200/books
Content-Type: application/json

{
  "mappings": {
    "properties": {
      "id": {
        "type": "keyword"
      },
      "name": {
        "type": "text",
        "analyzer": "ik_max_word",
        "copy_to": "all"
      },
      "type": {
        "type": "keyword"
      },
      "description": {
        "type": "text",
        "analyzer": "ik_max_word",
        "copy_to": "all"
      },
      "all": {
        "type": "text",
        "analyzer": "ik_max_word"
      }
    }
  }
}

说明:

  • "type": "keyword" 原词作为关键字进行查询
  • "type": "text" 可以分词
  • "copy_to": "all" 拷贝分词到其它的属性
  • namedescription 会进行分词
  • all 是虚拟属性,是 namedescription 分词的集合

# 13.15. ES - 文档操作

# 13.15.1. 创建文件 - 随机 ID

POST http://localhost:9200/books/_doc
Content-Type: application/json

{
  "id": 1,
  "name": "SpringBoot好啊", 
  "type": "SpringBoot", 
  "description": "SpringBoot顶呱呱" 
}

返回结果:

{
  "_index": "books",
  "_type": "_doc",
  "_id": "eqNud5sBhlh65n7D-V_q",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 0,
  "_primary_term": 1
}

# 13.15.2. 创建文件 - 指定 ID - 存在则报错

POST http://localhost:9200/books/_create/2
Content-Type: application/json

{
  "name": "张三美好的一天", 
  "type": "Person", 
  "description": "张三是个好孩子" 
}

# 13.15.3. 创建文件 - 指定 ID - 不存在则创建

不存在则创建,存在则更新

POST http://localhost:9200/books/_doc/2
Content-Type: application/json

{
  "name": "张三美好的一天2", 
  "type": "Person", 
  "description": "张三是个好孩子" 
}

返回结果:

{
  "_index": "books",
  "_type": "_doc",
  "_id": "2",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 1,
  "_primary_term": 1
}

说明:

  • _create/2_doc/2 都可以创建指定 id 的文档
  • 明确指定了 id 后,文档数据中就不需要额外指定 id 属性

# 13.15.4. 查询文档 - 按 ID

GET http://localhost:9200/books/_doc/2

# 13.15.5. 查询文档 - 全部

GET http://localhost:9200/books/_search

# 13.15.6. 查询文档 - 按 内容

GET http://localhost:9200/books/_search?q=name:springboot
GET http://localhost:9200/books/_search?q=all:Java

# 13.15.7. 删除文档 - 按 ID

DELETE http://localhost:9200/books/_doc/2

返回结果:

{
  "_index": "books",
  "_type": "_doc",
  "_id": "2",
  "_version": 2,
  "result": "deleted",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 2,
  "_primary_term": 1
}

# 13.15.8. 修改文档 - 全量修改

相当于替换文档

PUT http://localhost:9200/books/_doc/2
Content-Type: application/json

{
  "name": "张三美好的一天2", 
  "type": "Person", 
  "description": "张三是个好孩子" 
}

# 13.15.9. 修改文档 - 部分修改

POST http://localhost:9200/books/_update/2
Content-Type: application/json

{
  "doc": {
    "name": "张三美好的一天3"
  }
}

# 13.16. ES - SpringBoot 整合

# 13.16.1. 低级别 API

说明:

  • ES 更新较快,SpringBoot 提供的 API 相对较慢

依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

配置:

spring:
  elasticsearch:
    rest:
      # 默认
      uris: http://localhost:9200 

使用:

@Test
void testLowLeveApi(@Autowired ElasticsearchRestTemplate elasticsearchRestTemplate) {
    Book book = elasticsearchRestTemplate.get("2", Book.class, IndexCoordinates.of("books"));
    System.out.println("book = " + book);
}

# 13.16.2. 高级别 API

依赖:

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>

使用:

private RestHighLevelClient client;

@BeforeEach
void setUp() {
    HttpHost host = HttpHost.create("http://localhost:9200");
    RestClientBuilder restClientBuilder = RestClient.builder(host);
    client = new RestHighLevelClient(restClientBuilder);
}

@AfterEach
void tearDown() throws IOException {
    client.close();
}

@Test
void createIndex() throws IOException {
    CreateIndexRequest createIndexRequest = new CreateIndexRequest("books");
    client.indices().create(createIndexRequest, RequestOptions.DEFAULT);
}
@Test
void deleteIndex() throws IOException {
    DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("books");
    client.indices().delete(deleteIndexRequest, RequestOptions.DEFAULT);
}
@Test
void createIndexWithMappings() throws IOException {
    CreateIndexRequest createIndexRequest = new CreateIndexRequest("books");

    JSONObject body = JSONUtil.createObj();

    JSONObject mappings = JSONUtil.createObj();
    JSONObject properties = JSONUtil.createObj();

    JSONObject id = JSONUtil.createObj();
    id.set("type", "keyword");

    JSONObject name = JSONUtil.createObj();
    name.set("type", "text");
    name.set("analyzer", "ik_max_word");
    name.set("copy_to", "all");

    JSONObject type = JSONUtil.createObj();
    type.set("type", "keyword");

    JSONObject description = JSONUtil.createObj();
    description.set("type", "text");
    description.set("analyzer", "ik_max_word");
    description.set("copy_to", "all");

    JSONObject all = JSONUtil.createObj();
    all.set("type", "text");
    all.set("analyzer", "ik_max_word");

    body.set("mappings", mappings);
    mappings.set("properties", properties);
    properties.set("id", id);
    properties.set("name", name);
    properties.set("type", type);
    properties.set("description", description);
    properties.set("all", all);

    createIndexRequest.source(body.toString(), XContentType.JSON);
    client.indices().create(createIndexRequest, RequestOptions.DEFAULT);
}

# 13.17. ES - SpringBoot 整合 - 添加文档

添加单个:

@Test
void createOneDoc(@Autowired BookDao bookDao) throws IOException {
    Book book = bookDao.selectById(1);
    IndexRequest indexRequest = new IndexRequest("books").id(book.getId().toString());
    indexRequest.source(JSONUtil.toJsonStr(book), XContentType.JSON);
    client.index(indexRequest, RequestOptions.DEFAULT);
}

添加多个:

@Test
void createManyDoc(@Autowired BookDao bookDao) throws IOException {
    List<Book> bookList = bookDao.selectList(null);

    // 批量请求的容器
    BulkRequest bulkRequest = new BulkRequest();

    for (Book book : bookList) {
        IndexRequest indexRequest = new IndexRequest("books").id(book.getId().toString());
        indexRequest.source(JSONUtil.toJsonStr(book), XContentType.JSON);
        bulkRequest.add(indexRequest);
    }

    client.bulk(bulkRequest, RequestOptions.DEFAULT);
}

# 13.18. ES - SpringBoot 整合 - 查询文档

根据 ID 查:

@Test
void getById() throws IOException {
    GetRequest getRequest = new GetRequest("books", "1");
    GetResponse response = client.get(getRequest, RequestOptions.DEFAULT);
    String json = response.getSourceAsString();
    Book book = JSONUtil.toBean(json, Book.class);
    System.out.println("book = " + book);
}

根据内容查:

@Test
void search() throws IOException {
    SearchRequest request = new SearchRequest("books");

    SearchSourceBuilder builder = new SearchSourceBuilder();
    builder.query(QueryBuilders.termQuery("all", "java")); // 注意: 用“Java”搜不出来
    request.source(builder);

    SearchResponse response = client.search(request, RequestOptions.DEFAULT);

    SearchHits hits = response.getHits();

    for (SearchHit hit : hits) {
        String source = hit.getSourceAsString();
        System.out.println("source = " + source);
    }
}

# 14. 整合第三方技术

缓存

任务

邮件

消息

# 14.1. 缓存 - 作用

缓存:

  • 缓存是一种介于 数据永久存储介质 与 数据应用 之间的数据临时存储介质

作用:

  • 使用缓存可以有效减少 低速数据 读取过程的次数(如 磁盘IO),提高系统性能
  • 缓存还可以提供临时的数据存储空间(如 手机验证码)

# 14.2. 缓存 - SpringBoot 使用缓存的方式

设计:

  • 启用缓存
  • 设置 进入 缓存的数据
  • 设置 读取 缓存的数据

依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

开启:

@SpringBootApplication
// 开启缓存功能
@EnableCaching
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

使用: (设置当前操作的结果数据进入缓存)

@RestController
@RequestMapping("/books")
public class BookController {
    @GetMapping("/{id}")

    // 第一次调用该方法时,往缓存里放;后续再次调用会直接返回缓存的结果
    // value: 缓存名称 (名称空间)
    // key: 缓存的数据在 缓存空间中的 key, #id 代表使用参数列表中的 id 形参
    @Cacheable(value = "books:getById", key = "#id")
    public R getById(@PathVariable Integer id) {
        return R.ok(bookService.getById(id));
    }
}


@RestController
@RequestMapping("/sms")
public class SMSController {
    @GetMapping
    // 每次调用该方法,都会往缓存里放数据,返回新的数据
    @CachePut(value = "sms:code", key = "#telephone")
    public String sendCodeToSMS(String telephone) {
        return RandomUtil.randomNumbers(6);
    }
}

# 14.3. 缓存 - 存和取

@PostMapping
public Boolean checkCode(String telephone, String code) {
    SMSController proxy = (SMSController) AopContext.currentProxy();

    // @Cacheable 注解是 Spring 来管理的,必须通过 bean 来调用 getCacheCode()
    String cacheCode = proxy.getCacheCode(telephone);

    return code.equals(cacheCode);
}

// 此方法用于获取缓存中的数据
@Cacheable(value = "sms:code", key = "#telephone")
public String getCacheCode(String telephone) {
    return null;
}

# 14.4. 缓存 - 缓存技术

SpringBoot 提供的缓存技术除了默认的缓存方案

还可以通过统一的接口,对其它缓存技术进行整合,方便缓存技术的开发和管理

可整合的缓存技术:

  • Generic
  • JCache
  • Ehcache
  • Hazelcast
  • Infinispan
  • Couchbase
  • Redis
  • Caffenine
  • Simple (默认)

常用但默认没有整合的:

  • memcached

# 14.5. 缓存 - 更换实现 - Ehcache

依赖:

<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>

配置: (application.yml)

spring:
  cache:
    type: ehcache
    ehcache:
      # 默认值
      config: ehcache.xml

配置: (resources/ehcache.xml)

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
         updateCheck="false">
    <diskStore path="D:\ehcache" />

    <!--默认缓存策略 -->
    <!-- external:是否永久存在,设置为true则不会被清除,此时与timeout冲突,通常设置为false-->
    <!-- diskPersistent:是否启用磁盘持久化-->
    <!-- maxElementsInMemory:最大缓存数量-->
    <!-- overflowToDisk:超过最大缓存数量是否持久化到磁盘-->
    <!-- timeToIdleSeconds:最大不活动间隔,设置过长缓存容易溢出,设置过短无效果,可用于记录时效性数据,例如验证码-->
    <!-- timeToLiveSeconds:最大存活时间-->
    <!-- memoryStoreEvictionPolicy:缓存清除策略-->
    <defaultCache
        eternal="false"
        diskPersistent="false"
        maxElementsInMemory="1000"
        overflowToDisk="false"
        timeToIdleSeconds="60"
        timeToLiveSeconds="60"
        memoryStoreEvictionPolicy="LRU" />

    <!--
        @Cacheable(value = "sms:code") 使用 <cache name="sms:code" />
        @Cacheable 使用 <defaultCache />
    -->
    <cache
        name="sms:code"
        eternal="false"
        diskPersistent="false"
        maxElementsInMemory="1000"
        overflowToDisk="false"
        timeToIdleSeconds="10"
        timeToLiveSeconds="10"
        memoryStoreEvictionPolicy="LRU" />
</ehcache>

# 14.6. 知识加油站 - 数据淘汰策略

数据淘汰策略中的 LRU 和 LFU

影响数据淘汰的相关配置

检测易失数据(可能会过期的数据集 server.db[i].expires):

  • volatile-lru
  • volatile-lfu
  • volatile-ttl
  • volatile-random

volatile-lru:

  • Least Recently Used
  • 淘汰长时间不活动的
  • 按 最后使用时间 倒排,淘汰末尾的

volatile-lfu:

  • Least Frequently Used
  • 淘汰使用率低的
  • 按 使用次数 倒排,淘汰末尾的

volatile-ttl:

  • 按 创建时间 倒排,淘汰末尾的

volatile-random:

  • 随机淘汰

# 14.7. 缓存 - 更换实现 - Redis

依赖:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置:

spring:
  cache:
    type: redis
    redis:
      # TTL
      time-to-live: 10s   # 默认 不过期
      # key 是否带 value 前缀
      # 示例: @Cacheable(value = "sms:code", key = "#telephone")
      #   true  => sms:code:18707126666=123456
      #   false => 18707126666=123456
      use-key-prefix: true # 默认 true
      # 给 key 加前缀
      # 示例: @Cacheable(value = "sms:code", key = "#telephone")
      #  => example:sms:code:18707126666=123456
      key-prefix: "example:"
      # 是否缓存空值
      cache-null-values: true # 默认 true
  redis:
    # 默认值
    host: localhost
    # 默认值
    port: 6379

# 14.8. 缓存 - 更换实现 - memcached

下载:

  • windows 64位系统 1.4.4版本
  • 下载列表: https://www.runoob.com/memcached/window-install-memcached.html
    • 下载链接不是 HTTPS 协议,直接点击下载 会 “闪退”,需要拷贝 URL 到地址栏进行下载
  • 下载地址: http://static.jyshare.com/download/memcached-win64-1.4.4-14.zip

解压:

  • D:\soft\memcached

安装与运行:

# 安装为 windows 服务 memcached
# 安装时需要用管理员身份启动 cmd
memcached.exe -d install

# 启动 服务
memcached.exe -d start

# 停止 服务
memcached.exe -d stop

# 卸载服务
memcached.exe -d uninstall

memcached 的客户端:

  • Memcached Client for Java: 最早期的客户端,稳定可靠,用户群广
  • SpyMemcached: 效率高
  • Xmemcached: 并发处理好 【推荐】

SpringBoot 未整合 memcached 的,需要手动初始化客户端

依赖:

<dependency>
    <groupId>com.googlecode.xmemcached</groupId>
    <artifactId>xmemcached</artifactId>
    <version>2.4.7</version>
</dependency>

配置:

@Configuration
public class XMemcachedConfig {
    @Bean
    public MemcachedClient getMemcachedClient() throws IOException {
        MemcachedClientBuilder builder = new XMemcachedClientBuilder("localhost:11211");
        MemcachedClient memcachedClient = builder.build();
        return memcachedClient;
    }
}

使用:

@Test
void test(@Autowired MemcachedClient memcachedClient) throws Exception {
    // 存。 0 - 永不过期,单位秒
    memcachedClient.set("name", 0, "张三");

    Object name = memcachedClient.get("name");
    System.out.println("name = " + name);
}

# 14.9. 缓存 - jetcache (阿里) - 缓存框架

说明:

  • jetCache 对 SpringCache 进行封装
  • 额外扩展了 多级缓存、缓存统计、自动刷新、异步调用、数据报表 等功能

jetCache 兼容如下缓存方案:

  • 本地缓存(local)
    • LinkedHashMap
    • Caffeine
  • 远程缓存(remote)
    • Redis
    • Tair

# 14.10. 缓存 - jetcache - 远程缓存

依赖:

<dependency>
    <groupId>com.alicp.jetcache</groupId>
    <artifactId>jetcache-starter-redis</artifactId>
    <version>2.6.7</version>
</dependency>

配置: (整体配置)

jetcache:
  # 每隔 15 分钟在输入日志,统计各个缓存的命中情况
  statIntervalMinutes: 15
  local:
    default:
      # caffeine | linkedhashmap
      type: linkedhashmap
      # key 的转换: 使用 fastjson 来转换 对象类型 的 key (fastjson 在 starter 里已经导入)
      # fastjson | jackson
      keyConvertor: fastjson
      limit: 100
    sms:
      type: linkedhashmap
      keyConvertor: fastjson
      limit: 100
  remote:
    default:
      type: redis
      host: localhost
      port: 6379
      keyConvertor: fastjson
      # 处理值的序列号,注意 bean 需要实现 Serializable
      valueEncoder: java
      valueDecoder: java
      poolConfig:
        maxTotal: 50
    sms:
      type: redis
      host: localhost
      port: 6379
      keyConvertor: fastjson
      valueEncoder: java
      valueDecoder: java
      poolConfig:
        maxTotal: 50

启用:

@SpringBootApplication
// 启用缓存的开关
@EnableCreateCacheAnnotation
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

使用:

// area: 空间,默认为 "default"
// name: key 的前缀
// expire: 过期时间,
// timeUnit: 过期时间单位,默认单位 秒
@CreateCache(area = "default", name="captcha:", expire = 100, timeUnit = TimeUnit.SECONDS)
// 相当于定义了一个 HashMap
private Cache<String, String> jetCache;
// 最终 redis 中的 key 为 "default_captcha:18707127666"


// 放入缓存
jetCache.put("18707127666", "123456");

// 从缓存中取
String cacheCode = jetCache.get("18707127666");

# 14.11. 缓存 - jetcache - 本地缓存

使用:

@CreateCache(area = "sms", name="captcha:", expire = 100, timeUnit = TimeUnit.SECONDS, cacheType = CacheType.LOCAL)
private Cache<String, String> jetCache2;

注意:

  • 如果只使用 本地缓存,则可以不配置 jetcache.remote

配置:

属性 默认值 说明
jetcache.statIntervalMinutes 0 统计间隔,0表示不统计
jetcache.hiddenPackages 自动生成name时,隐藏指定的包名前缀
jetcache.[local\|remote].{area}.type 缓存类型,本地支持linkedhashmap、caffeine,远程支持redis、tair
jetcache.[local\|remote].{area}.keyConvertor key转换器,当前仅支持fastjson
jetcache.[local\|remote].{area}.valueEncoder java 仅remote类型的缓存需要指定,可选java和kryo
jetcache.[local\|remote].${area}.valueDecoder java 仅remote类型的缓存需要指定,可选java和kryo
jetcache.[local\|remote].${area}.limit 100 仅local类型的缓存需要指定,缓存实例最大元素数
jetcache.[local\|remote].${area}.expireAfterWriteInMillis 无穷大 默认过期时间,毫秒单位
jetcache.local.${area}.expireAfterAccessInMillis 0 仅local类型的缓存有效,毫秒单位,最大不活动间隔

# 14.12. 缓存 - jetcache - 方法注解

开启:

@SpringBootApplication
@EnableCreateCacheAnnotation
// 开启方法注解缓存
@EnableMethodCache(basePackages = "org.example")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

使用:

@Service
public class BookServiceImpl extends ServiceImpl<BookDao, Book> implements IBookService {
    @Override
    // set default_detail:1 book{...}
    @Cached(area = "default", name = "detail:", key="#id", expire = 100, cacheType = CacheType.REMOTE)
    // 每隔 60 秒,重新执行一次缓存中的每个条目对应的查询
    @CacheRefresh(refresh = 60)
    public Book getById(Integer id) { // 注意返回值需要实现 Serializable 接口
        return super.getById(id);
    }

    @Override
    public boolean save(Book entity) {
        return super.save(entity);
    }

    @Override
    // set default_detail:1 book{...}
    @CacheUpdate(area = "default", name = "detail:", key="#entity.id", value="#entity")
    public boolean updateById(Book entity) {
        return super.updateById(entity);
    }

    @Override
    // del default_detail:1
    @CacheInvalidate(area = "default", name = "detail:", key="#id")
    public boolean removeById(Integer id) {
        return super.removeById(id);
    }
}

# 14.13. 缓存 - j2cache - 基本操作

说明:

  • j2cache 是一个缓存整合框架,自身不提供缓存功能
  • 可以通过缓存的整合方案,使各种缓存搭配使用
  • 比如整合 ehcache + redis

依赖:

<dependency>
    <groupId>net.oschina.j2cache</groupId>
    <artifactId>j2cache-core</artifactId>
    <version>2.8.5-release</version>
</dependency>

<!-- 默认导入了 spring-boot-starter-data-redis -->
<dependency>
    <groupId>net.oschina.j2cache</groupId>
    <artifactId>j2cache-spring-boot2-starter</artifactId>
    <version>2.8.0-release</version>
</dependency>

<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>

查看配置文件:

打开 net.oschina.j2cache:j2cache-core:2.8.5-release 的依赖

里面有如下配置文件,可以供参考:

  caffeine.properties
  ehcache.xml
  ehcache3.xml
  j2cache.properties
  network.xml

配置: (application.yml)

j2cache:
  config-location: j2cache.properties

配置: (ehcache.xml)

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
         updateCheck="false">
    <diskStore path="D:\ehcache" />

    <!--默认缓存策略 -->
    <!-- external:是否永久存在,设置为true则不会被清除,此时与timeout冲突,通常设置为false-->
    <!-- diskPersistent:是否启用磁盘持久化-->
    <!-- maxElementsInMemory:最大缓存数量-->
    <!-- overflowToDisk:超过最大缓存数量是否持久化到磁盘-->
    <!-- timeToIdleSeconds:最大不活动间隔,设置过长缓存容易溢出,设置过短无效果,可用于记录时效性数据,例如验证码-->
    <!-- timeToLiveSeconds:最大存活时间-->
    <!-- memoryStoreEvictionPolicy:缓存清除策略-->
    <defaultCache
        eternal="false"
        diskPersistent="false"
        maxElementsInMemory="1000"
        overflowToDisk="false"
        timeToIdleSeconds="60"
        timeToLiveSeconds="60"
        memoryStoreEvictionPolicy="LRU" />
</ehcache>

配置: (j2cache.properties)

# 1 级缓存
j2cache.L1.provider_class = ehcache
ehcache.configXml = ehcache.xml

# 2 级缓存
j2cache.L2.provider_class = net.oschina.j2cache.cache.support.redis.SpringRedisProvider
j2cache.L2.config_section = redis
redis.hosts = localhost:6379

# 1 级缓存 的数据如何到 2 级缓存
j2cache.broadcast = net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy

使用:

@Autowired
private CacheChannel cacheChannel;

// 放入缓存
cacheChannel.set("sms", telephone, code);

// 从缓存中取
String cacheCode = cacheChannel.get("sms", telephone).asString();

# 14.14. 缓存 - j2cache - 相关配置

配置: (j2cache.properties)

# 单机模式的 redis
redis.mode = single

# key 的前缀
redis.namespace = j2cache

# 设置是否启用 2 级缓存,默认开启
j2cache.l2-cache-open = false

# 14.15. 任务 - 介绍

说明:

  • 定时任务是应用中的常见操作,比如 年度报表,缓存统计报告

用原生 Java API 实现:

public class TestTask {
    public static void main(String[] args) {
        Timer timer = new Timer();

        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                System.out.println(LocalDateTime.now());
            }
        };
        // 每隔 2 秒执行一次
        timer.schedule(task, 0, 2000);
    }
}

流行的定时任务技术:

  • Quartz /kwɔːts/
  • Spring Task

# 14.16. 任务 - quartz

相关概念:

  • 工作(Job): 定义具体执行的工作
  • 工作明细(JobDetail): 描述定时工作相关的信息
  • 触发器(Trigger): 描述触发工作的规则,通常用 cron 表达式定义调度规则
  • 调度器(Scheduler): 描述 工作明细 与 触发器 的对应关系

依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

工作:

// 具体要执行的任务
public class MyQuartzTask extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println(LocalDateTime.now() + " : quartz task run...");
    }
}

配置:

@Configuration
public class QuartzConfig {
    // 定义工作明细,绑定工作
    @Bean
    public JobDetail printJobDetail() {
        return JobBuilder.newJob(MyQuartzTask.class).storeDurably().build();
    }

    // 定义触发器,关联工作明细
    @Bean
    public Trigger printJobTrigger() {
        // 0 点开始,每隔 5 秒执行一次
        ScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");
        return TriggerBuilder.newTrigger().forJob(printJobDetail()).withSchedule(scheduleBuilder).build();
    }
}

# 14.17. 任务 - Spring Task

开启: (定时任务功能)

@SpringBootApplication
@EnableScheduling // 定时任务功能
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

配置: (可省略)

spring:
  task:
    scheduling:
      pool:
        # 任务调度 线程池 大小,默认 1
        size: 1
      # 线程名称前缀
      thread-name-prefix: my-task-
      shutdown:
        # 线程池关闭时,等待所有任务完成
        await-termination: false
        # 线程关闭前最大等待时间,确保最有一定关闭
        await-termination-period: 10s

使用:

@Component
public class MyTask {
    // 设置定时任务
    @Scheduled(cron = "*/5 * * * * *") // 每隔 5 秒钟执行一次
    public void print() {
        System.out.println(LocalDateTime.now() + " : Spring Task run...");
    }
}

# 14.18. 邮件 - JavaMail - 简单邮件

SMTP:

  • Simple Mail Transfer Protocol
  • 简单邮件传输协议
  • 用于 发送 电子邮件的传输协议

POP3:

  • Post Office Protocol - Version 3
  • 用于 接收 电子邮件的标准协议

IMAP:

  • Internet Mail Access Protocol
  • 互联网消息协议
  • 是 POP3 的替代协议,本地已读或删除邮件时,POP3 不会做同步

依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

配置:

spring:
  mail:
    host: smtp.qq.com
    username: wu****@qq.com
    # 登录网页版 QQ 邮箱: 设置 -> 账号
    # 开启 POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务
    # 短信验证后,获取第三方登录密码(授权码)
    password: fhrkohezikfqcajh
    # 启用 SSL
    # 参考: java实现邮件发送_显示对方是qq邮箱发短信 - https://cloud.tencent.com/developer/article/2150862
    properties:
      mail:
        smtp:
          ssl:
            enable: true

使用:

@Service
public class MailServiceImpl implements IMailService {
    @Autowired
    private JavaMailSender javaMailSender;

    @Override
    public void sendMail() {
        String username = "张三";
        String from = "wu****@qq.com";
        String to = "wu****@labwind.com";
        String subject = "测试邮件-主题";
        String content = "测试邮件-内容";

        SimpleMailMessage simpleMailMessage = new SimpleMailMessage();

        // wu****@qq.com(张三) : 收件箱里会显示 "张三"
        simpleMailMessage.setFrom(String.format("%s(%s)", from, username));
        simpleMailMessage.setTo(to);
        simpleMailMessage.setSubject(subject);
        simpleMailMessage.setText(content);

        javaMailSender.send(simpleMailMessage);
    }
}

# 14.19. 邮件 - JavaMail - 复杂邮件

@Override
public void sendMimeMail() {
    try {
        String username = "张三";
        String from = "wu****@qq.com";
        String to = "wu****@labwind.com";
        String subject = "测试邮件-主题";
        String content = "<a href='https://www.baidu.com'>百度</a>";

        MimeMessage mimeMessage = javaMailSender.createMimeMessage();
        // true - 可以发送媒体文件
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);

        helper.setFrom(String.format("%s(%s)", from, username));
        helper.setTo(to);
        helper.setSubject(subject);
        // true - 解析 HTML
        helper.setText(content, true);

        // 附件
        helper.addAttachment("1.png", new File("D:\\1.png"));
        helper.addAttachment("2.xlsx", new File("D:\\2.xlsx"));


        javaMailSender.send(mimeMessage);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

# 14.20. 邮件 - JavaMail - 自定义 sender

public void sendMailByCustomSender() {
    JavaMailSenderImpl sender = new JavaMailSenderImpl();

    sender.setHost("smtp.qq.com");
    sender.setUsername("wu****@qq.com");
    sender.setPassword("fhrkohezikfqcajh");
    sender.setDefaultEncoding("UTF-8");

    Properties javaMailProperties = new Properties();
    javaMailProperties.setProperty("mail.smtp.ssl.enable", "true");
    sender.setJavaMailProperties(javaMailProperties);

    String username = "张三";
    String from = "wu****@qq.com";
    String to = "wu****@labwind.com";
    String subject = "测试邮件-自定义-主题";
    String content = "测试邮件-自定义-内容";

    SimpleMailMessage simpleMailMessage = new SimpleMailMessage();

    simpleMailMessage.setFrom(String.format("%s(%s)", from, username));
    simpleMailMessage.setTo(to);
    simpleMailMessage.setSubject(subject);
    simpleMailMessage.setText(content);

    sender.send(simpleMailMessage);
}

# 14.21. 消息 - 简介

概念:

  • 消息发送方: 生产者
  • 消息接收方: 消费者
  • 同步消息: 等待对方确认
  • 异步消息: 不等待对方确认

常用的三种异步消息传递技术(规范):

  • JMS 【API】
  • AMQP 【消息格式】
  • MQTT 【小设备】

JMS:

  • Java Message Service
  • 一个规范,提供了与消息服务相关的 API 接口

JMS 消息模型:

  • peer-2-peer
    • 点对点模型
    • 消息发送到一个队列中,队列保存消息
    • 队列的消息只能被一个消费者消费 或 超时
  • publish-subscribe:
    • 发布订阅模型
    • 消息可以被多个消费者消费
    • 生产者和消费者完全对立,不需要感知对方的存在

JMS 消息种类:

  • TextMessage
  • MapMessage
  • BytesMessage
  • StreamMessage
  • ObjectMessage
  • Message (只有消息头和属性)

JMS 的实现:

  • ActiveMQ
  • Redis
  • HornetMQ
  • RabbitMQ
  • RocketMQ (没有完全遵守 JMS 规范)

AMQP:

  • Advanced Message Queuing Protocol
  • 高级消息队列协议
  • 一种协议,也就是消息代理规范
  • 规范了网络交换的数据格式,兼容 JMS
  • 约定消息的格式

AMQP 的优点:

  • 具有跨平台性
  • 服务器、生产者、消费者 可以使用不同的语音来实现

AMQP 消息模型:

  • direct exchange
  • fanout exchange
  • topic exchange
  • headers exchange
  • system exchange

AMQP 消息种类: byte[]

AMQP 的实现:

  • RabbitMQ
  • StormMQ
  • RocketMQ

MQTT:

  • Message Queueing Telemetry Transport
  • 消息队列遥测传输
  • 专为小设备设计,是物联网(IOT)生态系统中主要成分之一

Kafka:

  • 一种 高吞吐量 的 分布式 发布订阅 消息系统
  • 提供实时消息功能

常用产品:

  • ActiveMQ
  • RabbitMQ
  • RocketMQ
  • Kafka

# 14.22. 消息 - 购物订单案例 - 短信通知

购物订单业务:

  • 登录状态检测
  • 生成主单
  • 生成子单
  • 库存检测与变更
  • 积分变更
  • 支付
  • 短信通知 -- 异步
  • 购物车维护
  • 运单信息初始化
  • 商铺库存维护
  • 会员维护
  • ...

# 14.23. 消息 - ActiveMQ - 下载、安装

下载:

  • 列表: https://activemq.apache.org/components/classic/documentation/download-archives
  • 版本: apache-activemq-5.16.8-bin.zip
  • 地址: https://archive.apache.org/dist/activemq/5.16.8/apache-activemq-5.16.8-bin.zip

安装: 解压到 D:\soft\activemq

启动:

  • 双击执行 D:\soft\activemq\bin\win64\activemq.bat
  • 服务器端口 61616

管理界面:

  • http://127.0.0.1:8161/
  • admin / admin

# 14.24. 消息 - ActiveMQ - 整合

依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-activemq</artifactId>
</dependency>

配置:

spring:
  activemq:
    broker-url: tcp://localhost:61616
  jms:
    # false - 点对点 (默认) - 查看 Queues
    # true - 发布订阅 - 查看 Topics
    pub-sub-domain: false
    template:
      # 指定默认位置的名称
      default-destination: default.queue

使用: (手动放,手动取)

@Service
public class MessageServiceActiveMqImpl implements IMessageService {
    @Autowired
    private JmsMessagingTemplate messagingTemplate;

    @Override
    public void sendMessage(String id) {
        // 放: 默认位置
        // messagingTemplate.convertAndSend(id);

        // 放: 指定位置
        messagingTemplate.convertAndSend("order.queue.id", id);
    }

    @Override
    public String doMessage() {
        // 取: 默认位置
        // String id = messagingTemplate.receiveAndConvert(String.class);

        // 取: 指定位置
        String id = messagingTemplate.receiveAndConvert("order.queue.id", String.class);
        
        return id;
    }
}

使用: (自动取,转发)

@Component
public class MessageListener {
    // "order.queue.id" 队列一旦有消息,则取出来处理
    @JmsListener(destination = "order.queue.id")
    // 处理完毕后,将返回值放入 "order.other.queue.id" 队列
    @SendTo("order.other.queue.id")
    public String receive(String id) {
        System.out.println("MQ-Listener: 已完成短信发送的订单, id = " + id);
        return "new:" + id;
    }
}

# 14.25. 消息 - RabbitMQ - 下载、安装

说明:

  • RabbitMQ 是 Erlang 语言编写的,需要下载 Erlang

Erlang:

  • 下载:
    • https://www.erlang.org/downloads
    • otp_win64_26.2.5.16.exe
  • 安装:
    • D:\soft\erlang-26
    • 安装完毕后,重启电脑
  • 环境变量
    • ERLANG_HOME: D:\soft\erlang-26
    • PATH: %ERLANG_HOME%\bin

RabbitMQ:

  • 下载: https://www.rabbitmq.com/docs/3.13/install-windows
  • 文件: rabbitmq-server-3.13.7.exe
  • 位置: D:\soft\\RabbitMQ

命令:

# 管理员权限 CMD
cd D:\soft\RabbitMQ\rabbitmq_server-3.13.7\sbin

# 启动 ( 5672 端口)
rabbitmq-service.bat start
# 停止
rabbitmq-service.bat stop

# 状态
rabbitmq-service.bat status


# 查看 插件 列表
rabbitmq-plugins.bat list

# 开启 管理界面 插件(需要重启服务)
rabbitmq-plugins.bat enable rabbitmq_management

管理界面访问:

  • http://localhost:15672/
  • guest/guest

# 14.26. 消息 - RabbitMQ - 整合 - direct 模式

依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

配置:

spring:
  rabbitmq:
    host: localhost
    port: 5672

配置类: (创建 交换机、队列、路由)

@Configuration
public class RabbitDirectConfig {
    // 创建一个 direct_queue 的队列
    @Bean
    public Queue directQueue() {
        return new Queue("direct_queue");
    }

    // 创建一个 directExchange 的交换机
    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange("directExchange");
    }

    // 创建一个 direct 路由: 将 队列 绑定到 交换机
    @Bean
    public Binding bindingDirect() {
        return BindingBuilder.bind(directQueue()).to(directExchange()).with("direct");
    }
}

发送消息:

@Service
public class MessageServiceRabbitMqDirectImpl implements IMessageService {
    @Autowired
    private AmqpTemplate amqpTemplate;

    @Override
    public void sendMessage(String id) {
        System.out.println("Rabbit Direct: 发送短信的订单已进入队列, id = " + id);
        // 向 交换机的路由 发送消息
        amqpTemplate.convertAndSend("directExchange", "direct", id);
    }
}

侦听队列:

@Component
public class RabbitDirectMessageListener {
    // 侦听 direct_queue 队列,一旦有消息则执行
    @RabbitListener(queues = {"direct_queue"})
    public void receive(String id) {
        System.out.println("Rabbit Direct Listener 1: 已完成短信发送的订单, id = " + id);
    }

    // 同一个队列被多次侦听,则多个侦听器轮流处理消息
    @RabbitListener(queues = {"direct_queue"})
    public void receive2(String id) {
        System.out.println("Rabbit Direct Listener 2: 已完成短信发送的订单, id = " + id);
    }
}

# 14.27. 消息 - RabbitMQ - 整合 - topic 模式

发送消息:

@Service
public class MessageServiceRabbitMqTopicImpl implements IMessageService {
    @Autowired
    private AmqpTemplate amqpTemplate;

    @Override
    public void sendMessage(String id) {
        System.out.println("Rabbit Topic: 发送短信的订单已进入队列, id = " + id);
        // 向 交换机的路由 发送消息
        amqpTemplate.convertAndSend("topicExchange", "topic.order.id", id);
    }
}

配置类:

@Configuration
public class RabbitTopicConfig {
    // 创建一个 topic_queue 的队列
    @Bean
    public Queue topicQueue() {
        return new Queue("topic_queue");
    }
    // 创建一个 topic_queue2 的队列
    @Bean
    public Queue topicQueue2() {
        return new Queue("topic_queue2");
    }

    // 创建一个 topicExchange 的交换机
    @Bean
    public TopicExchange topicExchange() {
        return new TopicExchange("topicExchange");
    }

    // 创建一个 topic 路由: 将 topic_queue 队列 绑定到 topicExchange 交换机
    @Bean
    public Binding bindingTopic() {
        return BindingBuilder.bind(topicQueue()).to(topicExchange())
                // 模糊匹配, "*" 匹配任意单词
                .with("topic.*.id");
    }

    // 创建一个 topic2 路由: 将 topic_queue2 队列 绑定到 topicExchange 交换机
    @Bean
    public Binding bindingTopic2() {
        return BindingBuilder.bind(topicQueue2()).to(topicExchange())
                // 模糊匹配, "#" 匹配所有, 以 "topic." 打头的所有
                .with("topic.#");
    }
}

侦听器:

@Component
public class RabbitTopicMessageListener {
    // 侦听 topic_queue 队列,一旦有消息则执行
    @RabbitListener(queues = {"topic_queue"})
    public void receive(String id) {
        System.out.println("Rabbit Topic Listener 1: 已完成短信发送的订单, id = " + id);
    }

    // 侦听 topic_queue2 队列,一旦有消息则执行
    @RabbitListener(queues = {"topic_queue2"})
    public void receive2(String id) {
        System.out.println("Rabbit Topic Listener 2: 已完成短信发送的订单, id = " + id);
    }
}

# 14.28. 消息 - RocketMQ - 下载、安装

下载:

  • https://rocketmq.apache.org/download
  • rocketmq-all-4.9.8-bin-release.zip

安装:

  • 解压: D:\soft\rocketmq
  • 端口: 9876

环境变量:

  • ROCKETMQ_HOME: D:\soft\rocketmq
  • PATH: %ROCKETMQ_HOME%\bin
  • NAMESRV_ADDR: 127.0.0.1:9876

命名服务器 与 业务服务器(broker):

  • 生产者、消费者 与 命名服务器 打交道
  • 命名服务器 与 多台 broker 打交道

启动:

cd D:\soft\rocketmq\bin

# 先 启动 命名服务器
mqnamesrv

# 后 启动 业务服务器
mqbroker

测试: (D:\soft\rocketmq\lib\rocketmq-example-4.9.8.jar)

cd D:\soft\rocketmq\bin

# 生产 1000 条消息
tools org.apache.rocketmq.example.quickstart.Producer

# 消费 1000 条消息
tools org.apache.rocketmq.example.quickstart.Consumer

# 14.29. 消息 - RocketMQ - 整合

依赖:

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.2.3</version>
</dependency>

配置:

rocketmq:
  name-server: localhost:9876
  producer:
    # 必须给一个默认的组,否则启动失败
    group: group_rocketmq

发送消息:

@Service
public class MessageServiceRocketmqImpl implements IMessageService {
    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @Override
    public void sendMessage(String id) {
        System.out.println("Rocket: 发送短信的订单已进入队列, id = " + id);
        // 同步消息
        // rocketMQTemplate.convertAndSend("order_id", id);

        SendCallback callback = new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                System.out.println("消息发送 成功");
            }
            @Override
            public void onException(Throwable throwable) {
                System.out.println("消息发送 失败");
            }
        };
        // 异步消息
        rocketMQTemplate.asyncSend("order_id", id, callback);
    }
}

侦听器:

@Component
@RocketMQMessageListener(topic = "order_id", consumerGroup = "group_rocketmq")
public class RocketMqListener implements RocketMQListener<String> {
    @Override
    public void onMessage(String id) {
        System.out.println("Rabbit Listener: 已完成短信发送的订单, id = " + id);
    }
}

# 14.30. 消息 - Kafka - 下载、安装

下载:

  • windows 系统,kafka 3.0.0 存在 bug,建议使用 2.x 版本
  • https://kafka.apache.org/community/downloads
  • kafka_2.13-2.8.2.tgz

安装:

  • 解压: D:\soft\kafka

启动:

cd D:\soft\kafka\bin\windows

# 先启动 注册中心 - 默认端口: 2181
zookeeper-server-start.bat ../../config/zookeeper.properties

# 再启动 服务     - 默认端口: 9092
kafka-server-start.bat ../../config/server.properties

# win11 如果报错 wmic 找不到,则需要安装

测试:

cd D:\soft\kafka\bin\windows

# 创建 topic - my_test_topic
kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic my_test_topic

# 查看 topic
kafka-topics.bat --zookeeper 127.0.0.1:2181 --list

# 删除 topic
kafka-topics.bat --delete --zookeeper 127.0.0.1:2181 --topic my_test_topic

# 生产者功能测试
kafka-console-producer.bat --broker-list localhost:9092 --topic my_test_topic

# 消费者功能测试
kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic my_test_topic --from-beginning

参考:

# 14.31. 消息 - Kafka - 整合

依赖:

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

配置:

spring:
  kafka:
    bootstrap-servers: localhost:9092
    consumer:
      group-id: order

使用:

// 发送消息
@Service
public class MessageServiceKafkaImpl implements IMessageService {
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    @Override
    public void sendMessage(String id) {
        System.out.println("Kafka: 发送短信的订单已进入队列, id = " + id);
        kafkaTemplate.send("my_test_topic", id);
    }
}


// 监听消息
@Component
public class KafkaMessageListener {
    @KafkaListener(topics = "my_test_topic")
    public void onMessage(ConsumerRecord<String, String> record) {
        System.out.println("Kafka Listener: 已完成短信发送的订单, id = " + record.value());
    }
}

# 15. 监控

监控的意义

可视化监控平台

监控原理

自定义监控指标

# 15.1. 监控 - 意义

监控:

  • 监控服务状态是否宕机
  • 监控服务运行指标(内存、虚拟机、线程、请求)
  • 监控日志
  • 管理服务(服务下线)

实施方式:

  • 监控服务器: 用于 获取并显示 运行的服务的信息
  • 运行的服务: 启动时主动上报,告知监控服务器自己需要受到监控

# 15.2. 监控 - SpringBootAdmin

Spring Boot Admin:

  • 开源社区项目
  • 用于 管理和监控 SpringBoot 应用程序
  • 客户端注册到服务端后,通过 HTTP 请求方式,服务器定期从客户端获取对应的信息,并通过 UI 界面展示对应的信息

# 15.2.1. 监控服务器

依赖:

<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-server</artifactId>
    <!-- 版本要与 SpringBoot 完全一致 -->
    <version>2.5.4</version>
</dependency>

配置:

server:
  port: 8080

开启:

@SpringBootApplication
// 开启 Admin Server
@EnableAdminServer
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

访问: http://localhost:8080/

# 15.2.2. 运行的服务

依赖:

<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-client</artifactId>
    <version>2.5.4</version>
</dependency>

配置:

server:
  port: 80

management:
  endpoint:
    health:
      show-details: always
  endpoints:
    # 暴露所有的端点给 web 端
    web:
      exposure:
        include: "*"

# 15.3. 监控 - actuator

监控原理:

  • actuator 提供了 SpringBoot 生产就绪功能,通过端点的配置 即可访问端点信息
  • 端点描述了一组监控信息,SpringBoot 提供了内置的端点,也可以自定义端点信息
  • 当前应用端点列表: /actuator
  • 端点详情: /actuator/端点名称

端点:

端点 描述 默认启用
auditevents 暴露当前应用程序的审计事件信息。
beans 显示应用程序中所有 Spring bean 的完整列表。
caches 暴露可用的缓存。
conditions 显示在配置和自动配置类上评估的条件以及它们匹配或不匹配的原因。
configprops 显示所有 @ConfigurationProperties 的校对清单。
env 暴露 Spring ConfigurableEnvironment 中的属性。
flyway 显示已应用的 Flyway 数据库迁移。
health 显示应用程序健康信息
httptrace 显示 HTTP 追踪信息(默认情况下,最后 100 个 HTTP 请求/响应交换)。
info 显示应用程序信息。
integrationgraph 显示 Spring Integration 图。
loggers 显示和修改应用程序中日志记录器的配置。
liquibase 显示已应用的 Liquibase 数据库迁移。
metrics 显示当前应用程序的指标度量信息。
mappings 显示所有 @RequestMapping 路径的整理清单。
scheduledtasks 显示应用程序中的调度任务。
sessions 允许从 Spring Session 支持的会话存储中检索和删除用户会话。当使用 Spring Session 的响应式 Web 应用程序支持时不可用。
shutdown 正常关闭应用程序。
threaddump 执行线程 dump。
heapdump 返回一个 hprof 堆 dump 文件。
jolokia 通过 HTTP 暴露 JMX bean(当 Jolokia 在 classpath 上时,不适用于 WebFlux)。
logfile 返回日志文件的内容(如果已设置 logging.file 或 logging.path 属性)。支持使用 HTTP Range 头来检索部分日志文件的内容。
prometheus 以可以由 Prometheus 服务器抓取的格式暴露指标。

常用端点:

  • health : 应用健康状态
  • loggers : 显示和修改 应用程序中 日志记录器的配置
  • metrics : 显示当前应用程序的指标度量信息

配置:

management:
  # 控制当前应用程序的端点: 开启、信息
  endpoint:
    health:
      show-details: always
    info:
      enabled: false
  # 控制暴露哪些端点给外部
  endpoints:
    # web 端
    web:
      # 暴露的端点
      exposure:
        # 默认只暴露 health
        include: "*"
    # jdk 中的 jConsole 就是 jmx 提供的信息
    jmx:
      exposure:
        # 默认暴露所有
        include: "*"

# 15.4. 监控 - info 端点 - 配置

Insights -> Details: Info 面板,自定义内容

静态数据:

# 配置 info 端点
info:
  # 自定义 key 和 value
  # 显示 pom.xml 中的 project.artifactId 节点内容
  appName: @project.artifactId@
  appVersion: @project.version@
  author: 吴钦飞

动态数据:

package org.example.actuator;

@Component
public class InfoConfig implements InfoContributor {
    @Override
    public void contribute(Info.Builder builder) {
        // 设置单个 key-value
        builder.withDetail("runTime", System.currentTimeMillis());

        // 设置 key-value 集合
        Map<String, Object> map = new HashMap<>();
        map.put("buildTime", "2026");
        builder.withDetails(map);
    }
}

# 15.5. 监控 - health 端点 - 配置

Insights -> Details: Health 面板,自定义内容 及 状态

@Component
public class HealthConfig extends AbstractHealthIndicator {
    @Override
    protected void doHealthCheck(Health.Builder builder) throws Exception {
        // Status.DOWN
        // Status.UNKNOWN
        // Status.OUT_OF_SERVICE
        builder.status(Status.UP);
        
        builder.withDetail("health-time", LocalDateTime.now());
    }
}

# 15.6. 监控 - metrics 端点 - 配置

Insights -> Metrics: 设置性能监控指标

统计 URI 调用次数

@RestController
@RequestMapping("/books")
public class BookController {
    @Autowired
    private MeterRegistry meterRegistry;
    private Counter counter;

    @PostConstruct
    public void init() {
        this.counter = meterRegistry.counter("统计执行次数");
    }

    @GetMapping("/count")
    public double count() {
        this.counter.increment();
        return this.counter.count();
    }
}

# 15.7. 监控 - 自定义端点

自定义端点 (pay):

@Component
@Endpoint(id = "pay", enableByDefault = true)
public class PayEndpoint {
    // 提供读取端点信息的操作
    @ReadOperation
    public Object getPay() {
        System.out.println("========> pay 端点");
        Map<String, Object> info = new HashMap<>();
        info.put("name", "500g 苹果");
        info.put("price", "10 元");
        info.put("time", LocalDateTime.now());
        return info;
    }

    // 还可以提供写端点的操作
}

查看端点信息: http://localhost/actuator/pay

# 16. 原理篇 - 自动配置

bean 加载方式

bean 加载控制

bean 依赖属性配置

自动配置原理

变更自动配置

# 16.1. bean 加载方式 1 - xml 定义 bean

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="cat" class="org.example.bean.Cat" />
    <bean class="org.example.bean.Dog" />
</beans>
public class App1 {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext1.xml");

        // 根据 id 获取 bean
        Object cat = ctx.getBean("cat");
        System.out.println("cat = " + cat);
        //=> org.example.bean.Cat@e45f292

        String[] beanDefinitionNames = ctx.getBeanDefinitionNames();
        for (String name : beanDefinitionNames) {
            System.out.println("name = " + name);
        }
        /*
        name = cat
        name = org.example.bean.Dog#0
         */
    }
}

# 16.2. bean 加载方式 2 - XML+注解 定义 bean

配置:

<context:component-scan base-package="org.example.bean, org.example.config" />

自定义 bean:

// <bean id="Jerry" class="org.example.bean.Mouse" />
@Component("Jerry")
public class Mouse {
}

第三方 bean:

@Configuration
public class DbConfig {
    @Bean
    // <bean id="myDs" class="DruidDataSource" />
    public DruidDataSource myDs() {
        return new DruidDataSource();
    }
}

# 16.3. bean 加载方式 3 - 配置类+注解 定义 bean

配置类:

package org.example.config;

@ComponentScan({ "org.example" })
public class SpringConfig {
}

应用:

public class App3 {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
    }
}

# 16.4. 扩展: FactoryBean

工厂类:

public class DogFactory implements FactoryBean<Dog> {
    @Override
    public Dog getObject() throws Exception {
        return new Dog();
    }

    @Override
    public Class<?> getObjectType() {
        return Dog.class;
    }

    // 造出来的 bean,默认不是单例
    @Override
    public boolean isSingleton() {
        return false;
    }
}

获取:

System.out.println(ctx.getBean(Dog.class));
System.out.println(ctx.getBean(Dog.class));
/*=>
    org.example.bean.Dog@2bbf180e
    org.example.bean.Dog@2bbf180e
*/

# 16.5. 扩展: ImportResource

加载配置类的同时记载配置文件(系统迁移)

载入 xml 编写的 bean 配置:

@ImportResource("applicationContext1.xml")
public class SpringConfig2 {
}


public class App4 {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig2.class);
    }
}

# 16.6. 扩展: proxyBeanMethods

// proxyBeanMethods 为 true 时,创建的是代理对象,而非原生对象
@Configuration(proxyBeanMethods = true)
public class SpringConfig3 {
    // 该方法在类的内部(或通过 SpringConfig3 的代理对象)调用时,返回的也是单例对象,而非新对象
    @Bean
    public Dog getDog() {
        return new Dog();
    }
}
public class App4 {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig3.class);

        SpringConfig3 springConfig3 = ctx.getBean(SpringConfig3.class);

        System.out.println("springConfig3 = " + springConfig3);
        // org.example.config.SpringConfig3$$EnhancerBySpringCGLIB$$499361e0@58134517

        System.out.println(springConfig3.getDog());
        System.out.println(springConfig3.getDog());
        /* =>
            org.example.bean.Dog@4450d156
            org.example.bean.Dog@4450d156
         */
    }
}

# 16.7. bean 加载方式 4 - @Import

使用 @Import 加载 bean

@Import({Dog.class})
public class SpringConfig5 {
}

被加载的 bean 无需使用注解声明为 bean

public class Dog {
}
public class App5 {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig5.class);

        System.out.println(ctx.getBean(Dog.class));
        System.out.println(ctx.getBean(Dog.class));
    }
}

此形式能有效解耦

# 16.8. bean 加载方式 5 - ctx 动态注册 bean

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig6.class);

ctx.registerBean("myDog", Dog.class);
ctx.registerBean("myDog", Dog.class); // 后注册的 会覆盖 先注册的

ctx.registerBean(Cat.class);

# 16.9. bean 加载方式 6 - @Import 导入 ImportSelector

public class App7 {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig7.class);
    }
}
@Configuration
@Import(MyImportSelector.class)
public class SpringConfig7 {
}
public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        String annotationName = "org.springframework.context.annotation.Configuration";
        boolean flag = annotationMetadata.hasAnnotation(annotationName);

        // 使用 元数据 进行判定

        // SpringConfig7 上有 @Configuration 注解,则载入 Cat
        if (flag) {
            return new String[]{ "org.example.bean.Cat" };
        }

        return new String[]{ "org.example.bean.Dog" };
    }
}

# 16.10. bean 加载方式 7 - @Import 导入 ImportBeanDefinitionRegistrar

// app
public class App8 {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig8.class);
    }
}

// config
@Import(MyRegistrar.class)
public class SpringConfig8 {
}


public class MyRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // 1. 使用元数据进行判断加载哪些 bean

        // 2. 注册 bean
        BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(Dog.class).getBeanDefinition();

        registry.registerBeanDefinition("DaHuang", beanDefinition);
    }
}

# 16.11. bean 加载方式 8 - @Import 导入 BeanDefinitionRegistryPostProcessor

bean 后置处理器,等其它 bean 都注册完,后置处理器再执行

public class App9 {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig9.class);
        IBookService bookService = ctx.getBean("bookService", IBookService.class);
        bookService.check();
    }
}


@Import({MyPostProcessor.class, BookServiceImpl1.class})
public class SpringConfig9 {
}


public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl4.class).getBeanDefinition();
        beanDefinitionRegistry.registerBeanDefinition("bookService", beanDefinition);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
    }
}

# 16.12. bean 加载控制

说明:

  • 根据特定情况对 bean 进行选择性加载

XML 方式声明 bean:

<bean 
    id="bookService" 
    class="org.example.service.impl.BookServiceImpl" />

通过 @Component 定义的 bean:

@Component
public class BookServiceImpl implements BookService {}

通过 @Bean 定义的 bean:

@Component
public class DbConfig {
    @Bean
    public DruidDataSource getDataSource() {
        // ...
    }
}

通过 @Configuration 定义的 bean:

@Configuration
public class SpringConfig {}

上述方式都无法通过编程的方式 控制 加载哪些 bean

# 16.13. bean 加载控制 - 编程式

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        boolean hasMouseClass;
        try {
            Class<?> mouseClass = Class.forName("org.example.bean.Mouse");
            hasMouseClass = true;
        } catch (ClassNotFoundException e) {
            hasMouseClass = false;
        }
        // 如果存在 Mouse 类,则加载 Cat bean
        if (hasMouseClass) {
            return new String[]{ "org.example.bean.Cat" };
        }

        return new String[0];
    }
}

# 16.14. bean 加载控制 - 注解式

使用 @ConditionalOnXxx 注解为 bean 的加载设置条件

public class SpringConfig {
    @Bean
    public Mouse jerry() {
        return new Mouse();
    }

    @Bean
    // @ConditionalOnClass(name = "org.example.bean.Mouse") // 有 Mouse 类 则加载 Cat
    // @ConditionalOnMissingClass("org.example.bean.Xxx") // 没有 Xxx 类 则加载 Cat
    @ConditionalOnBean(name = "jerry") // 有 id 为 "jerry" 的 bean 则加载 Cat
    @ConditionalOnNotWebApplication // 非 web 应用
    public Cat tom() {
        return new Cat();
    }

    // 应用场景: 只要引入某项技术的依赖,则相关配置都会加载

    @Bean
    @ConditionalOnClass(name = "com.mysql.jdbc.Driver") // 导入 mysql 驱动的坐标才加载
    public DruidDataSource dataSource() {
        return new DruidDataSource();
    }
}

# 16.15. bean 依赖属性配置

将业务功能 bean 运行所需要的配置,抽取成独立的属性类(XxxProperties),该属性类可以读取配置文件(application.yml):

@ConfigurationProperties(prefix = "film")
@Data
public class FilmProperties {
    private Cat cat;
}

配置文件中使用 固定格式 为属性类注入数据:

film:
  cat:
    name: Tom2
    age: 4

业务 bean 关联 属性类,业务 bean 实际加载时才会加载 属性类的 bean:

@EnableConfigurationProperties(FilmProperties.class)
public class Film {
    private Cat cat;

    private FilmProperties filmProperties;

    public Film(FilmProperties filmProperties) {
        this.filmProperties = filmProperties;

        // 默认值
        this.cat = Cat.of("Tom", 3);

        Cat cat = this.filmProperties.getCat();

        // 配置值
        if (cat != null) {
            String name = cat.getName();
            Integer age = cat.getAge();
            if (StringUtils.hasText(name)) {
                this.cat.setName(name);
            }
            if (age != null) {
                this.cat.setAge(age);
            }
        }

    }

    public void play() {
        System.out.printf("%s岁的%s 开始表演", cat.getAge(), cat.getName());
    }
}

通过 @Import 加载 业务 bean:

@SpringBootApplication
@Import({Film.class})
public class App {
    public static void main(String[] args) {
        ConfigurableApplicationContext ctx = SpringApplication.run(App.class);
        Film cartoonFilm = ctx.getBean(Film.class);
        cartoonFilm.play();
    }
}

总结:

  1. 设置属性: 通过 配置文件 给 业务 bean 设置属性
  2. 默认属性: 业务 bean 的属性可以设置默认值
  3. 按需加载: 业务 bean 尽量避免直接加载,根据需要通过 @Import 加载,减轻 spring 容器的压力

# 16.16. 自动配置 - 思想

(1) 技术集A

  • 收集并整理 Spring 开发者常用技术列表

(2) 设置集B

  • 收集并整理 常用技术列表 中每个技术的常用设置

(3) 初始化环境

  • 初始化 SpringBoot 基础环境
  • 加载 用户自定义的 bean 及 依赖

(4) 定义技术集A

  • 定义 技术集A 的所有技术都做成 自动配置类,并全部加载(进类加载器)

(5) 按条件实例化

  • 技术集A 中的各个自动配置类,按条件实例化成 bean
  • 根据 初始化环境及开发者(引入的依赖) 决定实例化 那些技术 对应的 bean

(6) 默认配置

  • 将 设置集B 作为默认配置加载
  • 约定大于配置

(7) 个性化配置

  • 开放 设置集B 的配置接口,用于覆盖

# 16.17. 自动配置 - 原理

自定义自动配置: (src/main/resources/META-INF/spring.factories)

org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.example.bean.MyAutoConfiguration

SpringBoot 应用启动后,会 自动实例化 列出来的类

MyAutoConfiguration:

// 如果存在 Cat 类(通过依赖导入的类)则实例化为 bean
@ConditionalOnClass(name = "org.example.bean.Cat")
// 读取 application.yml 中的配置
@ConfigurationProperties(prefix = "file.cat")
public class MyAutoConfiguration {
    private String name = "默认名称";

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "MyAutoConfiguration{name='" + name + '\'' + '}';
    }
}

通过配置也可以排除掉自动装配的类:

spring:
  autoconfigure:
    exclude:
      - org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration

# 17. 原理篇 - 自定义 starter

案例: 统计独立 IP 访问次数

自定义 starter

辅助功能开发

# 17.1. 案例 - 统计独立 IP 访问次数

案例:

  1. 每次访问网站行为都进行统计
  2. 后台每 10 秒输出一次监控信息(格式: IP + 访问次数)

需求分析:

  1. 数据记录位置: Map / Redis
  2. 功能触发位置: 每次 web 请求(拦截器)
  3. 业务参数(配置项)
    • 输出频率: 默认 10 秒
    • 数据特征: 累计数据(默认) / 阶段数据
    • 输出格式: 详细模式(默认) / 极简模式

# 17.2. 案例 - 基本实现

业务功能开发:

public class IpCountService {
    private Map<String, Integer> ipCountMap = new HashMap<>();

    @Autowired
    private HttpServletRequest httpServletRequest;

    public void count() {
        String ip = httpServletRequest.getRemoteAddr();

        System.out.println("IP = " + ip);

        Integer count = ipCountMap.get(ip);

        if (count == null) {
            count = 0;
        }

        ipCountMap.put(ip, count + 1);
    }
}

自动配置类:

public class IpAutoConfiguration {
    @Bean
    public IpCountService ipCountService() {
        return new IpCountService();
    }
}

配置: (src/main/resources/META-INF/spring.factories)

org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.example.autoconfig.IpAutoConfiguration

其它项目引入:

<dependency>
    <groupId>cn.example</groupId>
    <artifactId>ip_spring_boot_starter</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

其它项目模拟调用:

@RestController
@RequestMapping("/books")
public class BookController {
    @Autowired
    private IpCountService ipCountService;

    @GetMapping("/{currentPage}/{pageSize}")
    public R getPage(@PathVariable Integer currentPage, @PathVariable Integer pageSize, Book book) {
        ipCountService.count();
        // ...
    }
    
}

# 17.3. 案例 - 定时任务报表开发

开启定时任务:

@EnableScheduling
public class IpAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public IpCountService ipCountService() {
        return new IpCountService();
    }
}

设置定时任务:

public class IpCountService {
    private Map<String, Integer> ipCountMap = new HashMap<>();

    // ...

    @Scheduled(cron = "*/5 * * * * *")
    public void print() {
        System.out.println("     IP 访问监控     ");
        System.out.println("+----- ip address -----+- num -+");
        // %s 字符串
        // %20s 20位的宽
        // %-20s 20位的宽,左对齐
        // System.out.println(String.format("|  %-20s|%5d  |", "abc", 123));

        for (Map.Entry<String, Integer> entry : ipCountMap.entrySet()) {
            System.out.println(String.format("|  %-20s|%5d  |", entry.getKey(), entry.getValue()));
        }
        System.out.println("+----------------------+-------+");
    }
}

# 17.4. 案例 - 设置功能参数 1

定义属性类,加载对应属性:

package cn.example.properties;

@ConfigurationProperties(prefix = "tools.ip")
@Data
public class IpProperties {
    /** 显示周期 */
    private Long cycle = 5L;

    /** 是否 新的周期开始时 重置数据 */
    private Boolean cycleReset = false;

    /** 输出模式。detail - 详细模式;simple - 极简模式 */
    private String model = LogModel.DETAIL.value;

    @Getter
    public enum LogModel {
        DETAIL("detail"),
        SIMPLE("simple");

        private final String value;

        LogModel(String value) {
            this.value = value;
        }

    }
}

设置加载 Properties 类为 bean:

@EnableScheduling
@EnableConfigurationProperties(IpProperties.class) // 加载
public class IpAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public IpCountService ipCountService() {
        return new IpCountService();
    }
}

根据配置类定制功能:

public class IpCountService {

    @Autowired
    private IpProperties ipProperties;

    @Scheduled(cron = "*/5 * * * * *")
    public void print() {
        // 获取配置 进行设置
        String model = ipProperties.getModel();
    }
}

引用该 starter 的项目,可以配置:

tools:
  ip:
    cycle: 10
    cycle-reset: false
    model: "simple"

# 17.5. 案例 - 设置功能参数 2

使用当前项目的配置,设置 频率。【不可取】

// 使用配置文件中的属性,并给与默认值(当没有该配置时)
@Scheduled(cron = "*/${tools.ip.cycle:5} * * * * *")

使用 bean 的属性,设置 频率:

// 使用 bean.属性名 的方式引用
@Scheduled(cron = "*/#{ipProperties.cycle} * * * * *")

通过 @Component 指定 bean 的名称:

@ConfigurationProperties(prefix = "tools.ip")
@Component("ipProperties")
@Data
public class IpProperties {
}

通过 @Import 加载 属性 bean:

@EnableScheduling
@Import(IpProperties.class)
public class IpAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public IpCountService ipCountService() {
        return new IpCountService();
    }
}

# 17.6. 案例 - 拦截器

定义:

public class IpInterceptor implements HandlerInterceptor {
    @Autowired
    private IpCountService ipCountService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        ipCountService.count();
        return true;
    }
}

注册:

@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(ipInterceptor()).addPathPatterns("/**");
    }

    @Bean
    public IpInterceptor ipInterceptor() {
        return new IpInterceptor();
    }
}

加载:

@EnableScheduling
@Import({IpProperties.class, SpringMvcConfig.class})
public class IpAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public IpCountService ipCountService() {
        return new IpCountService();
    }
}

# 17.7. 案例 - 开启 yml 提示功能

依赖: (生成提示文件)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

执行打包后,生成 target/classes/META-INF/spring-configuration-metadata.json

拷贝到资源目录 src/main/resources/META-INF/spring-configuration-metadata.json

编辑 hints 属性:

{
  "hints": [
    {
      "name": "tools.ip.model",
      "values": [
        {
          "value": "detail",
          "description": "详情."
        },
        {
          "value": "simple",
          "description": "极简."
        }
      ]
    }
  ]
}

注释掉依赖,重新打包

# 18. 原理篇 - 核心原理

SpringBoot 启动流程

容器类型选择

监听器

# 18.1. SpringBoot 启动流程

大体流程:

  1. 初始化属性,加载成对象
    • 读取环境属性(Environment)
    • 系统配置(spring.factories)
    • 参数(Arguments、application.properties)
  2. 创建 Spring 容器对象 ApplicationContext,加载各种配置
  3. 在容器创建前,通过监听器机制,应对不同阶段加载数据、更新数据的需求
  4. 容器初始化过程中追加各种功能,例如统计时间、输入日志等

详细流程:

Application【 9 】-> SpringApplication.run(Application.class, args);
    SpringApplication.java 【 1332 】 -> return run(new Class&lt;?>[] { primarySource }, args);
        SpringApplication.java 【 1343 】 -> return new SpringApplication(primarySources).run(args);
            SpringApplication.java 【 1343 】 -> new SpringApplication(primarySources)
            # 加载各种配置信息,初始化各种配置对象
                SpringApplication.java 【 266 】 -> this(null, primarySources);
                    SpringApplication.java 【 280 】 -> public SpringApplication(ResourceLoader resourceLoader, Class&lt;?>... primarySources) 
                        SpringApplication.java 【 281 】 -> this.resourceLoader = resourceLoader;
                        # 扩大应用范围,将局部变量 resourceLoader 升级为成员变量
                        SpringApplication.java 【 283 】 -> this.primarySources = new LinkedHashSet&lt;>(Arrays.asList(primarySources));
                        # 配置类 可变参数 转换为 Set
                        SpringApplication.java 【 284 】 -> this.webApplicationType = WebApplicationType.deduceFromClasspath();
                        # 判定容器的类型 - none | web
                        SpringApplication.java 【 285 】 -> this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories();
                        # 获取系统配置引导信息
                        SpringApplication.java 【 286 】 -> setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
                        # 获取 ApplicationContextInitializer.class 的实例
                        SpringApplication.java 【 287 】 -> setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
                        # 初始化监听器,对初始化过程及运行过程进行干预
                        SpringApplication.java 【 288 】 -> this.mainApplicationClass = deduceMainApplicationClass();
                        # 初始化引导类 类名信息

            SpringApplication.java 【1343】 -> new SpringApplication(primarySources).run(args);
            # 初始化容器,得到 ApplicationContext 对象
                StopWatch stopWatch = new StopWatch();
                # 计时器:初始化
                stopWatch.start();
                # 计时器:开始
                DefaultBootstrapContext bootstrapContext = createBootstrapContext();
                # 系统引导信息上下文对象
                ConfigurableApplicationContext context = null;
                #
                configureHeadlessProperty();
                # 模拟输入输出设置,避免没有而导致错误: java.awt.headless = true
                SpringApplicationRunListeners listeners = getRunListeners(args);
                # 注册监听器
                listeners.starting(bootstrapContext, this.mainApplicationClass);
                # 监听器执行对应的步骤: starting
                ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
                # 获取命令行参数
                ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
                # 将前期读取的数据 转换为 环境对象
                configureIgnoreBeanInfo(environment);
                # 配置一个系统参数
                Banner printedBanner = printBanner(environment);
                # 打印 banner
                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);
                # 监听器执行对应的步骤: started
                callRunners(context, applicationArguments);
                listeners.running(context);
                # 监听器执行对应的步骤: running

监听器类型:

...

本章目录