1 Star 0 Fork 111

wanghannew1 / spring-boot-study

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README
Apache-2.0

介绍

代码仓库地址:https://gitee.com/CandyWall/spring-boot-study 跟着黑马程序员spring boot教程做的学习笔记,本笔记跟视频内容的项目名称和代码略有不同,都是基于我自己的考量,代码都已经过运行验证过的,仅供参考。

视频教程地址:https://www.bilibili.com/video/BV15b4y1a7yG

注:四级标题和部分5级标题是使用子项目名称命名的,和我代码仓库的项目是一一对应的。

每个子项目对应的视频链接以及一些重要内容的笔记

一、基础篇

1.入门案例

springboot_01_01_quickstart

P3 基础篇-03-SpringBoot入门案例(Idea联网版)18:12

springboot_01_02_quickstart

P4 基础篇-04-SpringBoot入门案例(官网创建版)06:21

springboot_01_03_quickstart

P5 基础篇-05-SpringBoot入门案例(阿里云版)06:01

springboot_01_04_quickstart

P6 基础篇-06-SpringBoot入门案例(手工制作版)08:34

P7 基础篇-07-教你一招:隐藏文件或文件夹

P8 基础篇-08-入门案例解析:parent

P9 基础篇-09-入门案例解析:starter

P10 基础篇-10-入门案例解析:引导类

P11 基础篇-11-入门案例:辅助功能:更换SpringBoot内嵌的默认的web容器从tomcat换成jetty

1.1 补充

springboot_01_05_rest

P12 知识加油站-01-REST风格简介

P13 知识加油站-02-RESTful入门案例

P14 知识加油站-03-RESTful快速开发

springboot_0x_02x_xxxxxxxx

2. 基础配置

P15 基础篇-12-教你一招:复制模块

springboot_02_base_configuration

P16 基础篇-13-属性配置方式

P17 基础篇-14-基础配置

P18 基础篇-15-3种配置文件类型

  1. properties

  2. yml(主流格式)

  3. yaml

P19 基础篇-16-配置文件加载优先级

三种格式共存,优先级从先到后为properties、yml、yaml

P20 基础篇-17-教你一招:属性提示消失解决方案

3. yaml配置文件

springboot_03_yaml

P21 基础篇-18-yaml数据格式

P22 基础篇-19-读取yaml单一属性数据

P23 基础篇-20-yaml文件中的变量引用

P24 基础篇-21-读取yaml全部属性数据

P25 基础篇-22-读取yaml引用类型属性数据

4. 整合Junit

springboot_04_junit

P27基础篇-24-整合JUnit——classes属性

5. 整合mybatis

springboot_05_mybatis

P28基础篇-25-SpringBoot整合MyBatis

课程中使用到的数据库脚本

/*
 Navicat MySQL Data Transfer

 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 80023
 Source Host           : localhost:3306
 Source Schema         : springboot_db

 Target Server Type    : MySQL
 Target Server Version : 80023
 File Encoding         : 65001

 Date: 20/01/2022 11:50:34
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for tbl_book
-- ----------------------------
DROP TABLE IF EXISTS `tbl_book`;
CREATE TABLE `tbl_book`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `description` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of tbl_book
-- ----------------------------
INSERT INTO `tbl_book` VALUES (1, '三体', '科幻', '大刘的巅峰之作,将中国科幻推向世界舞台。总共分为三部曲:《地球往事》、《黑暗森林》、《死神永生》。');
INSERT INTO `tbl_book` VALUES (2, '格林童话', '童话', '睡前故事。');
INSERT INTO `tbl_book` VALUES (3, 'Spring 5设计模式', '计算机理论', '深入Spring源码剖析Spring源码中蕴含的10大设计模式');
INSERT INTO `tbl_book` VALUES (4, 'Spring MVC+ MyBatis开发从入门到项目实战', '计算机理论', '全方位解析面向Web应用的轻量级框架,带你成为Spring MVC开发高手');
INSERT INTO `tbl_book` VALUES (5, '轻量级Java Web企业应用实战', '计算机理论', '源码级剖析Spring框架,适合已掌握Java基础的读者');
INSERT INTO `tbl_book` VALUES (6, 'Java核心技术卷|基础知识(原书第11版)', '计算机理论', 'Core Java第11版,Jolt大奖获奖作品,针对Java SE9、10、 11全面更新');
INSERT INTO `tbl_book` VALUES (7, '深入理解Java虚拟机', '计算机理论', '5个维度全面剖析JVM,面试知识点全覆盖');
INSERT INTO `tbl_book` VALUES (8, 'Java编程思想(第4版)', '计算机理论', 'Java学习必读经典殿堂级著作!赢得了全球程序员的广泛赞誉');
INSERT INTO `tbl_book` VALUES (9, '零基础学Java (全彩版)', '计算机理论', '零基础自学编程的入门]图书,由浅入深,详解Java语言的编程思想和核心技术');
INSERT INTO `tbl_book` VALUES (10, '直播就该这么做:主播高效沟通实战指南', '市场营销', '李子柒、李佳琦、薇娅成长为网红的秘密都在书中');
INSERT INTO `tbl_book` VALUES (11, '直播销讲实战一本通', '市场营销', '和秋叶一起学系列网络营销书籍');
INSERT INTO `tbl_book` VALUES (12, '直播带货:淘宝、天猫直播从新手到高手', '市场营销', '一本教你如何玩转直播的书, 10堂课轻松实现带货月入3W+');
INSERT INTO `tbl_book` VALUES (13, 'Spring实战第5版', '计算机理论', 'Spring入门经典教程,深入理解Spring原理技术内幕');
INSERT INTO `tbl_book` VALUES (14, 'Spring 5核心原理与30个类手写实战', '计算机理论', '十年沉淀之作,写Spring精华思想');

SET FOREIGN_KEY_CHECKS = 1;

P29 基础篇-26-SpringBoot整合MyBatis常见问题处理

6. 整合mybatis-plus

springboot_06_mybatis_plus

P30 基础篇-27-SpringBoot整合MyBatisPlus

7. 整合druid数据库连接池

springboot_07_druid

P31 基础篇-28-SpringBoot整合Druid

8. springboot基础篇综合案例

springboot_08_ssmp

P32 基础篇-29-SSMP整合案例制作分析

P33 基础篇-30-模块创建

P34 基础篇-31-实体类快速开发(lombok)

P35 基础篇-32-数据层标准开发(基础CRUD)

P36 基础篇-33-开启MP运行日志

P38 基础篇-35-数据层标准开发(条件查询)

P39 基础篇-36-业务层标准开发(基础CRUD)

P40 基础篇-37-业务层快速开发(基于MyBatisPlus构建)

P41 基础篇-38-表现层标准开发

P42 基础篇-39-表现层数据一致性处理(R对象)

P43 基础篇-40-前后端调用(axios发送异步请求)

P44 基础篇-41-列表功能

P45 基础篇-42-添加功能

P46 基础篇-43-删除功能

P47 基础篇-44修改功能(加载数据)

P48 基础篇-45-修改功能

P49 基础篇-46-异常消息处理

P50 基础篇-47-分页

P51 基础篇-48-分页功能维护(删除BUG)

P52 基础篇-49-条件查询

P53 基础篇-50-基础篇完结

二、运维实用篇

P54 运维实用篇-51-工程打包与运行

P55 运维实用篇-52-打包插件

P56 运维实用篇-53-Boot工程快速启动(Linux版)

# 后台启动springboot项目jar包
nohup java -jar springboot_08_ssmp-0.0.1-SNAPSHOT.jar > server.log 2>&1 &
# 终止程序
# 查看java -jar 命令对应的进程号
ps -ef | grep "java -jar"
# 终止对应进程
kill -9 <pid>

P57 运维实用篇-54-临时属性

P58 运维实用篇-55-临时属性(开发环境)

P59 运维实用篇-56-配置文件4级分类

9. 使用自定义配置

springboot_09_config

P60 运维实用篇-57-自定义配置文件

  • 方法1:设置程序参数,指定文件名(不包含后缀),如:--spring.config.name=ebank

image-20220121134942535

  • 方法2:指定配置文件的路径,如:--spring.config.location=classpath:/ebank.yml

image-20220121135527619

  • 还可以指定多个配置文件,如:--spring.config.location=classpath:/ebank.yml,classpath:/ebank-server.yml

image-20220121140035574

10. 多环境配置

springboot_10_profiles

P61 运维实用篇-58-多环境开发(yaml版)

单文件版:

application.yml文件:

# 应用环境
# 公共配置
spring:
  profiles:
    active: dev
---
# 生产环境
spring:
  config:
    activate:
      on-profile: pro
server:
  port: 80
---
spring:
  config:
    activate:
      on-profile: dev
server:
  port: 8080
---
# 测试环境
spring:
  config:
    activate:
      on-profile: test
server:
  port: 8888

P62 运维实用篇-59-多环境开发多文件版(yaml版)

yaml多文件版

application.yml

# 应用环境
# 公共配置
spring:
  profiles:
    active: test

application-dev.yml

# 开发环境
server:
  port: 8080

application-pro.yml

# 生产环境
server:
  port: 80

application-test.yml

# 测试环境
server:
  port: 8888

P63 运维实用篇-60-多环境开发多文件版(properties版)

properties多文件版

application.properties

# 应用环境
spring.profiles.active=test

application-dev.properties

# 开发环境
server.port=80

application-pro.properties

# 生产环境
server.port=8080

application-test.properties

# 测试环境
server.port=8888

P64 运维实用篇-61-多环境分组管理

使用group属性配置同组的配置文件,active可以直接使一组配置文件同时生效

application.yml

#spring:
#  profiles:
#    active: dev
#    include: devDB, devMVC

spring:
  profiles:
    active: dev
    group:
      "dev": devDB, devMVC
      "pro": proDB, proMVC
      "test": testDB, TestMVC

application-dev.yml

server:
  port: 80

application-devDB.yml

server:
  port: 81

application-devMVCyml

server:
  servlet:
    context-path: /ebank
  port: 82

观察启动日志,配置文件加载的顺序为 : The following profiles are active: dev,devDB,devMVC

P65 运维实用篇-62-多环境开发控制

maven中使用多环境,然后在springboot中读取maven中的配置

pom.xml中的配置

<!--设置多环境-->
<profiles>
    <profile>
        <id>env_dev</id>
        <properties>
            <profile.active>dev</profile.active>
        </properties>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
    </profile>
    <profile>
        <id>env_pro</id>
        <properties>
            <profile.active>pro</profile.active>
        </properties>

    </profile>
    <profile>
        <id>env_test</id>
        <properties>
            <profile.active>test</profile.active>
        </properties>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
    </profile>
</profiles>

application.yml中的配置

#spring:
#  profiles:
#    active: dev
#    include: devDB, devMVC

spring:
  profiles:
    active: @profile.active@
    group:
      "dev": devDB, devMVC
      "pro": proDB, proMVC
      "test": testDB, TestMVC

11. 记录日志

springboot_11_log

P66 运维实用篇-63-日志基础操作

P67 运维实用篇-64-教你一招:快速创建日志对象

P68 运维实用篇-65-日志输出格式控制

P69 运维实用篇-66-文件记录日志(运维实用篇完结)

三、开发实用篇

12. 热部署

springboot_12_hot_deploy

P70 开发实用篇-67-手工启动热部署

想要拥有热部署的功能,需要在pom.xml文件中添加如下依赖:

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

P71 开发实用篇-68-自动启动热部署

自动启动热部署,即,CTRL + F9编译的操作操作由程序自动完成,需要进行如下配置:

  1. 在设置中勾选

image-20220122192552887

  1. 按快捷键CTRL + SHIFT + ALT + /,在弹出的菜单中选择第一项,注册表,或者直接按CTRL + ALT + M调出注册表;

image-20220122193117350

  1. 在注册表项中勾选compiler.automake.allow.when.app.running image-20220122194309467
  2. idea新版本2021.3.1中在注册表中找不到改选项,需要在高级设置中进行设置。 image-20220122193933991

P72 开发实用篇-69-热部署范围配置

P73 开发实用篇-70-关闭热部署功能

application.yml

spring:
  # 热部署范围配置
  devtools:
    restart:
      # 设置不参与热部署的文件和文件夹(即修改后不重启)
      exclude: static/**,public/**,config/application.yml
      #是否可用
      enabled: false

如果配置文件比较多的时候找热部署对应配置比较麻烦,可以在springboot启动类的main方法中设置,此处设置的优先级将比配置文件高,一定会生效。

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

13. 属性绑定

springboot_13_configuration

P74 实用开发篇-71-第三方bean属性绑定

  1. 先在要配置的类上面加@Component注解将该类交由spring容器管理;

  2. @ConfigurationProperties(prefix="xxx")xxxapplication.yml配置文件中的属性对应;

  3. 如果多个配置类想统一管理也可以通过@EnableConfigurationProperties({xxx.class, yyy.class})的方式完成配置,不过该注解会与@Component配置发生冲突,二选一即可;

  4. 第三方类对象想通过配置进行属性注入,可以通过创建一个方法,在方法体上加@Bean@ConfigurationProperties(prefix="xxx")注解,然后方法返回这个第三方对象的方式。

  5. 使用@ConfigurationProperties(prefix="xxx")注解后idea工具会报一个警告Spring Boot Configuration Annotation Processor not configured

    image-20220123172141642

    只需要在pom.xml中加上如下依赖刷新即可消除该警告

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
    </dependency>

P75 实用开发篇-72-松散绑定

@ConfigurationProperties绑定属性支持属性名宽松绑定,又叫松散绑定。

比如要将ServerConfig.class作为配置类,并通过配置文件application.yml绑定属性

ServerConfig.class

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

application.yml

serverConfig:
  # ipAddress: 192.168.0.1 # 驼峰模式
  # ipaddress: 192.168.0.1
  # IPADDRESS: 192.168.0.1
  ip-address: 192.168.0.1 # 主流配置方式,烤肉串模式
  # ip_address: 192.168.0.1 # 下划线模式
  # IP_ADDRESS: 192.168.0.1 # 常量模式
  # ip_Add_rEss: 192.168.0.1
  # ipaddress: 192.168.0.1
  port: 8888
  timeout: -1

ipAddress属性为例,上面的多种配置方式皆可生效,这就是松散绑定。而@Value不支持松散绑定,必须一一对应。

@ConfigurationProperties(prefix="serverconfig")中的prefix的值为serverconfig或者server-config,如果是serverConfig就会报错,这与松散绑定的前缀命名规范有关:仅能使用纯小写字母、数字、中划线作为合法的字符

P76 实用开发篇-73-常用计量单位应用

//@Component
@ConfigurationProperties(prefix = "server-config")
@Data
public class ServerConfig {
   private String ipAddress;
   private int port;
   @DurationUnit(ChronoUnit.MINUTES)
   private Duration timeout;

   @DataSizeUnit(DataUnit.MEGABYTES)
   private DataSize dataSize;
}

P77 实用开发篇-74-bean属性校验

引入Bean属性校验框架的步骤:

  1. pom.xml中添加JSR303规范和hibernate校验框架的依赖:
<!--导入JSR303规范-->
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
</dependency>
<!--使用hibernate框架提供的校验器-->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
</dependency>
  1. 在要校验的类上加@Validated注解
  2. 设置具体的校验规则,如:@Max(value=8888, message="最大值不能超过8888")
@ConfigurationProperties(prefix = "server-config")
@Data
// 2.开启对当前bean的属性注入校验
@Validated
public class ServerConfig {
    private String ipAddress;
    // 设置具体的规则
    @Max(value = 8888, message = "最大值不能超过8888")
    @Min(value = 1000, message = "最小值不能低于1000")
    private int port;
    @DurationUnit(ChronoUnit.MINUTES)
    private Duration timeout;

    @DataSizeUnit(DataUnit.MEGABYTES)
    private DataSize dataSize;
}

P78 实用开发篇-75-进制数据转换规则

进制转换中的一些问题:

application.yml文件中对数据库有如下配置:

datasource:
  driverClassName: com.mysql.cj.jdbc.Driver123
  # 不加引号读取的时候默认解析为了8进制数,转成十进制就是87
  # 所以想让这里正确识别,需要加上引号
  # password: 0127
  password: "0127"

14. 测试相关

P79 实用开发篇-76-加载测试专用属性

springboot_14_test

@SpringBootTest注解中可以设置propertiesargs属性,这里的args属性的作用跟idea工具中自带的程序参数类似,只不过这里的配置是源码级别的,会随着源码的移动而跟随,而idea中的程序参数的配置会丢失。并且这里的args属性的配置的作用范围比较小,仅在当前测试类生效。

application.yml

test:
  prop: testValue

PropertiesAndArgsTest.java

// properties属性可以为当前测试用例添加临时的属性配置
//@SpringBootTest(properties = {"test.prop=testValue1"})
// args属性可以为当前测试用例添加临时的命令行参数
//@SpringBootTest(args = {"--test.prop=testValue2"})
// 优先级排序: args > properties > 配置文件
@SpringBootTest(args = {"--test.prop=testValue2"}, properties = {"test.prop=testValue1"})
class PropertiesAndArgsTest {
    @Value("${test.prop}")
    private String prop;
    @Test
    public void testProperties() {
        System.out.println("prop = " + prop);
    }
}

P80 实用开发篇-77-加载测试专用配置

某些测试类中需要用到第三方的类,而其他测试类则不需要用到,这里可以在类上加载@Import({xxx.class, yyy.class})

P81 实用开发篇-78-测试类中启动web环境

P82 实用开发篇-79-发送虚拟请求

P83 实用开发篇-80-匹配响应执行状态

P84 实用开发篇-81-匹配响应体

P85 实用开发篇-82-匹配响应体(json)

P86 实用开发篇-83-匹配响应头

表现层BookController.java

@RestController
@RequestMapping("/books")
public class BookController {
    /*@GetMapping("/{id}")
    public String getById(@PathVariable int id) {
        System.out.println("id = " + id);
        return "getById...";
    }*/

    @GetMapping("/{id}")
    public Book getById(@PathVariable int id) {
        System.out.println("id = " + id);
        Book book = new Book();
        book.setId(5);
        book.setName("神秘岛");
        book.setType("科幻");
        book.setDescription("《神秘岛》是法国科幻小说家儒勒·凡尔纳创作的长篇小说,是他写的三部曲之一。");

        return book;
    }
}

