1. 整合LogBack
概念: spring-boot-starter 依赖中包含了 spring-boot-starter-logging 依赖,即springboot默认使用logback作为日志框架,它是log4j的改进版,不能单独使用,推荐配合slf4j一起使用:
- springboot默认加载
classpath:logback.xml或classpath: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.log和my.日期.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/resources,classpath:/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缓存,默认truespring.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类。
- IOC
- 开发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 服务器是否已启动。 |