springboot2 (2) 常用整合

486 阅读12分钟

1. 整合LogBack

概念: spring-boot-starter 依赖中包含了 spring-boot-starter-logging 依赖,即springboot默认使用logback作为日志框架,它是log4j的改进版,不能单独使用,推荐配合slf4j一起使用:

  • springboot默认加载 classpath:logback.xmlclasspath:logback-spring.xml
  • 自定义日志配置文件需要在主配文件中指定:logging.config=classpath:console-log.xml
  • 日志级别:TRACE < DEBUG < INFO < WARN < ERROR < ALL,不区分大小写。

流程: 开发 classpath:logback/console-log.xml:将日志打印在控制台:

  • 配置控制台追加器 <appender name="STDOUT" class="c.q.l.c.ConsoleAppender>
    • <encoder> + <pattern>/<charset>:配置日志格式/字符集。
  • 配置根记录器 <root level="INFO">:仅记录INFO及以上级别的启动日志和运行时日志:
    • <appender-ref> 使用 ref 属性关联某个追加器名,表示使用该追加器,可存在0或多个。
  • 开发动作类并通过 LoggerFactory.getLogger(getClass()) 获取当前类的 Logger 实例:
    • logger.debug/info/warn/error(""):记录一条DEBUG/INFO/WARN/ERROR级别的日志信息。

流程: 开发 classpath:logback/file-log.xml:将日志输出到日志文件:

  • 配置文件追加器 <appender name="INFO" class="c.q.l.c.r.RollingFileAppender">
    • <encoder> + <pattern>/<charset>:配置日志格式/字符集。
    • <file>:指定日志文件路径,默认位置相对于所在项目,支持使用绝对路径。
    • <append>:设置是否追加日志,默认为true。
  • 配置过滤器:<filter class="c.q.l.c.f.LevelFilter">
    • <level>ERROR</level>:指定参考日志等级为ERROR。
    • <onMatch>DENY</onMatch>:ERROR级别日志阻止记录。
    • <onMismatch>ACCEPT</onMismatch>:除ERROR之外的级别日志允许记录。
  • 配置滚动策略:<rollingPolicy class="c.q.l.c.r.SizeAndTimeBasedRollingPolicy">
    • 策略:先将日志记录到日志文件中,如 my.log
      • 日期变化时将昨天的日志文件重命名为 my.日期.1.log,今天的日志文件名仍用 my.log
      • 日志文件超过拆分阈值时,将 my.log 拆为 my.日期.1.logmy.日期.2.log
    • <fileNamePattern>:日志文件格式,建议使用 log/warn.%d.%i.log
    • <maxHistory>:日志文件最长时效,单位根据 <fileNamePattern> 自动识别。
    • <totalSizeCap>:全部日志文件最大值,如 10GB,超出 10GB 立刻删除多余日志。
    • <maxFileSize>:每个日志文件拆分阈值,默认 10MB,文件大小超过此值时进行切分。
  • 配置运行时记录器:<logger name="c.y.app" level="WARN">:仅记录指定包中WARN及以上级别日志:
    • <appender-ref> 使用 ref 属性关联某个追加器名,表示使用该追加器,可存在0或多个。
    • <logger> 针对指定包进行日志记录,可存在多个,但无法记录启动日志,启动日志需使用根记日志录器。
  • 开发动作类 c.y.s.logback.LogBackController
    • LoggerFactory.getLogger(getClass()):获取当前类的 Logger 实例。
    • logger.debug/info/warn/error(""):记录一条DEBUG/INFO/WARN/ERROR级别的日志信息。
  • psm测试 api/logback/test

logback-pattern.md

概念:

  • <pattern> 中的规则越复杂,效率越低,尤其像类,方法,行号之类的占位符,应尽量避免使用。
  • <pattern> 中的日志输出格式支持使用空格,.-:[] 等进行分割,其余占位符见下表。