对应的测试类WebTest.java

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
// 开启虚拟mvc调用
@AutoConfigureMockMvc
public class WebTest {
    @Test
    public void testRandomPort() {
    }

    @Test
    public void testWeb(@Autowired MockMvc mvc) throws Exception {
        // 创建虚拟请求,当前访问 /books
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/5");
        mvc.perform(builder);
    }

    @Test
    public void testStatus(@Autowired MockMvc mvc) throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books1/6");
        ResultActions action = mvc.perform(builder);
        // 设定预期值,与真实值进行比较,成功测试通过,失败测试不通过
        // 定义本次调用的预期值
        StatusResultMatchers srm = MockMvcResultMatchers.status();
        // 预计本次调用成功的状态码:200
        ResultMatcher ok = srm.isOk();
        // 添加预计值到本次调用过程中进行匹配
        action.andExpect(ok);
    }

    @Test
    public void testBody(@Autowired MockMvc mvc) throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/6");
        ResultActions action = mvc.perform(builder);
        // 设定预期值,与真实值进行比较,成功测试通过,失败测试不通过
        // 定义本次调用的预期值
        ContentResultMatchers crm = MockMvcResultMatchers.content();
        // 预计本次调用成功的状态码:200
        ResultMatcher rm = crm.string("getById...");
        // 添加预计值到本次调用过程中进行匹配
        action.andExpect(rm);
    }

    @Test
    public void testJson(@Autowired MockMvc mvc) throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/7");
        ResultActions action = mvc.perform(builder);
        // 设定预期值,与真实值进行比较,成功测试通过,失败测试不通过
        // 定义本次调用的预期值
        ContentResultMatchers jsonMatcher = MockMvcResultMatchers.content();
        ResultMatcher rm = jsonMatcher.json("{\"id\":5,\"name\":\"神秘岛\",\"type\":\"科幻\",\"description\":\"《神秘岛》是法国科幻小说家儒勒·凡尔纳创作的长篇小说,是他写的三部曲之一。1\"}");
        action.andExpect(rm);
    }

    @Test
    public void testContentType(@Autowired MockMvc mvc) throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/7");
        ResultActions action = mvc.perform(builder);
        // 设定预期值,与真实值进行比较,成功测试通过,失败测试不通过
        // 定义本次调用的预期值
        HeaderResultMatchers hrm = MockMvcResultMatchers.header();
        ResultMatcher rm = hrm.string("Content-Type", "application/json");
        action.andExpect(rm);
    }

    @Test
    // 完整测试
    public void testGetById(@Autowired MockMvc mvc) throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/8");
        ResultActions action = mvc.perform(builder);

        // 1、比较状态码
        StatusResultMatchers statusResultMatchers = MockMvcResultMatchers.status();
        ResultMatcher statusResultMatcher = statusResultMatchers.isOk();
        action.andExpect(statusResultMatcher);

        // 2、比较返回值类型
        HeaderResultMatchers headerResultMatchers = MockMvcResultMatchers.header();
        ResultMatcher headerResultMatcher = headerResultMatchers.string("Content-Type", "application/json");
        action.andExpect(headerResultMatcher);

        /// 3、比较json返回值
        ContentResultMatchers contentResultMatchers = MockMvcResultMatchers.content();
        ResultMatcher jsonResultMatcher = contentResultMatchers.json("{\"id\":5,\"name\":\"神秘岛\",\"type\":\"科幻\",\"description\":\"《神秘岛》是法国科幻小说家儒勒·凡尔纳创作的长篇小说,是他写的三部曲之一。\"}");
        action.andExpect(jsonResultMatcher);
    }
}

P87 实用开发篇-84-业务层测试事务回滚

测试过程中对数据库的增删改操作的影响是否回滚,由下面两个注解控制,需要在测试类上加:

@Transactional@Rollback(value=true):回滚,@Rollback(value=true)为默认值,也可以省略;

@Transactional@Rollback(value=false):不回滚,跟什么注解都不加的效果一样。

P88 实用开发篇-85-测试用例设置随机数据

可以把测试用例中的属性值都按照一定规则设置成随机值,可以让测试结果更具有普适性。并且可以把测试用例的属性的随机规则写在配置文件中,方便更改。

application.yml

testcase:
  randomBook:
    id: ${random.int}
    id2: ${random.int(10)} # 生成10以内的整数
    type: ${random.int(5, 10)} # 生成5-10之间的整数
    name: 糖果墙${random.value}
    uuid: ${random.uuid}
    publicTime: ${random.long}

BookCase.java

@Data
@Component
@ConfigurationProperties(prefix = "test-case.random-book")
public class BookCase {
    private Integer id;
    private Integer id2;
    private String type;
    private String name;
    private String uuid;
    private Long publishTime;
}

15. 数据层解决方案

springboot_15_01_datasource

P89 实用开发篇-86-内置数据源

springboot项目中使用Druid数据源,需要先在pom.xml中加上Druid的依赖:

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

然后在application.yml中有两种配置方法,两种方法实现的效果一样

# 配法1:
spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/springboot_db?serverTimezone=UTC
      username: root
      password: 123

# 配法2:
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springboot_db?serverTimezone=UTC
    username: root
    password: 123
    type: com.alibaba.druid.pool.DruidDataSource # 去掉type属性,Druid数据源依然会启用

配法2去掉type属性后,再启动项目,发现Druid数据源依然启用了,这是由于引入了Druid数据源的依赖后,springboot会自动配置Druid

如果不引入Druid数据源的依赖,springboot默认的数据源是Hikari数据源

# 默认为Hikari数据源
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springboot_db?serverTimezone=UTC
    username: root
    password: 123
    
# Hikari数据源详细配置,这里需要注意url需要和Hikari属性并列,而Hikari下的jdbc-url无效
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/springboot_db?serverTimezone=UTC
    hikari:
      # jdbc-url: jdbc:mysql://localhost:3306/springboot_db?serverTimezone=UTC # 此项无效
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: 123
      maximum-pool-size: 50
      minimum-idle: 30
      idle-timeout: 30000

springboot_15_02_jdbc_template

P90 实用开发篇-87-JdbcTemplate

使用JdbcTemplate

  1. pom.xml中加入相关依赖

  2. application.yml中配置数据源,另外还可以对JdbcTemplate进行一些简单的配置

    # 配置数据库和连接池
    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/springboot_db?serverTimezone=UTC
        hikari:
          driver-class-name: com.mysql.cj.jdbc.Driver
          username: root
          password: TGQ@candywall123
          maximum-pool-size: 50
          minimum-idle: 30
          # idle-timeout: 30000
    
      # JdbcTemplate的一些配置
      jdbc:
        template:
          query-timeout: 30s # 指定查询超时时间
          max-rows: 500 # 最大查询条数
          fetch-size: 500 # 数据条数比较多的时候,一次拿多少条数据
  3. 测试类

    @SpringBootTest
    class JdbcTemplateApplicationTests {
        @Autowired
        private JdbcTemplate jdbcTemplate;
        @Test
        void testJdbcTemplateSelect() {
            String sql = "select * from tbl_book";
            // List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
            // System.out.println(maps);
            RowMapper<Book> rm = new RowMapper<Book>() {
                @Override
                public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
                    Book book = new Book();
                    book.setId(rs.getInt("id"));
                    book.setName(rs.getString("name"));
                    book.setType(rs.getString("type"));
                    book.setDescription(rs.getString("description"));
                    return book;
                }
            };
            List<Book> bookList = jdbcTemplate.query(sql, rm);
            bookList.forEach(System.out::println);
        }
        
        @Test
        void testJdbcTemplateInsert() {
            String sql = "insert into tbl_book values(null, ?, ?, ?)";
            jdbcTemplate.update(sql, "springboot1", "springboot2", "springboot3");
        }
    }

springboot_15_03_h2

P91 实用开发篇-88-H2数据库

使用H2数据库

  1. pom.xml中加入相关依赖

  2. application.yml中配置数据源并且启用H2数据库的控制台

    server:
      port: 80
      
    spring:
      # 配置数据库和连接池
      datasource:
        url: jdbc:h2:~/test
        hikari:
          driver-class-name: org.h2.Driver
          username: sa
          password: 123456
    
      # 启用H2的控制台
      h2:
        console:
          enabled: true
          path: /h2
  3. 在浏览器中输入访问H2控制台地址

    image-20220124170318554

  4. 输入默认密码:123456,然后点连接,会跳转到控制台主页

    image-20220124170509519

  5. 可以写sql语句建表,并插入几条数据

    create table tbl_book(
       id int,
       type varchar,
       name varchar,
       description varchar
    );
    
    insert into tbl_book values(1, 'springboot1', 'springboot2', 'springboot3');
    insert into tbl_book values(2, 'springboot4', 'springboot5', 'springboot6');
    insert into tbl_book values(3, 'springboot7', 'springboot8', 'springboot9');
    insert into tbl_book values(4, 'springboot10', 'springboot11', 'springboot12');

    image-20220124170949962

    image-20220124171610075

    查询tbl_book表中的数据

    select * from tbl_book;

    image-20220124171707774

  6. 写代码连接H2数据库,需要注意启动测试类连接H2数据库的时候需要将之前的H2控制台的springboot程序先停止,否则会造成端口占用,测试类报错。

    测试类:

    @SpringBootTest
    class H2ApplicationTests {
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Test
        public void testH2Select() {
            String sql = "select * from tbl_book";
    
            RowMapper<Book> rm = new RowMapper<Book>() {
                @Override
                public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
                    Book book = new Book();
                    book.setId(rs.getInt("id"));
                    book.setName(rs.getString("name"));
                    book.setType(rs.getString("type"));
                    book.setDescription(rs.getString("description"));
                    return book;
                }
            };
            List<Book> bookList = jdbcTemplate.query(sql, rm);
            bookList.forEach(System.out::println);
        }
    
        @Test
        public void testH2Save() {
            String sql = "insert into tbl_book values(?, ? ,? ,?)";
            jdbcTemplate.update(sql, 5, "啊哈算法", "计算机", "数据结构和算法");
        }
    }
  7. H2数据库线上运行时请务必关闭。

    bind 0.0.0.0
    protected-mode no
    port 6379
    timeout 0
    save 900 1 # 900s内至少一次写操作则执行bgsave进行RDB持久化
    save 300 10
    save 60 10000
    rdbcompression yes
    dbfilename dump.rdb
    dir /data
    appendonly yes
    appendfsync everysec
    requirepass 12345678

springboot_15_04_redis

P92 实用开发篇-89-redis下载安装与基本使用

redis.conf

bind 0.0.0.0
protected-mode no
port 6379
timeout 0
save 900 1 # 900s内至少一次写操作则执行bgsave进行RDB持久化
save 300 10
save 60 10000
rdbcompression yes
dbfilename dump.rdb
dir /data
appendonly yes
appendfsync everysec
requirepass 123456

docker-compose.yml

version: '3'

services:
  redis:
    image: redis:latest
    container_name: redis
    restart: always
    ports:
      - 6379:6379
    volumes:
      - ./redis.conf:/etc/redis/redis.conf:rw
      - ./data:/data:rw
    command: ["redis-server","/etc/redis/redis.conf"]

我的redis安装在虚拟机中的linux系统上,可以通过以下两种方式测试连通性:

  1. 通过windowsredis自带的redis-cli来远程连接linux上的redis服务,指令如下:
redis-cli.exe -h 192.168.0.110 -p 6379 -a "123456" # 123456是密码
  1. 还可以使用AnotherRedisDesktopManager:这是一款非常稳定并且拥有美观的图形界面的redis客户端,操作起来也是相当简单,一看就会用,下载地址:https://github.com/qishibo/AnotherRedisDesktopManager/releases

P93 实用开发篇-90-SpringBoot整合Redis

  1. pom.xml中加入spring整合redis的依赖

    <!--springboot整合redis-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  2. application.yml

    spring:
      # redis配置
      redis:
        host: 192.168.0.110
        port: 6379
        password: 123456
  3. 测试类

    @SpringBootTest
    class RedisApplicationTests {
        @Autowired
        private StringRedisTemplate redisTemplate;
    
        @Test
        public void testSet() {
            ValueOperations valueOperations = redisTemplate.opsForValue();
    
            valueOperations.set("age", "41");
        }
        @Test
        public void testGet() {
            ValueOperations valueOperations = redisTemplate.opsForValue();
            System.out.println("age = " + valueOperations.get("age"));
            System.out.println("username = " + valueOperations.get("username"));
        }
    }

    注:这里如果使用RedisTemplate而不使用StringRedisTemplate,去redis客户端里面查看会发现键值包含\xac\xed\x00\x05t\x00\特殊字符,这是由于RedisTemplate<K, V>模板类在操作redis时默认使用JdkSerializationRedisSerializer来进行序列化。而存取序列化的方式从org.springframework.data.redis.serializer.JdkSerializationRedisSerializer

    将序列化的方式改为 org.springframework.data.redis.serializer.StringRedisSerializer 会自动去掉\xac\xed\x00\x05t\x00前缀,所以有两种解决方法:

    1. 直接使用StringRedisTemplate

    2. 方案2 修改默认的序列化方式:

      private RedisTemplate redisTemplate;
      @Autowired(required = false)
      public void setRedisTemplate(RedisTemplate redisTemplate) {
          RedisSerializer stringSerializer = new StringRedisSerializer();
          redisTemplate.setKeySerializer(stringSerializer);
          redisTemplate.setValueSerializer(stringSerializer);
          redisTemplate.setHashKeySerializer(stringSerializer);
          redisTemplate.setHashValueSerializer(stringSerializer);
          this.redisTemplate = redisTemplate;
      }

P94 实用开发篇-91-SpringBoot读写Redis的客户端

P95 实用开发篇-92-SpringBoot操作Redis客户端实现技术切换(jedis)

java操作redis底层有两种实现分别为lettuce和jedis,其中lettucespringbootRedisTemplate默认使用的技术。如果想要切换到jedis:

  1. 引入jedis的jar包

  2. application.yml中加入配置

    spring:
      # redis配置
      redis:
        host: 192.168.0.110
        port: 6379
        password: 123456
        client-type: jedis # 默认为lettuce
        # 还可以进一步配置
        jedis:
          pool:
            enabled: true
            max-active: 16
            min-idle: 8

springboot_15_05_mongodb

P96 实用开发篇-93-Mongodb简介

P97 实用开发篇-94-Mongodb下载与安装

解压mongodb安装包,然后在软件根目录下新建data\db,进入到bin目录下启动黑窗口,输入如下命令,启动mongodb数据库,并指定数据保存到data\db目录下。

.\mongod.exe --dbpath=..\data\db

在bin目录下再开一个黑窗口,然后输入

.\mongo.exe

会默认连接iplocalhostport27017mongodb服务,连接成功会输出mongodb的版本等信息。

P98 实用开发篇-95-Mongodb基础操作

由于黑窗口操作较为繁琐,这里推荐使用带图形化界面的客户端robo3t,启动robo3t,创建一个连接

image-20220125005852352 打开这个连接

image-20220125005941396

右击左侧连接名,在右键菜单中选择Create Database,新建一个数据库

image-20220125010258643

填写数据库名称

image-20220125010907061

展开数据库名,右击Collections再新建一个Collection

image-20220125011100353

填写collection名称

image-20220125011142924

右击新建的Collection book,会弹出一个查询界面,可以在文本框中填写指令对Collection book进行操作

image-20220125011509450

// 查询所有
db.getCollection('book').find({})
// 可以简写为
db.book.find();
// 条件查询
db.book.find({type: "springboot"})

// 保存文档
db.book.save({"name": "springboot", type: "springboot"})

// 删除操作
db.book.remove({type: "springboot"});

// 修改操作
// 修改满足条件的第一条数据
db.book.update({name: "springboot"}, {$set:{name: "springboot2"}});
// 修改满足条件的所有数据
db.book.updateMany({name: "springboot"}, {$set:{name: "springboot2"}});

P99 实用开发篇-96-SpringBoot整合Mongodb

pom.xml中加入springboot整合MongoDB的依赖

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

application.yml中进行MongoDB的配置

spring:
  data:
    mongodb:
      uri: mongodb://localhost/springboot_mongodb

测试代码

@SpringBootTest
class MongodbApplicationTests {
    @Autowired
    private MongoTemplate mongoTemplate;

    @Test
    void testSave() {
        Book book = new Book();
        book.setId(2);
        book.setName("springboot2");
        book.setType("springboot2");
        book.setDescription("springboot2");
        mongoTemplate.save(book);
    }

    @Test
    public void testFindAll() {
        List<Book> books = mongoTemplate.findAll(Book.class);
        books.forEach(System.out::println);
    }
}

springboot_15_06_elasticsearch

P100 实用开发篇-97-ES简介

P101 实用开发篇-98-ES下载与安装

  1. 下载地址:https://www.elastic.co/cn/start

  2. 安装

    解压es的安装包,然后去bin目录下双击elasticsearch.bat启动es服务器,然后就可以去浏览器输入http://localhost:9200/

image-20220125164120683

​ 如果有正常的json返回值,那么说明es启动正常

P102 实用开发篇-99-ES索引操作

  1. 安装IK分词器插件

    我们希望es再新建索引的时候应用分词效果,所以需要先给es安装IK分词器插件

    下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases

    esplugin目录下新建一个ik文件夹(建文件夹是为了方便管理),然后把下载好的ik分词器压缩包中的内容解压到ik目录下

image-20220125165532370

​ 关闭当前es启动的黑窗口,去es的bin目录下,双击elasticsearch.bat,重新启动es

  1. 新建索引

    打开postman,发送一个PUT请求,新建一个books索引

    请求参数

    {
        "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"
                }
            }
        }
    }

image-20220125182316594

​ 返回如下提示就表示新建索引成功

image-20220125182719019

  1. 查询索引

image-20220125182950127

  1. 删除索引

    image-20220125192002248

P103 实用开发篇-100-ES文档操作

  1. 新建文档

    有3种请求方式:

    方式1:http://localhost:9200/books/_doc

image-20220125194113251

​ 方式2:http://localhost:9200/books/_doc/221432414,其中221432414是文档中的_id属性,如果不指定,则随机生成

image-20220125205247540

​ 方式3:http://localhost:9200/books/_create/221432414,其中221432414是文档中的_id属性,这里不指定会报错

image-20220125203036653

​ 注:如果在新建的过程中出现[TOO_MANY_REQUESTS/12/disk usage exceeded flood-stage watermark, index has read-only-allow-delete block]的问题,可以发送下面的请求解决

image-20220125203418217

如果上面的方法也不能解决,检查一下自己的es安装目录所在磁盘的可用空间是否太小,默认必须大于5%才可以,比如磁盘空间500G, 需要至少25G的可用空间才可以。后来清了磁盘大于5%也不行,后来用这个方法解决了:Elasticsearch flood stage disk watermark exceeded

  1. 查询文档

    • 查询全部文档

    image-20220125205830308

    • 查询单个文档

    image-20220125205708735

    • 按条件查询

      image-20220126004351659

  2. 删除文档

    image-20220126005031342

  3. 修改文档

    _id221432414的文档的name修改的值修改为springboot 非常好

    先查询一下

    image-20220126005606644

    发起请求执行修改操作,这里请求体里面只填写要修改的属性

    image-20220126005900814

    再查询一下

    image-20220126005941445

    发现修改后的文档,另外两个没有修改的属性没有了,这不是期望的效果,这种请求的修改方式是全覆盖方式的修改。

    如果要想只修改文档中name属性的值,需要使用新的请求方式(操作之前先将_id221432414的文档数据恢复一下)

    注意:这里发送的是POST请求,而上面的全量修改发送的是PUT请求

    image-20220126011856475

    修改之后再查询一下

    image-20220126011948653

P104 实用开发篇-101-SpringBoot整合ES客户端操作

P105 实用开发篇-102-添加文档

P106 实用开发篇-103-查询文档

参考整合h2redismongodb的方式,整合es的时候应该先在pom.xml中加入spring整合es的依赖,spring-boot-starter-data-elasticsearch,然后再去application.yml中编写es的配置,最后再去测试类里面注入es的template对象,进行相关操作。可是需要注意的是,springboot整合es有两套整合方案,一个是整合低级别的es客户端,另一个是整合高级别的es客户端。而开头所说的是springboot整合低级别的方式,这里不采用这种方式。直接整合高级别的es客户端,分为以下几个步骤:

  1. pom.xml中加入es的依赖,由于测试的过程中还要用到对象转json字符串,所以这里把json解析的依赖也一同加上

    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
  2. 整合高级别的es客户端,意味着springboot没有提供默认的配置,所以就不能在application.yml中配置有关访问es客户端的url等参数了,这里直接编写测试类,采用硬编码的方式指定这些参数。

    @SpringBootTest
    class ElasticsearchApplicationTests {
    //    @Autowired
    //    private ElasticsearchRestTemplate esTemplate;
        @Autowired
        private BookMapper bookMapper;
        private RestHighLevelClient client;
        // json转换工具
        private static ObjectMapper objectMapper;
    
        @Test
        public void testConnect() throws IOException {
            HttpHost host = HttpHost.create("http://localhost:9200");
            RestClientBuilder builder = RestClient.builder(host);
            client = new RestHighLevelClient(builder);
    
            client.close();
        }
    
        @Test
        // 创建索引
        public void testCreateIndex() throws IOException {
            CreateIndexRequest request = new CreateIndexRequest("books");
            client.indices().create(request, RequestOptions.DEFAULT);
        }
    
        @Test
        // 使用IK分词器创建索引
        public void testCreateIndexByIK() throws IOException {
            CreateIndexRequest request = new CreateIndexRequest("books");
            // 设置请求参数
            String jsonParam = "{\n" +
                    "    \"mappings\": {\n" +
                    "        \"properties\": {\n" +
                    "            \"id\": {\n" +
                    "                \"type\": \"keyword\"\n" +
                    "            },\n" +
                    "            \"name\": {\n" +
                    "                \"type\": \"text\",\n" +
                    "                \"analyzer\": \"ik_max_word\",\n" +
                    "                \"copy_to\": \"all\"\n" +
                    "            },\n" +
                    "            \"type\": {\n" +
                    "                \"type\": \"keyword\"\n" +
                    "            },\n" +
                    "            \"description\": {\n" +
                    "                \"type\": \"text\",\n" +
                    "                \"analyzer\": \"ik_max_word\",\n" +
                    "                \"copy_to\": \"all\"\n" +
                    "            },\n" +
                    "            \"all\": {\n" +
                    "                \"type\": \"text\",\n" +
                    "                \"analyzer\": \"ik_max_word\"\n" +
                    "            }\n" +
                    "        }\n" +
                    "    }\n" +
                    "}";
            request.source(jsonParam, XContentType.JSON);
            client.indices().create(request, RequestOptions.DEFAULT);
        }
    
        @Test
        public void testCreateDoc() throws Exception {
            Book book = bookMapper.selectById(1);
    
            IndexRequest request = new IndexRequest("books").id(book.getId() + "");
            String jsonParams = objectMapper.writeValueAsString(book);
            request.source(jsonParams, XContentType.JSON);
            client.index(request, RequestOptions.DEFAULT);
        }
    
        @Test
        // 将mysql数据库tbl_book表中的数据都存到es中
        public void testCreateDocAllFromMySQL() throws Exception {
            List<Book> books = bookMapper.selectList(null);
            // 批处理请求,相当于一个request容器,可以把单个请求加进来
            BulkRequest requests = new BulkRequest();
            for (Book book : books) {
                IndexRequest request = new IndexRequest("books").id(book.getId() + "");
                String jsonParams = objectMapper.writeValueAsString(book);
                request.source(jsonParams, XContentType.JSON);
                requests.add(request);
            }
            client.bulk(requests, RequestOptions.DEFAULT);
        }
    
        @Test
        // 按ID查询
        public void testGet() throws IOException {
            GetRequest request = new GetRequest("books", "1");
            GetResponse response = client.get(request, RequestOptions.DEFAULT);
            System.out.println("book:" + response.getSourceAsString());
        }
    
        @Test
        public void testSearch() throws IOException {
            SearchRequest request = new SearchRequest("books");
    
            SearchSourceBuilder builder = new SearchSourceBuilder();
            builder.query(QueryBuilders.termQuery("all", "spring"));
            request.source(builder);
    
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            SearchHits hits = response.getHits();
            for (SearchHit hit : hits) {
                System.out.println(hit.getSourceAsString());
            }
        }
    
        @BeforeEach
        void setUp() {
            objectMapper = new ObjectMapper();
            HttpHost host = HttpHost.create("http://localhost:9200");
            RestClientBuilder builder = RestClient.builder(host);
            client = new RestHighLevelClient(builder);
        }
    
        @AfterEach
        void tearDown() throws IOException {
            client.close();
        }
    }

16. 整合第三方技术

16.1 缓存

springboot_16_01_01_cache_book_quickstart

P107 实用开发篇-104-缓存的作用

P108 实用开发篇-105-Spring缓存使用方式