占位符表:

  • %d{yyyy-MM-dd HH:mm:ss.SSS}:日期,括号中为日期格式。
  • %highlight():对括号中的日志级别单词进行高亮显示,日志文件不支持:
    • DEBUG/INFO/WARN/ERROR单词分别显示为黑色/蓝色/浅红/深红。
  • %-5p:日志级别,不够5字符在末尾补空格到5字符,同 %-5level 写法。
  • %4.15():对括号中的字符串进行长度判断:
    • 小于4,则左补空格至长度为4,若为负数则右补空格。
    • 大于15,从开头开始截取保留15个。
  • %t:打印日志的线程名,同 %thead
  • %cyan():对括号中的日志级别进行蓝色修饰,日志文件不支持。
  • %c{50}:日志对应的类全名,括号中判断字符串长度,同 %logger{50}
    • 长度少于50:正常显示。
    • 长度大于50:从第一个包开始折叠包名为首字母,如 com.yap 变为 c.y,直到长度小于50。
  • %M:日志对应的方法名,同 %method
  • %L:日志对应代码行号,同 %line
  • %msg:日志输出内容。
  • %n:换行。 源码: /springboot2/
  • res: classpath:logback/console-log.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <!--控制台日志Appender-->
    <!--STDOUT意为标准输出-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5p) - [%4.15(%t)] %cyan(%c{50}.%M:%L) : %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!--根记录器,最低级别为INFO,建议放在最后-->
    <root level="INFO">
        <appender-ref ref="STDOUT" />
    </root>

</configuration>
  • src: classpath:logback/file-log.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <!--文件日志Appender-->
    <!--仅WARN级别的日志输出到文件中-->
    <appender name="info-appender" class="ch.qos.logback.core.rolling.RollingFileAppender">

        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p - [%4.10(%t)] %c{50}.%M:%L : %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>

        <file>springboot2/log/warn.log</file>

        <append>true</append>

        <!--过滤器:不记录ERROR,只记录WARN-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>

            <!--日志信息为ERROR级别时阻止(DENY)-->
            <onMatch>DENY</onMatch>

            <!--日志信息不为ERROR级别时允许记录(ACCEPT)-->
            <onMismatch>ACCEPT</onMismatch>
        </filter>

        <!--滚动策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>log/warn.%d.%i.log</fileNamePattern>
            <maxHistory>30</maxHistory>
            <totalSizeCap>5GB</totalSizeCap>
            <maxFileSize>5MB</maxFileSize>
        </rollingPolicy>

    </appender>

    <!--日志记录器-->
    <logger name="com.yap.springboot2.shield" level="INFO">
        <appender-ref ref="info-appender"/>
    </logger>

    <!--根记录器,最低级别为INFO,建议放在最后-->
    <root level="INFO">
        <appender-ref ref="info-appender" />
    </root>


</configuration>
  • src:com.yap.springboot2.logback
package com.yap.springboot2.logback;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author yap
 */
@RestController
@RequestMapping("api/logback")
public class LogBackController {

    private Logger logger = LoggerFactory.getLogger(LogBackController.class);

    @RequestMapping("test")
    public String test() {
        logger.debug("DEBUG级别信息...");
        logger.info("INFO级别信息...");
        logger.warn("WARN级别信息...");
        logger.error("ERROR级别信息...");
        return "success";
    }
}

2. 整合Thymeleaf