先自己用javaHashMap模拟一个缓存

BookController.java

@GetMapping("{id}")
// 模拟缓存
public R getById(@PathVariable Integer id) {
    return new R(true, bookService.getById(id));
}

BookServiceImpl.java

@Override
// 模拟缓存
private HashMap<Serializable, Book> cache = new HashMap<>();

public Book getById(Serializable id) {
    // 如果当前缓存中没有本次要查询的数据,则进行查询,否则直接从缓存中获取数据返回
    Book book = cache.get(id);
    if (book == null) {
        book = super.getById(id);
        cache.put(id, book);
    }
    return book;
}
springboot_16_01_02_cache_book_simple

使用spring中自带的缓存技术

  • pom.xml中添加如下依赖

    <!--cache-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
  • springboot启动类上加@EnableCaching注解

  • 在业务层要使用缓存的方法上加上@Cacheable(value = "cacheSpace", key = "#id")注解,如下所示

    @Cacheable(value = "cacheSpace", key = "#id")
    public Book getById(Serializable id) {
        System.out.println("id = " + id);
        return super.getById(id);
    }

    其中value表示缓存空间,key=“#id”,表示将方法参数id的值作为缓存中的一个key

springboot_16_01_03_cache_smscode_simple

P109 实用开发篇-106-手机验证码案例-生成验证码

  1. 手机验证码案例基础代码准备

    • 存储手机号和验证码的实体类SMSCode.java

      @Data
      public class SMSCode {
          private String phone;
          private String code;
      }
    • 生成验证码的工具类CodeUtils.java

      @Component
      public class CodeUtils {
          private final String padding = "000000";
          // 生成验证码(位数少于6位左边填充0,填充方法1)
          public String generateCode(String phone) {
              int hash = phone.hashCode();
              int encryption = 20228888;
              long result = hash ^ encryption;
              long nowTime = System.nanoTime();
              result = (result ^ nowTime) % 1000000;
              String code = result + "";
              // code = phone;
              // padding.substring(code.length())   code.length()
              //                                         6
              //             0                           5
              //             00                          4
              //             000                         3
              //             000                         3
              //             0000                        2
              //             00000                       1
              //             000000                      0
              code = padding.substring(code.length()) + code;
              // System.out.println(code);
              return code;
          }
      
          private final String[] patch = {"000000", "00000", "0000", "000", "00", "0", ""};
          // 生成验证码(位数少于6位左边填充0,填充方法2)
          public String generateCode1(String phone) {
              int hash = phone.hashCode();
              int encryption = 20228888;
              long result = hash ^ encryption;
              long nowTime = System.nanoTime();
              result = (result ^ nowTime) % 1000000;
              String code = result + "";
              // code = phone;
              // patch[code.length]    code.length()
              //   000000                    0
              //   00000                     1
              //   0000                      2
              //   000                       3
              //   00                        4
              //   0                         5
              //                             6
              code = patch[code.length()] + code;
              // System.out.println(code);
              return code;
          }
      
          // 根据手机号从缓存中获取验证码,缓存中有的话返回缓存中的值,没有的话就返回null
          @Cacheable(value = "smsCode", key = "#phone")
          public String getCodeByPhoneFromCache(String phone) {
              return null;
          }
      }
      
    • 业务层接口SMSCodeService.java

      public interface SMSCodeService {
          String sendCodeToSMS(String phone);
      
          boolean checkCode(SMSCode smsCode);
      }

      业务层接口实现类SMSCodeServiceImpl.java

      @Service
      public class SMSCodeServiceImpl implements SMSCodeService {
          @Autowired
          private CodeUtils codeUtils;
      
          @Override
          public String sendCodeToSMS(String phone) {
              return codeUtils.generateCode(phone);
          }
      
          @Override
          public boolean checkCode(SMSCode smsCode) {
              return false;
          }
      }
  2. 加入spring默认的缓存功能

    • pom.xml中添加缓存依赖

      <!--cache-->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-cache</artifactId>
      </dependency>
    • SMSCodeServiceImplsendCodeToSMS()方法上添加@CachePut(value = "smsCode", key = "#phone"),如下所示

      @Override
      // @Cacheable(value = "smsCode", key = "#phone")
      // 这里@Cacheable注解不适用,因为@Cacheable注解的功能是:如果缓存中没有值就去执行一次方法,然后将值存到缓存中,
      // 如果有值就直接从缓存中取值并返回,并不会执行方法,因而缓存中值不会发生改变。
      // 而用户点击界面发送一次验证码就调用了一次该方法,需要获取到新的验证码。
      // 使用新的缓存注解@CachePut可以解决这个问题,每次调用都会执行方法,向缓存中存新的值并返回
      @CachePut(value = "smsCode", key = "#phone")
      public String sendCodeToSMS(String phone) {
          return codeUtils.generateCode(phone);
      }
    • 编写checkCode()方法:校验验证码是否正确

      错误的写法:

      @Override
      public boolean checkCode(SMSCode smsCode) {
          // 取出缓存中的验证码并与传递过来的验证码比对,如果相同,返回true,否则,返回false
          // 用户填写的验证码
          String code = smsCode.getCode();
          // 缓存中存的验证码
          String cacheCode = getCodeByPhoneFromCache(smsCode.getPhone());
          return cacheCode.equals(code);
      }
      
      // 根据手机号从缓存中获取验证码,缓存中有的话返回缓存中的值,没有的话就返回null
      @Cacheable(value = "cacheSpace", key = "#phone")
      public String getCodeByPhoneFromCache(String phone) {
          return null;
      }

      getCodeByPhoneFromCache()方法上加了@Cacheable(value = "cacheSpace", key = "#phone"),然后在checkCode()方法中调用getCodeByPhoneFromCache()方法,这种方式看似是正确的,实际上@Cacheable注解不会生效,导致getCodeByPhoneFromCache()的返回值始终是null。这是由于被spring管理的类内的方法互调注解不会被解析。

      由此可以想到解决办法,将getCodeByPhoneFromCache()放到另外一个类(这里为了方便起见,直接放到CodeUtils类中),并将这个类交由spring管理(在类上面加@Component注解),然后再用codeUtils.getCodeByPhoneFromCache(smsCode.getPhone())即可正常从缓存中拿到值。代码如下:

      @Override
      public boolean checkCode(SMSCode smsCode) {
          // 取出缓存中的验证码并与传递过来的验证码比对,如果相同,返回true,否则,返回false
          // 用户填写的验证码
          String code = smsCode.getCode();
          // 缓存中存的验证码
          String cacheCode = codeUtils.getCodeByPhoneFromCache(smsCode.getPhone());
          return cacheCode.equals(code);
      }
springboot_16_01_04_cache_smscode_ehcache

P111 实用开发篇-108-变更缓存供应商Ehcache

基于验证码案例的代码和配置,使用ehcache替换spring默认的simple缓存

  1. pom.xml中加入ehcache的依赖

    <!--ehcache-->
    <dependency>
        <groupId>net.sf.ehcache</groupId>
        <artifactId>ehcache</artifactId>
    </dependency>
  2. application.yml中加入如下配置

    spring:
      cache:
        type: ehcache
        # 如果配置文件改名了,可以打开下面的配置指定配置文件路径
        # ehcache:
          # config: ehcache-xxx.xml
  3. resources目录下新建一个ehcache.xml配置文件,内容如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:noNamespaceSchemaLocation="https://www.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" />
    
        <cache
            name="smsCode"
            eternal="false"
            diskPersistent="false"
            maxElementsInMemory="1000"
            overflowToDisk="false"
            timeToIdleSeconds="10"
            timeToLiveSeconds="10"
            memoryStoreEvictionPolicy="LRU" />
    
    </ehcache>

    直接启动项目,并且验证码获取和验证的过程不报错,说明缓存替换成成功。

P112 补 知识加油站-04-数据淘汰策略

  • LRU: Least recently used, 淘汰最近最少被使用的数据
  • LFU: Least frequently used,淘汰使用频率最低的数据
springboot_16_01_05_cache_smscode_redis

P113 实用开发篇-109-变更缓存供应商Redis

基于验证码案例的代码和配置,使用redis替换spring默认的simple缓存

  1. pom.xml中加入redis的依赖

    <!--redis-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  2. application.yml中加入如下配置

    spring:
      cache:
        type: redis
        redis:
          # 是否使用缓存命名空间作为前缀,如:smsCode::18866668888,默认为true
          use-key-prefix: true
          cache-null-values: true # 是否缓存空值
          key-prefix: aa # use-key-prefix为false的时候该项不生效
          time-to-live: 10s # 缓存失效时间
      redis:
        host: 192.168.0.110
        port: 6379
        password: 123456

    直接启动项目,并且验证码获取和验证的过程不报错,说明缓存替换成成功。

springboot_16_01_06_cache_smscode_memcached

P114 实用开发篇-110-memcached下载与安装

安装步骤以及下载地址:https://www.runoob.com/memcached/window-install-memcached.html

P115 实用开发篇-111-变更缓存供应商memcached

基于验证码案例的代码和配置,使用memcached替换spring默认的simple缓存,memcached最新的客户端技术是xmemcached

  1. pom.xml中加入xmemcached的依赖

    <!-- xmemcached -->
    <dependency>
        <groupId>com.googlecode.xmemcached</groupId>
        <artifactId>xmemcached</artifactId>
        <version>2.4.7</version>
    </dependency>
  2. 由于springboot并未收录memcached,所以只能通过硬编码的方式完成相关配置

    XMemcachedConfig.java

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

    SMSCodeServiceImpl.java

    @Service
    public class SMSCodeServiceImpl implements SMSCodeService {
        @Autowired
        private CodeUtils codeUtils;
    
        @Autowired
        private MemcachedClient memcachedClient;
    
        @Override
        public String sendCodeToSMS(String phone) {
            String code = codeUtils.generateCode(phone);
            try {
                memcachedClient.set(phone, 10, code);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return code;
        }
    
        @Override
        public boolean checkCode(SMSCode smsCode) {
            String code = null;
            try {
                code = memcachedClient.get(smsCode.getPhone()).toString();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return smsCode.getCode().equals(code);
        }
    }

    直接启动项目,并且验证码获取和验证的过程不报错,说明缓存替换成成功。

到这里整合memcached算是成功了,可是配置都写死在代码中了,不太方便,这里可以结合前面的Configuration属性绑定的内容将配置抽取到application.yml文件中。

  1. 先定义一个实体类XMemcachedProperties,保存XMemcached的配置属性,并加上@Component@ConfigurationProperties(prefix = "memcached")注解

    @Component
    @ConfigurationProperties(prefix = "memcached")
    @Data
    public class XMemcachedProperties {
        private String addressList;
        private int poolSize;
        private long opTimeout;
    }
  2. application.yml中加入如下配置:

    memcached:
      address-list: 192.168.0.102:11211
      poolSize: 10
      opTimeout: 3000
  3. XMemcachedConfig类中使用XMemcachedProperties类中封装的配置属性

    @Component
    public class XMemcachedConfig {
        @Autowired
        private XMemcachedProperties xMemcachedProperties;
    
        @Bean
        public MemcachedClient getMemcachedClient() throws IOException {
            MemcachedClientBuilder memcachedClientBuilder = new XMemcachedClientBuilder(xMemcachedProperties.getAddressList());
            memcachedClientBuilder.setConnectionPoolSize(xMemcachedProperties.getPoolSize());
            memcachedClientBuilder.setOpTimeout(xMemcachedProperties.getOpTimeout());
            MemcachedClient memcachedClient = memcachedClientBuilder.build();
            return memcachedClient;
        }
    }
springboot_16_01_07_cache_smscode_jetcache

P116 实用开发篇-112-jetcache远程缓存方案

P117 实用开发篇-113-jetcache本地缓存方案

jetcache是阿里巴巴公司的缓存技术,可以同时支持本地和远程两种缓存,所谓本地就是使用HashMap类似的技术做的缓存,远程就是可以连接到redis

基于验证码案例的代码和配置,使用jetcache替换spring默认的simple缓存

springboot整合jetcache的时候使用2.6.3版本会报循环依赖的错误,我在网上搜索了一个解决循环依赖的方法,链接地址:https://blog.csdn.net/chengxuyuanjava123/article/details/122459521较复杂,仅供参考。教程的弹幕中有个小伙伴讲springboot 2.5.4版本和jetcache整合不会出现这个问题,我就尝试了一下,居然真的可以,所以我就暂时使用了这个简单的方法。使用了springboot 2.5.4,有更好的办法解决这个问题的小伙伴,可以评论区告知,谢谢。

首先用jetcache的远程方案,也就是底层用redis作为真正的缓存工具。

  1. pom.xml中加入jetcache的依赖

    <!--jetcache-->
    <dependency>
        <groupId>com.alicp.jetcache</groupId>
        <artifactId>jetcache-starter-redis</artifactId>
        <version>2.6.2</version>
    </dependency>
  2. application.yml中加入如下配置

    # 配置jetcache
    jetcache:
      areaInCacheName: false # 是否把area加入的缓存的key中,默认为true
      # 远程方案
      remote:
        default:
          type: redis
          host: 192.168.0.110
          port: 6379
          password: 123456
          # keyConvertor: fastjson
          # valueEncoder: java
          # valueDecoder: java
          poolConfig:
            # minIdle: 5
            # maxIdle: 20
            maxTotal: 50
          # 如果自定义命名空间,需要在@CreateCache注解中添加area=“sms”,不写默认为default
    #    sms:
    #      type: redis
    #        host: localhost
    #        port: 6379
    #        password: 123456
    #        poolConfig:
    #          maxTotal: 50
  3. springboot启动类上加@EnableCreateCacheAnnotation注解

  4. 在业务层类SMSCodeServiceImpl中使用缓存

    @Service
    public class SMSCodeServiceImpl implements SMSCodeService {
        @Autowired
        private CodeUtils codeUtils;
    
        // 1、定义一个缓存
        @CreateCache(name = "jetCache_", expire = 10, timeUnit = TimeUnit.SECONDS)
        private Cache<String, String> jetCache;
    
        @Override
        public String sendCodeToSMS(String phone) {
            String code = codeUtils.generateCode(phone);
            // 2、向缓存中存值
            jetCache.put(phone, code);
            return code;
        }
    
        @Override
        public boolean checkCode(SMSCode smsCode) {
            if (smsCode == null) {
                return false;
            }
            // 取出缓存中的验证码并与传递过来的验证码比对,如果相同,返回true,否则,返回false
            // 用户填写的验证码
            String code = smsCode.getCode();
            // 3、获取缓存中存的验证码
            String cacheCode = jetCache.get(smsCode.getPhone());
            return code.equals(cacheCode);
        }
    }

    直接启动项目,并且验证码获取和验证的过程不报错,说明缓存替换成成功。

再使用jetcache的本地方案

  1. 修改application.ymljetcache的配置

    # 配置jetcache
    jetcache:
      areaInCacheName: false # 是否把area加入的缓存的key中,默认为true
      # 本地方案
      local:
        default:
          type: linkedhashmap
          keyConvertor: fastjson # 将对象转换成key使用的技术,这里转成json
          # limit: 100
      # 远程方案
      remote:
        default:
          type: redis
          host: 192.168.0.110
          port: 6379
          password: 123456
          # keyConvertor: fastjson
          # valueEncoder: java
          # valueDecoder: java
          poolConfig:
            # minIdle: 5
            # maxIdle: 20
            maxTotal: 50
          # 如果自定义命名空间,需要在@CreateCache注解中添加area=“sms”,不写默认为default
    #    sms:
    #      type: redis
    #        host: localhost
    #        port: 6379
    #        password: 123456
    #        poolConfig:
    #          maxTotal: 50
  2. SMSCodeServiceImpl.java中的@CreateCache注解中指定cacheType = CacheType.LOCAL

    @Service
    public class SMSCodeServiceImpl implements SMSCodeService {
        @Autowired
        private CodeUtils codeUtils;
    
        // 1、定义一个缓存
        // remote方案
        // @CreateCache(area="sms", name = "jetCache_", expire = 10, timeUnit = TimeUnit.SECONDS)
        // 本地方案(查看源码知晓,如果不指定cacheType = CacheType.LOCAL,默认为远程。)
        @CreateCache(name = "jetCache_", expire = 10, timeUnit = TimeUnit.SECONDS, cacheType = CacheType.LOCAL)
        // 还可以指定远程和本地两种缓存方案共存
        // @CreateCache(name = "jetCache_", expire = 10, timeUnit = TimeUnit.SECONDS, cacheType = CacheType.BOTH)
        private Cache<String, String> jetCache;
    
        @Override
        public String sendCodeToSMS(String phone) {
            String code = codeUtils.generateCode(phone);
            // 2、向缓存中存值
            jetCache.put(phone, code);
            return code;
        }
    
        @Override
        public boolean checkCode(SMSCode smsCode) {
            if (smsCode == null) {
                return false;
            }
            // 取出缓存中的验证码并与传递过来的验证码比对,如果相同,返回true,否则,返回false
            // 用户填写的验证码
            String code = smsCode.getCode();
            // 3、获取缓存中存的验证码
            String cacheCode = jetCache.get(smsCode.getPhone());
            return code.equals(cacheCode);
        }
    }

如果远程和本地两种方案都启用,只需要将SMSCodeServiceImpl.java中的@CreateCache注解中指定cacheType = CacheType.BOTH即可。

另附:jetcache详细配置属性

image-20220214140351959

springboot_16_01_08_cache_smscode_jetcache_method

P118 实用开发篇-114-jetcache方法缓存

jetcache也可以和spring默认的simple-cache一样在方法上加上缓存注解。这里为了体现对缓存的增删改查更多的操作,基于springboot_16_01_02_cache_Book_simple案例的代码进行修改。

  1. pom.xml中加入jetcache的依赖

    <!--jetcache-->
    <dependency>
        <groupId>com.alicp.jetcache</groupId>
        <artifactId>jetcache-starter-redis</artifactId>
        <version>2.6.2</version>
    </dependency>
  2. application.yml中加入如下配置

    # 配置jetcache
    jetcache:
      statIntervalMinutes: 1 # 按照统计间隔打印输出缓存命中率,0表示不统计,默认为0
      areaInCacheName: false # 是否把area加入的缓存的key中,默认为true
      # 远程方案
      remote:
        default:
          type: redis
          host: 192.168.0.110
          port: 6379
          password: 123456
          keyConvertor: fastjson
          valueEncoder: java
          valueDecoder: java
          poolConfig:
            # minIdle: 5
            # maxIdle: 20
            maxTotal: 50
          # 如果自定义命名空间,需要在@CreateCache注解中添加area=“sms”,不写默认为default
    #    sms:
    #      type: redis
    #        host: localhost
    #        port: 6379
    #        password: 123456
    #        poolConfig:
    #          maxTotal: 50
  3. springboot的启动类上加@EnableMethodCache(basePackages = "top.jacktgq"),其中basePackages=“top.jacktgq"中的包名需要覆盖到用到缓存的业务类,另外@EnableMethodCache注解需要依赖@EnableCreateCacheAnnotation注解。如下所示。

    @SpringBootApplication
    // jetcache启用缓存的主开关
    @EnableCreateCacheAnnotation
    // 开启方法缓存注解,
    @EnableMethodCache(basePackages = "top.jacktgq")
    public class CacheApplication {
        public static void main(String[] args) {
            SpringApplication.run(CacheApplication.class, args);
        }
    }
  4. BookServiceImpl类中添加方法缓存注解

    • 这里需要参考springboot_16_01_03_cache_smscode,在CodeUtils.javagetCodeByPhoneFromCache()方法上添加@Cached(name = "smsCode", key = "#", expire = 10),其中name和之前的spring自带缓存的@Cacheable注解中的value一样都表示缓存的命名空间,key@Cacheable中的key一样,expire表示缓存失效时间,默认单位为秒,不指定就是永不失效。如下所示。

    • 如果在修改操作后想更新缓存,可以在修改方法上加上@CacheUpdate(name = "book_", key = "#book.id", value = "#book")注解;

    • 如果想在删除操作后删除对应缓存,可以在删除方法上加上@CacheInvalidate(name = "book_", key = "#id")注解。

    • 如果数据库由多个业务系统共用,一个系统对数据库的修改不能同步到另一个系统的缓存,这时就需要使用的到@CacheRefresh(refresh = 5),其中refresh=5,表示每隔5秒钟刷新一次缓存。

    具体代码如下:

    @Override
    @Cached(name = "book_", key="#id", expire = 3600)
    @CacheRefresh(refresh = 5)
    public Book getById(Serializable id) {
        return super.getById(id);
    }
    
    @Override
    @CacheUpdate(name = "book_", key = "#book.id", value = "#book")
    public boolean updateById(Book book) {
        return super.updateById(book);
    }
    
    @Override
    @CacheInvalidate(name = "book_", key = "#id")
    public boolean removeById(Serializable id) {
        return super.removeById(id);
    }
    
    @Override
    public IPage<Book> getPage(int currentPage, int pageSize) {
        IPage<Book> page = new Page<>(currentPage, pageSize);
        bookMapper.selectPage(page, null);
        return page;
    }

    注:如果这里被@Cache注解修饰的方法的返回值为普通的实体类,那么这个实体类需要实现Serializable接口,并且在application.yml配置文件的jetcache下配置 keyConvertor: fastjson, valueEncoder: java, valueDecoder: java,否则会报错。

    1. Book实体类要实现Serializable接口

       @Component
      @Data
      public class Book implements Serializable {
          private Integer id;
          private String name;
          private String type;
          private String description;
      }
    2. application.yml配置文件的jetcache下配置valueEncoder: java, valueDecoder: java

      # 配置jetcache
      jetcache:
        statIntervalMinutes: 1 # 按照统计间隔打印输出缓存命中率,0表示不统计,默认为0
        areaInCacheName: false # 是否把area加入的缓存的key中,默认为true
        # 本地方案
        local:
          default:
            type: linkedhashmap
            keyConvertor: fastjson # 将对象转换成key使用的技术,这里转成json
            # limit: 100
        # 远程方案
        remote:
          default:
            type: redis
            host: 192.168.0.110
            port: 6379
            password: 123456
            keyConvertor: fastjson
            valueEncoder: java
            valueDecoder: java
            poolConfig:
              # minIdle: 5
              # maxIdle: 20
              maxTotal: 50
springboot_16_01_09_cache_smscode_j2cache_ehcache_redis

P119 实用开发篇-115-j2cache基本操作

j2cache是一个缓存整合框架,可以提供缓存的整合方案,使个各种缓存搭配使用,自身不提供缓存功能。

这里基于验证码案例的代码和配置,使用j2cache整合ehcacheredis替换spring原有的simple缓存。

  1. pom.xml中加入j2cache的相关依赖

    <!--j2cache-->
    <dependency>
        <groupId>net.oschina.j2cache</groupId>
        <artifactId>j2cache-core</artifactId>
        <version>2.8.4-release</version>
    </dependency>
    <dependency>
        <groupId>net.oschina.j2cache</groupId>
        <artifactId>j2cache-spring-boot2-starter</artifactId>
        <version>2.8.0-release</version>
    </dependency>
    <!--j2cache默认帮忙引入了redis的依赖-->
    <!--ehcache-->
    <dependency>
        <groupId>net.sf.ehcache</groupId>
        <artifactId>ehcache</artifactId>
    </dependency>
  2. resources目录下创建配置文件

    ehcache.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:noNamespaceSchemaLocation="https://www.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" />
    
        <cache
            name="smsCode"
            eternal="false"
            diskPersistent="false"
            maxElementsInMemory="1000"
            overflowToDisk="false"
            timeToIdleSeconds="10"
            timeToLiveSeconds="10"
            memoryStoreEvictionPolicy="LRU" />
    
    </ehcache>

    j2cache.properties

    # 一级缓存
    j2cache.L1.provider_class = ehcache
    ehcache.configXml = ehcache.xml
    # 二级缓存
    j2cache.L2.provider_class = net.oschina.j2cache.cache.support.redis.SpringRedisProvider
    j2cache.L2.config_section = redis
    redis.hosts = 192.168.0.110:6379
    redis.timeout = 2000
    redis.password = 123456
    
    # 指定模式,可以消除一行警告信息
    redis.mode = single
    
    # 一级缓存中的数据如何到达二级缓存
    j2cache.broadcast = net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy

    更详细的配置内容可以去j2cachejar包路径下寻得

    image-20220214181819635

  3. SMSCodeServiceImpl类中编写j2cache相关代码

    @Service
    public class SMSCodeServiceImpl implements SMSCodeService {
        @Autowired
        private CodeUtils codeUtils;
    
        @Autowired
        private CacheChannel cacheChannel;
    
        @Override
        public String sendCodeToSMS(String phone) {
            String code = codeUtils.generateCode(phone);
            cacheChannel.set("sms", phone, code);
            return code;
        }
    
        @Override
        public boolean checkCode(SMSCode smsCode) {
            if (smsCode == null) {
                return false;
            }
            // 取出缓存中的验证码并与传递过来的验证码比对,如果相同,返回true,否则,返回false
            // 用户填写的验证码
            String code = smsCode.getCode();
            // 缓存中存的验证码
            String cacheCode = cacheChannel.get("sms", smsCode.getPhone()).asString();
            return code.equals(cacheCode);
        }
    }

    注:如果运行工过程中报如下错误:

    Caused by: net.sf.ehcache.CacheException: Another unnamedCacheManager already exists in the same VM. Please provide uniquenames for each CacheManagerxxxxxxxxxx1 1Caused by: net.sf.ehcache.CacheException: Another unnamed CacheManager alreadyCaused by: net.sf.ehcache.CacheException: Another unnamedCacheManager already exists in the same VM. Please provide uniquenames for each CacheManager

    检查一下springboot启动类上面有没有多余的缓存注解,如@EnableCaching,这会跟j2cache发生冲突,j2cache不需要在springboot启动类上面加注解,复制项目的时候需要小心。

  4. 消除一些警告日志

    到此项目是可以正常启动的,功能也正常,但是控制台报了一些警告信息

    • 日志冲突异常

      SLF4J: Class path contains multiple SLF4J bindings.
      SLF4J: Found binding in [jar:file:/D:/repository/ch/qos/logback/logback-classic/1.2.10/logback-classic-1.2.10.jar!/org/slf4j/impl/StaticLoggerBinder.class]
      SLF4J: Found binding in [jar:file:/D:/repository/org/slf4j/slf4j-simple/1.7.33/slf4j-simple-1.7.33.jar!/org/slf4j/impl/StaticLoggerBinder.class]
      SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
      SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]

      冲突的意思是slf4j-simplelogback-classic发生了冲突,这里选择保留springboot默认的logback日志,排除slf4j-simple。打开maven面板,选中项目,然后点击显示依赖项按钮,

      image-20220214213153252

      会弹出一个依赖项拓扑图,在拓扑图中寻找到slf4j-simple的上层包为j2cache-core

      image-20220214213640009

      pom.xml中的j2cache-core的依赖中排除slf4j-simple即可

      <dependency>
          <groupId>net.oschina.j2cache</groupId>
          <artifactId>j2cache-core</artifactId>
          <version>2.8.4-release</version>
          <exclusions>
              <exclusion>
                  <groupId>org.slf4j</groupId>
                  <artifactId>slf4j-simple</artifactId>
              </exclusion>
          </exclusions>
      </dependency>

      再启动项目,就不会再报这个警告了。

P120 实用开发篇-116-j2cache相关配置

application.yml中对j2cache的其他配置

# 指定命名空间,可以作为key的公共前缀
redis.namespace = j2cache
# 设置是否启用二级缓存,默认为true
j2cache.l2-cache-open = false

16.2 任务

springboot_16_02_01_task_quartz

P121 实用开发篇-117-springboot整合quartz

springboot整合quartz分为以下几个步骤:

  1. pom.xml中加入Springboot整合quartz的依赖

    <!--quartz-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-quartz</artifactId>
    </dependency>
  2. springboot程序启动类上加@EnableScheduling注解,开启定时任务功能

    //开启定时任务功能
    @EnableScheduling
  3. 编写一个类MyQuartz继承QuartzJobBean,作为Quartz要执行的工作(任务)

    public class MyQuartz extends QuartzJobBean {
        @Override
        protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
            System.out.println("Quartz task run...");
        }
    }
  4. 编写一个QuartzConfig类,配置Quartz具体的执行过程,并加上@Configuration注解

    @Configuration
    public class QuartzConfig {
        @Bean
        public JobDetail printJobDetail() {
            //绑定具体的工作
            return JobBuilder.newJob(MyQuartz.class).storeDurably().build();
        }
    
        @Bean
        public Trigger printJobTrigger() {
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");
            // 绑定具体的工作明细
            return TriggerBuilder.newTrigger().forJob(printJobDetail()).withSchedule(scheduleBuilder).build();
        }
    }
  5. 直接启动springboot程序,任务便会自动执行