概念: Thymeleaf是一种模版引擎,能处理HTML,XML,TEXT,JAVASCRIPT和CSS模板内容,springboot默认会顺序从 /META-INF/resourcesclasspath:/resources/classpath:/static/classpath:/public/ 中寻找资源,有则直接返回,若都没有404:

  • 配置pom依赖:spring-boot-starter-thymeleaf
  • 主配添加:
    • spring.web.resources.static-locations:默认静态资源加载位置。
    • spring.thymeleaf.enabled:启用thymeleaf,默认true。
    • spring.thymeleaf.cache:启用thymeleaf缓存,默认true
    • spring.thymeleaf.prefix:配置thymeleaf响应路径前缀,默认classpath:/templates/。
    • spring.thymeleaf.suffix:配置thymeleaf响应路径后缀,默认.html。
    • spring.thymeleaf.encoding:配置thymeleaf编码,默认UTF-8。
  • 开发动作类 c.y.s.thymeleaf.ThymeleafController:响应路径自动补充主配中thymeleaf的前后缀。
  • 开发页面 classpath:templates/thymeleaf-test.html
    • <html> 中添加thymeleaf的命名空间:xmlns:th="http://www.thymeleaf.org"
    • 在HTML标签中使用 th:text="${msg }" 可取出请求域中的值,该值支持 +| 进行字符拼接。
  • 开发友好页面 classpath:public/error/4xx.html 以自定义4xx状态的反馈页面,仅浏览器生效。
  • 开发友好页面 classpath:public/error/5xx.html 以自定义5xx状态的反馈页面,仅浏览器生效。
  • psm测试:api/thymeleaf/test

源码: /springboot2/

  • 整合bootstrap
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Bootstrap 101 Template</title>
    <link href="/bootstrap337/css/bootstrap.min.css" th:src="@{/bootstrap337/css/bootstrap.min.css}" rel="stylesheet">
</head>
<body>

<button class="btn button">aaaaaaaaaaaa</button>


<script th:src="@{/jquery321/jquery-3.2.1.min.js}" ></script>
<script th:src="@{/bootstrap337/js/bootstrap.min.js}"></script>
</body>
</html>

  • res: pom.xml
  <!--spring-boot-starter-thymeleaf-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
  • res:application.properties
### 整合thymeleaf
# 默认静态资源加载位置
spring.web.resources.static-locations=\
  classpath:/META-INF/resources/,\
  classpath:/resources/,\
  classpath:/static/,\
  classpath:/public/,\
  classpath:/templates/
# 关闭thymeleaf缓存,默认true
spring.thymeleaf.cache=false
  • src:c.y.s.thymeleaf.ThymeleafController
package com.yap.springboot2.thymeleaf;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.Map;

/**
 * @author yap
 */
@Controller
@RequestMapping("api/thymeleaf")
public class ThymeleafController {

    @RequestMapping("test")
    public String test(Map<String, Object> map, String name) {
        map.put("msg", name);
        // /templates/thymeleaf-test.html
        return "thymeleaf-test";
    }
}
  • res:classpath:templates/thymeleaf-test.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>thymeleaf</title>
</head>
<body>

<h1>thymeleaf!</h1>

<!--从map中取出msg-->
<h2 th:text="${msg }"></h2>

<!--${msg}可以直接跟字符串使用加号拼接-->
<h2 th:text="'前置字符串内容: ' + ${msg } + ' :后置字符串内容'"></h2>

<!--${msg}可以在两个竖线(|)之内格式化输出-->
<h2 th:text="|前置字符串内容: ${msg } :后置字符串内容|"></h2>

</body>
</html>
  • res:classpath:public/error/4xx.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1><a>路径不存在,点我返回主页!</a></h1>
</body>
</html>
  • res:classpath:public/error/5xx.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1><a>服务器异常,点我返回主页!</a></h1>
</body>
</html>

3. 整合MyBatis

概念: springboot支持原生JDBC,apache-dbutils,JPA,Hibernate,MyBatis等持久层框架:

  • 开发数据库:README.md + springboot.sql
  • 配置pom依赖:
    • spring-boot-starter-jdbc/mybatis-spring-boot-starter/mysql-connector-java/druid
  • 主配添加:
    • mybatis.mybatis.config-location:配置mybatis主配文件位置,可忽略。
    • mybatis.mapper-locations:配置SQL配置文件位置(使用注解配置SQL可忽略此项)。
    • mybatis.type-aliases-package:别名包扫描。
    • mybatis.configuration.log-impl=o.a.i.l.s.StdOutImpl:控制台SQL。
    • spring.datasource.driver-class-name/url/username/password:数据源四项。
    • spring.datasource.type:连接池,默认 c.z.h.HikariDataSource
  • 开发ORM实体类 c.y.s.pojo.Student
  • 开发SQL配置文件,若使用注解配置SQL可忽略此项。
  • 开发数据接口 c.y.s.mybatis.mapper.StudentMapper:标记 @Repository
  • 开发数据测试类 c.y.s.mybatis.StudentMapperTest
  • 开发业务接口 c.y.s.mybatis.StudentService
  • 开发业务类 c.y.s.mybatis.StudentServiceImpl:标记 @Service
  • 开发业务测试类 c.y.s.mybatis.StudentServiceTest
  • 开发动作类 c.y.s.mybatis.StudentController:标记 @Controller
  • 入口类:使用 @MapperScan 包扫描Mapper接口包以替代对每个Mapper接口标记 @Mapper
    • @MapperScan 包扫描需要指定到具体的mapper包(非父包),且包中不要存在其他接口。
  • psm测试:api/student/select-by-id