springboot_16_02_02_task_spring

P122 实用开发篇-118-springboot整合task

使用springboot自带的定时任务

  1. springboot程序启动类上加@EnableScheduling注解,开启定时任务功能

    //开启定时任务功能
    @EnableScheduling
  2. 编写一个任务类,加上@Component注解,要定时执行的方法上加上@Scheduled(cron = "0/3 * * * * ?"),用cron表达式指定执行的周期

    @Component
    public class MyTask {
        @Scheduled(cron = "0/3 * * * * ?")
        public void print() {
            System.out.println("spring task run...");
        }
    }
  3. 直接启动springboot程序,任务便会自动执行

  4. Spring Task还可以在application.yml文件中进行更细致的配置

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

16.3 邮件

springboot_16_03_mail

P123 实用开发篇-119-发送简单邮件

完成代码发邮件案例之前先准备好两个邮箱,这里采用一个QQ邮箱和一个163邮箱,后面用这两个邮箱互相发送消息。

代码发邮件采用的是SMTP协议,收邮件采用的是POP3或者IMAP协议,使用代码发邮件需要在配置文件中填写账号和授权码,授权码需要去邮箱管理后台界面进行设置。

  • QQ邮箱开启POP3/SMTP服务,获取授权码

    1. 进入QQ邮箱主页,找到设置,然后点击账户选项

      image-20220215014949660

    2. 鼠标滚轮往下滑,找到POP3/SMTP服务开启的地方

      image-20220215015249141

      1. 按照提示发送短信,点击我已发送,然后记录一下授权码,一会儿配置文件中使用

      image-20220215015351683

  • 163邮箱开启POP3/SMTP服务,获取授权码

    1. 进入163邮箱主页,点击设置,选择POP3/SMTP/IMAP

      image-20220215015802106

    2. 点击开启IMAP/SMTP服务

      image-20220215020216002

    3. 发送短信获取授权码

      image-20220215020459960

spring整合mail

  1. pom.xml中加入spring整合mail的依赖

    <!--mail-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>
  2. application.yml中添加邮箱的相关配置

    spring:
      mail:
        host: smtp.qq.com # 邮件服务供应商
        username: xxxxxxxx@qq.com # 邮箱账号
        password: xxxxxxxxxxxx # 授权码
  3. 业务层接口

    public interface MailService {
        // 发送简单消息
        void sendMail();
    }

    业务层实现类,实现sendMail()方法,使用JavaMailSender类对象进行邮件的发送,这里先发送一个简单的邮件,仅包含发送人、接收人、主题、正文四项,消息需要设置到SimpleMailMessage类对象中。

    @Service
    public class MailServiceImpl implements MailService {
        @Autowired
        private JavaMailSender javaMailSender;
    
        // 发送人
        private String from = "xxxxxxxx@qq.com";
    
        // 接收人
        private String to = "xxxxxxxx@163.com";
    
        // 主题
        private String subject = "测试发邮件";
    
        // 正文
        private String context = "测试邮件的正文内容";
    
        @Override
        public void sendMail() {
            SimpleMailMessage msg = new SimpleMailMessage();
            msg.setFrom(from);
            msg.setTo(to);
            msg.setSubject(subject);
            msg.setText(context);
            javaMailSender.send(msg);
        }
    }
  4. 测试类

    @SpringBootTest
    class MailApplicationTests {
    	@Autowired
    	private MailService mailService;
    
    	@Test
    	void contextLoads() {
    		mailService.sendMail();
    	}
    }

    运行测试类,去收件邮箱中查看消息接收到了

    image-20220215022452235

    如果msg.setFrom(from + "(章北海)"),那么会将收件人的邮箱前加上这个备注名称

    image-20220215022959431

P124 实用开发篇-120-发送多部件邮件

  • 要想在发送邮件的时候可以带上图片或者文件等附件,需要使用MimeMessage类对象封装消息

  • 要想正文以html格式解析,需要指定第二个参数为true

  • 要想发送附件,需要指定第二个参数为true

  • 具体代码如下:

    @Override
    // 发送带链接和附件的消息
    public void sendMailWithLinkAttachment() throws MessagingException {
        MimeMessage msg = javaMailSender.createMimeMessage();
        // 要想发送附件,需要指定第二个参数为true
        MimeMessageHelper msgHelper = new MimeMessageHelper(msg, true);
        msgHelper.setFrom(from + "(章北海)");
        msgHelper.setTo(to);
        // 主题
        String subject = "测试发送带链接和附件的消息";
        // 正文
        String context = "<a href='https://www.baidu.com'>百度一下</a><br><img src='https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg01.vgtime.com%2Fgame%2Fcover%2F2021%2F06%2F09%2F210609232854124_u93176.jpg&refer=http%3A%2F%2Fimg01.vgtime.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1647456598&t=8808f4643e61d9d2790f9f83d9f620a5'>";
        msgHelper.setSubject(subject);
        // 要想正文以html格式解析,需要指定第二个参数为true
        msgHelper.setText(context, true);
    
        // 添加附件
        File file1 = new File("D:\\ideacode\\spring-boot-study\\springboot_16_03_mail\\src\\main\\resources\\test.jpg");
        File file2 = new File("D:\\ideacode\\spring-boot-study\\springboot_16_03_mail\\target\\springboot_16_03_mail-0.0.1-SNAPSHOT.jar");
        msgHelper.addAttachment(file1.getName(), file1);
        msgHelper.addAttachment(file2.getName(), file2);
    
        javaMailSender.send(msg);
    }

    去邮箱中查看消息,图片和文件都顺利接收到了

image-20220215030553525

16.4 消息

springboot_16_04_01_mq_prepare

P125 实用开发篇-121-消息简介

image-20220215032443690

image-20220215032353037

image-20220215032923713

image-20220215033030437

image-20220215033127986

P126 实用开发篇-122-购物订单案例-发送短信

  1. 购物订单案例-发送短信基础代码准备

    • 发送短信的业务层接口MessageService.java

      public interface MessageService {
          void sendMessage(String orderId);
      
          String doMessage();
      }

      发送短信的业务层实现类MessageServiceImpl.java

      @Service
      public class MessageServiceImpl implements MessageService {
          private ArrayList<String> msgList = new ArrayList<String>();
          @Override
          public void sendMessage(String orderId) {
              System.out.println("待发送短信的订单已纳入处理队列,id:" + orderId);
              msgList.add(orderId);
          }
      
          @Override
          public String doMessage() {
              String orderId = msgList.remove(0);
              System.out.println("已发送短信发送业务,id:" + orderId);
              return orderId;
          }
      }

      发送短信的表现层控制类MessageController.java

      @RestController
      @RequestMapping("/msgs")
      public class MessageController {
          @Autowired
          private MessageService messageService;
      
          @GetMapping
          public String doMessage() {
              String id = messageService.doMessage();
              return id;
          }
      }
    • 处理订单的业务层接口OrderService.java

      public interface OrderService {
          void order(String id);
      }

      处理订单的业务层实现类OrderServiceImpl.java

      @Service
      public class OrderServiceImpl implements OrderService {
          @Autowired
          private MessageService messageService;
      
          @Override
          public void order(String orderId) {
              // 一系列的操作,包含各种服务调用,处理各种业务
              System.out.println("订单开始处理");
              // 短信消息处理
              messageService.sendMessage(orderId);
      
              System.out.println("订单处理结束");
              System.out.println();
          }
      }

      处理订单的表现层控制类OrderController.java

      @RestController
      @RequestMapping("/orders")
      public class OrderController {
          @Autowired
          private OrderService orderService;
      
          @PostMapping("{id}")
          public void order(@PathVariable String orderId) {
              orderService.order(orderId);
          }
      }
    • 到此案例基础代码准备完成

springboot_16_04_02_mq_activemq

P127 实用开发篇-123-ActiveMQ安装

下载地址:https://activemq.apache.org/components/classic/download/

这里以windows平台为例,下载好安装包以后解压缩,去软件目录的bin\win64下面双击activemq.bat即可启动。

运行过程中报错了,发现了我电脑上的1883端口被占用

image-20220215145018673

启动一个命令行,输入netstat -ano | findstr "1883"

image-20220215145208792

结果居然什么也没查出来,这就奇怪了。没办法先去配置文件里面改个端口吧,编辑软件目录的conf\activemq.xml文件,然后找到标签下面的mqtt的那一项,将uri里面的端口改成8888

image-20220215145557387

改完后再启动一下ActiveMQ,没有报错

image-20220215152546926

去浏览器输入http://localhost:8161,账号密码都是admin,可以登录到主界面,表示ActiveMQ启动正常

注:如果ActiveMQ不是安装在本地,比如阿里云或者局域网,需要修改软件目录的conf\jetty.xmljettyPort属性,将默认的localhost改成实际IP地址即可。

image-20220215162120814

P128 实用开发篇-124-springboot整合ActiveMQ

基于购物订单案例-发送短信案例的代码,整合ActiveMQ

  1. pom.xml中加入springboot整合ActiveMQ的依赖

    <!--ActiveMQ-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-activemq</artifactId>
    </dependency>
  2. application.yml中加入activemq的配置

    spring:
      activemq:
        broker-url: tcp://192.168.0.102:61616
      jms:
        template:
          # 指定消息队列的名字
          default-destination: jacktgq
  3. 编写业务层接口MessageService的实现类MessageServiceActivemqImpl,注意需要将上个案例中的MessageServiceImpl类上的@Service注解去掉,要不然自动注入的时候会发生冲突。

    @Service
    public class MessageServiceActivemqImpl implements MessageService {
        @Autowired
        private JmsMessagingTemplate messagingTemplate;
    
        @Override
        public void sendMessage(String orderId) {
            System.out.println("待发送短信的订单已纳入处理队列,id:" + orderId);
            // 将消息存到消息队列的时候如果不指定destination,也就是队列名,就会存到默认的队列中, 如果application.yml中没有配置默认的队列名, 就会报错
            // messagingTemplate.convertAndSend(orderId);
            // 也可以手动指定队列名
            messagingTemplate.convertAndSend("order.queue.id", orderId);
        }
    
        @Override
        public String doMessage() {
            // 从默认队列中获取消息
            // String orderId = messagingTemplate.receiveAndConvert(String.class);
            // 从指定队列中获取消息
            String orderId = messagingTemplate.receiveAndConvert("order.queue.id", String.class);
            System.out.println("已发送短信发送业务,id:" + orderId);
            return orderId;
        }
    }

    启动项目,访问下面的地址,往MQ中存取一些消息

    http://localhost:8080/orders/1
    http://localhost:8080/orders/2
    http://localhost:8080/orders/3
    http://localhost:8080/orders/4
    http://localhost:8080/msgs/

    MQ的后台管理页面查看一下数据储存情况

    image-20220215170440244

    image-20220215170527955

    消息存取正常。

  4. 要想消息存到MQ中以后自动去消费,就要用到JMS的消息监听器技术,只要被监听的队列中接收到新的数据就会去取出来执行。

    • 定义一个MessageListener类,在类头上加上@Component注解,将这个类交给spring管理
    • 定义一个方法receive(String orderId), 在方法上加上@JmsListener(destination = "order.queue.id"),需要指定队列名称。
    • 还可以在方法上加@SendTo("order.other.queue.id")注解将当前方法的返回值再发送到其他队列
    • 然后启动项目即可。
    @Component
    public class MessageListener {
        @JmsListener(destination = "order.queue.id")
        // 还可以将当前方法的返回值再发送到其他队列
        @SendTo("order.other.queue.id")
        public String receive(String orderId) {
            System.out.println("已完成短信发送业务,id:" + orderId);
            return "done::id::" + orderId;
        }
    }
  5. application.yml文件中使用pub-sub-domain属性设置消息模型

    spring:
      activemq:
        broker-url: tcp://192.168.0.102:61616
      jms:
        # 是否使用发布订阅模型,默认为false,点对点模型
        pub-sub-domain: true
        template:
          # 指定消息队列的名字
          default-destination: jacktgq

    如果使用了发布订阅模型,那么在ActiveMQ的管理界面中查看消息,需要在Topics选项下面查找(不会发到Queues选项下面)。

    image-20220215172938462

springboot_16_04_03_mq_rabbitmq_direct

P129 实用开发篇-125-RabbitMQ安装

RabbitMQ基于Erlang语言编写,需要先安装Erlang环境,类似执行java程序需要先安装jdk(jre)

  • Erlang安装

    下载地址:https://www.erlang.org/downloads

    安装:一键傻瓜式安装,安装完毕需要重启,需要依赖windows组件

    配置环境变量:ERLANG_HOMEPATH

    image-20220215175957645

    image-20220215180107622

  • 安装RabbitMQ

    • 下载地址:https://www.rabbitmq.com/install-windows.html

    • 找到Direct Downloads的地方,点击直接下载exe文件

      image-20220215181030165

    • 安装:一键傻瓜式安装

    • 启动:先以管理员权限启动一个黑窗口,cd到软件安装目录的rabbitmq_server-3.9.13\sbin下,输入rabbitmq-service.bat start,提示服务已经启动即可。

      image-20220215182444713

      在任务管理器的服务选项下面也可以找到RabbitMQ服务,以后启动和停止就可以在这里操作了

      image-20220215182808721

    • 开启图形化管理页面插件

      继续在刚才上面的黑窗口中输入rabbitmq-plugins.bat list,会以列表的形式枚举自带的所有插件,找到rabbitmq_management就是我们需要开启的插件。

      image-20220215183330490

      继续输入命令rabbitmq-plugins.bat enable rabbitmq_management,然后回车,会显示开启了三个关联的插件。

      image-20220215183725933

      再次输入rabbitmq-plugins.bat list,这次列表中显示rabbitmq_management前面多了E*,表示这个插件处于启用状态。

      去浏览器中输入http://localhost:15672/,会显示登录界面

      image-20220215184050880

      输入账号密码(都是guest),会跳转到RabbitMQ管理页面

      image-20220215184201664

      image-20220215184224846

P130 实用开发篇-126-springboot整合RabbitMQ(direct模式)

  1. pom.xml中加入springboot整合RabbitMQ的依赖

    <!--RabbitMQ-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
  2. application.yml中加入RabbitMQ的配置

    spring:
      rabbitmq:
        host: localhost
        port: 5672
  3. 编写RabbitDirectConfig类,配置直连模式的交换机和队列

    @Configuration
    public class RabbitDirectConfig {
        @Bean
        // Direct模式的队列
        public Queue directQueue() {
            // durable:是否持久化,默认false
            // exclusive:是否当前连接专用,默认False,连接关闭后队列即被删除
            // autoDelete:是否自动删除,当生产者或消费者不再使用此队列,自动删除
            // 一个参数的构造方法内部默认调用了4个参数的构造方法,后三个参数依次为true,false,false
            return new Queue("direct_queue");
        }
    
        @Bean
        // Direct模式的交换机
        public DirectExchange directExchange() {
            return new DirectExchange("directExchange");
        }
    
        @Bean
        public Binding bindingDirect() {
            return BindingBuilder.bind(directQueue()).to(directExchange()).with("direct");
        }
    }
  4. 编写业务层接口MessageService的实现类MessageServiceRabbitmqDirectImpl,注意需要将上个案例中的MessageServiceImpl类上的@Service注解去掉,要不然自动注入的时候会发生冲突。

    @Service
    public class MessageServiceRabbitmqDirectImpl implements MessageService {
        private AmqpTemplate amqpTemplate;
        @Override
        public void sendMessage(String orderId) {
            System.out.println("待发送短信的订单已纳入处理队列,id:" + orderId);
            // 指定RabbitDirectConfig中的内容
            amqpTemplate.convertAndSend("directExchange", "direct", orderId);
        }
    
        @Override
        public String doMessage() {
            String orderId = (String) amqpTemplate.receiveAndConvert("directExchange");
            System.out.println("已发送短信发送业务,id:" + orderId);
            return orderId;
        }
    }
  5. 要想消息存到MQ中以后自动去消费,就要用到JMS的消息监听器技术,只要被监听的队列中接收到新的数据就会去取出来执行。

    • 定义一个MessageListener类,在类头上加上@Component注解,将这个类交给spring管理
    • 定义一个方法receive(String orderId), 在方法上加上@RabbitListener(queues = "direct_queue"),需要指定队列名称。
    • 然后启动项目即可。
    @Component
    public class MessageListener {
        @RabbitListener(queues = "direct_queue")
        public void receive(String orderId) {
            System.out.println("已完成短信发送业务(RabbitMQ Direct one),id:" + orderId);
        }
    }
    • 注: 如果这里receive()方法带了返回值,则会报错如下错误:

      Caused by: org.springframework.amqp.AmqpException: Cannot determine ReplyTo message

      网上查找相关资料后得知,出错的原因是:@RabbitListener注解修饰监听的方法添加了返回值。此方法返回的消息没有设置目的地,解决:只要把监听的方法的返回值改为void即可。

  6. MessageListener.java复制出来一份,然后把仅修改receive方法中的输出语句,代码如下所示:

    @Component
    public class MessageListener2 {
        @RabbitListener(queues = "direct_queue")
        public void receive(String orderId) {
            System.out.println("已完成短信发送业务(RabbitMQ Direct two),id:" + orderId);
        }
    }

    启动项目以后向消息队列中存值,查看控制台的输出语句,可以知道两个监听器轮询交替被调用。

    image-20220215214619331

springboot_16_04_04_mq_rabbitmq_topic

P131 实用开发篇-127-springboot整合RabbitMQ(topic模式)

Topic模式的代码和Direct模式的代码很相似,所以,只需要根据Direct模式的代码复制一份修改即可。

  1. 编写RabbitTopicConfig类,配置Topic模式的交换机和队列,这里为了看效果,配置两个队列topic_queue1和topic_queue2

    @Configuration
    public class RabbitTopicConfig {
        @Bean
        // Topic模式的队列
        public Queue topicQueue1() {
            // durable:是否持久化,默认false
            // exclusive:是否当前连接专用,默认False,连接关闭后队列即被删除
            // autoDelete:是否自动删除,当生产者或消费者不再使用此队列,自动删除
            // 一个参数的构造方法内部默认调用了4个参数的构造方法,后三个参数依次为true,false,false
            return new Queue("topic_queue1");
        }
    
        @Bean
        // Topic模式的队列
        public Queue topicQueue2() {
            // durable:是否持久化,默认false
            // exclusive:是否当前连接专用,默认False,连接关闭后队列即被删除
            // autoDelete:是否自动删除,当生产者或消费者不再使用此队列,自动删除
            // 一个参数的构造方法内部默认调用了4个参数的构造方法,后三个参数依次为true,false,false
            return new Queue("topic_queue2");
        }
    
        @Bean
        // Topic模式的交换机
        public TopicExchange topicExchange() {
            return new TopicExchange("topicExchange");
        }
    
        @Bean
        public Binding bindingTopic1() {
            return BindingBuilder.bind(topicQueue1()).to(topicExchange()).with("topic.order.id");
        }
    
        @Bean
        public Binding bindingTopic2() {
            return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with("id");
        }
    }
  2. 编写业务层接口MessageService的实现类MessageServiceRabbitmqTopicImpl

    @Service
    public class MessageServiceRabbitmqTopicImpl implements MessageService {
        @Autowired
        private AmqpTemplate amqpTemplate;
        @Override
        public void sendMessage(String orderId) {
            System.out.println("待发送短信的订单已纳入处理队列,id:" + orderId);
            // 指定RabbitDirectConfig中的内容
            amqpTemplate.convertAndSend("topicExchange", "top.order.id", orderId);
        }
    
        @Override
        public String doMessage() {
            String orderId = (String) amqpTemplate.receiveAndConvert("topicExchange");
            System.out.println("已发送短信发送业务,id:" + orderId);
            return orderId;
        }
    }
  3. 要想消息存到MQ中以后自动去消费,就要用到JMS的消息监听器技术,只要被监听的队列中接收到新的数据就会去取出来执行。

    • 定义一个MessageListener类,在类头上加上@Component注解,将这个类交给spring管理
    • 上面定义了两个队列,所以这里定义2个方法receive1(String orderId)receive2(String orderId), 分别在方法上加上@RabbitListener(queues = "topic_queue1")@RabbitListener(queues = "topic_queue2")监听两个队列
    • 然后启动项目即可。
    @Component
    public class MessageListener {
        @RabbitListener(queues = "topic_queue1")
        public void receive1(String orderId) {
            System.out.println("已完成短信发送业务(RabbitMQ topic_queue111111),id:" + orderId);
        }
    
        @RabbitListener(queues = "topic_queue2")
        public void receive2(String orderId) {
            System.out.println("已完成短信发送业务(RabbitMQ topic_queue222222),id:" + orderId);
        }
    }
  4. Direct模式不同的是,Topic模式在定义队列的时候,routingKey支持模糊匹配,比如可以在RabbitTopicConfig类中的bindingTopic1()bindingTopic2()方法,两个方法里面的routingKey参数的指定都是绝对匹配的,发送消息的时候必须指定成一样routingKey的值才能将消息存到对应的队列中。

    @Bean
    public Binding bindingTopic1() {
        return BindingBuilder.bind(topicQueue1()).to(topicExchange()).with("topic.order.id");
    }
    
    @Bean
    public Binding bindingTopic2() {
        return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with("id");
    }

    如果对于with里面填的routingKeybindingTopic1()方法中的配置为topic.*.idbindingTopic2()方法中的配置为topic.order.*,其中*可以匹配任意单词,具体代码如下:

    @Bean
    public Binding bindingTopic1() {
        return BindingBuilder.bind(topicQueue1()).to(topicExchange()).with("topic.*.id");
    }
    @Bean
    public Binding bindingTopic2() {
        return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with("topic.order.*");
    }

    这样修改后,MessageServiceRabbitmqTopicImpl类中的消息发送到消息队列的时候,amqpTemplate.convertAndSend()方法的第二个参数routingKey的值,

    • 如果指定为topic.order.ids,那么top.order.*能匹配上,而topic.*.id不能匹配上,消息会存到名字为topic_queue2的消息队列中;

    • 如果routingKey的值指定为top.orders.id,那么只有topic.*.id能匹配上,消息会存到名字为topic_queue1

    • 如果routingKey的值指定为top.orders.id,那么top.order.*topic.*.id都能匹配上,消息就会同时存到两个队列中。

    @Service
    public class MessageServiceRabbitmqTopicImpl implements MessageService {
        @Autowired
        private AmqpTemplate amqpTemplate;
        @Override
        public void sendMessage(String orderId) {
            System.out.println("待发送短信的订单已纳入处理队列,id:" + orderId);
            // 指定routingKey的值为topic.order.id,消息会同时存到两个消息队列中
            amqpTemplate.convertAndSend("topicExchange", "topic.order.id", orderId);
        }
    
        @Override
        public String doMessage() {
            String orderId = (String) amqpTemplate.receiveAndConvert("topicExchange");
            System.out.println("已发送短信发送业务,id:" + orderId);
            return orderId;
        }
    }
  5. image-20220215225726767

springbooot_16_05_rocketmq

P132 实用开发篇-128-RocketMQ安装

  • RocketMQ安装

    • 下载地址:https://rocketmq.apache.org/

    • 下载完解压缩即可,默认服务端口:9876

    • 配置环境变量:ROCKETMQ_HOMEPATHNAMESRV_ADDR(建议):127.0.0.1:9876

    image-20220228003122616

    image-20220228003242985

    image-20220228003422761

  • RocketMQ启动

    RocketMQ的启动分为两部分,包括NameServer服务和Broker服务。

    首先启动NameServer服务

    • 双击安装目录的bin目录下的mqnamesrv.cmd启动命名服务器,这时候发生诡异的事情了,黑窗口一闪而过,根据经验,绝对是报错了。为了看到错误,我编辑了mqnamesrv.cmd文件,在最后加了一句pause,这样执行的时候就算报错了,黑窗口也不会立马关闭。

      image-20220228015834363

      再次双击mqnamesrv.cmd,看到报了如下的错误:

      image-20220228020052679

      我先自己排查下原因,

      image-20220228021851485

      在网上查阅了相关资料后,出现问题的原因是,jdk安装路径的问题,我安装jdk的时候默认了C盘的Program Files目录下,这个目录中间有空格,就是这个空格导致的问题,可是我去黑窗口输入javacjava -version等命令,并且tomcatmaven都可以启动啊,就很奇怪。参考博客:https://blog.csdn.net/weixin_41720239/article/details/118340382,知道了问题,下面就有两种解决方案了:

      1. 重装jdk,装到一个纯英文且路径中没有空格的目录下,然后重新配置环境变量,比较麻烦,我没有使用;

      2. 经过网上查阅资料,我找到了第二种解决方案,顺便记录一下我为了解决这个问题的思考过程:

        • 首先看最开始报的错误是找不到或无法加载主类 Files\Java\jdk1.8.0_131\jre\lib\ext,这个路径提到了jdk的安装路径的一部分,应该跟Java的环境变量JAVA_HOME有关,而mqnamesrv.cmd中根本没有提到;

        • 而又发现第19行是call "%ROCKETMQ_HOME%\bin\runserver.cmd",调用了另外一个叫runserver.cmd的cmd脚本,所以再编辑当前目录下的runserver.cmd文件,看到第34行%JAVA_HOME%\jre\lib\ext,问题应该就在这里;

        • JAVA_HOME路径中的Program Files的空格导致的这个问题,那么在JAVA_HOME外面加上双引号不就行了;

          image-20220228015445505

        • 然后再去双击mqnamesrv.cmd,运行成功,如下图所示。

          image-20220228022559775

    其次再启动Broker服务

    • 同样的,启动Broker服务的时候也会跟上面一样报错,解决方案是修改runbroker.cmd文件,给39行的JAVA_HOME外面加上双引号,如下图所示。

      image-20220228023751164

    • 双击mqbroker.cmd文件,等待启动即可,成功的话会提示服务监听在9876端口。

      image-20220228030913052

  • 下面验证一下RocketMQ服务是否可用

    RocketMQlib包下自带了一个rocketmq-example-4.9.2.jarjar包,用压缩软件打开一下,下面将会使用ProducerComsumer这两个类来验证RocketMQ的可用性。步骤如下:

    image-20220228025121291

    • RocketMQbin目录下,启动黑窗口,敲如下命令:

      tools org.apache.rocketmq.example.quickstart.Producer

      image-20220228025749691

      如果RocketMQ服务正常的话,会生产1000条消息,

      可是我报了如下错误:

      org.apache.rocketmq.client.exception.MQClientException: Send [3] times, still failed, cost [116]ms, Topic: TopicTest, BrokersSent: [DESKTOP-LTIN79F, DESKTOP-LTIN79F, DESKTOP-LTIN79F]
      See http://rocketmq.apache.org/docs/faq/ for further details.
              at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendDefaultImpl(DefaultMQProducerImpl.java:681)
              at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.send(DefaultMQProducerImpl.java:1391)
              at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.send(DefaultMQProducerImpl.java:1335)
              at org.apache.rocketmq.client.producer.DefaultMQProducer.send(DefaultMQProducer.java:336)
              at org.apache.rocketmq.example.quickstart.Producer.main(Producer.java:67)
      Caused by: org.apache.rocketmq.client.exception.MQBrokerException: CODE: 14  DESC: service not available now. It may be caused by one of the following reasons: the broker's disk is full [CL:  0.97 CQ:  0.97 INDEX: -1.00], messages are put to the slave, message store has been shut down, etc. BROKER: 172.25.96.1:10911
      For more information, please visit the url, http://rocketmq.apache.org/docs/faq/
              at org.apache.rocketmq.client.impl.MQClientAPIImpl.processSendResponse(MQClientAPIImpl.java:668)
              at org.apache.rocketmq.client.impl.MQClientAPIImpl.sendMessageSync(MQClientAPIImpl.java:507)
              at org.apache.rocketmq.client.impl.MQClientAPIImpl.sendMessage(MQClientAPIImpl.java:489)
              at org.apache.rocketmq.client.impl.MQClientAPIImpl.sendMessage(MQClientAPIImpl.java:433)
              at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendKernelImpl(DefaultMQProducerImpl.java:870)
              at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendDefaultImpl(DefaultMQProducerImpl.java:606)
              ... 4 more

      如果你在安装的过程中也碰到这个问题可以参考我的解决办法, 如果没有遇到可以跳过这段。

      网上查阅相关资料了解到这是由于日志文件所在的磁盘的空间太小,RocketMQ是根据ratio来决定磁盘是否满的,默认是90%,每60秒扫描一次磁盘,如果大于ratio就会对所有的producer发送过来的请求返回磁盘满的错。RocketMQ服务默认的日志位置是user.home,windows上默认在C盘的C:\Users\CandyWalllinux上默认在/root位置,我这里用安装在了windows上,而我C盘装了很多应用,空间已经所剩不多,所以如下两种解决方法:

      • 方法1:清理C盘,腾出更多空间,让C盘的可用空间达到10%以上,较麻烦,有耐心的可以尝试;

      • 方法2:修改RocketMQuser.home属性,从而修改日志记录位置,将位置改到可用空间较大的其他磁盘,打开runserver.cmdrunbroker.cmd文件,添加一行配置

        set "JAVA_OPT=%JAVA_OPT% -Duser.home=D:\mq\rocketmq\rocketmq\"

        image-20220228131015786

        改完以后,再依次执行mqnamesrv.cmdmqbroker.cmd,启动一个黑窗口,

        输入

        tools org.apache.rocketmq.example.quickstart.Producer

        可以看到Producer生产的消息发送成功。

        image-20220228131921007

        再启动一个黑窗口,输入

        tools org.apache.rocketmq.example.quickstart.Consumer

        image-20220228132431255

        可以看到消息的消费者也是成功取到了消息,到此验证了安装的RocketMQ是可用的。