源码: /springboot2/

  • res:README.md
## url
- cli: `http://locahost:8080/`

## database
- cmd: `create database springboot character set utf8mb4;`
- cmd: `use springboot;`
- cmd: `grant all on springboot.* to 'springboot'@'localhost' identified by 'springboot';`
- cmd: `flush privileges;`
  • res:springboot.sql
DROP TABLE IF EXISTS `STUDENT`;
CREATE TABLE `STUDENT`
(
    `id`     INT(11) AUTO_INCREMENT COMMENT '主键',
    `name`   VARCHAR(50) NOT NULL COMMENT '学生姓名',
    `gender` TINYINT(2) NOT NULL COMMENT '学生性别',
    `age`    TINYINT(3) COMMENT '学生年龄',
    `info`   VARCHAR(500) COMMENT '学生信息',
    PRIMARY KEY (`id`)
)
    COMMENT '学生表';

INSERT INTO `STUDENT`
VALUES (1, '赵四', 1, 58, '亚洲舞王'),
       (2, '刘能', 2, 59, '玉田花圃'),
       (3, '大脚', 0, 18, '大脚超市');
COMMIT;
  • res:pom.xml
   <!--spring-boot-starter-jdbc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!--mybatis-spring-boot-starter-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>

        <!--mysql-connector-java-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!--druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.6</version>
        </dependency>

  • res:application.properties

### 整合mybatis
# 引入SQL配置文件
mybatis.mapper-locations=classpath:/mapper/*Mapper.xml

# 别名包扫描
mybatis.type-aliases-package=com.yap.springboot2.mybatis

# 控制台打印SQL语句,一般用于本地开发测试
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

# 数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/springboot?serverTimezone=UTC&characterEncoding=utf-8
spring.datasource.username=springboot
spring.datasource.password=springboot

# 连接池,默认com.zaxxer.hikari.HikariDataSource
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

  • src:c.y.s.mybatis.Student
package com.yap.springboot2.mybatis;

import lombok.Data;

import java.io.Serializable;

/**
 * @author yap
 */
@Data
public class Student implements Serializable {

    private Integer id;
    private String name;
    private Integer gender;
    private Integer age;
    private String info;
}

  • src:c.y.s.mybatis.mapper.StudentMapper
package com.yap.springboot2.mybatis.mapper;

import com.yap.springboot2.mybatis.Student;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

/**
 * @author yap
 */
@Repository
public interface StudentMapper {

    /**
     * 根据主键查询学生记录
     *
     * @param id 学生ID
     * @return 一条学生记录
     */
    @Select("<script>" +
            "select id, name, age, gender, info from student where " +
            "<if test='id != null'>id = #{id}</if>" +
            "</script>")
    Student selectById(@Param("id") Integer id);
}

  • tst:c.y.s.mybatis.StudentMapperTest
package com.yap.springboot2.mybatis;

import com.yap.springboot2.mybatis.mapper.StudentMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @author yap
 */
@SpringBootTest
class StudentMapperTest {

    @Autowired
    private StudentMapper studentMapper;

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