P133实用开发篇-129-springboot整合RocketMQ

基于购物订单案例-发送短信案例的代码,整合RocketMQ

  1. pom.xml中加入springboot整合ActiveMQ的依赖

    <!--AMQP-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    
    <!--RocketMQ-->
    <dependency>
        <groupId>org.apache.rocketmq</groupId>
        <artifactId>rocketmq-spring-boot-starter</artifactId>
        <version>2.2.1</version>
    </dependency>
  2. application.yml中加入RocketMQ的配置

    rocketmq:
      name-server: localhost:9876
      # 指定生产者所属组
      producer:
        group: group_rocketmq
  3. 编写业务层接口MessageService的实现类MessageServiceRocketmqImpl,注意需要将上个案例中的MessageServiceImpl类上的@Service注解去掉,要不然自动注入的时候会发生冲突。

    @Service
    public class MessageServiceRocketmqImpl implements MessageService {
        @Autowired
        private RocketMQTemplate rocketMQTemplate;
        @Override
        public void sendMessage(String orderId) {
            System.out.println("待发送短信的订单已纳入处理队列,id:" + orderId);
            // 注意RocketMQ不支持类似order.queue.id这样的带特殊字符的destination
            // 发送同步消息,基本不用
            // rocketMQTemplate.convertAndSend("order_id", orderId);
            // 发送异步消息
            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", orderId, callback);
        }
    
     @Override
        public String doMessage() {
         return null;
        }
    }
  4. 要想消息存到MQ中以后自动去消费,就要用到JMS的消息监听器技术,只要被监听的队列中接收到新的数据就会去取出来执行。

    • 定义一个MessageListener类,在类头上加上@Component注解,将这个类交给spring管理
    • 实现接口RocketMQListener<String>和接口中的方法
    • 在类上加@RocketMQMessageListener(topic = "order_id", consumerGroup = "group_rocketmq")topicrocketMQTemplate.asyncSend()方法的第一个参数destination的值对应,consumerGroupapplication.yml中的group属性的值对应。
    @Component
    @RocketMQMessageListener(topic = "order_id", consumerGroup = "group_rocketmq")
    public class MessageListener implements RocketMQListener<String> {
       @Override
       public void onMessage(String orderId) {
           System.out.println("已完成短信发送业务(RocketMQ topic_queue),id:" + orderId);
       }
    }
  5. 启动项目,访问下面的地址,往MQ中发消息

    http://localhost:8080/orders/1

    image-20220228195328272

    消息存取成功。

springboot_16_06_kafka

P134实用开发篇-130-Kafka安装

下载地址:https://kafka.apache.org/downloads

安装:直接解压即可。

启动:去bin目录下可以看到都是.sh的执行脚本,这些都是在linux上才可以启动,我这里要在windows上进行测试,所以要找.bat或者.cmd的脚本了,看bin目录下还有一个windows目录,这个目录下就是相关的.bat执行脚本了。

  1. 启动命名服务器zookeeper

    bin/windows目录下输入如下命令:

    zookeeper-server-start.bat ../../config/zookeeper.properties

    启动正常如下,默认端口:2181

    image-20220228201159709

  2. 启动kafka

    再启动一个黑窗口,输入如下命令:

    kafka-server-start.bat ../../config/server.properties

    启动正常如下,默认端口:9092

    image-20220228201450690

  3. 验证kafka是否可用

    • 创建Topic

      • 开一个黑窗口,创建一个topic

        kafka-topics.bat --zookeeper 127.0.0.1:2181 --create --replication-factor 1 --partions 1 --topic candywall

        image-20220228202306958

      • 查看Topic

        kafka-topics.bat --zookeeper 127.0.0.1:2181 --list

        可以看到有我上面创建的名字为candywall的topic

        image-20220228202354376

      • 删除topic

        kafka-topics.bat --delete --zookeeper 127.0.0.1:2181 --topic candywall
    • 生产消息

      继续使用这个黑窗口,作为消息的生产者,产生一些消息,输入如下命令:

      kafka-console-producer.bat --broker-list 127.0.0.1:9092 --topic candywall
    • 消费消息

      再开一个黑窗口,作为消息的消费者,去消费一些消息,输入如下命令:

      kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic candywall --from-beginning
    • 如果两个窗口里面可以互相收发消息,说明kafka功能是可用的。

      image-20220228204539647

P135实用开发篇-131-springboot整合Kafka

基于购物订单案例-发送短信案例的代码,整合RocketMQ

  1. pom.xml中加入springboot整合Kafka的依赖

    <!--Kafka-->
    <dependency>
        <groupId>org.springframework.kafka</groupId>
        <artifactId>spring-kafka</artifactId>
    </dependency>
  2. application.yml中加入kafka的配置

    spring:
      kafka:
        bootstrap-servers: localhost:9092
        consumer:
          group-id: order
  3. 编写业务层接口MessageService的实现类MessageServiceKafkaImpl,注意需要将上个案例中的MessageServiceImpl类上的@Service注解去掉,要不然自动注入的时候会发生冲突。

    @Service
    public class MessageServiceKafkaImpl implements MessageService {
        @Autowired
        private KafkaTemplate<String, String> kafkaTemplate;
        @Override
        public void sendMessage(String orderId) {
            System.out.println("待发送短信的订单已纳入处理队列(kafka),id:" + orderId);
            kafkaTemplate.send("candywall", orderId);
        }
    
        @Override
        public String doMessage() {
            return null;
        }
    }
  4. 要想消息存到MQ中以后自动去消费,就要用到JKafka的消息监听器技术,只要被监听的队列中接收到新的数据就会去取出来执行。

    • 定义一个MessageListener类,在类头上加上@Component注解,将这个类交给spring管理;
    • 编写onMessage()方法,在方法上加上@KafkaListener(topics = "candywall"),其中topics的值和MessageServiceKafkaImpl中的kafkaTemplate.send()方法的第一个参数topic的值对应;
    @Component
    @RocketMQMessageListener(topic = "order_id", consumerGroup = "group_rocketmq")
    public class MessageListener implements RocketMQListener<String> {
       @Override
       public void onMessage(String orderId) {
           System.out.println("已完成短信发送业务(RocketMQ topic_queue),id:" + orderId);
       }
    }
  5. 启动项目,访问下面的地址,往MQ中发消息

    http://localhost:8080/orders/1

    image-20220228211417129

    消息存取成功。

17. 监控

springboot_17_01_admin_server

P136实用开发篇-132-监控的意义

P137实用开发篇-133-SpringBootAdmin

SpringBootAdmin分为服务器端和客户端:服务器端不涉及业务方法,近用来监控其他被监控的项目;客户端会将一些参数上报给服务器端,还会涉及一些业务方法。

  • springboot中引入SpringBootAdminServer(监控控制台)的步骤

    1. pom.xml中加入SpringBootAdminServer的依赖,跟SpringBoot的版本号一致

      <!--SpringbootAdminServer-->
      <dependency>
          <groupId>de.codecentric</groupId>
          <artifactId>spring-boot-admin-starter-server</artifactId>
          version>2.6.2</version>
      </dependency>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
     
  1. application.yml中加入指定SpringbootAdminServer Web服务的端口号

    ```yaml
    server:
      port: 8080
  2. springboot启动类上加@EnableAdminServer注解开启SpringBootAdmin服务

  3. 启动项目,打开浏览器输入localhost:8080就可以进入监控系统的主页了

    image-20220228222034286

    由于目前没有监控其他项目,所以显示为空,下面建几个项目由监控系统监控
springboot_17_02_admin_client
  • 在项目中引入SpringBootAdminClient(被监控)的步骤

    1. pom.xml中加入SpringbootAdminClient的依赖,跟SpringBoot的版本号一致

      <!--SpringbootAdminClient-->
      <dependency>
          <groupId>de.codecentric</groupId>
          <artifactId>spring-boot-admin-starter-client</artifactId>
          version>2.6.2</version>
      </dependency>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
    2. application.yml中指定当前SpringBootAdminClient客户端端口号,并且指定要加入SpringbootAdminServer Web服务url地址(包括IP和端口号),这样SpringBootAdmin客户端就可以注册到SpringBootAdmin上了,从而实现被监控。

      # 当前SpringBootAdminClient客户端端口
      server:
        port: 8081
        
      spring:
        boot:
          admin:
            client:
              # SpringbootAdminServer Web服务url地址
              url: http://localhost:8080
    3. 启动项目,打开浏览器输入localhost:8080,可以看到当前客户端已经注册到监控系统上了

      image-20220228225141758

      点监控墙

      image-20220228225214785

      再点击这个注册上来的实例,可以看到当前客户端的详细运行状况信息,不过看到的信息还是很有限,接下来去客户端开放更多的参数信息给监控服务器。

      image-20220228225429908

    4. 配置开放所有客户端指标信息给服务器

      application.yml中加入如下配置:

      # 当前SpringBootAdminClient客户端端口号
      server:
        port: 8081
      
      spring:
        boot:
          admin:
            client:
              # SpringbootAdminServer Web服务url地址
              url: http://localhost:8080
      # 配置开放给服务器端监控信息
      management:
        endpoint:
          health:
            # 健康状况的详细信息是否上传到服务器,默认为never
            show-details: always
        endpoints:
          web:
            exposure:
              # 开放所有的指标信息,默认为health
              include: "*"

      再启动项目,然后去浏览器查看对当前客户端的监控信息,可以看到监控信息非常丰富,包括内存磁盘的占用率,日志、JVM、缓存等信息应有尽有。

      image-20220228231135649

springboot_17_03_admin_client_ssmp
  • 下面基于springboot_08_ssmp拷贝出一个新项目springboot_17_03_admin_client_ssmp,然后把这个项目注册到监控服务器上。步骤同上,故略。配置完之后,启动这个项目,然后去浏览器的监控界面上可以看到,有两个监控实例了。

    image-20220228235028057

  • 点击查看这个ssmp项目的详细信息,可以看到被监控的指标给多了,还可以看到连接的是mysql数据库

    image-20220228235143461

  • 去浏览器输入http://localhost/pages/books.html,多刷新几次,然后再去监控页面去查看关于ssmp项目的http请求的次数和访问时间的统计

    image-20220228235744365

P138实用开发篇-134-actuator

image-20220301004201049

SSMP项目启动在80端口,使用Postman访问http://localhost/actuator,结果返回了一堆更具体的访问地址

image-20220301133645720

以其中的health为例,我们点击这个地址

image-20220301134219713

postman会建立一个新的http请求,再点击send发送请求

image-20220301134344858

可以看到返回的信息就是SpringBootAdmin页面关于SSMP项目运行时的健康状况信息

image-20220301134458497

所以这个SpringBootAdmin就是通过http:/ip:port/actuator/端点名称访问到不同项目的运行指标的。

其他常用端点名称和描述信息如下:

image-20220301134946953

image-20220301135054788

image-20220301135119792

SSMP项目启动的时候会打印一行日志,表示暴露给监控系统的端点数:

2022-03-01 13:54:43.124  INFO 8500 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 13 endpoint(s) beneath base path '/actuator'
  • 可以通过配置application.ymlmanagement.endpoints.web.exposure.include属性来指定开放给web监控系统的端点,比如给的值为"*",表示开发所有端点,值为healthinfo,表示仅开放当前项目运行时的健康信息和的项目描述信息。

  • 不过最终决定这个端点是否开放的源头是在management.endpoint.info.enabled属性,默认为true,如果指定为false,那么management.endpoints.web.exposure.include属性指定了info,也不会开放了。

  • 还有一个属性management.endpoints.enabled-by-default表示开放所有的端点,默认值为true,这个所有指的不仅仅是web端点,还包括jmx等其他访问方式的端点,如Win+R输入jconsole,在进程列表中选择SSMP项目

    image-20220301141637121

    也可以监控这个项目运行时的一些信息

    image-20220301141809957

  • 需要知道的是JMX默认开放所有的端点,而web只开放一部分。

    image-20220301142142625

P139实用开发篇-135-info端点指标控制

如果要自定义SpringBootAdmin客户端的info端点返回的属性值,有两种方式来实现

  1. 配置文件法:

    • 在application.yml中加入如下配置:

      # 配置开放给服务器端监控信息
      management:
        endpoint:
          health:
            # 健康状况的详细信息是否上传到服务器,默认为never
            show-details: always
          info:
            # 默认为true
            enabled: true
        endpoints:
          web:
            exposure:
              # 开放所有的指标信息,默认为health
              include: "*"
      # 保证info端点被启用
      info:
        appName: @project.artifactId@
        version: @project.version@
        company: 糖果编程之家
        author: 糖果墙
    • 我这里给info下配置了4个属性,其中appNameversion都引用了pom.xml中的属性值,这里需要先在pom.xmlbuild标签下面加下面的配置,否则会报一个@符号的解析错误。

      <!-- 使用 @@ application.yml 获取pom文件中的配置  -->
      <resources>
          <resource>
              <directory>src/main/resources</directory>
              <filtering>true</filtering>
          </resource>
      </resources>
    • 启动项目,然后去监控系统界面查看

P140实用开发篇-136-health端点指标控制

P141实用开发篇-137-metrics端点指标控制

P142实用开发篇-138-自定义端点(实用开发篇完结)

Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

简介

springboot学习 展开 收起
Java
Apache-2.0
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
Java
1
https://gitee.com/wanghannew1/spring-boot-study.git
git@gitee.com:wanghannew1/spring-boot-study.git
wanghannew1
spring-boot-study
spring-boot-study
master

搜索帮助