  • src:c.y.s.mybatis.StudentService
package com.yap.springboot2.mybatis;

/**
 * @author yap
 */
public interface StudentService {
    /**
     * 根据主键查询学生记录
     *
     * @param student 学生实体
     * @return 一条学生记录
     */
    Student selectById(Student student);
}

  • src:c.y.s.mybatis.StudentServiceImpl
package com.yap.springboot2.mybatis;

import com.yap.springboot2.mybatis.mapper.StudentMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author yap
 */
@Service
public class StudentServiceImpl implements StudentService {

    private StudentMapper studentMapper;

    @Autowired
    public StudentServiceImpl(StudentMapper studentMapper) {
        this.studentMapper = studentMapper;
    }

    @Override
    public Student selectById(Student student) {
        Integer id;
        if (student == null || (id = student.getId()) == null) {
            return new Student();
        }
        return studentMapper.selectById(id);
    }
}

  • tst:`c.y.s.mybatis.StudentController
package com.yap.springboot2.mybatis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author yap
 */
@RestController
@RequestMapping("api/student")
public class StudentController {

    private StudentService studentService;

    @Autowired
    public StudentController(StudentService studentService) {
        this.studentService = studentService;
    }

    @RequestMapping("select-by-id")
    public Student selectById(Student student) {
        return studentService.selectById(student);
    }

}

4. 整合WebSocket

概念: WebSocket是HTML5提供的一种在单个TCP连接上进行全双工通讯的协议,B端和S端只需完成一次握手即可建立一条快速的,持久的双向数据通道,更节省服务器资源和带宽:

  • 配置pom依赖:spring-boot-starter-websocket
  • 开发配置类 c.y.s.websocket.WebSocketConfig
    • IOC o.s.w.s.s.s.ServerEndpointExporter 类。
  • 开发S端类 c.y.s.websocket.WebSocketServer:用于接收B端 ws:// 开头的连接请求:
    • 使用 @ServerEndpoint 指定端点地址,建议使用REST风格请求配合 @PathParam 取值。
    • 使用Map容器存储所有B端ID及其S端实例,必须保证容器线程安全,共享且唯一。
    • 每个B端都独有一个S端实例和 javax.websocket.Session 长连接会话属性。
  • 开发S端类 @OnOpen 方法:当B端上线时触发,且只触发一次:
    • 使用Map容器存储上线的B端ID及其S端实例。
    • 使用 session.getAsyncRemote()/getBasicRemote().sendText() 向B端异步/同步推送消息。
  • 开发S端类 @OnMessage 方法:当B端发消息时执行,形参中可直接获取此字符串消息:
    • 遍历Map容器并向所有在线B端推送该B端的消息(群推送)。
  • 开发S端类 @OnClose 方法:当B端下线时触发:
    • 从Map容器中移除下线的B端ID及其S端实例,并群推送该B端的下线消息。
  • 开发S端类 @OnError 方法:当连接或通信异常时执行,形参中可直接获取 Throwable 对象。
  • 开发页面 classpath:templates/cli-*.html 模拟多个B端:需提前判断 WebSocket 兼容性:
    • new WebSocket("ws://端点地址"): 获取websocket对象。
    • socket.onopen():B端上线时触发,调用 socket.send() 可向S端发送消息。
    • socket.onmessage():B端收到消息推送时触发,回调函数中用 response["data"] 取出消息内容。
    • socket.onclose():当B端下线时触发,调用 socket.close() 可手动下线。
    • socket.onerror():当连接或通信异常时触发。
  • cli测试:cli-1/cli-2.html 源码: /springboot2/
  • res:pom.xml
       <!--spring-boot-starter-websocket-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
  • src:c.y.s.websocket.WebSocketConfig
package com.yap.springboot2.websocket;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * @author yap
 */
@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
  • src:c.y.s.websocket.WebSocketServer
package com.yap.springboot2.websocket;

import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author yap
 */
@ServerEndpoint("/websocket-server/{id}")
@Component
public class WebSocketServer {

    /**
     * 用于存储每个BS用户的WebSocketServer实例
     * ConcurrentHashMap保证线程安全,static保证实例唯一
     */
    private static Map<String, WebSocketServer> servers = new ConcurrentHashMap<>();
    private Session session;

    @OnOpen
    public void onOpen(@PathParam("id") String id, Session session) {
        this.session = session;

        servers.put(id, this);
        System.out.printf("cli-%s login, total %d clients...\n", id, servers.size());
        sendToAll("cli-" + id + " login...");
    }

    @OnClose
    public void onClose(@PathParam("id") String id, Session session) {
        servers.remove(id);
        System.out.printf("cli-%s logoff, total %d clients...\n", id, servers.size());
        sendToAll("cli-" + id + " logoff...");
    }

    @OnMessage
    public void onMessage(@PathParam("id") String id, String msg, Session session) {
        sendToAll("cli-" + id + " say: " + msg);
    }

    @OnError
    public void onError(@PathParam("id") String id, Throwable e, Session session) {
        System.out.println("cli-" + id + " error...");
        e.printStackTrace();
    }

    private void sendToAll(String msg){
        for (String key : servers.keySet()) {
            servers.get(key).session.getAsyncRemote().sendText(msg);
        }
    }
}
  • web:classpath:templates/cli-1.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>用户1</title>
</head>
<body>
<section>
    <h1>用户1</h1>
    <button type="button" onclick="login()">上线</button>
    <button type="button" onclick="logoff()">下线</button>
    <hr/>
    <label>
        <input id="msg-ipt" placeholder="input msg here..."/>
    </label>
    <button type="button" onclick="sendMsg()">发送</button>
    <button type="button" onclick="clearMsg()">清屏</button>
    <hr/>
    <div id="screen"></div>
</section>

<script type="text/javascript">

    let socket, screen, msgIpt;

    onload = () => {
        screen = document.getElementById("screen");
        msgIpt = document.getElementById("msg-ipt");
    };

    function login() {
        if (WebSocket) {
            // 使用ws://协议连接S端,并传递B端id过去
            socket = new WebSocket("ws://localhost:8080/websocket-server/1");
            socket.onopen = () => screen.innerText += "client: cli-1 login...\n";
            socket.onmessage = msg => screen.innerText += "server: " + msg["data"] + "\n";
            socket.onclose = () => screen.innerText += "client: cli-1 logoff...\n";
            socket.onerror = () => screen.innerText += "client: cli-1 error...\n";
        } else {
            screen.innerText = "browser not support websocket!\n";
        }
    }

    function logoff() {
        if (socket) {
            socket.close();
        }
    }

    function sendMsg() {
        if (socket) {
            socket.send(msgIpt.value);
            msgIpt.value = "";
        }
    }

    function clearMsg(){
        screen.innerText = "";
    }

</script>
</body>
</html>
  • web:classpath:templates/cli-2.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>用户2</title>
</head>
<body>
<section>
    <h1>用户2</h1>
    <button type="button" onclick="login()">上线</button>
    <button type="button" onclick="logoff()">下线</button>
    <hr/>
    <label>
        <input id="msg-ipt" placeholder="input msg here..."/>
    </label>
    <button type="button" onclick="sendMsg()">发送</button>
    <button type="button" onclick="clearMsg()">清屏</button>
    <hr/>
    <div id="screen"></div>
</section>

<script type="text/javascript">

    let socket, screen;

    onload = () => {
        screen = document.getElementById("screen");
    };

    function login() {
        if (WebSocket) {
            // 使用ws://协议连接S端,并传递B端id过去
            socket = new WebSocket("ws://localhost:8080/websocket-server/2");
            socket.onopen = () => screen.innerText += "client: cli-2 login...\n";
            socket.onmessage = msg => screen.innerText += "server: " + msg["data"] + "\n";
            socket.onclose = () => screen.innerText += "client: cli-2 logoff...\n";
            socket.onerror = () => screen.innerText += "client: cli-2 error...\n";
        } else {
            screen.innerText = "browser not support websocket!\n";
        }
    }

    function logoff() {
        if (socket) {
            socket.close();
        }
    }

    function sendMsg() {
        if (socket) {
            socket.send(msgIpt.value);
            msgIpt.value = "";
        }
    }

    function clearMsg() {
        screen.innerText = "";
    }

</script>
</body>
</html>

4.1 SSE推送技术

概念: SSE(server-sent events)可以替代Ajax轮询,不断发送请求接收S端推送消息,学习成本低:

  • 开发动作类 c.y.s.sse.SseController
    • 动作方法路由中需指定 produces="text/event-stream;charset=UTF-8"
    • 动作方法必须标记 @ResponseBody,且 return 值使用 data: 前缀和 \n\n 后缀。
  • 开发页面 classpath:/sse.html 模拟B端:需提前判断 EventSource 兼容性:
    • new EventSource(): 获取EventSource对象。
    • eventSource.onopen():当B端连接到S端时触发。
    • eventSource.onmessage(): B端收到消息推送时触发。
  • cli测试:sse.html

源码: /springboot2/

  • src:c.y.s.sse.SseController
package com.yap.springboot2.sse;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

/**
 * @author yap
 */
@RestController
@RequestMapping("api/sse")
public class SseController {

    @RequestMapping(value = "test", produces = "text/event-stream;charset=UTF-8")
    public String test() {
        try {
            TimeUnit.SECONDS.sleep(1L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "data:" + Math.random() + "\n\n";
    }
}
  • web:classpath:/sse.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<div id="screen"></div>

<script type="text/javascript">
    let screen;
    onload = () => {
        if (EventSource) {
            screen = document.getElementById("screen");
            let eventSource = new EventSource("api/sse/test");
            eventSource.onopen = () => screen.innerText += "sse open...\n";
            eventSource.onmessage = ev => {
                ev = ev || event;
                screen.innerText += ev.data + "\n";
            };
        } else {
            screen.innerText = "browser not support sse!\n";
        }
    };
</script>
</body>
</html>

5. 整合Actuator

概念: actuator 监控组件负责使用HTTP端点对线上项目进行健康检查,审计,收集指标等,目的是保持项目的高可用:

  • 配置pom依赖:spring-boot-starter-actuator
  • psm测试 /actuator:查看actuator对外暴露的所有端点。
  • 主配选配:注意 启用 指开启功能,暴露 指对外开放:
    • management.server.port:actuator服务端口,默认8080。
    • management.endpoints.enabled-by-default:是否启用shutdown外的其他端点,默认true。
    • management.endpoint.shutdown.enabled:是否启用shutdown端点,默认false。
    • management.endpoints.web.exposure.include:暴露端点列表,逗号分隔,* 为全部暴露:
      • actuator常用端点.md
    • management.endpoints.web.exposure.exclude:禁用端点列表,逗号分隔,* 为全部禁用:
      • exclude 优先级高于 include
  • /actuator/health 端点用于查看项目健康信息,status=UP 表示项目健康:
    • management.endpoint.health.show-details=always:详细展示项目健康信息。
    • management.endpoint.health.cache.time-to-live:健康信息缓存时间,默认0,即不缓存。
  • health健康检查器:health内置健康检查器的生效前提是项目中正确配置对应技术:
    • health内置健康检查器.md
  • 开发自定义健康检查器类 c.y.s.actuator.MyHealthIndicator,标记 @Component
    • 实现 o.s.b.a.h.HealthIndicator 接口并实现 health()
    • Health.up().withDetail().bulid() 用于创建一个UP状态的Health对象。
    • Health.down().bulid() 用于创建一个DOWN状态的Health对象。
  • /actuator/info 端点用于查看项目配置信息,默认展示主配中以 info 为前缀的配置项内容。
  • /actuator/metrics:用于查看运行状态如jvm内存信息,tomcat信息,垃圾回收信息等,需手动暴露:
    • /actuator/metrics/jvm.memory.max,查看jvm内存最大值,其余项同理。
  • 自定义端点类 c.y.s.actuator.MyEndPoint:标记 @Configuration,并手动暴露:
    • 标记 @Endpoint 以声明为端点类,建议使用 - 分割或简单的的id值设置端点名。
    • 开发端点方法,标记 @ReadOperation,构建一个List/Map数据,其内容即为端点内容。
    • psm测试 /actuator/students

源码: /springboot2/

  • res:classpath:application.properties
# 整合actuator
# management.server.port=8090
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
info.yap=yap
info.groupId=com.yap
info.artifactId=springboot2
info.version=0.0.1-SNAPSHOT
  • src:c.y.s.actuator.MyHealthIndicator
package com.yap.springboot2.actuator;

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

import java.io.File;
import java.util.Arrays;

/**
 * @author yap
 */
@Component
public class MyHealthIndicator implements HealthIndicator {

    @Override
    public Health health() {

        long totalDiskSpace = 0L;
        long freeDiskSpace = 0L;

        // show all root disk name , [C:\, D:\, F:\]
        File[] rootFiles = File.listRoots();
        System.out.println(Arrays.toString(rootFiles));

        if (rootFiles != null && rootFiles.length > 0) {

            for (File file : rootFiles) {
                totalDiskSpace += file.getTotalSpace();
                freeDiskSpace += file.getUsableSpace();
            }

            return Health.up()
                    .withDetail("total", totalDiskSpace + "byte")
                    .withDetail("free", freeDiskSpace + "byte")
                    .build();
        }
        return Health.down().build();
    }
}
  • src:c.y.s.actuator.MyEndPoint
package com.yap.springboot2.actuator;

import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author yap
 */
@Configuration
@Endpoint(id = "students")
public class MyEndPoint {

    @ReadOperation
    public List<Map<String, String>> execute() {
        List<Map<String, String>> users = new ArrayList<>();
        Map<String, String> map;
        map = new HashMap<>(3);
        map.put("id", "1");
        map.put("name", "赵四");
        map.put("age", "58");
        users.add(map);
        map = new HashMap<>(3);
        map.put("id", "2");
        map.put("name", "刘能");
        map.put("age", "59");
        users.add(map);
        return users;
    }
}

actuator常用端点

端点描述
health展示项目中的健康信息,默认暴露
info展示任意的项目信息,默认暴露
metrics展示运行状态的度量信息
auditevents展示当前项目中的审计事件信息,需要 AuditEventRepository 的bean
beans展示当前项目中所有Spring bean的完整列表
caches展示项目中可用缓存列表
scheduledtasks展示项目中的定时任务列表
loggers展示和修改项目中的日志
mappings展示 @RequestMapping 路径列表
configprops展示 @ConfigurationProperties 的路径列表
env展示所有来自于 ConfigurableEnvironment 的属性列表
conditions展示在配置和自动配置类上评估的条件,以及它们匹配或不匹配的原因
httptrace展示HTTP跟踪信息(默认情况下,最后100个HTTP请求-响应交换),需要 HttpTraceRepository 的bean
liquibase展示 Liquibase 数据库迁移信息
flyway展示 Flyway 数据库迁移信息,需要一个或多个 Flyway 的bean
sessions允许操作session
shutdown允许以优雅的方式关闭,默认禁用,需要使用POST请求
threaddump执行线程转储

health内置健康检查器

health内置的健康检查器描述信息
CassandraHealthIndicator检查 Cassandra 数据库是否启动。
DiskSpaceHealthIndicator检查磁盘空间不足。
DataSourceHealthIndicator检查是否可以获得连接 DataSource
ElasticsearchHealthIndicator检查 Elasticsearch 集群是否启动。
InfluxDbHealthIndicator检查 InfluxDB 服务器是否启动。
JmsHealthIndicator检查 JMS 代理是否启动。
MailHealthIndicator检查邮件服务器是否启动。
MongoHealthIndicator检查 Mongo 数据库是否启动。
Neo4jHealthIndicator检查 Neo4j 服务器是否启动。
RabbitHealthIndicator检查 Rabbit 服务器是否启动。
RedisHealthIndicator检查 Redis 服务器是否启动。
SolrHealthIndicator检查 Solr 服务器是否已启动。