1 应用1
1.1 断点:3种
01.Debug分类
a.行断点
行断点的投标就是一个红色的圆形点。在需要断点的代码行头点击即可打上
b.方法断点
方法断点就是将断点打在某个具体的方法上面,当方法执行的时候,就会进入断点
这个当我们阅读源码或者跟踪业务流程时比较有用
c.属性断点
在某个属性字段上面打断点,这样就可以监听这个属性的读写变化过程
02.断点技巧
a.条件断点
a.代码
public class DebugTest {
public static void main(String[] args) {
List<Student> studentList = new ArrayList<>();
for (int i = 1 ; i < 1000 ; i++) {
if (new Random().nextInt(100) % 10 == 0) {
studentList.add(new Student("skjava-" + i, i));
} else {
studentList.add(new Student("damingge-" + i, i));
}
}
for (Student student : studentList) {
System.out.println(student.toString());
}
}
}
b.说明
System.out.println(student.toString()); 打个断点
但是要 name 以 "skjava" 开头时才进入,这个时候我们就可以使用条件断点
b.模拟异常
a.代码
public class DebugTest {
public static void main(String[] args) {
methodA();
try {
methodB();
} catch (Exception e) {
e.printStackTrace();
// do something
}
methodC();
}
public static void methodA() {
System.out.println("methodA...");
}
public static void methodB() {
System.out.println("methodA...");
}
public static void methodC() {
System.out.println("methodA...");
}
}
b.说明
在开发阶段我们就需要人为制造异常场景来验证我们的异常处理逻辑是否正确
c.多线程调试
a.说明
在你和前端进行本地调试时,你同时又要调试自己写的代码
前端也要访问你的本地调试,这个时候你打断点了,前端是无法你本地的
b.为什么呢?
因为 Idea 在 debug 时默认阻塞级别为 ALL
如果你进入 debug 场景了,idea 就会阻塞其他线程,只有当前调试线程完成后才会走其他线程
c.调试
这个时候,我们可以在 View Breakpoints 中选择 Thread,同时点击 Make Default设置为默认选项
这样,你就可以调试你的代码,前端又可以访问你的应用了
d.调试Stream
a.代码
public class DebugTest {
public static void main(String[] args) {
List<Student> studentList = new ArrayList<>();
for (int i = 1; i < 1000; i++) {
if (new Random().nextInt(100) % 10 == 0) {
studentList.add(new Student("skjava-" + i, i));
} else {
studentList.add(new Student("damingge-" + i, i));
}
}
studentList = studentList.stream()
.filter(student -> student.getName().startsWith("skjava-"))
.peek(item -> {
item.setName(item.getName() + "-**");
item.setAge(item.getAge() * 10);
}).collect(Collectors.toList());
}
}
b.说明
利用 Stream 处理一个 List 对象后,发现结果不对,但是你很难判断到底是哪一行出来问题
在 stream() 打上断点,运行代码,进入断点后
在这个窗口中会记录这个 Stream 操作的每一个步骤
我们可以点击每个标签来看数据处理是否符合预期。这样是不是就非常方便了
有些小伙伴的 idea 版本可能过低,需要安装 Java Stream Debugger 插件才能使用
e.操作回退
a.说明
debug 调试的时候肯定不是一行一行代码的调试,而是在每个关注点处打断点,然后跳着看
但是跳到某个断点处时,突然发现有个变量的值你没有关注到需要回退到这个变量值的赋值处,这个时候怎么办?
我们通常的做法是重新来一遍。虽然,可以达到我们的预期效果,但是会比较麻烦,其实 idea 有一个回退断点的功能
b.在IDEA中有两种回退
a.Reset Frame
public class DebugTest {
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = (a + b) * 2;
int d = addProcessor(a, b,c);
System.out.println();
}
private static int addProcessor(int a, int b, int c) {
a = a++;
b = b++;
return a + b + c;
}
}
---------------------------------------------------------------------------------------------
在 addProcessor() 的 return a + b + c; 打上断点
到了这里 a 和 b 的值已经发生了改变,如果我们想要知道他们两的原始值,就只能回到开始的地方
idea 提供了一个 Reset Frame 功能,这个功能可以回到上一个方法处
b.Jump To Line
Reset Frame 虽然可以用,但是它有一定的局限性,它只能方法级别回退,是没有办法向前或向后跳着我们想要执行的代码处
但 Jump To Line 可以做到,Jump To Line 是一个插件
1.2 防盗链:图片
00.开始
a.说明
可以对付一般情况下的图片盗链,但并不能保证绝对安全
b.可能出现以下等情况
Referer 伪造:恶意客户端可以伪造referer头,攻击者可以伪造有效的 referer 来绕过保护
漏报:攻击者可能找到绕过referer检查的方法(例如使用 data URI 或 base64 编码的图片)
误报:合法用户可能因为referer不匹配而被阻止(例如隐私浏览器或代理服务器)
反向代理:攻击者可以在url路径中,添加域名白名单作为反向代理路径,绕开代码的contains方法检查
01.简易版:代码写死配置
a.创建拦截器类
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class ImageProtectionInterceptor implements HandlerInterceptor {
private static final String ALLOWED_DOMAIN = "baidudu.com"; // 允许的域名
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取请求的 URL
String requestUrl = request.getRequestURL().toString();
// 判断请求是否以图片后缀结尾
if (requestUrl.endsWith(".jpg") || requestUrl.endsWith(".png") || requestUrl.endsWith(".jpeg")) {
// 获取请求的来源域名
String referer = request.getHeader("Referer");
// 检查来源域名是否符合预期
if (referer != null && referer.contains(ALLOWED_DOMAIN)) {
returntrue; // 符合防盗链要求,放行请求
} else {
response.sendError(HttpServletResponse.SC_FORBIDDEN); // 返回 403 Forbidden
returnfalse; // 拦截请求
}
}
returntrue; // 对非图片资源请求放行
}
}
b.注册拦截器
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器,拦截所有请求
registry.addInterceptor(new ImageProtectionInterceptor())
.addPathPatterns("/**"); // 拦截所有请求
}
}
02.灵活配置
a.配置
# 图片防盗链配置
img-protect:
# 图片防盗链保护开关
enabled: true
# 是否允许浏览器直接访问
allowBrowser: false
# 图片防盗链白名单,多个用逗号分隔【不填则所有网站都拦截】
allowReferer: baidudu.com
b.创建配置文件映射类
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties("img-protect")
public class ImgProtectConfig {
private boolean enabled;
private boolean allowBrowser;
private String allowReferer;
public boolean getEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean getAllowBrowser() {
return allowBrowser;
}
public void setAllowBrowser(boolean allowBrowser) {
this.allowBrowser = allowBrowser;
}
public String getAllowReferer() {
return allowReferer;
}
public void setAllowReferer(String allowReferer) {
this.allowReferer = allowReferer;
}
}
c.创建拦截器类
import 上方2-2创建的类路径.ImgProtectConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
@Component
public class ImageProtectionInterceptor implements HandlerInterceptor {
@Autowired
private ImgProtectConfig imgProtectConfig;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 判断是否开启图片防盗链功能
if (!imgProtectConfig.getEnabled()){
returntrue;
}
// 获取请求的 URL
String requestUrl = request.getRequestURL().toString();
// 判断请求是否以图片后缀结尾
if (requestUrl.endsWith(".jpg") || requestUrl.endsWith(".png") || requestUrl.endsWith(".jpeg")) {
// 获取请求的来源域名
String referer = request.getHeader("Referer");
// 检查来源域名是否符合预期,referer 为 null 则说明是浏览器直接访问。
if (referer == null && imgProtectConfig.getAllowBrowser()){
returntrue; // 符合防盗链要求,放行请求
}elseif (referer != null && isAllowedDomain(referer)) {
returntrue; // 符合防盗链要求,放行请求
} else {
response.sendError(HttpServletResponse.SC_FORBIDDEN); // 返回 403 Forbidden
returnfalse; // 拦截请求
}
}
returntrue; // 对非图片资源请求放行
}
// 检查是否来自允许的域名
private boolean isAllowedDomain(String referer) {
// 获取允许的域名
String allowedReferers = imgProtectConfig.getAllowReferer();
// 如果允许的域名不为空
if (allowedReferers.trim() != null && !"".equals(allowedReferers.trim())) {
// 将允许的域名分割成字符串数组
Set<String> allowedDomains = new HashSet<>(Arrays.asList(allowedReferers.split(",")));
// 遍历允许的域名
for (String allowedDomain : allowedDomains) {
// 如果请求的域名包含允许的域名,则返回true
if (referer.contains(allowedDomain.trim())) {
returntrue;
}
}
}
// 否则返回false
returnfalse;
}
}
d.注册拦截器
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
// 不能再使用 new 方式创建对象 !!!
@Autowired
private ImageProtectionInterceptor imageProtectionInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器,拦截所有请求
registry.addInterceptor(imageProtectionInterceptor)
.addPathPatterns("/**"); // 拦截所有请求
}
}
1.3 验证码:tianai
00.介绍
http://doc.captcha.tianai.cloud/
https://gitee.com/dromara/tianai-captcha
TIANAI-CAPTCHA,基于 Java 实现的开源行为验证码,涵盖滑块验证码、旋转验证码、滑动还原验证码、文字点选验证码等
01.java项目
a.依赖
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId>
<version>1.5.1</version>
</dependency>
b.使用ImageCaptchaApplication生成和校验验证码
public class TianAiCaptcha {
public static void main(String[] args) {
// 1、构建 ImageCaptchaApplication
ImageCaptchaApplication imageCaptchaApplication = TACBuilder.builder()
// 1.1 添加默认模板 -- 可自定义添加模板
.addDefaultTemplate()
// 1.2 添加验证码资源信息 参数一:验证码类型(参考 CaptchaTypeConstant) 参数二:Resource("classpath/file/url", "图片路径")
// 添加【滑块验证码】资源,背景图来自类路径下 背景图大小为:600 * 360
.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "META-INF/cut-image/resource/1.jpg"))
// 1.2.1 添加【旋转验证码】资源,背景图来自文件系统
.addResource(CaptchaTypeConstant.ROTATE, new Resource("file", "E:\\CodeChen.png"))
// 1.2.2 添加【滑动还原验证码】资源,背景图来自远程地址
.addResource(CaptchaTypeConstant.CONCAT, new Resource("url", "https://chencoding.top:8090/_media/logo_2.png"))
// 1.2.3 添加【文字点验证码】资源,背景图来自类路径下
.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "META-INF/cut-image/resource/1.jpg"))
.build();
// 2、生成验证码
// 2.1 生成验证码的类型必须在 ImageCaptchaApplication 先添加对应的资源,否则会提示:【随机获取资源错误,store中资源为空, type:SLIDER,tag:null】
CaptchaResponse<ImageCaptchaVO> response = imageCaptchaApplication.generateCaptcha(CaptchaTypeConstant.CONCAT);
System.out.println(response);
// 2.2 验证码模板图片
System.out.println(response.getCaptcha().getTemplateImage());
// 2.3 验证码背景图片
System.out.println(response.getCaptcha().getBackgroundImage());
// 3、校验验证码:id 和 ImageCaptchaTrack 需要前端传来参数
String id = response.getId();
ImageCaptchaTrack imageCaptchaTrack = new ImageCaptchaTrack();
ApiResponse<?> valid = imageCaptchaApplication.matching(id, imageCaptchaTrack);
System.out.println(valid.isSuccess());
}
}
02.springboot项目
a.依赖
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha-springboot-starter</artifactId>
<version>1.5.1</version>
</dependency>
b.配置
# 验证码配置,详细请看 cloud.tianai.captcha.autoconfiguration.ImageCaptchaProperties 类
captcha:
# 如果项目中使用到了 Redis, 滑块验证码会自动把验证码数据存到 Redis 中,这里配置 Redis 的 key 的前缀 默认是 captcha:slider
prefix: captcha
# 验证码过期时间,默认:2 分钟,单位:毫秒
expire:
# 默认缓存时间 2分钟
default: 10000
# 针对【点选验证码】的配置,因为点选验证码验证比较慢,把过期时间调整大一些
WORD_IMAGE_CLICK: 20000
# 使用加载系统自带的资源,默认是 false(这里系统的默认资源包含滑动验证码模板/旋转验证码模板,如果想使用系统的模板,这里设置为true)
init-default-resource: true
# 缓存控制,默认为 false 不开启
local-cache-enabled: true
# 验证码会提前缓存一些生成好的验证数据,默认是 20
local-cache-size: 20
# 缓存拉取失败后等待时间 默认是 5 秒钟
local-cache-wait-time: 5000
# 缓存检查间隔 默认是 2 秒钟
local-cache-period: 2000
# 配置字体包,供文字点选验证码使用,可以配置多个,不配置使用默认的字体
font-path:
- classpath:font/SimHei.ttf
secondary:
# 二次验证,默认 false 不开启
enabled: false
# 二次验证过期时间,默认 2 分钟
expire: 120000
# 二次验证缓存 key 前缀,默认是 captcha:secondary
keyPrefix: "captcha:secondary"
c.使用ImageCaptchaApplication生成和校验验证码
@RestController
@RequestMapping("captcha")
public class CaptchaController {
@Autowired
private ImageCaptchaApplication imageCaptchaApplication;
@GetMapping("/generate")
public Result<CaptchaResponse<ImageCaptchaVO> > generate() {
CaptchaResponse<ImageCaptchaVO> response = imageCaptchaApplication.generateCaptcha(CaptchaTypeConstant.SLIDER);
return Result.success(response);
}
@PostMapping("/valid/${id}")
public Result<Boolean> valid(@PathVariable String id,
@RequestBody ImageCaptchaTrack imageCaptchaTrack) {
boolean valid = imageCaptchaApplication.matching(id, imageCaptchaTrack).isSuccess();
return Result.success(valid);
}
}
1.4 数据源:druid
01.依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.16</version> <!-- 请根据需要选择合适的版本 -->
</dependency>
02.配置
spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/your_database?useSSL=false&serverTimezone=UTC
username: root
password: password
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
filters: stat,wall,log4j
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
03.配置监控
spring:
datasource:
druid:
stat-view-servlet:
enabled: true
url-pattern: /druid/*
login-username: admin
login-password: admin
web-stat-filter:
enabled: true
url-pattern: /*
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
04.启动应用程序
http://localhost:8080/druid
admin
admin
05.使用Druid数据源
在代码中使用 Spring Data JPA、MyBatis 或其他数据访问技术时,Spring Boot 会自动使用配置的 Druid 数据源
1.5 分库分表:sphere
00.汇总
搭建项目架构
分库分表配置
分库分表中的关键点
01.搭建项目架构
a.配置方式
方式1:Java编码
方式2:YAML配置
b.引入Maven依赖
a.单独引入shardingsphere-jdbc-core依赖
<!-- https://mvnrepository.com/artifact/org.apache.shardingsphere/shardingsphere-jdbc-core -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core</artifactId>
<version>5.4.0</version>
</dependency>
b.通过 Spring Boot Starter 引入相关依赖
<!-- https://mvnrepository.com/artifact/org.apache.shardingsphere/shardingsphere-jdbc-core-spring-boot-starter -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.2.1</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.33</version>
</dependency>
c.注意
如果这里报错 The following method did not exist: org.apache.shardingsphere.infra.util.yaml.constructor.ShardingSphereYamlConstructor$1.setCodePointLimit(I)V
可以降低shardingsphere版本号或调整高版本snakeyaml版本
c.创建YAML配置文件
# JDBC 逻辑库名称。在集群模式中,使用该参数来联通 ShardingSphere-JDBC 与 ShardingSphere-Proxy。
# 默认值:logic_db
databaseName (?):
mode:
dataSources:
rules:
- !FOO_XXX
...
- !BAR_XXX
...
props:
key_1: value_1
key_2: value_2
d.Spring Boot配置使用ShardingSphere JDBC驱动
# 配置 DataSource Driver
spring.datasource.driver-class-name=org.apache.shardingsphere.driver.ShardingSphereDriver
# 指定 YAML 配置文件
spring.datasource.url=jdbc:shardingsphere:classpath:xxx.yaml
e.数据库准备
a.创建两个数据库
创建两个数据库 db_test_01 和 db_test_02,用于分库分表演示
并且在这两个数据库中都创建相同的表 user_info,production,order,order_item_00,order_item_01
b.SQL 脚本
# 分别创建两个数据库
CREATE DATABASE `db_test_01` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE DATABASE `db_test_02` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
# 在两个数据库中都创建以下表
# 创建单表:用户表
create table `user_info` (
`user_id` bigint not null comment '用戶id',
`user_name` varchar(255) comment '用戶姓名',
`user_sex` varchar(255) comment '用戶性別',
`user_age` int(8) not null comment '用戶年齡',
primary key (`user_id`) using btree
) engine = InnoDB character set = utf8mb4 collate = utf8mb4_general_ci row_format = compact;
# 创建单表:商品表
create table `production` (
`production_id` bigint not null comment '商品id',
`production_name` varchar(255) comment '商品名称',
`production_price` int(8) not null comment '商品价格',
primary key (`production_id`) using btree
) engine = InnoDB character set = utf8mb4 collate = utf8mb4_general_ci row_format = compact;
# 创建单表:订单表
create table `order` (
`order_id` bigint not null comment '订单号',
`order_price` int(8) not null comment '订单总金额',
`user_id` bigint not null comment '用戶id',
primary key (`order_id`) using btree
) engine = InnoDB character set = utf8mb4 collate = utf8mb4_general_ci row_format = compact;
# 创建分表:订单项表1
create table `order_item_00` (
`order_info_id` bigint not null comment '订单详情号',
`order_id` bigint not null comment '订单号',
`production_name` varchar(255) comment '商品名称',
`production_price` int(8) not null comment '商品价格',
primary key (`order_info_id`) using btree,
index `key_order_id`(`order_id`) using btree
) engine = InnoDB character set = utf8mb4 collate = utf8mb4_general_ci row_format = compact;
# 创建分表:订单项表2
create table `order_item_01` (
`order_info_id` bigint not null comment '订单详情号',
`order_id` bigint not null comment '订单号',
`production_name` varchar(255) comment '商品名称',
`production_price` int(8) not null comment '商品价格',
primary key (`order_info_id`) using btree,
index `key_order_id`(`order_id`) using btree
) engine = InnoDB character set = utf8mb4 collate = utf8mb4_general_ci row_format = compact;
c.创建数据库用户
create user 'sharding'@'%' identified by 'sharding123!@#';
grant all privileges on db_test_01.* to 'sharding'@'%' with grant option;
grant all privileges on db_test_02.* to 'sharding'@'%' with grant option;
flush privileges;
f.生成代码
a.通过MyBatis-Plus代码生成service、mapper、entity
service
├── UserInfoService.java
├── OrderService.java
├── OrderItemService.java
└── ProductionService.java
impl
├── UserInfoServiceImpl.java
├── OrderServiceImpl.java
├── OrderItemServiceImpl.java
└── ProductionServiceImpl.java
dao
├── UserInfoMapper.java
├── OrderMapper.java
├── OrderItemMapper.java
└── ProductionMapper.java
mapper
├── UserInfoMapper.xml
├── OrderMapper.xml
├── OrderItemMapper.xml
└── ProductionMapper.xml
entity
├── UserInfo.java
├── Order.java
├── OrderItem.java
└── Production.java
02.分库分表配置
a.配置多数据源
如果是使用 Spring Boot 引入 shardingsphere-jdbc-core-spring-boot-starter依赖的,作如下配置
-----------------------------------------------------------------------------------------------------
spring:
shardingsphere:
mode: # 不配置则默认单机模式
type: Standalone # 运行模式类型。可选配置:Standalone、Cluster
repository: # 持久化仓库配置
type: JDBC
datasource: # 配置多个数据源
names: ds0,ds1,ds2
# 配置第一个数据源
ds0:
url: jdbc:mysql://localhost:3306/db_test_01?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=true&tinyInt1isBit=false&allowMultiQueries=true
username: sharding
password: sharding123!@#
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
# 配置第二个数据源
ds1:
url: jdbc:mysql://localhost:3306/db_test_02?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=true&tinyInt1isBit=false&allowMultiQueries=true
username: sharding
password: sharding123!@#
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
b.多数据源可用性测试
a.配置分片规则
spring:
shardingsphere:
rules: # 规则配置
sharding: # 数据分片规则
tables: # 配置所有分片表
order_item: # 逻辑表
actual-data-nodes: ds0.order_item_00 # 声明商品表所在的真实数据节点(这里先显式声明一个节点测试)
props: #
sql-show: true # 日志显示具体的SQL
b.编写测试代码,并调用测试接口
@RestController
@RequestMapping("/admin-service")
public class AdminTestController {
@Resource
private OrderItemService orderItemService;
@PostMapping("/test")
public void test() {
OrderItem orderItem = new OrderItem();
orderItem.setOrderInfoId(1L);
orderItem.setOrderId(1L);
orderItem.setProductionName("商品1");
orderItem.setProductionPrice(9);
orderItemService.save(orderItem);
}
}
c.查看打印 SQL 日志
可以分别看到逻辑上的Logic SQL,和实际执行的Actual SQL。
d.验证分库分表
在 db_test_01.order_item_00 表中查看数据,如此验证了分库分表已应用成功了。
---
mysql> select * from db_test_01.order_item_00;
+----+----------+-----------------+------------------+--------+
| id | order_id | production_name | production_price | is_del |
+----+----------+-----------------+------------------+--------+
| 1 | 1 | ??1 | 9 | 0 |
+----+----------+-----------------+------------------+--------+
1 row in set (0.00 sec)
c.配置分库规则
a.编写分片算法
也就是数据入库的规则,这里按id奇数或偶数入不同的数据库
-------------------------------------------------------------------------------------------------
spring:
shardingsphere:
rules: # 规则配置
sharding: # 数据分片规则
tables: # 配置所有分片表
order_item: # 逻辑表
actual-data-nodes: ds$->{0..1}.order_item_00 # 声明表所在的真实数据节点(这里先显式声明一个节点测试)
database-strategy: # 分库策略
standard:
sharding-column: id # 分片列名称
sharding-algorithm-name: db-inline-mod # 分片算法名称
# 分片算法配置
sharding-algorithms:
db-inline-mod: # 分片算法名称
type: INLINE # 分片算法类型
props: # 分片算法属性配置
algorithm-expression: ds$->{id % 2}
b.编写测试代码
这里循环插入总共10个商品,用于后面验证分片算法的实际效果
-------------------------------------------------------------------------------------------------
@RestController
@RequestMapping("/admin-service")
public class AdminTestController {
@Resource
private OrderItemService orderItemService;
@PostMapping("/test")
public void test() {
for(long i = 10; i < 20; i++) {
OrderItem orderItem = new OrderItem();
orderItem.setId(i);
orderItem.setOrderId(1L);
orderItem.setProductionName("商品" + i);
orderItem.setProductionPrice(9);
orderItemService.save(orderItem);
}
}
}
c.查看数据
查看 db_test_01.order_item_00 数据,新增了5条数据,id 从 10-18,为偶数
查看 db_test_02.order_item_00 数据,新增了5条数据,id 从 11-19,为奇数
d.使用内置的分片算法
参考:ShardingSphere > 用户手册 > 通用配置 > 内置算法 > 分片算法
-------------------------------------------------------------------------------------------------
# 分片算法配置
sharding-algorithms:
db-inline-mod: # 分片算法名称
type: MOD # 分片算法类型
props: # 分片算法属性配置
sharding-count: 2
d.配置分表规则
a.内容
现在在分库的基础上,增加分表规则
这里以production_name作为分片键,由于它是字符类型,所以应用HASH_MOD,先计算HASH再取模
-------------------------------------------------------------------------------------------------
spring:
shardingsphere:
rules: # 规则配置
sharding: # 数据分片规则
tables: # 配置所有分片表
order_item: # 逻辑表
actual-data-nodes: ds$->{0..1}.order_item_0$->{0..1} # 声明表所在的真实数据节点(这里先显式声明一个节点测试)
database-strategy: # 分库策略
standard:
sharding-column: id # 分片列名称
sharding-algorithm-name: db-inline # 分片算法名称
table-strategy: # 分表策略
standard:
sharding-column: production_name
sharding-algorithm-name: tb-key-hash
# 分片算法配置
sharding-algorithms:
db-inline: # 分片算法名称
type: INLINE # 分片算法类型
props: # 分片算法属性配置
algorithm-expression: ds$->{id % 2}
tb-key-hash:
type: HASH_MOD
props:
sharding-count: 2
b.编写测试代码
这里循环插入共 20 条数据
-------------------------------------------------------------------------------------------------
@RestController
@RequestMapping("/admin-service")
public class AdminTestController {
@Resource
private OrderItemService orderItemService;
@PostMapping("/test")
public void test() {
for(long i = 1; i <= 20; i++) {
OrderItem orderItem = new OrderItem();
orderItem.setId(i);
orderItem.setOrderId(1L);
orderItem.setProductionName("商品" + i + "abc");
orderItem.setProductionPrice(9);
orderItemService.save(orderItem);
}
}
}
c.查看数据
查看 db_test_01.order_item_00 数据,id 为偶数,新增 5 条数据。
查看 db_test_01.order_item_01 数据,id 为偶数,新增 5 条数据。
查看 db_test_02.order_item_00 数据,id 为奇数,新增 5 条数据。
查看 db_test_02.order_item_01 数据,id 为奇数,新增 5 条数据。
03.分库分表中的关键点
a.分表策略的分片键
a.说明
在前面的例子中,对order_item的分表策略使用了production_name作为分片键
假设我们要以order_item.id作为查询条件,如下:
-------------------------------------------------------------------------------------------------
@RestController
@RequestMapping("/admin-service")
public class AdminTestController {
@Resource
private OrderItemService orderItemService;
@PostMapping("/test")
public void test() {
orderItemService.findById(1L);
}
}
b.说明
查看SQL日志会发现,实际查询语句用了UNION ALL,这是因为使用的production_name作为分片键
当使用非分片键查询时,由于没有配置相关的规则,也就无法知道要从哪张表查询
因此,选择合适的分片键是十分重要的
-------------------------------------------------------------------------------------------------
2023-07-19 20:03:40.031 [XNIO-1 task-1] INFO ShardingSphere-SQL [74] - Logic SQL: select `id`, `order_id`, `production_name`, `production_price`, `is_del` from order_item
where `id` = ? and is_del = 0
2023-07-19 20:03:40.032 [XNIO-1 task-1] INFO ShardingSphere-SQL [74] - Actual SQL: ds1 ::: select `id`, `order_id`, `production_name`, `production_price`, `is_del` from order_item_00
where `id` = ? and is_del = 0 UNION ALL select `id`, `order_id`, `production_name`, `production_price`, `is_del` from order_item_01
where `id` = ? and is_del = 0 ::: [1, 1]
b.分布式序列算法
a.说明
前面的例子中,我们都是手动设置的主键ID的值,大多数业务中都是使用自增主键ID
然而在分布式系统中,使用自增主键ID会导致唯一ID冲突
解决唯一ID冲突可以设置不同表应用不同的增长步长,不过这对后续扩容不太友好
因此大多数是使用具有全局唯一性、递增性的分布式ID,例如 Snowflake 算法生成ID
b.配置雪花算法
spring:
shardingsphere:
rules: # 规则配置
sharding: # 数据分片规则
tables: # 配置所有分片表
order_item: # 逻辑表
actual-data-nodes: ds$->{0..1}.order_item_0$->{0..1} # 声明表所在的真实数据节点(这里先显式声明一个节点测试)
key-generate-strategy: # 分布式序列策略
column: id # 自增列名称,缺省表示不使用自增主键生成器
keyGeneratorName: global-id # 分布式序列算法名称
database-strategy: # 分库策略
standard:
sharding-column: id # 分片列名称
sharding-algorithm-name: db-inline # 分片算法名称
table-strategy:
standard:
sharding-column: production_name
sharding-algorithm-name: tb-key-hash
# 分片算法配置
sharding-algorithms:
db-inline: # 分片算法名称
type: INLINE # 分片算法类型
props: # 分片算法属性配置
algorithm-expression: ds$->{id % 2}
tb-key-hash:
type: HASH_MOD
props:
sharding-count: 2
# 分布式序列算法配置
key-generators:
global-id:
type: SNOWFLAKE # 分布式序列算法类型
props: # 分布式序列算法属性配置
worker-id: 1 # 工作机器唯一标识
c.编写测试代码
@RestController
@RequestMapping("/admin-service")
public class AdminTestController {
@Resource
private OrderItemService orderItemService;
@PostMapping("/test")
public void test() {
for(long i = 1; i <= 20; i++) {
OrderItem orderItem = new OrderItem();
orderItem.setOrderId(1L);
orderItem.setProductionName("商品" + i + "abc");
orderItem.setProductionPrice(9);
orderItemService.save(orderItem);
}
}
}
d.查看数据
查看 db_test_01.order_item_00 数据,id 不再是简单自增了,不过还是可以从production_name 看出商品的插入顺序。
查看 db_test_01.order_item_01 数据。
查看 db_test_02.order_item_00 数据。
查看 db_test_02.order_item_01 数据。
c.绑定表配置
a.说明
在我们设计的表中,order 与 order_item 存在一对多的关系
它们以 order.id 和 order_item.order_id 建立逻辑上的主外键关系
因此在分库后,要求数据分片时,将 order_item.order_id 与 order.id 有关联的数据存在相同数据库中
ShardingSphere 要求绑定表的分库策略和分表策略一致
b.代码
spring:
shardingsphere:
rules: # 规则配置
sharding: # 数据分片规则
tables: # 配置所有分片表
base_order: # 逻辑表
actual-data-nodes: ds_$->{0..1}.base_order # 声明表所在的真实数据节点(这里先显式声明一个节点测试)
key-generate-strategy: # 分布式序列策略
column: id # 自增列名称,缺省表示不使用自增主键生成器
keyGeneratorName: snowflake # 分布式序列算法名称
database-strategy: # 分库策略
standard:
sharding-column: id # 分片列名称
sharding-algorithm-name: db-mod # 分片算法名称
order_item: # 逻辑表
actual-data-nodes: ds_$->{0..1}.order_item # 声明表所在的真实数据节点(这里先显式声明一个节点测试)
key-generate-strategy: # 分布式序列策略
column: id # 自增列名称,缺省表示不使用自增主键生成器
keyGeneratorName: snowflake # 分布式序列算法名称
database-strategy: # 分库策略
standard:
sharding-column: order_id # 分片列名称
sharding-algorithm-name: db-mod # 分片算法名称
# table-strategy:
# standard:
# sharding-column: id
# sharding-algorithm-name: tb-mod
binding-tables:
- base_order,order_item
# 分片算法配置
sharding-algorithms:
db-mod: # 分片算法名称
type: MOD # 分片算法类型
props: # 分片算法属性配置
sharding-count: 2
tb-mod:
type: MOD
props:
sharding-count: 2
# 分布式序列算法配置
key-generators:
snowflake:
type: SNOWFLAKE # 分布式序列算法类型
props: # 分布式序列算法属性配置
worker-id: 1 # 工作机器唯一标识
1.6 数据隔离:多租户
00.目的
如何根据当前的运行环境来实现数据隔离:SpringBoot+Mybatis拦截器+JSqlParser全解析
构建多租户系统或数据权限控制:数据隔离是一个关键问题,通过在项目的数据库访问层实现数据过滤
01.工具介绍
a.Mybatis拦截器
Mybatis 支持在 SQL 执行的不同阶段拦截并插入自定义逻辑
本文将通过拦截 StatementHandler 接口的 prepare方法修改SQL语句,实现数据隔离的目的
b.JSqlParser
JSqlParser 是一个开源的 SQL 语句解析工具,它可以对 SQL 语句进行解析、重构等各种操作
能够将 SQL 字符串转换成一个可操作的抽象语法树(AST),这使得程序能够理解和操作 SQL 语句的各个组成部分
根据需求对解析出的AST进行修改,比如添加额外的过滤条件,然后再将AST转换回SQL字符串,实现需求定制化的SQL语句构建
02.详细步骤
a.导入依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>4.6</version>
</dependency>
注意:如果项目选择了 Mybatis Plus 作为数据持久层框架,那么就无需另外添加 Mybatis 和 JSqlParser 的依赖。
Mybatis Plus 自身已经包含了这两项依赖,并且保证了它们之间的兼容性。重复添加这些依赖可能会引起版本冲突,从而干扰项目的稳定性。
b.定义一个拦截器
拦截所有 query 语句并在条件中加入 env 条件
---
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.RowConstructor;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.statement.values.ValuesStatement;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@Intercepts(
{
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
}
)
public class DataIsolationInterceptor implements Interceptor {
/**
* 从配置文件中环境变量
*/
@Value("${spring.profiles.active}")
private String env;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
//确保只有拦截的目标对象是 StatementHandler 类型时才执行特定逻辑
if (target instanceof StatementHandler) {
StatementHandler statementHandler = (StatementHandler) target;
// 获取 BoundSql 对象,包含原始 SQL 语句
BoundSql boundSql = statementHandler.getBoundSql();
String originalSql = boundSql.getSql();
String newSql = setEnvToStatement(originalSql);
// 使用MetaObject对象将新的SQL语句设置到BoundSql对象中
MetaObject metaObject = SystemMetaObject.forObject(boundSql);
metaObject.setValue("sql", newSql);
}
// 执行SQL
return invocation.proceed();
}
private String setEnvToStatement(String originalSql) {
net.sf.jsqlparser.statement.Statement statement;
try {
statement = CCJSqlParserUtil.parse(originalSql);
} catch (JSQLParserException e) {
throw new RuntimeException("EnvironmentVariableInterceptor::SQL语句解析异常:"+originalSql);
}
if (statement instanceof Select) {
Select select = (Select) statement;
PlainSelect selectBody = select.getSelectBody(PlainSelect.class);
if (selectBody.getFromItem() instanceof Table) {
Expression newWhereExpression;
if (selectBody.getJoins() == null || selectBody.getJoins().isEmpty()) {
newWhereExpression = setEnvToWhereExpression(selectBody.getWhere(), null);
} else {
// 如果是多表关联查询,在关联查询中新增每个表的环境变量条件
newWhereExpression = multipleTableJoinWhereExpression(selectBody);
}
// 将新的where设置到Select中
selectBody.setWhere(newWhereExpression);
} else if (selectBody.getFromItem() instanceof SubSelect) {
// 如果是子查询,在子查询中新增环境变量条件
// 当前方法只能处理单层子查询,如果有多层级的子查询的场景需要通过递归设置环境变量
SubSelect subSelect = (SubSelect) selectBody.getFromItem();
PlainSelect subSelectBody = subSelect.getSelectBody(PlainSelect.class);
Expression newWhereExpression = setEnvToWhereExpression(subSelectBody.getWhere(), null);
subSelectBody.setWhere(newWhereExpression);
}
// 获得修改后的语句
return select.toString();
} else if (statement instanceof Insert) {
Insert insert = (Insert) statement;
setEnvToInsert(insert);
return insert.toString();
} else if (statement instanceof Update) {
Update update = (Update) statement;
Expression newWhereExpression = setEnvToWhereExpression(update.getWhere(),null);
// 将新的where设置到Update中
update.setWhere(newWhereExpression);
return update.toString();
} else if (statement instanceof Delete) {
Delete delete = (Delete) statement;
Expression newWhereExpression = setEnvToWhereExpression(delete.getWhere(),null);
// 将新的where设置到delete中
delete.setWhere(newWhereExpression);
return delete.toString();
}
return originalSql;
}
/**
* 将需要隔离的字段加入到SQL的Where语法树中
* @param whereExpression SQL的Where语法树
* @param alias 表别名
* @return 新的SQL Where语法树
*/
private Expression setEnvToWhereExpression(Expression whereExpression, String alias) {
// 添加SQL语法树的一个where分支,并添加环境变量条件
AndExpression andExpression = new AndExpression();
EqualsTo envEquals = new EqualsTo();
envEquals.setLeftExpression(new Column(StringUtils.isNotBlank(alias) ? String.format("%s.env", alias) : "env"));
envEquals.setRightExpression(new StringValue(env));
if (whereExpression == null){
return envEquals;
} else {
// 将新的where条件加入到原where条件的右分支树
andExpression.setRightExpression(envEquals);
andExpression.setLeftExpression(whereExpression);
return andExpression;
}
}
/**
* 多表关联查询时,给关联的所有表加入环境隔离条件
* @param selectBody select语法树
* @return 新的SQL Where语法树
*/
private Expression multipleTableJoinWhereExpression(PlainSelect selectBody){
Table mainTable = selectBody.getFromItem(Table.class);
String mainTableAlias = mainTable.getAlias().getName();
// 将 t1.env = ENV 的条件添加到where中
Expression newWhereExpression = setEnvToWhereExpression(selectBody.getWhere(), mainTableAlias);
List<Join> joins = selectBody.getJoins();
for (Join join : joins) {
FromItem joinRightItem = join.getRightItem();
if (joinRightItem instanceof Table) {
Table joinTable = (Table) joinRightItem;
String joinTableAlias = joinTable.getAlias().getName();
// 将每一个join的 tx.env = ENV 的条件添加到where中
newWhereExpression = setEnvToWhereExpression(newWhereExpression, joinTableAlias);
}
}
return newWhereExpression;
}
/**
* 新增数据时,插入env字段
* @param insert Insert 语法树
*/
private void setEnvToInsert(Insert insert) {
// 添加env列
List<Column> columns = insert.getColumns();
columns.add(new Column("env"));
// values中添加环境变量值
List<SelectBody> selects = insert.getSelect().getSelectBody(SetOperationList.class).getSelects();
for (SelectBody select : selects) {
if (select instanceof ValuesStatement){
ValuesStatement valuesStatement = (ValuesStatement) select;
ExpressionList expressions = (ExpressionList) valuesStatement.getExpressions();
List<Expression> values = expressions.getExpressions();
for (Expression expression : values){
if (expression instanceof RowConstructor) {
RowConstructor rowConstructor = (RowConstructor) expression;
ExpressionList exprList = rowConstructor.getExprList();
exprList.addExpressions(new StringValue(env));
}
}
}
}
}
}
c.测试
a.Select
<select id="queryAllByOrgLevel" resultType="com.lyx.mybatis.entity.AllInfo">
SELECT a.username,a.code,o.org_code,o.org_name,o.level
FROM admin a left join organize o on a.org_id=o.id
WHERE a.dr=0 and o.level=#{level}
</select>
刚进入拦截器时,Mybatis 解析的 SQL 语句:
SELECT a.username,a.code,o.org_code,o.org_name,o.level
FROM admin a left join organize o on a.org_id=o.id
WHERE a.dr=0 and o.level=?
执行完 setEnvToStatement(originalSql) 方法后,得到的新 SQL 语句:
SELECT a.username, a.code, o.org_code, o.org_name, o.level
FROM admin a LEFT JOIN organize o ON a.org_id = o.id
WHERE a.dr = 0 AND o.level = ? AND a.env = 'test' AND o.env = 'test'
b.Insert
刚进入拦截器时,Mybatis 解析的 SQL 语句:
INSERT INTO admin ( id, username, code, org_id ) VALUES ( ?, ?, ?, ? )
执行完 setEnvToInsert(insert) 方法后,得到的新 SQL 语句:
INSERT INTO admin (id, username, code, org_id, env) VALUES (?, ?, ?, ?, 'test')
c.Update
刚进入拦截器时,Mybatis 解析的 SQL 语句:
UPDATE admin SET username=?, code=?, org_id=? WHERE id=?
执行完 setWhere(newWhereExpression) 方法后,得到的新 SQL 语句:
UPDATE admin SET username = ?, code = ?, org_id = ? WHERE id = ? AND env = 'test'
d.Delete
刚进入拦截器时,Mybatis 解析的 SQL 语句:
DELETE FROM admin WHERE id=?
执行完 setWhere(newWhereExpression) 方法后,得到的新 SQL 语句:
DELETE FROM admin WHERE id = ? AND env = 'test'
03.为什么要拦截 StatementHandler 接口的 prepare 方法?
a.说明
可以注意到,在这个例子中定义拦截器时 @Signature 注解中拦截的是 StatementHandler 接口的 prepare 方法,为什么拦截的是 prepare 方法而不是 query 和 update 方法?为什么拦截 query 和 update 方法修改 SQL 语句后仍然执行的是原 SQL ?
这是因为 SQL 语句是在 prepare 方法中被构建和参数化的。prepare 方法是负责准备 PreparedStatement 对象的,这个对象表示即将要执行的 SQL 语句。在 prepare 方法中可以对 SQL 语句进行修改,而这些修改将会影响最终执行的 SQL
而 query 和 update 方法是在 prepare 方法之后被调用的。它们主要的作用是执行已经准备好的 PreparedStatement 对象。在这个阶段,SQL 语句已经被创建并绑定了参数值,所以拦截这两个方法并不能改变已经准备好的 SQL 语句
简单来说,如果想要修改SQL语句的内容(比如增加 WHERE 子句、改变排序规则等),那么需要在 SQL 语句被准备之前进行拦截,即在 prepare 方法的执行过程中进行
b.以下是 MyBatis 执行过程中的几个关键步骤
解析配置和映射文件: MyBatis 启动时,首先加载配置文件和映射文件,解析里面的 SQL 语句
生成 StatementHandler 和 BoundSql: 当执行一个操作,比如查询或更新时,MyBatis 会创建一个 StatementHandler 对象,并包装了 BoundSql 对象,后者包含了即将要执行的 SQL 语句及其参数
执行 prepare 方法: StatementHandler 的 prepare 方法被调用,完成 PreparedStatement 的创建和参数设置
执行 query 或 update: 根据执行的是查询操作还是更新操作,MyBatis 再调用 query 或 update 方法来实际执行 SQL
c.总结
通过在 prepare 方法进行拦截,我们可以在 SQL 语句被最终确定之前更改它,从而使修改生效
如果在 query 或 update 方法中进行拦截,则无法更改 SQL 语句,只能在执行前后进行其他操作,比如日志记录或者结果处理
1.7 数据处理:jdframe
00.介绍
一个jvm层级的仿DataFrame工具,语意化和简化java8的stream流式处理工具
并且提供了更加强大的流式处理能力以及基本的DataFrame模型功能
01.快速开始
a.引入依赖
<dependency>
<groupId>io.github.burukeyou</groupId>
<artifactId>jdframe</artifactId>
<version>0.1.7</version>
</dependency>
b.案例
统计每个学校的里学生年龄不为空并且年龄在9到16岁间的合计分数,并且获取合计分前2名的学校
c.代码1
static List<Student> studentList = new ArrayList<>();
static {
studentList.add(new Student(1,"a","一中","一年级",11, new BigDecimal(1)));
studentList.add(new Student(2,"a","一中","一年级",11, new BigDecimal(1)));
studentList.add(new Student(3,"b","一中","三年级",12, new BigDecimal(2)));
studentList.add(new Student(4,"c","二中","一年级",13, new BigDecimal(3)));
studentList.add(new Student(5,"d","二中","一年级",14, new BigDecimal(4)));
studentList.add(new Student(6,"e","三中","二年级",14, new BigDecimal(5)));
studentList.add(new Student(7,"e","三中","二年级",15, new BigDecimal(5)));
}
// 等价于SQL:
// select school,sum(score)
// from students
// where age is not null and age >=9 and age <= 16
// group by school
// order by sum(score) desc
// limit 2
SDFrame<FI2<String, BigDecimal>> sdf2 = SDFrame.read(studentList)
.whereNotNull(Student::getAge)
.whereBetween(Student::getAge,9,16)
.groupBySum(Student::getSchool, Student::getScore)
.sortDesc(FI2::getC2)
.cutFirst(2);
sdf2.show();
c.代码2
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private int id;
private String name;
private String school;
private String level;
private Integer age;
private BigDecimal score;
private Integer rank;
public Student(String level, BigDecimal score) {
this.level = level;
this.score = score;
}
public Student(int id, String name, String school, String level, Integer age, BigDecimal score) {
this.id = id;
this.name = name;
this.school = school;
this.level = level;
this.age = age;
this.score = score;
}
}
02.API案例1
a.矩阵查看相关
void show(int n); 打印矩阵信息到控制台
List<String> columns(); 获取矩阵的表头字段名
List<R> col(Function<T, R> function); 获取矩阵某一列值
T head(); 获取第一个元素
List<T> head(int n); 获取前n个元素
T tail(); 获取最后一个元素
List<T> tail(int n); 获取后n个元素
List<T> page(int page,int pageSize) 获取分页数据
b.筛选相关
SDFrame.read(studentList)
.whereBetween(Student::getAge,3,6) // 过滤年龄在[3,6]岁的
.whereBetweenR(Student::getAge,3,6) // 过滤年龄在(3,6]岁的, 不含3岁
.whereBetweenL(Student::getAge,3,6) // 过滤年龄在[3,6)岁的, 不含6岁
.whereNotNull(Student::getName) // 过滤名字不为空的数据, 兼容了空字符串''的判断
.whereGt(Student::getAge,3) // 过滤年龄大于3岁
.whereGe(Student::getAge,3) // 过滤年龄大于等于3岁
.whereLt(Student::getAge,3) // 过滤年龄小于3岁的
.whereIn(Student::getAge, Arrays.asList(3,7,8)) // 过滤年龄为3岁 或者7岁 或者 8岁的数据
.whereNotIn(Student::getAge, Arrays.asList(3,7,8)) // 过滤年龄不为为3岁 或者7岁 或者 8岁的数据
.whereEq(Student::getAge,3) // 过滤年龄等于3岁的数据
.whereNotEq(Student::getAge,3) // 过滤年龄不等于3岁的数据
.whereLike(Student::getName,"jay") // 模糊查询,等价于 like "%jay%"
.whereLikeLeft(Student::getName,"jay") // 模糊查询,等价于 like "jay%"
.whereLikeRight(Student::getName,"jay"); // 模糊查询,等价于 like "%jay"
c.汇总相关
JDFrame<Student> frame = JDFrame.read(studentList);
Student s1 = frame.max(Student::getAge);// 获取年龄最大的学生
Integer s2 = frame.maxValue(Student::getAge);// 获取学生里最大的年龄
Student s3 = frame.min(Student::getAge);// 获取年龄最小的学生
Integer s4 = frame.minValue(Student::getAge); // 获取学生里最小的年龄
BigDecimal s5 = frame.avg(Student::getAge); // 获取所有学生的年龄的平均值
BigDecimal s6 = frame.sum(Student::getAge); // 获取所有学生的年龄合计
MaxMin<Student> s7 = frame.maxMin(Student::getAge); // 同时获取年龄最大和最小的学生
MaxMin<Integer> s8 = frame.maxMinValue(Student::getAge); // 同时获取学生里最大和最小的年龄
d.去重相关
原生steam只支持对象去重,不支持按特定字段去重
List<Student> std = null;
std = SDFrame.read(studentList).distinct().toLists(); // 根据对象hashCode去重
std = SDFrame.read(studentList).distinct(Student::getSchool).toLists(); // 根据学校名去重
std = SDFrame.read(studentList).distinct(e -> e.getSchool() + e.getLevel()).toLists(); // 根据学校名拼接级别去重复
std =SDFrame.read(studentList).distinct(Student::getSchool).distinct(Student::getLevel).toLists(); // 先根据学校名去除重复再根据级别去除重复
e.分组聚合相关
类似sql的 group by语义 简化处理分组和聚合的逻辑, 如果用原生stream需要写可能一大串逻辑.
JDFrame<Student> frame = JDFrame.from(studentList);
// 等价于 select school,sum(age) ... group by school
List<FI2<String, BigDecimal>> a = frame.groupBySum(Student::getSchool, Student::getAge).toLists();
// 等价于 select school,max(age) ... group by school
List<FI2<String, Integer>> a2 = frame.groupByMaxValue(Student::getSchool, Student::getAge).toLists();
// 与 groupByMaxValue 含义一致,只是返回的是最大的值对象
List<FI2<String, Student>> a3 = frame.groupByMax(Student::getSchool, Student::getAge).toLists();
// 等价于 select school,min(age) ... group by school
List<FI2<String, Integer>> a4 = frame.groupByMinValue(Student::getSchool, Student::getAge).toLists();
// 等价于 select school,count(*) ... group by school
List<FI2<String, Long>> a5 = frame.groupByCount(Student::getSchool).toLists();
// 等价于 select school,avg(age) ... group by school
List<FI2<String, BigDecimal>> a6 = frame.groupByAvg(Student::getSchool, Student::getAge).toLists();
// 等价于 select school,sum(age),count(age) group by school
List<FI3<String, BigDecimal, Long>> a7 = frame.groupBySumCount(Student::getSchool, Student::getAge).toLists();
// (二级分组)等价于 select school,level,sum(age),count(age) group by school,level
List<FI3<String, String, BigDecimal>> a8 = frame.groupBySum(Student::getSchool, Student::getLevel, Student::getAge).toLists();
// (三级分组)等价于 select school,level,name,sum(age),count(age) group by school,level,name
List<FI4<String, String, String, BigDecimal>> a9 = frame.groupBySum(Student::getSchool, Student::getLevel, Student::getName, Student::getAge).toLists();
f.排序相关
简化原生stream的排序方式,直接指定字段即可,不用使用Comparator还要去关注升序还是降序.
如果是多级排序使用Compartor或者Sorter去指定多级排序的逻辑。 Sorter也是Compartor的一种实现,只是提供了更加语义化的多级排序指定逻辑, 相当于内置了Compartor的thenComparing
// 等价于 order by age desc
SDFrame.read(studentList).sortDesc(Student::getAge);
// (多级排序) 等价于 order by age desc, level asc.
SDFrame.read(studentList).sortAsc(Sorter.sortDescBy(Student::getAge).sortAsc(Student::getLevel));
// 等价于 order by age asc
SDFrame.read(studentList).sortAsc(Student::getAge);
// 使用Comparator 排序
SDFrame.read(studentList).sortAsc(Comparator.comparing(e -> e.getLevel() + e.getId()));
g.连接矩阵相关
a.API列表
append(T t); // 等价于集合 add
union(IFrame<T> other); // 等价于集合 addAll
join(IFrame<K> other, JoinOn<T,K> on, Join<T,K,R> join); // 等价于 sql内连接
leftJoin(IFrame<K> other, JoinOn<T,K> on, Join<T,K,R> join); // 等价于sql左连接,如果左连接失败,K值为null,需手动判断
rightJoin(IFrame<K> other, JoinOn<T,K> on, Join<T,K,R> join); // 等价于sql右连接,如果右连接失败,T值为null,需手动判断
b.内连接例子
System.out.println("======== 矩阵1 =======");
SDFrame<Student> sdf = SDFrame.read(studentList);
sdf.show(20);
// 获取学生年龄在9到16岁的学学校合计分数最高的前10名
SDFrame<FI2<String, BigDecimal>> sdf2 = SDFrame.read(studentList)
.whereNotNull(Student::getAge)
.whereBetween(Student::getAge,9,16)
.groupBySum(Student::getSchool, Student::getScore)
.sortDesc(FI2::getC2)
.cutFirst(10);
System.out.println("======== 矩阵2 =======");
sdf2.show();
SDFrame<UserInfo> frame = sdf.join(sdf2, (a, b) -> a.getSchool().equals(b.getC1()), (a, b) -> {
UserInfo userInfo = new UserInfo();
userInfo.setKey1(a.getSchool());
userInfo.setKey2(b.getC2().intValue());
userInfo.setKey3(String.valueOf(a.getId()));
return userInfo;
});
System.out.println("======== 连接后结果 =======");
frame.show(5);
c.打印信息
======== 矩阵1 =======
id name school level age score rank
1 a 一中 一年级 11 1
2 a 一中 一年级 11 1
3 b 一中 一年级 12 2
4 c 二中 一年级 13 3
5 d 二中 一年级 14 4
6 e 三中 二年级 14 5
7 e 三中 二年级 15 5
======== 矩阵2 =======
c1 c2
三中 10
二中 7
一中 4
======== 连接后结果 =======
key1 key2 key3 key4
一中 4 1
一中 4 2
一中 4 3
二中 7 4
二中 7 5
类似于
select a.*,b.* from sdf a inner join sdf2 b on a.school = b.c1
h.截取相关
cutFirst(int n); // 截取前N个
cutLast(int n); // 截取后N个
cut(Integer startIndex,Integer endIndex) // 按照索引范围截取 [startIndex,endIndex). 等价于 List.subList
cutPage(int page,int pageSize) // 按分页截取
cutFirstRank(Sorter<T> sorter, int n); // 截取前N排名的数据
i.Frame参数设置相关
defaultScale(int scale, RoundingMode roundingMode); // 设置计算结果的默认小数精度
03.API案例2
a.百分数转换
// 等价于 select round(score*100,2) from student
SDFrame<Student> map2 = SDFrame.read(studentList).mapPercent(Student::getScore, Student::setScore,2);
b.分区
将每个5个元素分成一个小集合,用于将大任务拆成小任务
List<List<Student>> t = SDFrame.read(studentList).partition(5).toLists();
c.生成序号列
按照age排序,然后根据当前顺序生成排序号到rank字段 (序号从1开始)
SDFrame.read(studentList)
.sortDesc(Student::getAge)
.addRowNumberCol(Student::setRank)
.show(30);
输出信息:
id name school level age score rank
7 e 三中 二年级 15 5 1
5 d 二中 一年级 14 4 2
6 e 三中 二年级 14 5 3
4 c 二中 一年级 13 3 4
3 b 一中 三年级 12 2 5
1 a 一中 一年级 11 1 6
2 a 一中 一年级 11 1 7
d.补充条目
a.补充缺失的学校条目
// 所有需要的学校条目
List<String> allDim = Arrays.asList("一中","二中","三中","四中");
// 根据学校字段和allDim比较去补充缺失的条目, 缺失的学校按照ReplenishFunction生成补充条目作为结果一起返回
SDFrame.read(studentList).replenish(Student::getSchool,allDim,(school) -> new Student(school)).show();
输出
id name school level age score rank
1 a 一中 一年级 11 1
2 a 一中 一年级 11 1
3 b 一中 一年级 12 2
4 c 二中 一年级 13 3
5 d 二中 一年级 14 4
6 e 三中 二年级 14 5
7 e 三中 二年级 15 5
0 四中
b.分组补充组内缺失的条目
按照学校进行分组,汇总所有年级allDim,然后与allDim比较补充每个分组内缺失的年级,缺失的年级按照ReplenishFunction生成补充条目
SDFrame.read(studentList).replenish(Student::getSchool,Student::getLevel,(school,level) -> new Student(school,level)).show(30);
输出
id name school level age score rank
1 a 一中 一年级 11 1
2 a 一中 一年级 11 1
3 b 一中 三年级 12 2
0 一中 二年级
4 c 二中 一年级 13 3
5 d 二中 一年级 14 4
0 二中 三年级
0 二中 二年级
6 e 三中 二年级 14 5
7 e 三中 二年级 15 5
0 三中 一年级
0 三中 三年级
应用场景举例:要求计算近两年每个月的数据,但是数据的年月可能不全,这时就补充缺失的年月数据作为结果一起返回
01.串行
a.说明
串行比较好设计,那就来三个任务依次执行即可。在该框架中任务需要实现 IWorker 接口和 ICallback 接口,并重写以下方法:
begin() 任务开始前执行的方法
action() 任务中耗时操作执行的位置
result() 任务执行后的结果,可以在此处理 action 中的结果值
defaultValue() 当执行中有异常后的默认返回值
b.模拟串行场景
A 任务对参数 +1,之后 B 任务对参数 +2,之后 C 任务对参数 +3。
c.A任务
public class WorkerA implements IWorker<Integer, Integer>, ICallback<Integer, Integer> {
@Override
public void begin() {
System.out.println("A - Thread:" + Thread.currentThread().getName() + "- start --" + SystemClock.now());
}
@Override
public Integer action(Integer object, Map<String, WorkerWrapper> allWrappers) {
Integer res = object + 1;
return res;
}
@Override
public void result(boolean success, Integer param, WorkResult<Integer> workResult) {
System.out.println("A - param:" + JSON.toJSONString(param));
System.out.println("A - result:" + JSON.toJSONString(workResult));
System.out.println("A - Thread:" + Thread.currentThread().getName() + "- end --" + SystemClock.now());
}
@Override
public Integer defaultValue() {
System.out.println("A - defaultValue");
return 101;
}
}
d.B任务
public class WorkerB implements IWorker<Integer, Integer>, ICallback<Integer, Integer> {
@Override
public void begin() {
System.out.println("B - Thread:" + Thread.currentThread().getName() + "- start --" + SystemClock.now());
}
@Override
public Integer action(Integer object, Map<String, WorkerWrapper> allWrappers) {
Integer res = object + 2;
return res;
}
@Override
public void result(boolean success, Integer param, WorkResult<Integer> workResult) {
System.out.println("B - param:" + JSON.toJSONString(param));
System.out.println("B - result:" + JSON.toJSONString(workResult));
System.out.println("B - Thread:" + Thread.currentThread().getName() + "- end --" + SystemClock.now());
}
@Override
public Integer defaultValue() {
System.out.println("B - defaultValue");
return 102;
}
}
e.C任务
public class WorkerC implements IWorker<Integer, Integer>, ICallback<Integer, Integer> {
@Override
public void begin() {
System.out.println("C - Thread:" + Thread.currentThread().getName() + "- start --" + SystemClock.now());
}
@Override
public Integer action(Integer object, Map<String, WorkerWrapper> allWrappers) {
Integer res = object + 3;
return res;
}
@Override
public void result(boolean success, Integer param, WorkResult<Integer> workResult) {
System.out.println("C - param:" + JSON.toJSONString(param));
System.out.println("C - result:" + JSON.toJSONString(workResult));
System.out.println("C - Thread:" + Thread.currentThread().getName() + "- end --" + SystemClock.now());
}
@Override
public Integer defaultValue() {
System.out.println("C - defaultValue");
return 103;
}
}
f.WorkerWrapper包装类
// A 没有 depend
WorkerWrapper wrapperA = new WorkerWrapper.Builder<Integer, Integer>()
.id("workerA")
.worker(workerA)
.callback(workerA)
.param(1)
.build();
// B 的 depend 是 A
WorkerWrapper wrapperB = new WorkerWrapper.Builder<Integer, Integer>()
.id("workerB")
.worker(workerB)
.callback(workerB)
.param(2)
.depend(wrapperA)
.build();
// C 的 depend 是 B
WorkerWrapper wrapperC = new WorkerWrapper.Builder<Integer, Integer>()
.id("workerC")
.worker(workerC)
.callback(workerC)
.param(3)
.depend(wrapperB)
.build();
// begin
Async.beginWork(1000, wrapperA);
02.并行
并行只需 3 个任务一并丢进 beginWork 中即可
Async.beginWork(1000, wrapperA, wrapperB, wrapperC);
03.阻塞等待:先串行,后并行
a.代码
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Worker1 worker1 = new Worker1();
Worker2 worker2 = new Worker2();
Worker3 worker3 = new Worker3();
WorkerWrapper wrapper1 = new WorkerWrapper.Builder<Integer, Integer>()
.id("worker1")
.worker(worker1)
.callback(worker1)
.param(1)
.build();
WorkerWrapper wrapper2 = new WorkerWrapper.Builder<Integer, Integer>()
.id("worker2")
.worker(worker2)
.depend(wrapper1)
.callback(worker2)
.param(2)
.build();
WorkerWrapper wrapper3 = new WorkerWrapper.Builder<Integer, Integer>()
.id("worker3")
.worker(worker3)
.depend(wrapper1)
.callback(worker3)
.param(3)
.build();
Async.beginWork(5000, wrapper1);
}
}
b.说明
让 BC 都依赖于 A 然后把 A 丢进 beginWork 即可
05.阻塞等待:先并行,后串行
a.代码
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Worker1 worker1 = new Worker1();
Worker2 worker2 = new Worker2();
Worker3 worker3 = new Worker3();
WorkerWrapper wrapper1 = new WorkerWrapper.Builder<Integer, Integer>()
.id("worker1")
.worker(worker1)
.callback(worker1)
.param(1)
.build();
WorkerWrapper wrapper2 = new WorkerWrapper.Builder<Integer, Integer>()
.id("worker2")
.worker(worker2)
.callback(worker2)
.param(2)
.build();
WorkerWrapper wrapper3 = new WorkerWrapper.Builder<Integer, Integer>()
.id("worker3")
.worker(worker3)
.depend(wrapper1, wrapper2)
.callback(worker3)
.param(3)
.build();
Async.beginWork(5000, wrapper1, wrapper2);
}
}
b.说明
C 依赖 AB,把 AB 一起丢入即可
c.异常/超时回调
这两种场景,可以基于以上场景微调,即可 debug 调试。
Async.beginWork(long timeout, ExecutorService executorService, WorkerWrapper... workerWrapper)
1.基于全组设定的 timeout,如果超时了,则 worker 中的返回值使用 defaultValue()
2.如果当前 Worker 任务异常了,则当前任务使用 defaultValue(),并且 depend 当前任务的,也 FastFail,返回 defaultValue()
1.9 数据操作:apache
01.普通操作
a.流程
通过POI读取需要导入的Excel
以文件名为表名、列头为列名、并将数据拼接成sql
通过JDBC或mybatis插入数据库
b.示例:读取一个10万行的Excel,居然用了191s,我还以为它卡死了呢!
private void readXls(String filePath, String filename) throws Exception {
@SuppressWarnings("resource")
XSSFWorkbook xssfWorkbook = new XSSFWorkbook(new FileInputStream(filePath));
// 读取第一个工作表
XSSFSheet sheet = xssfWorkbook.getSheetAt(0);
// 总行数
int maxRow = sheet.getLastRowNum();
StringBuilder insertBuilder = new StringBuilder();
insertBuilder.append("insert into ").append(filename).append(" ( UUID,");
XSSFRow row = sheet.getRow(0);
for (int i = 0; i < row.getPhysicalNumberOfCells(); i++) {
insertBuilder.append(row.getCell(i)).append(",");
}
insertBuilder.deleteCharAt(insertBuilder.length() - 1);
insertBuilder.append(" ) values ( ");
StringBuilder stringBuilder = new StringBuilder();
for (int i = 1; i <= maxRow; i++) {
XSSFRow xssfRow = sheet.getRow(i);
String id = "";
String name = "";
for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) {
if (j == 0) {
id = xssfRow.getCell(j) + "";
} else if (j == 1) {
name = xssfRow.getCell(j) + "";
}
}
boolean flag = isExisted(id, name);
if (!flag) {
stringBuilder.append(insertBuilder);
stringBuilder.append('\'').append(uuid()).append('\'').append(",");
for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) {
stringBuilder.append('\'').append(value).append('\'').append(",");
}
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
stringBuilder.append(" )").append("\n");
}
}
List<String> collect = Arrays.stream(stringBuilder.toString().split("\n")).collect(Collectors.toList());
int sum = JdbcUtil.executeDML(collect);
}
private static boolean isExisted(String id, String name) {
String sql = "select count(1) as num from " + static_TABLE + " where ID = '" + id + "' and NAME = '" + name + "'";
String num = JdbcUtil.executeSelect(sql, "num");
return Integer.valueOf(num) > 0;
}
private static String uuid() {
return UUID.randomUUID().toString().replace("-", "");
}
02.优化
a.思路
优化1:先查询全部数据,缓存到map中,插入前再进行判断,速度快了很多
优化2:如果单个Excel文件过大,可以采用 异步 + 多线程 读取若干行,分批入库
优化3:如果文件数量过多,可以采一个Excel一个异步,形成完美的双异步读取插入
b.readExcelCacheAsync控制类
@RequestMapping(value = "/readExcelCacheAsync", method = RequestMethod.POST)
@ResponseBody
public String readExcelCacheAsync() {
String path = "G:\\测试\\data\\";
try {
// 在读取Excel之前,缓存所有数据
USER_INFO_SET = getUserInfo();
File file = new File(path);
String[] xlsxArr = file.list();
for (int i = 0; i < xlsxArr.length; i++) {
File fileTemp = new File(path + "\\" + xlsxArr[i]);
String filename = fileTemp.getName().replace(".xlsx", "");
readExcelCacheAsyncService.readXls(path + filename + ".xlsx", filename);
}
} catch (Exception e) {
logger.error("|#ReadDBCsv|#异常: ", e);
return "error";
}
return "success";
}
c.分批读取超大Excel文件
@Async("async-executor")
public void readXls(String filePath, String filename) throws Exception {
@SuppressWarnings("resource")
XSSFWorkbook xssfWorkbook = new XSSFWorkbook(new FileInputStream(filePath));
// 读取第一个工作表
XSSFSheet sheet = xssfWorkbook.getSheetAt(0);
// 总行数
int maxRow = sheet.getLastRowNum();
logger.info(filename + ".xlsx,一共" + maxRow + "行数据!");
StringBuilder insertBuilder = new StringBuilder();
insertBuilder.append("insert into ").append(filename).append(" ( UUID,");
XSSFRow row = sheet.getRow(0);
for (int i = 0; i < row.getPhysicalNumberOfCells(); i++) {
insertBuilder.append(row.getCell(i)).append(",");
}
insertBuilder.deleteCharAt(insertBuilder.length() - 1);
insertBuilder.append(" ) values ( ");
int times = maxRow / STEP + 1;
//logger.info("将" + maxRow + "行数据分" + times + "次插入数据库!");
for (int time = 0; time < times; time++) {
int start = STEP * time + 1;
int end = STEP * time + STEP;
if (time == times - 1) {
end = maxRow;
}
if(end + 1 - start > 0){
//logger.info("第" + (time + 1) + "次插入数据库!" + "准备插入" + (end + 1 - start) + "条数据!");
//readExcelDataAsyncService.readXlsCacheAsync(sheet, row, start, end, insertBuilder);
readExcelDataAsyncService.readXlsCacheAsyncMybatis(sheet, row, start, end, insertBuilder);
}
}
}
d.异步批量入库
@Async("async-executor")
public void readXlsCacheAsync(XSSFSheet sheet, XSSFRow row, int start, int end, StringBuilder insertBuilder) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = start; i <= end; i++) {
XSSFRow xssfRow = sheet.getRow(i);
String id = "";
String name = "";
for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) {
if (j == 0) {
id = xssfRow.getCell(j) + "";
} else if (j == 1) {
name = xssfRow.getCell(j) + "";
}
}
// 先在读取Excel之前,缓存所有数据,再做判断
boolean flag = isExisted(id, name);
if (!flag) {
stringBuilder.append(insertBuilder);
stringBuilder.append('\'').append(uuid()).append('\'').append(",");
for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) {
stringBuilder.append('\'').append(value).append('\'').append(",");
}
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
stringBuilder.append(" )").append("\n");
}
}
List<String> collect = Arrays.stream(stringBuilder.toString().split("\n")).collect(Collectors.toList());
if (collect != null && collect.size() > 0) {
int sum = JdbcUtil.executeDML(collect);
}
}
private boolean isExisted(String id, String name) {
return ReadExcelCacheAsyncController.USER_INFO_SET.contains(id + "," + name);
}
e.异步线程池工具类
a.@Async的作用就是异步处理任务
在方法上添加@Async,表示此方法是异步方法
在类上添加@Async,表示类中的所有方法都是异步方法
使用此注解的类,必须是Spring管理的类
需要在启动类或配置类中加入@EnableAsync注解,@Async才会生效
-------------------------------------------------------------------------------------------------
在使用@Async时,如果不指定线程池的名称,也就是不自定义线程池
@Async是有默认线程池的,使用的是Spring默认的线程池SimpleAsyncTaskExecutor
b.默认线程池的默认配置如下
默认核心线程数:8
最大线程数:Integet.MAX_VALUE
队列使用LinkedBlockingQueue
容量是:Integet.MAX_VALUE
空闲线程保留时间:60s
线程池拒绝策略:AbortPolicy
-------------------------------------------------------------------------------------------------
从最大线程数可以看出,在并发情况下,会无限制的创建线程,我勒个吗啊
c.通过yml重新配置
spring:
task:
execution:
pool:
max-size: 10
core-size: 5
keep-alive: 3s
queue-capacity: 1000
thread-name-prefix: my-executor
d.自定义线程池
@EnableAsync// 支持异步操作
@Configuration
public class AsyncTaskConfig {
/**
* com.google.guava中的线程池
* @return
*/
@Bean("my-executor")
public Executor firstExecutor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("my-executor").build();
// 获取CPU的处理器数量
int curSystemThreads = Runtime.getRuntime().availableProcessors() * 2;
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(curSystemThreads, 100,
200, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(), threadFactory);
threadPool.allowsCoreThreadTimeOut();
return threadPool;
}
/**
* Spring线程池
* @return
*/
@Bean("async-executor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
// 核心线程数
taskExecutor.setCorePoolSize(24);
// 线程池维护线程的最大数量,只有在缓冲队列满了之后才会申请超过核心线程数的线程
taskExecutor.setMaxPoolSize(200);
// 缓存队列
taskExecutor.setQueueCapacity(50);
// 空闲时间,当超过了核心线程数之外的线程在空闲时间到达之后会被销毁
taskExecutor.setKeepAliveSeconds(200);
// 异步方法内部线程名称
taskExecutor.setThreadNamePrefix("async-executor-");
/**
* 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略
* 通常有以下四种策略:
* ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
* ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
* ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
* ThreadPoolExecutor.CallerRunsPolicy:重试添加当前的任务,自动重复调用 execute() 方法,直到成功
*/
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
}
e.异步失效的原因
注解@Async的方法不是public方法
注解@Async的返回值只能为void或Future
注解@Async方法使用static修饰也会失效
没加@EnableAsync注解
调用方和@Async不能在一个类中
在Async方法上标注@Transactional是没用的,但在Async方法调用的方法上标注@Transcational是有效的
03.文件导出
a.思路
为了解决上述性能问题,用一种基于多线程技术的解决方案
具体来说,通过同时启动多个(例如三个)独立的工作线程来并行执行数据检索任务
从而在一定程度上缓解了单一请求带来的压力,并实现了用额外计算资源换取时间效率的目标
根据初步测试结果,在处理十万级别的数据集时,这种多线程方法相比传统单线程方式可以节省大约50%的时间成本
b.定义线程池
//借鉴的初始化线程池
public class TaskThreadPool {
/*
* 并发比例
* */
public static final int concurrentRate = 3;
/*
* 核心线程数
* */
private static final int ASYNC_CORE_THREADS = 3, CONCURRENT_CORE_THREADS = ASYNC_CORE_THREADS * concurrentRate;
/*
* 最大线程数
* */
private static final int ASYNC_MAX_THREADS = ASYNC_CORE_THREADS + 1, CONCURRENT_MAX_THREADS = ASYNC_MAX_THREADS * concurrentRate;
/*
* 队列大小
* */
private static final int ASYNC_QUEUE_SIZE = 2000, CONCURRENT_QUEUE_SIZE = 20000;
/*
* 线程池的线程前缀
* */
public static final String ASYNC_THREAD_PREFIX = "excel-async-pool-", CONCURRENT_THREAD_PREFIX = "excel-concurrent-pool-";
/*
* 空闲线程的存活时间(单位秒),三分钟
* */
private static final int KEEP_ALIVE_SECONDS = 60 * 3;
/*
* 拒绝策略:如果队列、线程数已满,本次提交的任务返回给线程自己执行
* */
public static final ThreadPoolExecutor.AbortPolicy ASYNC_REJECTED_HANDLER =
new ThreadPoolExecutor.AbortPolicy();
public static final ThreadPoolExecutor.CallerRunsPolicy CONCURRENT_REJECTED_HANDLER =
new ThreadPoolExecutor.CallerRunsPolicy();
/*
* 异步线程池
* */
private volatile static ThreadPoolTaskExecutor asyncThreadPool, concurrentThreadPool;
/*
* DCL单例式懒加载:获取异步线程池
* */
public static ThreadPoolTaskExecutor getAsyncThreadPool() {
if (asyncThreadPool == null) {
synchronized (TaskThreadPool.class) {
if (asyncThreadPool == null) {
asyncThreadPool = new ThreadPoolTaskExecutor();
asyncThreadPool.setCorePoolSize(ASYNC_CORE_THREADS);
asyncThreadPool.setMaxPoolSize(ASYNC_MAX_THREADS);
asyncThreadPool.setQueueCapacity(ASYNC_QUEUE_SIZE);
asyncThreadPool.setKeepAliveSeconds(KEEP_ALIVE_SECONDS);
asyncThreadPool.setThreadNamePrefix(ASYNC_THREAD_PREFIX);
asyncThreadPool.setWaitForTasksToCompleteOnShutdown(true);
asyncThreadPool.setRejectedExecutionHandler(ASYNC_REJECTED_HANDLER);
asyncThreadPool.initialize();
return asyncThreadPool;
}
}
}
return asyncThreadPool;
}
/*
* DCL单例式懒加载:获取并发线程池
* */
public static ThreadPoolTaskExecutor getConcurrentThreadPool() {
if (concurrentThreadPool == null) {
synchronized (TaskThreadPool.class) {
if (concurrentThreadPool == null) {
concurrentThreadPool = new ThreadPoolTaskExecutor();
concurrentThreadPool.setCorePoolSize(CONCURRENT_CORE_THREADS);
concurrentThreadPool.setMaxPoolSize(CONCURRENT_MAX_THREADS);
concurrentThreadPool.setKeepAliveSeconds(KEEP_ALIVE_SECONDS);
concurrentThreadPool.setQueueCapacity(CONCURRENT_QUEUE_SIZE);
concurrentThreadPool.setThreadNamePrefix(CONCURRENT_THREAD_PREFIX);
concurrentThreadPool.setWaitForTasksToCompleteOnShutdown(true);
concurrentThreadPool.setRejectedExecutionHandler(CONCURRENT_REJECTED_HANDLER);
concurrentThreadPool.initialize();
return concurrentThreadPool;
}
}
}
return concurrentThreadPool;
}
}
c.主要流程
try (SXSSFWorkbook workbook = new SXSSFWorkbook(100);
FileOutputStream fileOut = new FileOutputStream(filePath)) {
String[] reportHeaderArray = reportHeader.split(",");
String[] reportContentArray = reportContent.split(",");
StringBuilder sqlStr = buildSqlQuery(reportContentArray);
log.info("开始构建 Excel,总计数据行数: " + total);
int sheetNum = (total + SHEET_SIZE - 1) / SHEET_SIZE;
log.info("总 Sheet 数: " + sheetNum);
for (int i = 1; i <= sheetNum; i++) {
log.info("开始构建第 " + i + " 个sheet");
processSheet(workbook, dataSource, reportHeaderArray, sqlStr.toString(), queryText, dbName, i,uuId,total);
}
workbook.write(fileOut);
uploadFileToMinioAndSetProgress(filePath, reportName + formattedDate + ".xlsx", uuId);
} catch (Exception e) {
log.error("导出本地临时文件写流数据异常:", e);
throw new RuntimeException("导出本地临时文件异常", e);
} finally {
cleanUpTempFiles(uuId);
}
d.核心逻辑
/**
* workbook: Excel工作簿对象。
* dataSource: 数据源对象,用于连接数据库。
* reportHeaderArray: 报告的列头数组。
* sqlStr: SQL查询字符串。
* queryText: 查询文本。
* dbName: 数据库名。
* sheetIndex: 当前工作表的索引。
* uuId: 唯一标识符,用于跟踪进度。
* total: 总记录数
*/
private void processSheet(SXSSFWorkbook workbook, DruidDataSource dataSource, String[] reportHeaderArray,
String sqlStr, String queryText, String dbName, int sheetIndex, String uuId,int total) throws InterruptedException, JSONException {
ThreadPoolTaskExecutor concurrentPool = TaskThreadPool.getConcurrentThreadPool();
// 创建一个新的工作表并写入列头。
Sheet sheet = workbook.createSheet("页码" + sheetIndex);
writeHeaderRow(sheet, reportHeaderArray);
// 参数初始化
// rowsPerFetch: 每次查询的行数。
// fetchCount: 每个工作表需要查询的次数。
// concurrentRate: 并发执行的任务数。
int sheetRow = 1;
int rowsPerFetch = 500; // 每次查询500条
int fetchCount = SHEET_SIZE / rowsPerFetch; // 每个Sheet页需要多少次查询
int concurrentRate = TaskThreadPool.concurrentRate; // 并发数
// 循环次数 = 总次数 / 并发数,确保每次并发执行
// 根据 fetchCount 和 concurrentRate 计算需要执行的轮数。
int rounds = (fetchCount + concurrentRate - 1) / concurrentRate; // 向上取整
// 遍历多轮数据获取与处理
for (int round = 0; round < rounds; round++) {
// 初始化计数器,用于同步线程
CountDownLatch countDownLatch = new CountDownLatch(concurrentRate);
// 用一个有序的Map来存储每轮的数据,key为fetchIndex,确保顺序
ConcurrentHashMap<Integer, JSONArray> jsonArrayMap = new ConcurrentHashMap<>();
// 提交concurrentRate个任务到线程池
for (int j = 0; j < concurrentRate; j++) {
// 计算当前任务的索引
int fetchIndex = round * concurrentRate + j;
// 如果索引超过需获取数据的总数,减少计数并继续
if (fetchIndex >= fetchCount) {
countDownLatch.countDown();
continue; // 超过fetchCount时,跳过不再提交任务
}
// 计算当前任务的起始ID
final int startId = fetchIndex * rowsPerFetch + (sheetIndex - 1) * SHEET_SIZE;
// 提交异步任务到线程池
concurrentPool.submit(() -> {
try (Connection con = dataSource.getConnection(30000)) {
// 执行数据获取和解析
JSONArray resultData = fetchAndParseData(sqlStr, queryText, startId, con, dbName);
if (resultData != null) {
// 将结果放入Map中,key为fetchIndex,保证按顺序存储
jsonArrayMap.put(fetchIndex, resultData);
}
} catch (SQLException | JSONException e) {
// 记录错误并抛出异常
log.error("获取数据时出错", e);
throw new RuntimeException(e);
} finally {
// 任务完成,计数减一
countDownLatch.countDown();
}
});
}
// 等待本轮所有任务完成
countDownLatch.await();
boolean allEmpty = jsonArrayMap.values().stream().allMatch(array -> array.length() == 0);
if(allEmpty){
break;
}
// 按fetchIndex的顺序写入数据到Sheet页 按照读取顺序写入
for (int index = round * concurrentRate; index < (round + 1) * concurrentRate && index < fetchCount; index++) {
JSONArray resultArray = jsonArrayMap.get(index);
if (resultArray != null) {
sheetRow = writeDataToSheet(sheet, resultArray, sheetRow); // 写入顺序保持一致
}
}
jsonArrayMap.clear();
// 更新进度
int offset = round * concurrentRate * rowsPerFetch + sheetIndex * SHEET_SIZE; // 当前的进度偏移量
updateProgress(offset, total, uuId); // 调用更新进度的方法 可以实时查看进度
}
}
e.主要流程
a.初始化 CountDownLatch 和 jsonArrayMap
CountDownLatch 用于同步线程
jsonArrayMap 用于存储每轮查询的结果
b.提交任务到线程池
对于每一轮的每个任务,计算起始ID并提交到线程池
每个任务负责从数据库获取数据并解析成 JSONArray
c.等待所有任务完成
countDownLatch.await() 等待所有子任务完成
d.检查结果是否为空
如果所有结果都为空,则跳出循环
e.按顺序写入数据到Sheet页
按照 fetchIndex 顺序写入结果数据
f.更新进度
更新当前进度并调用 updateProgress 方法
f.辅助方法
writeHeaderRow:写入工作表的列头
writeDataToSheet:将数据写入工作表
fetchAndParseData:从数据库查询并解析数据
updateProgress:更新进度信息
g.三个线程导出数据库表测试的数据结果
线程模式 数据量(万) 耗时
单线程 31 80秒
多线程 31 39秒
单线程 160 28分钟
多线程 160 10分钟
1.10 数据操作:easyexcel
01.读取并插入数据库
a.ReadEasyExcelController
@RequestMapping(value = "/readEasyExcel", method = RequestMethod.POST)
@ResponseBody
public String readEasyExcel() {
try {
String path = "G:\\测试\\data\\";
String[] xlsxArr = new File(path).list();
for (int i = 0; i < xlsxArr.length; i++) {
String filePath = path + xlsxArr[i];
File fileTemp = new File(path + xlsxArr[i]);
String fileName = fileTemp.getName().replace(".xlsx", "");
List<UserInfo> list = new ArrayList<>();
EasyExcel.read(filePath, UserInfo.class, new ReadEasyExeclAsyncListener(readEasyExeclService, fileName, batchCount, list)).sheet().doRead();
}
}catch (Exception e){
logger.error("readEasyExcel 异常:",e);
return "error";
}
return "suceess";
}
b.ReadEasyExeclAsyncListener
public ReadEasyExeclService readEasyExeclService;
// 表名
public String TABLE_NAME;
// 批量插入阈值
private int BATCH_COUNT;
// 数据集合
private List<UserInfo> LIST;
public ReadEasyExeclAsyncListener(ReadEasyExeclService readEasyExeclService, String tableName, int batchCount, List<UserInfo> list) {
this.readEasyExeclService = readEasyExeclService;
this.TABLE_NAME = tableName;
this.BATCH_COUNT = batchCount;
this.LIST = list;
}
@Override
public void invoke(UserInfo data, AnalysisContext analysisContext) {
data.setUuid(uuid());
data.setTableName(TABLE_NAME);
LIST.add(data);
if(LIST.size() >= BATCH_COUNT){
// 批量入库
readEasyExeclService.saveDataBatch(LIST);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
if(LIST.size() > 0){
// 最后一批入库
readEasyExeclService.saveDataBatch(LIST);
}
}
public static String uuid() {
return UUID.randomUUID().toString().replace("-", "");
}
}
c.ReadEasyExeclServiceImpl
@Service
public class ReadEasyExeclServiceImpl implements ReadEasyExeclService {
@Resource
private ReadEasyExeclMapper readEasyExeclMapper;
@Override
public void saveDataBatch(List<UserInfo> list) {
// 通过mybatis入库
readEasyExeclMapper.saveDataBatch(list);
// 通过JDBC入库
// insertByJdbc(list);
list.clear();
}
private void insertByJdbc(List<UserInfo> list){
List<String> sqlList = new ArrayList<>();
for (UserInfo u : list){
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder.append("insert into ").append(u.getTableName()).append(" ( UUID,ID,NAME,AGE,ADDRESS,PHONE,OP_TIME ) values ( ");
sqlBuilder.append("'").append(ReadEasyExeclAsyncListener.uuid()).append("',")
.append("'").append(u.getId()).append("',")
.append("'").append(u.getName()).append("',")
.append("'").append(u.getAge()).append("',")
.append("'").append(u.getAddress()).append("',")
.append("'").append(u.getPhone()).append("',")
.append("sysdate )");
sqlList.add(sqlBuilder.toString());
}
JdbcUtil.executeDML(sqlList);
}
}
d.UserInfo
@Data
public class UserInfo {
private String tableName;
private String uuid;
@ExcelProperty(value = "ID")
private String id;
@ExcelProperty(value = "NAME")
private String name;
@ExcelProperty(value = "AGE")
private String age;
@ExcelProperty(value = "ADDRESS")
private String address;
@ExcelProperty(value = "PHONE")
private String phone;
}
02.多线程导出百万数据
a.多线程导出主要实现类
@Service
public class ExportExcel {
@Resource
CacheService cacheService;
@Resource
private CommonThreadManage commonThreadManage;
private static final long SYS_REDIS_EXPIRE_TIME = 30;
// private static final int ROW_SIZE = 100000;
// private static final int ROW_PAGE = 10000;
private static final int ROW_SIZE = 10000;
private static final int ROW_PAGE = 10;
public String exportTable(ExportTable exportTable) {
StringBuffer path = new StringBuffer();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
StringBuffer sign = new StringBuffer();
// redis key
sign.append(exportTable.getId());
try {
String fileName = exportTable.getFileName();
int rowCount = exportTable.getRowCount();
List<List<String>> head = exportTable.getHead();
Set<Integer> mergeIndex = exportTable.getMergeIndex();
List<ExportTable.ExportColumn> fields = exportTable.getFields();
// 用来记录需要为 行 列设置样式
Map<String, Map<Integer, List<Map<Integer, ExportTable.ExportColumn.Font>>>> map = new HashMap<>();
sign.append("#").append(fields.stream().map(e -> e.isShow()? "true" : "false").collect(Collectors.joining(",")));
setFontStyle(0, exportTable.getFields(), map);
// 获取表头长度
int headRow = head.stream().max(Comparator.comparingInt(List::size)).get().size();
// 数据量超过十万 则不带样式
// 只处理表头:表头合并 表头隐藏 表头冻结
if (rowCount * fields.size() > ROW_SIZE * 6.4) {
map.put("cellStyle", null);
}
sign.append("#").append(exportTable.getStyle());
// 数据量超过百万或者数据为空,只返回有表头得单元格
if (rowCount == 0 || rowCount * fields.size() >= ROW_SIZE * 1800) {
EasyExcel.write(outputStream)
// 这里放入动态头
.head(head).sheet("数据")
// 传入表头样式
.registerWriteHandler(EasyExcelUtils.getStyleStrategy())
// 当然这里数据也可以用 List<List<String>> 去传入
.doWrite(new LinkedList<>());
byte[] bytes = outputStream.toByteArray();
// 上传文件到FaS stDF 返回上传路径
// return fastWrapper.uploadFile(bytes, bytes.length, "xlsx") + "?filename=" + fileName + ".xlsx";
return FileUtil.writeFileByBytes(fileName, bytes);
}
/**
* 相同的下载文件请求 直接返回
* the redis combines with datasetId - filter - size of data
*/
if (cacheService.exists(sign.toString())) {
return cacheService.get(sign.toString());
}
/**
* 分sheet页
* divide into sheets with 10M data per sheet
*/
int sheetCount = (rowCount / (ROW_SIZE * ROW_PAGE)) + 1;
String[] paths = new String[sheetCount];
ByteArrayInputStream[] ins = new ByteArrayInputStream[sheetCount];
// 创建线程池
// ExecutorService threadExecutor = Executors.newFixedThreadPool(10);
// 自定义线程池
Executor threadExecutor = commonThreadManage.asyncCommonExecutor();
CountDownLatch threadSignal = new CountDownLatch(sheetCount);
for (int i = 0; i < sheetCount; i++) {
int finalI = i;
threadExecutor.execute(() -> {
// excel文件流
ByteArrayOutputStream singleOutputStream = new ByteArrayOutputStream();
ExcelWriter excelWriter = EasyExcel.write(singleOutputStream).build();
// 单sheet页写入数
int sheetThreadCount = finalI == (sheetCount - 1)? (rowCount - finalI * (ROW_SIZE * ROW_PAGE)) / ROW_SIZE + 1 : ROW_PAGE;
CountDownLatch sheetThreadSignal = new CountDownLatch(sheetThreadCount);
for (int j = 0; j < sheetThreadCount; j++) {
int page = finalI * ROW_PAGE + j + 1;
// 最后一页数据
int pageSize = j == (sheetThreadCount - 1) && finalI == (sheetCount - 1)? rowCount % ROW_SIZE : ROW_SIZE;
threadExecutor.execute(() -> {
try {
writeExcel(page, pageSize, head, map, headRow, excelWriter, mergeIndex, finalI);
sheetThreadSignal.countDown();
} catch (Exception e) {
e.printStackTrace();
}
});
}
try {
sheetThreadSignal.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 关闭写入流
excelWriter.finish();
paths[finalI] = (finalI + 1) + "-" + fileName + ".xlsx";
ins[finalI] = new ByteArrayInputStream(singleOutputStream.toByteArray());
// 单文件
if (sheetCount == 1) {
byte[] bytes = singleOutputStream.toByteArray();
try {
path.append(FileUtil.writeFileByBytes(fileName + ".xlsx",bytes));
} catch (IOException e) {
throw new RuntimeException(e);
}
// 将sign存入redis并设置过期时间
cacheService.setEx(sign.toString(), path.toString(), SYS_REDIS_EXPIRE_TIME, TimeUnit.MINUTES);
}
threadSignal.countDown();
});
}
threadSignal.await();
if (sheetCount!= 1) {
ZipUtil.zip(outputStream, paths, ins);
byte[] bytes = outputStream.toByteArray();
// 上传文件到FastDFS 返回上传路径
// path.append(fastWrapper.uploadFile(bytes, bytes.length, "zip"))
// .append("?filename=").append(fileName).append(".zip");
path.append(FileUtil.writeFileByBytes(fileName + ".zip",bytes));
// 将sign存入redis并设置过期时间
cacheService.setEx(sign.toString(), path.toString(), SYS_REDIS_EXPIRE_TIME, TimeUnit.MINUTES);
}
} catch (Exception e) {
// 更详细的错误处理
System.err.println("An error occurred during export: " + e.getMessage());
e.printStackTrace();
}
return path.toString();
}
private void writeExcel(int page, int pageSize, List<List<String>> head,
Map<String, Map<Integer, List<Map<Integer, ExportTable.ExportColumn.Font>>>> map, int headRow,
ExcelWriter excelWriter, Set<Integer> mergeIndex, int sheetCount) {
// todo 这里进行数据拼装
WriteSheet writeSheet = EasyExcel.writerSheet(sheetCount, "第" + (sheetCount + 1) + "页数据")
// 这里放入动态头
.head(head)
// 传入样式
.registerWriteHandler(EasyExcelUtils.getStyleStrategy())
.registerWriteHandler(new CellColorSheetWriteHandler(map, headRow))
.registerWriteHandler(new MergeStrategy(CollectionUtils.size(data), mergeIndex))
// 当然这里数据也可以用 List<List<String>> 去传入
.build();
excelWriter.write(data, writeSheet);
}
}
private void setFontStyle(int row, List<ExportTable.ExportColumn> fields,
Map<String, Map<Integer, List<Map<Integer, ExportTable.ExportColumn.Font>>>> map) {
Map<Integer, List<Map<Integer, ExportTable.ExportColumn.Font>>> rowStyle = new HashMap<>();
List<Map<Integer, ExportTable.ExportColumn.Font>> columnStyles = new ArrayList<>();
Map<Integer, ExportTable.ExportColumn.Font> columnStyle = new HashMap<>();
for (int column = 0; column < fields.size(); column++) {
int finalColumn = column;
Optional<ExportTable.ExportColumn> any = fields.stream().filter(x -> x.getColumnNum() == finalColumn).findAny();
if (any.isPresent()) {
columnStyle.put(column, any.get().getFont());
}
}
columnStyles.add(columnStyle);
rowStyle.put(row, columnStyles);
map.put("head", rowStyle);
}
}
b.excel导出处理单元格
/**
* @Author Ash
* @description 拦截处理单元格创建
*/
public class CellColorSheetWriteHandler implements CellWriteHandler {
/**
* 多行表头行号
*/
private int headRow;
/**
* 字体
*/
private ExportTable.ExportColumn.Font columnFont = new ExportTable.ExportColumn.Font();
private static volatile XSSFCellStyle cellStyle = null;
public static XSSFCellStyle getCellStyle(Workbook workbook, WriteCellStyle contentWriteCellStyle) {
if (cellStyle == null) {
synchronized (XSSFCellStyle.class) {
if (cellStyle == null) {
cellStyle = (XSSFCellStyle) StyleUtil.buildHeadCellStyle(workbook, contentWriteCellStyle);
}
}
}
return cellStyle;
}
/**
* 字体
* Map<Integer, ExportTable.ExportColumn.Font> 当前列的字段样式
* Map<Integer, List<Map<...>>> 当前行包含那几列需要设置样式
* String head:表头;
* String cell:内容;
*/
private Map<String, Map<Integer, List<Map<Integer, ExportTable.ExportColumn.Font>>>> map;
/**
* 有参构造
*/
public CellColorSheetWriteHandler(Map<String, Map<Integer, List<Map<Integer, ExportTable.ExportColumn.Font>>>> map, int headRow) {
this.map = map;
this.headRow = headRow;
}
public CellColorSheetWriteHandler() {
}
@Override
public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer integer, Integer integer1, Boolean aBoolean) {
}
@Override
public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer integer, Boolean aBoolean) {
}
/**
* 在单元上的所有操作完成后调用
*/
@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
// 当前行的第column列
int column = cell.getColumnIndex();
// 当前第row行
int row = cell.getRowIndex();
AtomicInteger fixNum = new AtomicInteger();
// 处理行,表头
if (headRow > row && map.containsKey("head")) {
Map<Integer, List<Map<Integer, ExportTable.ExportColumn.Font>>> fonts = map.get("head");
fonts.get(row).forEach(e -> {
e.entrySet().forEach(ele -> {
// 获取冻结字段
if (null != ele.getValue().getFixed() && !StringUtils.isEmpty(ele.getValue().getFixed())) {
fixNum.getAndIncrement();
}
// 字段隐藏
if (!ele.getValue().isShow()) {
writeSheetHolder.getSheet().setColumnHidden(ele.getKey(), true);
}
});
});
if (fixNum.get() > 0 && row == 0) {
writeSheetHolder.getSheet().createFreezePane(fixNum.get(), headRow, fixNum.get(), headRow);
} else {
writeSheetHolder.getSheet().createFreezePane(0, headRow, 0, headRow);
}
setStyle(fonts, row, column, cell, writeSheetHolder, head);
}
// 处理内容
if (headRow <= row && map.containsKey("cell") && !map.containsKey("cellStyle")) {
Map<Integer, List<Map<Integer, ExportTable.ExportColumn.Font>>> fonts = map.get("cell");
setStyle(fonts, -1, column, cell, writeSheetHolder, head);
}
}
private void setStyle(Map<Integer, List<Map<Integer, ExportTable.ExportColumn.Font>>> fonts, int row, int column, Cell cell, WriteSheetHolder writeSheetHolder, Head head) {
fonts.get(row).forEach(e -> {
if (e.containsKey(column)) {
// 根据单元格获取workbook
Workbook workbook = cell.getSheet().getWorkbook();
//设置列宽
if (null != e.get(column).getWidth() && !e.get(column).getWidth().isEmpty()) {
writeSheetHolder.getSheet().setColumnWidth(head.getColumnIndex(), Integer.parseInt(e.get(column).getWidth()) * 20);
} else {
writeSheetHolder.getSheet().setColumnWidth(head.getColumnIndex(), 2000);
}
// 单元格策略
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
// 设置垂直居中为居中对齐
contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
// 设置左右对齐方式
if (null != e.get(column).getAlign() && !e.get(column).getAlign().isEmpty()) {
contentWriteCellStyle.setHorizontalAlignment(getHorizontalAlignment(e.get(column).getAlign()));
} else {
contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.LEFT);
}
if (!e.get(column).equals(columnFont) || column == 0) {
/**
* Prevent the creation of a large number of objects
* Defects of the EasyExcel tool(巨坑,简直脱发神器)
*/
cellStyle = (XSSFCellStyle) StyleUtil.buildHeadCellStyle(workbook, contentWriteCellStyle);
// 设置单元格背景颜色
if (null != e.get(column).getBackground() && !e.get(column).getBackground().isEmpty()) {
cellStyle.setFillForegroundColor(new XSSFColor(hex2Color(e.get(column).getBackground())));
} else {
if (cell.getRowIndex() >= headRow)
cellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex());
}
// 创建字体实例
Font font = workbook.createFont();
// 设置字体是否加粗
if (null != e.get(column).getFontWeight() && !e.get(column).getFontWeight().isEmpty())
font.setBold(getBold(e.get(column).getFontWeight()));
// 设置字体和大小
if (null != e.get(column).getFontFamily() && !e.get(column).getFontFamily().isEmpty())
font.setFontName(e.get(column).getFontFamily());
if (0 != e.get(column).getFontSize())
font.setFontHeightInPoints((short) e.get(column).getFontSize());
XSSFFont xssfFont = (XSSFFont) font;
//设置字体颜色
if (null != e.get(column).getColor() && !e.get(column).getColor().isEmpty())
xssfFont.setColor(new XSSFColor(hex2Color(e.get(column).getColor())));
cellStyle.setFont(xssfFont);
// 记录上一个样式
columnFont = e.get(column);
}
// 设置当前行第column列的样式
cell.getRow().getCell(column).setCellStyle(cellStyle);
// 设置行高
cell.getRow().setHeight((short) 400);
}
});
}
private HorizontalAlignment getHorizontalAlignment(String align) {
switch (align) {
case "center":
return HorizontalAlignment.CENTER;
case "right":
return HorizontalAlignment.RIGHT;
default:
return HorizontalAlignment.LEFT;
}
}
private boolean getBold(String fontWeight) {
return "bold".equalsIgnoreCase(fontWeight);
}
private Color hex2Color(String hexStr) {
if(hexStr != null && hexStr.length() == 7){
int[] rgb = new int[3];
rgb[0] = Integer.valueOf(hexStr.substring( 1, 3 ), 16);
rgb[1] = Integer.valueOf(hexStr.substring( 3, 5 ), 16);
rgb[2] = Integer.valueOf(hexStr.substring( 5, 7 ), 16);
return new Color(rgb[0], rgb[1], rgb[2]);
}
return null;
}
public static void main(String[] args) {
CellColorSheetWriteHandler cellColorSheetWriteHandler = new CellColorSheetWriteHandler();
System.out.println(cellColorSheetWriteHandler.hex2Color("#C71585"));
}
}
c.excel格式类
public class EasyExcelUtils {
public static HorizontalCellStyleStrategy getStyleStrategy(){
// 头的策略
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
// 背景设置为灰色
headWriteCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
WriteFont headWriteFont = new WriteFont();
headWriteFont.setFontHeightInPoints((short)12);
// 字体样式
headWriteFont.setFontName("Frozen");
// 字体颜色
headWriteFont.setColor(IndexedColors.BLACK1.getIndex());
headWriteCellStyle.setWriteFont(headWriteFont);
// 自动换行
headWriteCellStyle.setWrapped(false);
// 水平对齐方式(修改默认对齐方式——4.14 版本1.3.2)
headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.LEFT);
// 垂直对齐方式
headWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
// 内容的策略
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
// 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.头默认了 FillPatternType所以可以不指定
// contentWriteCellStyle.setFillPatternType(FillPatternType.SQUARES);
// 背景白色
contentWriteCellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex());
// 水平对齐方式(修改默认对齐方式——4.14 版本1.3.2)
contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.LEFT);
WriteFont contentWriteFont = new WriteFont();
// 字体大小
contentWriteFont.setFontHeightInPoints((short)12);
// 字体样式
contentWriteFont.setFontName("Calibri");
contentWriteCellStyle.setWriteFont(contentWriteFont);
// 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现
return new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
}
}
d.excel导出通用格式类
public class ExportTable {
private String id;
private String style;
private int rowCount;
private List<ExportColumn> fields;
private String fileName;
private List<List<String>> head;
private Set<Integer> mergeIndex;
public ExportTable() {
}
public ExportTable(String id, String style, int rowCount, List<ExportColumn> fields, String fileName, List<List<String>> head, Set<Integer> mergeIndex) {
this.id = id;
this.style = style;
this.rowCount = rowCount;
this.fields = fields;
this.fileName = fileName;
this.head = head;
this.mergeIndex = mergeIndex;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getStyle() {
return style;
}
public void setStyle(String style) {
this.style = style;
}
public int getRowCount() {
return rowCount;
}
public void setRowCount(int rowCount) {
this.rowCount = rowCount;
}
public List<ExportColumn> getFields() {
return fields;
}
public void setFields(List<ExportColumn> fields) {
this.fields = fields;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public List<List<String>> getHead() {
return head;
}
public void setHead(List<List<String>> head) {
this.head = head;
}
public Set<Integer> getMergeIndex() {
return mergeIndex;
}
public void setMergeIndex(Set<Integer> mergeIndex) {
this.mergeIndex = mergeIndex;
}
public static class ExportColumn {
private long columnNum;
private String columnName;
private boolean isShow;
private Font font;
public ExportColumn() {
}
public long getColumnNum() {
return columnNum;
}
public void setColumnNum(long columnNum) {
this.columnNum = columnNum;
}
public String getColumnName() {
return columnName;
}
public void setColumnName(String columnName) {
this.columnName = columnName;
}
public boolean isShow() {
return isShow;
}
public void setShow(boolean show) {
isShow = show;
}
public Font getFont() {
return font;
}
public void setFont(Font font) {
this.font = font;
}
public static class Font {
private String fontName;
private int fontSize;
private String fixed;
private boolean isShow;
private String fontFamily;
private String fontWeight;
private String color;
private String background;
private String align;
private String width;
public Font() {
}
public Font(String fontName, int fontSize, String fixed, boolean isShow, String fontFamily, String fontWeight, String color, String background, String align, String width) {
this.fontName = fontName;
this.fontSize = fontSize;
this.fixed = fixed;
this.isShow = isShow;
this.fontFamily = fontFamily;
this.fontWeight = fontWeight;
this.color = color;
this.background = background;
this.align = align;
this.width = width;
}
public String getFontName() {
return fontName;
}
public void setFontName(String fontName) {
this.fontName = fontName;
}
public int getFontSize() {
return fontSize;
}
public void setFontSize(int fontSize) {
this.fontSize = fontSize;
}
public String getFixed() {
return fixed;
}
public void setFixed(String fixed) {
this.fixed = fixed;
}
public boolean isShow() {
return isShow;
}
public void setShow(boolean show) {
isShow = show;
}
public String getFontFamily() {
return fontFamily;
}
public void setFontFamily(String fontFamily) {
this.fontFamily = fontFamily;
}
public String getFontWeight() {
return fontWeight;
}
public void setFontWeight(String fontWeight) {
this.fontWeight = fontWeight;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public String getBackground() {
return background;
}
public void setBackground(String background) {
this.background = background;
}
public String getAlign() {
return align;
}
public void setAlign(String align) {
this.align = align;
}
public String getWidth() {
return width;
}
public void setWidth(String width) {
this.width = width;
}
}
}
}
e.excel单元格合并
public class MergeStrategy extends AbstractMergeStrategy {
/**
* 合并的列编号,从0开始
* 指定的index或自己按字段顺序数
*/
private Set<Integer> mergeCellIndex = new HashSet<>();
/**
* 数据集大小,用于区别结束行位置
*/
private Integer maxRow = 0;
// 禁止无参声明
private MergeStrategy() {
}
public MergeStrategy(Integer maxRow, Set<Integer> mergeCellIndex) {
this.mergeCellIndex = mergeCellIndex;
this.maxRow = maxRow;
}
private final Map<Integer, MergeRange> lastRow = new HashMap<>();
@Override
protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
int currentCellIndex = cell.getColumnIndex();
// 判断该行是否需要合并
if (mergeCellIndex != null && mergeCellIndex.contains(currentCellIndex)) {
String currentCellValue = cell.getStringCellValue();
int currentRowIndex = cell.getRowIndex();
if (!lastRow.containsKey(currentCellIndex)) {
// 记录首行起始位置
lastRow.put(currentCellIndex, new MergeRange(currentCellValue, currentRowIndex, currentRowIndex, currentCellIndex, currentCellIndex));
return;
}
//有上行这列的值了,拿来对比.
MergeRange mergeRange = lastRow.get(currentCellIndex);
if (!(mergeRange.lastValue != null && mergeRange.lastValue.equals(currentCellValue))) {
// 结束的位置触发下合并.
// 同行同列不能合并,会抛异常
if (mergeRange.startRow != mergeRange.endRow || mergeRange.startCell != mergeRange.endCell) {
sheet.addMergedRegionUnsafe(new CellRangeAddress(mergeRange.startRow, mergeRange.endRow, mergeRange.startCell, mergeRange.endCell));
}
// 更新当前列起始位置
lastRow.put(currentCellIndex, new MergeRange(currentCellValue, currentRowIndex, currentRowIndex, currentCellIndex, currentCellIndex));
}
// 合并行 + 1
mergeRange.endRow += 1;
// 结束的位置触发下最后一次没完成的合并
if (relativeRowIndex.equals(maxRow - 1)) {
MergeRange lastMergeRange = lastRow.get(currentCellIndex);
// 同行同列不能合并,会抛异常
if (lastMergeRange.startRow != lastMergeRange.endRow || lastMergeRange.startCell != lastMergeRange.endCell) {
sheet.addMergedRegionUnsafe(new CellRangeAddress(lastMergeRange.startRow, lastMergeRange.endRow, lastMergeRange.startCell, lastMergeRange.endCell));
}
}
}
}
}
class MergeRange {
public int startRow;
public int endRow;
public int startCell;
public int endCell;
public String lastValue;
public MergeRange(String lastValue, int startRow, int endRow, int startCell, int endCell) {
this.startRow = startRow;
this.endRow = endRow;
this.startCell = startCell;
this.endCell = endCell;
this.lastValue = lastValue;
}
}
f.通用线程池
/**
* 通用线程池,对于使用频次比较低的,异步执行不需要返回结果的,可以使用此方法
*/
@Configuration
@EnableAsync
public class CommonThreadManage {
/**
* 日志服务
*/
private static final Logger logger = LoggerFactory.getLogger(CommonThreadManage.class);
/**
* 线程数量
*/
private static final int THREAD_COUNT = 100;
/**
* 线程数量
*/
private static final int THREAD_MAX_COUNT = 150;
/**
* 线程数量最大任务队列数量
*/
private static final int THREAD_TASK_MAX_COUNT = 1000;
/**
* 异步线程配置
*
* @return 返回线程池配置
*/
@Bean
public Executor asyncCommonExecutor() {
logger.info("start asyncCommonExecutor");
ThreadPoolTaskExecutor executor = new VisiableThreadPoolTaskExecutor();
//配置核心线程数
executor.setCorePoolSize(THREAD_COUNT);
//配置最大线程数
executor.setMaxPoolSize(THREAD_MAX_COUNT);
//配置队列大小
executor.setQueueCapacity(THREAD_TASK_MAX_COUNT);
//配置线程池中的线程的名称前缀
executor.setThreadNamePrefix("async-hotel-common-");
// rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//执行初始化
executor.initialize();
return executor;
}
}
g.文件读取工具类
public class FileUtil {
/**
* 以byte[]方式读取文件
*
* @param fileName 文件名
* @return
* @throws IOException
*/
public static byte[] readFileByBytes(String fileName) throws IOException {
try (InputStream in = new BufferedInputStream(new FileInputStream(fileName));
ByteArrayOutputStream out = new ByteArrayOutputStream();) {
byte[] tempbytes = new byte[in.available()];
for (int i = 0; (i = in.read(tempbytes)) != -1;) {
out.write(tempbytes, 0, i);
}
return out.toByteArray();
}
}
/**
* 向文件写入byte[]
*
* @param fileName 文件名
* @param bytes 字节内容
* @param append 是否追加
* @throws IOException
*/
public static void writeFileByBytes(String fileName, byte[] bytes, boolean append) throws IOException {
try(OutputStream out = new BufferedOutputStream(new FileOutputStream(fileName, append))){
out.write(bytes);
}
}
/**
* 从文件开头向文件写入byte[]
*
* @param fileName 文件名
* @param bytes 字节
* @throws IOException
*/
public static String writeFileByBytes(String fileName, byte[] bytes) throws IOException {
writeFileByBytes(fileName, bytes, false);
return fileName;
}
}
h.缓存操作工具
@Component
@Getter
public class CacheService extends CachingConfigurerSupport {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 是否存在key
*
* @param key
* @return
*/
public Boolean exists(String key) {
return stringRedisTemplate.hasKey(key);
}
/**
* 获取指定 key 的值
*
* @param key
* @return
*/
public String get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
/**
* 将值 value 关联到 key ,并将 key 的过期时间设为 timeout
*
* @param key
* @param value
* @param timeout 过期时间
* @param unit 时间单位, 天:TimeUnit.DAYS 小时:TimeUnit.HOURS 分钟:TimeUnit.MINUTES
* 秒:TimeUnit.SECONDS 毫秒:TimeUnit.MILLISECONDS
*/
public void setEx(String key, String value, long timeout, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, value, timeout, unit);
}
/**
* 获取字符串的长度
*
* @param key
* @return
*/
public Long size(String key) {
return stringRedisTemplate.opsForValue().size(key);
}
/**
* 追加到末尾
*
* @param key
* @param value
* @return
*/
public Integer append(String key, String value) {
return stringRedisTemplate.opsForValue().append(key, value);
}
/**
* 获取集合的元素, 从小到大排序
*
* @param key
* @param start 开始位置
* @param end 结束位置, -1查询所有
* @return
*/
public Set<String> zRange(String key, long start, long end) {
return stringRedisTemplate.opsForZSet().range(key, start, end);
}
}
1.11 数据操作:jdbctemplate
01.基础概念及使用场景
a.基础概念
JdbcTemplate是Spring框架核心包的一部分,位于spring-jdbc模块中,因此通常不用我们再额外引入依赖,直接就可以。
它封装了JDBC的核心流程,消除了传统的JDBC开发中大量重复的代码。当然最核心的就是直接在代码中执行SQL:
-----------------------------------------------------------------------------------------------------
// 插入数据
jdbcTemplate.update("INSERT INTO users(name, email) VALUES(?, ?)", "张三", "[email protected] ");
// 查询单条记录
User user = jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE id = ?",
new Object[]{1},
new BeanPropertyRowMapper<>(User.class));
-----------------------------------------------------------------------------------------------------
因此它和 mybatis 这些 orm 框架相比使用就更加的简单了,效率也更高。但是在开发中我们不会去用它来写业务代码,毕竟规范通常来说更加的重要。
b.使用场景
它的使用场景在哪些地方呢,以本次工作场景为例。这次的业务需求需要实现动态建表的功能,以及在插入数据的时候,表名和字段都是动态的。
利用orm框架去处理ddl语句显示不是很好,以及表名和字段都是需要计算才知道显然在java代码中拼接好SQL之后直接执行更好。
所以在执行ddl语句,存储过程,以及SQL必须在程序中进行拼接的场景,用JdbcTemplate更为合适。
在实际项目中,JdbcTemplate常与JPA或MyBatis等ORM框架混合使用。ORM框架处理常规业务逻辑,
而JdbcTemplate处理特殊场景如ddl、复杂查询、批量操作等。这种混合模式能兼顾开发效率和执行性能。
02.基本使用语法
a.基本配置
a.在Spring Boot中,只需在application.properties中配置数据源,Spring会自动创建JdbcTemplate bean:
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=username
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
b.导入bean就可以直接使用了
@Resource
private JdbcTemplate jdbcTemplate;
b.执行查询操作
a.查询单个值
// 查询用户总数
int count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM users", Integer.class);
b.查询单条记录
// 使用RowMapper将结果集映射为对象
User user = jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE id = ?",
new Object[]{1},
new BeanPropertyRowMapper<>(User.class));
c.查询多条记录
// 查询所有用户
List<User> users = jdbcTemplate.query(
"SELECT * FROM users",
new BeanPropertyRowMapper<>(User.class));
c.执行更新操作
a.插入数据
jdbcTemplate.update(
"INSERT INTO users(name, email) VALUES(?, ?)",
"张三", "[email protected] ");
b.更新数据
int rowsAffected = jdbcTemplate.update(
"UPDATE users SET email = ? WHERE id = ?",
"[email protected] ", 1);
c.删除数据
int rowsDeleted = jdbcTemplate.update(
"DELETE FROM users WHERE id = ?", 1);
d.批量操作
jdbcTemplate.batchUpdate(
"INSERT INTO users(name, email) VALUES(?, ?)",
new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
ps.setString(1, "User" + i);
ps.setString(2, "user" + i + "@example.com");
}
@Override
public int getBatchSize() {
return 10;
}
});
e.命名参数支持:NamedParameterJdbcTemplate
NamedParameterJdbcTemplate namedTemplate =
new NamedParameterJdbcTemplate(dataSource);
Map<String, Object> params = new HashMap<>();
params.put("id", 1);
params.put("email", "[email protected] ");
namedTemplate.update(
"UPDATE users SET email = :email WHERE id = :id",
params);
f.执行DDL:直接把原生的SQL语句执行就行了
public void createUserTable() {
jdbcTemplate.execute("CREATE TABLE users ("
+ "id BIGINT PRIMARY KEY AUTO_INCREMENT,"
+ "username VARCHAR(50) NOT NULL UNIQUE,"
+ "password VARCHAR(100) NOT NULL,"
+ "email VARCHAR(100),"
+ "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,"
+ "updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"
+ ")");
}
03.注意事项
a.SQL注入防护
a.问题
直接拼接SQL字符串会导致SQL注入风险
b.正确做法
1.始终使用参数化查询
2.绝对不要直接拼接用户输入到SQL中
c.错误做法(有SQL注入风险)
String sql = "SELECT * FROM users WHERE name = '" + name + "'";
jdbcTemplate.query(sql, rowMapper);
d.正确做法(使用参数占位符)
jdbcTemplate.query("SELECT * FROM users WHERE name = ?", new Object[]{name}, rowMapper);
b.资源管理
a.问题
虽然JdbcTemplate会关闭连接,但某些资源仍需注意
b.注意事项
1.对于大型结果集,使用RowCallbackHandler而非RowMapper进行流式处理
2.使用JdbcTemplate.query()方法时,确保结果集不会过大导致内存溢出
c.代码
// 处理大型结果集的推荐方式
jdbcTemplate.query("SELECT * FROM large_table",
rs -> {
// 逐行处理,不一次性加载所有数据
while (rs.next()) {
processRow(rs);
}
});
c.异常处理
a.问题
JdbcTemplate会将SQLException转换为DataAccessException
b.正确处理
1.捕获特定的DataAccessException子类而非泛化的异常
2.记录完整的异常信息以便排查问题
c.代码
try {
jdbcTemplate.update("UPDATE accounts SET balance = ? WHERE id = ?", amount, accountId);
} catch (DuplicateKeyException e) {
// 处理唯一键冲突
log.error("Duplicate key violation", e);
throw new BusinessException("Account already exists");
} catch (DataAccessException e) {
// 处理其他数据库异常
log.error("Database access error", e);
throw new ServiceException("Database operation failed");
}
d.事务管理
a.问题
默认情况下每个操作是独立事务;
b.注意
DDL语句通常是自动提交的,不受常规事务管理控制
c.最佳实践
1.对需要原子性的一组操作使用@Transactional注解
2.注意事务传播行为和隔离级别设置
d.代码
@Transactional
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
jdbcTemplate.update("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromId);
jdbcTemplate.update("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toId);
}
e.性能优化
a.注意事项
1.批量操作使用batchUpdate而非循环执行单条更新
2.重用预编译语句(JdbcTemplate内部已优化)
3.考虑使用NamedParameterJdbcTemplate提高复杂SQL可读性
b.代码
// 批量插入优化示例
jdbcTemplate.batchUpdate(
"INSERT INTO users (name, email) VALUES (?, ?)",
new BatchPreparedStatementSetter() {
public void setValues(PreparedStatement ps, int i) throws SQLException {
User user = users.get(i);
ps.setString(1, user.getName());
ps.setString(2, user.getEmail());
}
public int getBatchSize() {
return users.size();
}
});
f.SQL可维护性
a.问题
SQL硬编码在Java类中难以维护,这也是我们平时做业务开发,不用使用jdbcTemplate重要原因之一
b.解决方案
1.将SQL语句提取到常量或配置文件中
2.使用Spring的@Sql注解管理测试数据
3.考虑使用SQL构建工具如QueryDSL
c.代码
// 将SQL提取为常量
private static final String FIND_USER_BY_ID =
"SELECT id, name, email FROM users WHERE id = ?";
public User findById(Long id) {
return jdbcTemplate.queryForObject(
FIND_USER_BY_ID,
new Object[]{id},
new BeanPropertyRowMapper<>(User.class));
}
1.12 数据测试:junit4、junit5
01.SpringBootTest
a.默认
包含测试工具和框架,如 JUnit、Mockito,默认包括 slf4j
spring-boot-starter-test 包含了 JUnit 4 和 JUnit 5 的支持
默认情况下,它会使用 JUnit 5,但你可以通过配置来使用 JUnit 4
从 Spring Boot 2.4 开始,JUnit 5 的 Vintage 引擎已从 spring-boot-starter-test 中移除
b.依赖
一旦依赖了spring-boot-starter-test,下面这些类库将被一同依赖进去
JUnit:java测试事实上的标准,默认依赖版本是4.12(JUnit5和JUnit4差别比较大,集成方式有不同)。
Spring Test & Spring Boot Test:Spring的测试支持。
AssertJ:提供了流式的断言方式。
Hamcrest:提供了丰富的matcher。
Mockito:mock框架,可以按类型创建mock对象,可以根据方法参数指定特定的响应,也支持对于mock调用过程的断言。
JSONassert:为JSON提供了断言功能。
JsonPath:为JSON提供了XPATH功能。
c.容器种类
1.@SpringBootTest:启动完整 Spring Boot 上下文的方式
2.@WebMvcTest:针对 Web 层测试
3.@DataJpaTest:针对 JPA 数据层测试
4.@RestClientTest:针对 REST 客户端测试
5.@JdbcTest:针对 JDBC 数据层测试
6.@JmsTest:针对 JMS 测试
7.@JsonTest:针对 JSON 序列化/反序列化测试
8.手动加载特定配置:更灵活但更复杂
9.纯单元测试:不启动任何 Spring 容器
d.注解种类
@Test:表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
@ParameterizedTest:表示方法是参数化测试,下方会有详细介绍
@RepeatedTest:表示方法可重复执行,下方会有详细介绍
@DisplayName:为测试类或者测试方法设置展示名称
@BeforeEach:表示在每个单元测试之前执行
@AfterEach:表示在每个单元测试之后执行
@BeforeAll:表示在所有单元测试之前执行
@AfterAll:表示在所有单元测试之后执行
@Tag:表示单元测试类别,类似于JUnit4中的@Categories
@Disabled:表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
@Timeout:表示测试方法运行如果超过了指定时间将会返回错误
@ExtendWith:为测试类或测试方法提供扩展类引用
e.@SpringBootTest注解中,给出了webEnvironment参数指定了web的environment,该参数的值一共有四个可选值:
MOCK:此值为默认值,该类型提供一个mock环境,可以和@AutoConfigureMockMvc或@AutoConfigureWebTestClient搭配使用,开启Mock相关的功能。注意此时内嵌的服务(servlet容器)并没有真正启动,也不会监听web服务端口。
RANDOM_PORT:启动一个真实的web服务,监听一个随机端口。
DEFINED_PORT:启动一个真实的web服务,监听一个定义好的端口(从application.properties读取)。
NONE:启动一个非web的ApplicationContext,既不提供mock环境,也不提供真实的web服务。
02.注解分类
a.Spring Boot Test中的注解分类
a.配置类型
@TestConfiguration等。提供一些测试相关的配置入口。
b.mock类型
@MockBean等。提供mock支持。
c.启动测试类型
@SpringBootTest。以Test结尾的注解,具有加载applicationContext的能力。
d.自动配置类型
@AutoConfigureJdbc等。以AutoConfigure开头的注解,具有加载测试支持功能的能力。
b.配置类型的注解
a.@TestComponent
该注解是另一种@Component,在语义上用来指定某个Bean是专门用于测试的。该注解适用于测试代码和正式混合在一起时,不加载被该注解描述的Bean,使用不多。
b.@TestConfiguration
该注解是另一种@TestComponent,它用于补充额外的Bean或覆盖已存在的Bean。在不修改正式代码的前提下,使配置更加灵活。
c.@TypeExcludeFilters
用来排除@TestConfiguration和@TestComponent。适用于测试代码和正式代码混合的场景,使用不多。
d.@OverrideAutoConfiguration
可用于覆盖@EnableAutoConfiguration,与ImportAutoConfiguration结合使用,以限制所加载的自动配置类。在不修改正式代码的前提下,提供了修改配置自动配置类的能力。
e.@PropertyMapping
定义@AutoConfigure*注解中用到的变量名称,例如在@AutoConfigureMockMvc中定义名为spring.test.mockmvc.webclient.enabled的变量。一般不使用。
c.mock类型的注解
a.@MockBean
用于mock指定的class或被注解的属性。
b.@MockBeans
使@MockBean支持在同一类型或属性上多次出现。
c.@SpyBean
用于spy指定的class或被注解的属性。
d.@SpyBeans
使@SpyBean支持在同一类型或属性上多次出现。
e.MockBean与SpyBean的区别
MockBean和SpyBean功能非常相似,都能模拟方法的各种行为。不同之处在于MockBean是全新的对象,跟正式对象没有关系;而SpyBean与正式对象紧密联系,可以模拟正式对象的部分方法,没有被模拟的方法仍然可以运行正式代码。
d.自动配置类型的注解(@AutoConfigure*)
a.@AutoConfigureJdbc
自动配置JDBC
b.@AutoConfigureCache
自动配置缓存
c.@AutoConfigureDataLdap
自动配置LDAP
d.@AutoConfigureJson
自动配置JSON
e.@AutoConfigureJsonTesters
自动配置JsonTester
f.@AutoConfigureDataJpa
自动配置JPA
g.@AutoConfigureTestEntityManager
自动配置TestEntityManager
h.@AutoConfigureRestDocs
自动配置Rest Docs
i.@AutoConfigureMockRestServiceServer
自动配置MockRestServiceServer
j.@AutoConfigureWebClient
自动配置WebClient
k.@AutoConfigureWebFlux
自动配置WebFlux
l.@AutoConfigureWebTestClient
自动配置WebTestClient
m.@AutoConfigureMockMvc
自动配置MockMvc
n.@AutoConfigureWebMvc
自动配置WebMvc
o.@AutoConfigureDataNeo4j
自动配置Neo4j
p.@AutoConfigureDataRedis
自动配置Redis
q.@AutoConfigureJooq
自动配置Jooq
r.@AutoConfigureTestDatabase
自动配置Test Database,可以使用内存数据库
e.启动测试类型的注解(@*Test)
a.@SpringBootTest
自动侦测并加载@SpringBootApplication或@SpringBootConfiguration中的配置,默认web环境为MOCK,不监听任务端口。
b.@DataRedisTest
测试对Redis操作,自动扫描被@RedisHash描述的类,并配置Spring Data Redis的库。
c.@DataJpaTest
测试基于JPA的数据库操作,同时提供了TestEntityManager替代JPA的EntityManager。
d.@DataJdbcTest
测试基于Spring Data JDBC的数据库操作。
e.@JsonTest
测试JSON的序列化和反序列化。
f.@WebMvcTest
测试Spring MVC中的controllers。
g.@WebFluxTest
测试Spring WebFlux中的controllers。
h.@RestClientTest
测试对REST客户端的操作。
i.@DataLdapTest
测试对LDAP的操作。
j.@DataMongoTest
测试对MongoDB的操作。
k.@DataNeo4jTest
测试对Neo4j的操作。
03.JUnit4:@RunWith(SpringRunner.class)
a.说明
Spring Boot 2.x 默认集成了 JUnit 5。如果你想使用 JUnit 4,需要进行一些额外的配置。
-----------------------------------------------------------------------------------------------------
@RunWith是Junit4提供的注解,将Spring和Junit链接了起来。
假如使用Junit5,不再需要使用@ExtendWith注解,@SpringBootTest和其它@*Test默认已经包含了该注解。
@SpringBootTest替代了spring-test中的@ContextConfiguration注解,目的是加载ApplicationContext,启动spring容器。
使用@SpringBootTest时并没有像@ContextConfiguration一样显示指定locations或classes属性,
原因在于@SpringBootTest注解会自动检索程序的配置文件,检索顺序是从当前包开始,
逐级向上查找被@SpringBootApplication或@SpringBootConfiguration注解的类。
b.依赖
为了使用 JUnit 4,你需要排除 spring-boot-starter-test 中默认的 JUnit 5 依赖,并手动引入 JUnit 4。
-----------------------------------------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version> <!-- 请根据你的Spring Boot版本调整 -->
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 排除 JUnit 5 并引入 JUnit 4 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
<exclusion>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
c.创建一个简单的 Spring Boot 服务类
// src/main/java/com/example/demo/service/MyService.java
package com.example.demo.service;
import org.springframework.stereotype.Service;
@Service
public class MyService {
public String sayHello(String name) {
return "Hello, " + name + "!";
}
public int add(int a, int b) {
return a + b;
}
}
d.创建 JUnit 4 测试类
package com.example.demo.service;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@RunWith(SpringRunner.class) // 启用 Spring Runner
@SpringBootTest // 启动 Spring Boot 上下文
public class MyServiceJUnit4Test {
@Autowired
private MyService myService;
@Before
public void setUp() {
// 在每个测试方法执行前执行,可以用于初始化
System.out.println("Executing setUp for JUnit 4 test...");
}
@Test
public void testSayHello() {
String result = myService.sayHello("World");
assertEquals("Hello, World!", result);
assertNotNull(result);
}
@Test
public void testAdd() {
int result = myService.add(2, 3);
assertEquals(5, result);
}
@Test(expected = ArithmeticException.class) // 期望抛出特定异常
public void testDivideByZero() {
// 假设 MyService 有一个除法方法
// myService.divide(10, 0);
}
}
e.关键点:
@RunWith(SpringRunner.class): 这是 JUnit 4 中集成 Spring 测试的关键注解,它告诉 JUnit 使用 Spring 的测试运行器来运行测试。
@SpringBootTest: 这个注解会启动 Spring Boot 应用程序的完整上下文,包括所有的 beans。
@Before: 在每个测试方法执行前运行。
@Test: 标记一个方法为测试方法。
assertEquals, assertNotNull: JUnit 4 提供的断言方法。
@Test(expected = ...): 用于测试方法是否抛出预期的异常。
04.JUnit5:@ExtendWith(SpringExtension.class)
a.说明
Spring Boot 2.x 及更高版本默认使用 JUnit 5。因此,集成 JUnit 5 更加简单。
b.依赖
spring-boot-starter-test 已经包含了 JUnit 5 的所有必要依赖,所以你不需要做额外的配置。
-----------------------------------------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version> <!-- 请根据你的Spring Boot版本调整 -->
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
c.测试示例 (JUnit 5)
package cn.jggroup.device.service.impl;
import cn.jggroup.device.YdszDeviceApplication;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = YdszDeviceApplication.class)
@SuppressWarnings({"FieldCanBeLocal", "SpringJavaAutowiredMembersInspection"})
@ActiveProfiles("dev")
@Slf4j
public class CjsbgzmsServiceTest {
@Autowired
CjsbgzmsServiceImpl service;
@Test
public void testAnalyseTrends_MissingMonth_NoTrend() {
// 构造三个月数据,第二个月缺失"油脂劣化"
List<List<Map<String, Object>>> monthResults = new ArrayList<>();
// 第一个月
List<Map<String, Object>> month1 = new ArrayList<>();
month1.add(new HashMap<String, Object>() {{
put("bkReasonName", "油脂劣化");
put("totalBreakTimeRate", "3.00%");
}});
monthResults.add(month1);
// 第二个月(缺失"油脂劣化")
List<Map<String, Object>> month2 = new ArrayList<>();
month2.add(new HashMap<String, Object>() {{
put("bkReasonName", "油脂劣化");
put("totalBreakTimeRate", "4.00%");
}});
monthResults.add(month2);
// 第三个月
List<Map<String, Object>> month3 = new ArrayList<>();
month3.add(new HashMap<String, Object>() {{
put("bkReasonName", "油脂劣化");
put("totalBreakTimeRate", "5.00%");
}});
monthResults.add(month3);
// 调用分析方法
String result = service.analyseTrends(monthResults);
// 断言:不应输出递增趋势
System.out.println(result);
}
}
05.pom.xml依赖说明
a.starter
<!-- Spring Boot Test: 包含测试工具和框架,如 JUnit、Mockito,默认包括 slf4j
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.6.6</version>
<scope>compile</scope>
</dependency>
b.解决IDEA与junit版本不一致
<!--解决Failed to resolve org.junit.platform:junit-platform-launcher:1.x.x这样的问题,最主要的问题是IntelliJ IDEA版本和junit版本不适配问题-->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>1.8.2</version>
<scope>compile</scope>
</dependency>
c.starter对应的pom.xml
´´<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- This module was also published with a richer model, Gradle metadata, -->
<!-- which should be used instead. Do not delete the following line which -->
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
<!-- that they should prefer consuming it instead. -->
<!-- do_not_remove: published-with-gradle-metadata -->
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.6.6</version>
<name>spring-boot-starter-test</name>
<description>Starter for testing Spring Boot applications with libraries including JUnit Jupiter, Hamcrest and Mockito</description>
<url>https://spring.io/projects/spring-boot</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>https://spring.io</url>
</organization>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
</license>
</licenses>
<developers>
<developer>
<name>Pivotal</name>
<email>[email protected] </email>
<organization>Pivotal Software, Inc.</organization>
<organizationUrl>https://www.spring.io</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/spring-projects/spring-boot.git</connection>
<developerConnection>scm:git:ssh://[email protected] /spring-projects/spring-boot.git</developerConnection>
<url>https://github.com/spring-projects/spring-boot</url>
</scm>
<issueManagement>
<system>GitHub</system>
<url>https://github.com/spring-projects/spring-boot/issues</url>
</issueManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.6.6</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<version>2.6.6</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test-autoconfigure</artifactId>
<version>2.6.6</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.6.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>2.3.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.21.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.skyscreamer</groupId>
<artifactId>jsonassert</artifactId>
<version>1.5.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.18</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.18</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.xmlunit</groupId>
<artifactId>xmlunit-core</artifactId>
<version>2.8.4</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<artifactId>jaxb-api</artifactId>
<groupId>javax.xml.bind</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
d.由于pom.xml无法在IDEA的maven树依赖正确显示,故而直接将pom.xml内容拷贝至此,同时注释到spring-boot-starter-test
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.6.6</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<version>2.6.6</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test-autoconfigure</artifactId>
<version>2.6.6</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.6.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>2.3.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.21.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.skyscreamer</groupId>
<artifactId>jsonassert</artifactId>
<version>1.5.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.18</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.18</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.xmlunit</groupId>
<artifactId>xmlunit-core</artifactId>
<version>2.8.4</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<artifactId>jaxb-api</artifactId>
<groupId>javax.xml.bind</groupId>
</exclusion>
</exclusions>
</dependency>
1.13 数据恢复:binlog2sql,在线
00.binlog2sql适用于在线恢复误操作的数据,但不适用于以下情况
a.数据恢复建议控制在 50W 以内
数据量越大,逆向生成的语句越多,超过这个数值,恢复时间可能会超过 15 分钟
b.不支持DDL恢复操作
因为即使在 row 模式下,binlog对于 DDL 操作不会记录每行数据的变化
要实现 DDL 快速回滚,必须修改 MySQL 源码,使得在执行 DDL 前先备份老数据
阿里林晓斌团队提交了 patch 给 MySQL 官方,相关实现方案可以查阅 MySQL闪回方案讨论及实现
根据官方说法,在线召回数据推荐使用 binlog2sql 工具,离线解析使用 mysqlbinlog 工具
MySQL 闪回特性最早由阿里彭立勋开发,具体可以查阅 MySQL Flashback Feature
01.准备工作
a.安装 binlog2sql 工具
> git clone https://github.com/danfengcao/binlog2sql.git && cd binlog2sql
# > yum install python3-pip
# > whereis pip
# > pip3.6 install -r requirements.txt
> pip install -r requirements.txt
b.MySQL 服务端配置以下参数,注意 binlog2sql 仅支持 row 格式
[mysqld]
server_id = 1
log_bin = /var/log/mysql/mysql-bin.log
max_binlog_size = 1G
binlog_format = row
binlog_row_image = full
c.指定执行脚本的数据库用户授权
-- SELECT 权限:查询 information_schema.COLUMNS
-- REPLICATION SLAVE:通过 BINLOG_DUMP 协议获取 binlog 内容
-- REPLICATION CLIENT:执行 SHOW MASTER STATUS 获取 binlog 信息
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO user
d.准备一张用户表 user,并填充 1W 条数据
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) DEFAULT NULL,
`gmt_create` date DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4
DELIMITER $$
CREATE PROCEDURE InsertRandomData()
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE randomName CHAR(10);
DECLARE randomDate DATE;
WHILE i <= 10000 DO
-- 生成随机 name (随机字符串)
SET randomName = CONCAT(
CHAR(FLOOR(RAND() * 26) + 65),
CHAR(FLOOR(RAND() * 26) + 65),
CHAR(FLOOR(RAND() * 26) + 65),
CHAR(FLOOR(RAND() * 26) + 65),
CHAR(FLOOR(RAND() * 26) + 65)
);
-- 生成随机日期 (2013-11-11 起始,随机范围约为一年内)
SET randomDate = DATE_ADD('2023-01-01', INTERVAL FLOOR(RAND() * 365) DAY);
-- 插入数据
INSERT INTO `user` (`name`, `gmt_create`) VALUES (randomName, randomDate);
SET i = i + 1;
END WHILE;
END$$
DELIMITER ;
-- 调用存储过程
CALL InsertRandomData();
e.查看大于 11 月份的数据总数,共 363 条
mysql > SELECT count(*) FROM user WHERE gmt_create > '2023-11-01 00:00:00';
+----------+
| count(*) |
+----------+
| 363 |
+----------+
f.模拟误删除,假设在 15:30 左右删除了 11 月份之后的数据。
mysql > DELETE FROM user WHERE gmt_create > '2023-11-01 00:00:00';
02.恢复数据
a.查看主库 binlog 状态,最新的文件为 mysql-bin.000003。
-- 低版本使用 SHOW MASTER STATUS;
mysql > SHOW BINARY LOGS;
+------------------+-----------+-----------+
| Log_name | File_size | Encrypted |
+------------------+-----------+-----------+
| mysql-bin.000001 | 1871 | No |
| mysql-bin.000002 | 181 | No |
| mysql-bin.000003 | 917878 | No |
+------------------+-----------+-----------+
3 rows in set (0.04 sec)
b.筛选出需要回滚的SQL,误操作人一般知道大致的误操作时间,首先根据时间做一次过滤
shell> python binlog2sql/binlog2sql.py -h地址 -P端口 -u用户 -p'密码' -d库民 -t表名 --start-file='mysql-bin.000003' --start-datetime='2023-11-02 15:00:00' --stop-datetime='2023-11-02 16:00:00' > /tmp/raw.sql
raw.sql输出:
DELETE FROM `test`.`user` WHERE `gmt_create`='2023-11-01 00:00:00' AND `id`=1351 AND `name`='TPUDJ' LIMIT 1; #start 105311 end 262311 time 2023-11-02 15:31:10
DELETE FROM `test`.`user` WHERE `gmt_create`='2023-11-01 00:00:00' AND `id`=1352 AND `name`='YKIIS' LIMIT 1; #start 105311 end 262311 time 2023-11-02 15:31:10
...
DELETE FROM `test`.`user` WHERE `gmt_create`='2023-12-31 00:00:00' AND `id`=1714 AND `name`='SHKBC' LIMIT 1; #start 105311 end 265754 time 2023-11-02 15:31:10
c.根据 raw.sql 的位置信息,可以判断误操作的 SQL 来自同一个事务,准确位置在 105311-265754 之间,根据位置过滤,使用 -B 选项生成回滚 SQL
shell> python binlog2sql/binlog2sql.py -h地址 -P端口 -u用户 -p'密码' -d库民 -t表名 --start-file='mysql-bin.000003' --start-position=105311 --stop-position=265754 -B > /tmp/rollback.sql
rollback.sql输出:
INSERT INTO `test`.`user`(`gmt_create`, `id`, `name`) VALUES ('2023-11-01 00:00:00', 1351, 'TPUDJ'); #start 105311 end 262311 time 2023-11-02 15:31:10
INSERT INTO `test`.`user`(`gmt_create`, `id`, `name`) VALUES ('2023-11-01 00:00:00', 1352, 'YKIIS'); #start 105311 end 262311 time 2023-11-02 15:31:10
...
INSERT INTO `test`.`user`(`gmt_create`, `id`, `name`) VALUES ('2023-12-31 00:00:00', 1714, 'SHKBC'); #start 105311 end 265754 time 2023-11-02 15:31:10
03.结果验证
a.确认回滚 SQL 总行数是否对应误删除的 363 条
shell> wc -l /tmp/rollback.sql
363 /tmp/rollback.sql
b.与业务方确认回滚 SQL 没问题,执行回滚语句。登录 MySQL,确认回滚成功
shell> mysql -h地址 -P端口 -u用户 -p'密码' < /tmp/rollback.sql
mysql> SELECT count(*) FROM user WHERE gmt_create > '2023-11-01 00:00:00';
+----------+
| count(*) |
+----------+
| 363 |
+----------+
1.14 数据恢复:mysqlbinlog,离线
01.定义
使用 mysqlbinlog 工具可以从 MySQL 的二进制日志中提取和分析 SQL 语句,帮助恢复误删除的数据
02.准备工作
a.确保 MySQL 已开启二进制日志
在 MySQL 配置文件中设置 log_bin 参数,并确保 binlog_format 为 ROW 格式
b.确认二进制日志文件
使用 SHOW BINARY LOGS; 命令查看当前的二进制日志文件
02.恢复数据
a.查看二进制日志状态
首先,确认当前的二进制日志文件和位置:SHOW BINARY LOGS;
假设最新的文件为 mysql-bin.000003
b.提取误操作的 SQL
使用 mysqlbinlog 提取指定时间段内的 SQL 语句:
mysqlbinlog --no-defaults --start-datetime="2023-11-02 15:00:00" --stop-datetime="2023-11-02 16:00:00" /var/log/mysql/mysql-bin.000003 > /tmp/raw.sql
这将提取在指定时间段内的所有 SQL 语句,并保存到 /tmp/raw.sql 文件中
c.分析并生成回滚 SQL
手动查看 /tmp/raw.sql 文件,找到误删除的 SQL 语句
假设这些语句在事务中,可以通过事务的起始和结束位置来确定
d.使用位置过滤生成回滚 SQL
假设误操作的 SQL 在位置 105311 到 265754 之间,使用 mysqlbinlog 提取这些位置的 SQL:
mysqlbinlog --no-defaults --start-position=105311 --stop-position=265754 /var/log/mysql/mysql-bin.000003 > /tmp/rollback.sql
手动编辑 /tmp/rollback.sql 文件,将 DELETE 语句转换为 INSERT 语句以恢复数据
e.执行回滚 SQL
在确认回滚 SQL 没有问题后,执行回滚:
mysql -h地址 -P端口 -u用户 -p'密码' < /tmp/rollback.sql
f.验证结果
验证数据是否已成功恢复:
SELECT count(*) FROM user WHERE gmt_create > '2023-11-01 00:00:00';
确保返回的行数与误删除前一致
03.注意事项
a.备份数据
在执行任何恢复操作之前,确保已经备份了当前数据
b.验证 SQL
在执行回滚 SQL 之前,仔细检查生成的 SQL 语句,确保其准确性
c.测试环境
在生产环境执行之前,建议在测试环境中验证整个恢复过程
1.15 请求合并:buffer-trigger
01.问题
a.背景
假设我们3个用户(用户id分别是1、2、3),
现在他们都要查询自己的基本信息,请求到服务器,服务器端请求数据库,发出3次请求。
我们都知道数据库连接资源是相当宝贵的,那么我们怎么尽可能节省连接资源呢?
b.接口请求合并
我们在服务器端把请求合并,只发出一条SQL查询数据库
数据库返回后,服务器端处理返回数据,根据一个唯一请求ID,把数据分组,返回给对应用户
c.实现手段
LinkedBlockQueue:阻塞队列
ScheduledThreadPoolExecutor:定时任务线程池
CompleteableFuture future:阻塞机制(JDK8的CompletableFuture并没有timeout机制,后面优化,使用了队列替代)
02.实现1:硬编码
a.查询用户的代码
public interface UserService {
Map<String, Users> queryUserByIdBatch(List<UserWrapBatchService.Request> userReqs);
}
@Service
public class UserServiceImpl implements UserService {
@Resource
private UsersMapper usersMapper;
@Override
public Map<String, Users> queryUserByIdBatch(List<UserWrapBatchService.Request> userReqs) {
// 全部参数
List<Long> userIds = userReqs.stream().map(UserWrapBatchService.Request::getUserId).collect(Collectors.toList());
QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
// 用in语句合并成一条SQL,避免多次请求数据库的IO
queryWrapper.in("id", userIds);
List<Users> users = usersMapper.selectList(queryWrapper);
Map<Long, List<Users>> userGroup = users.stream().collect(Collectors.groupingBy(Users::getId));
HashMap<String, Users> result = new HashMap<>();
userReqs.forEach(val -> {
List<Users> usersList = userGroup.get(val.getUserId());
if (!CollectionUtils.isEmpty(usersList)) {
result.put(val.getRequestId(), usersList.get(0));
} else {
// 表示没数据
result.put(val.getRequestId(), null);
}
});
return result;
}
}
b.合并请求的实现
package com.springboot.sample.service.impl;
import com.springboot.sample.bean.Users;
import com.springboot.sample.service.UserService;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.*;
/***
* zzq
* 包装成批量执行的地方
* */
@Service
public class UserWrapBatchService {
@Resource
private UserService userService;
/**
* 最大任务数
**/
public static int MAX_TASK_NUM = 100;
/**
* 请求类,code为查询的共同特征,例如查询商品,通过不同id的来区分
* CompletableFuture将处理结果返回
*/
public class Request {
// 请求id 唯一
String requestId;
// 参数
Long userId;
//TODO Java 8 的 CompletableFuture 并没有 timeout 机制
CompletableFuture<Users> completableFuture;
public String getRequestId() {
return requestId;
}
public void setRequestId(String requestId) {
this.requestId = requestId;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public CompletableFuture getCompletableFuture() {
return completableFuture;
}
public void setCompletableFuture(CompletableFuture completableFuture) {
this.completableFuture = completableFuture;
}
}
/*
LinkedBlockingQueue是一个阻塞的队列,内部采用链表的结果,通过两个ReenTrantLock来保证线程安全
LinkedBlockingQueue与ArrayBlockingQueue的区别
ArrayBlockingQueue默认指定了长度,而LinkedBlockingQueue的默认长度是Integer.MAX_VALUE,也就是无界队列,在移除的速度小于添加的速度时,容易造成OOM。
ArrayBlockingQueue的存储容器是数组,而LinkedBlockingQueue是存储容器是链表
两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,
而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,
也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
*/
private final Queue<Request> queue = new LinkedBlockingQueue();
@PostConstruct
public void init() {
//定时任务线程池,创建一个支持定时、周期性或延时任务的限定线程数目(这里传入的是1)的线程池
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleAtFixedRate(() -> {
int size = queue.size();
//如果队列没数据,表示这段时间没有请求,直接返回
if (size == 0) {
return;
}
List<Request> list = new ArrayList<>();
System.out.println("合并了 [" + size + "] 个请求");
//将队列的请求消费到一个集合保存
for (int i = 0; i < size; i++) {
// 后面的SQL语句是有长度限制的,所以还要做限制每次批量的数量,超过最大任务数,等下次执行
if (i < MAX_TASK_NUM) {
list.add(queue.poll());
}
}
//拿到我们需要去数据库查询的特征,保存为集合
List<Request> userReqs = new ArrayList<>();
for (Request request : list) {
userReqs.add(request);
}
//将参数传入service处理, 这里是本地服务,也可以把userService 看成RPC之类的远程调用
Map<String, Users> response = userService.queryUserByIdBatch(userReqs);
//将处理结果返回各自的请求
for (Request request : list) {
Users result = response.get(request.requestId);
request.completableFuture.complete(result); //completableFuture.complete方法完成赋值,这一步执行完毕,下面future.get()阻塞的请求可以继续执行了
}
}, 100, 10, TimeUnit.MILLISECONDS);
//scheduleAtFixedRate是周期性执行 schedule是延迟执行 initialDelay是初始延迟 period是周期间隔 后面是单位
//这里我写的是 初始化后100毫秒后执行,周期性执行10毫秒执行一次
}
public Users queryUser(Long userId) {
Request request = new Request();
// 这里用UUID做请求id
request.requestId = UUID.randomUUID().toString().replace("-", "");
request.userId = userId;
CompletableFuture<Users> future = new CompletableFuture<>();
request.completableFuture = future;
//将对象传入队列
queue.offer(request);
//如果这时候没完成赋值,那么就会阻塞,直到能够拿到值
try {
return future.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return null;
}
}
c.控制层调用
/***
* 请求合并
* */
@RequestMapping("/merge")
public Callable<Users> merge(Long userId) {
return new Callable<Users>() {
@Override
public Users call() throws Exception {
return userBatchService.queryUser(userId);
}
};
}
d.模拟高并发查询的代码
package com.springboot.sample;
import org.springframework.web.client.RestTemplate;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
public class TestBatch {
private static int threadCount = 30;
private final static CountDownLatch COUNT_DOWN_LATCH = new CountDownLatch(threadCount); //为保证30个线程同时并发运行
private static final RestTemplate restTemplate = new RestTemplate();
public static void main(String[] args) {
for (int i = 0; i < threadCount; i++) {//循环开30个线程
new Thread(new Runnable() {
public void run() {
COUNT_DOWN_LATCH.countDown();//每次减一
try {
COUNT_DOWN_LATCH.await(); //此处等待状态,为了让30个线程同时进行
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int j = 1; j <= 3; j++) {
int param = new Random().nextInt(4);
if (param <=0){
param++;
}
String responseBody = restTemplate.getForObject("http://localhost:8080/asyncAndMerge/merge?userId=" + param, String.class);
System.out.println(Thread.currentThread().getName() + "参数 " + param + " 返回值 " + responseBody);
}
}
}).start();
}
}
}
e.使用队列的超时解决Jdk8的CompletableFuture并没有timeout机制
package com.springboot.sample.service.impl;
import com.springboot.sample.bean.Users;
import com.springboot.sample.service.UserService;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.*;
/***
* 包装成批量执行的地方,使用queue解决超时问题
* */
@Service
public class UserWrapBatchQueueService {
@Resource
private UserService userService;
/**
* 最大任务数
**/
public static int MAX_TASK_NUM = 100;
/**
* 请求类,code为查询的共同特征,例如查询商品,通过不同id的来区分
* CompletableFuture将处理结果返回
*/
public class Request {
// 请求id
String requestId;
// 参数
Long userId;
// 队列,这个有超时机制
LinkedBlockingQueue<Users> usersQueue;
public String getRequestId() {
return requestId;
}
public void setRequestId(String requestId) {
this.requestId = requestId;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public LinkedBlockingQueue<Users> getUsersQueue() {
return usersQueue;
}
public void setUsersQueue(LinkedBlockingQueue<Users> usersQueue) {
this.usersQueue = usersQueue;
}
}
/*
LinkedBlockingQueue是一个阻塞的队列,内部采用链表的结果,通过两个ReenTrantLock来保证线程安全
LinkedBlockingQueue与ArrayBlockingQueue的区别
ArrayBlockingQueue默认指定了长度,而LinkedBlockingQueue的默认长度是Integer.MAX_VALUE,也就是无界队列,在移除的速度小于添加的速度时,容易造成OOM。
ArrayBlockingQueue的存储容器是数组,而LinkedBlockingQueue是存储容器是链表
两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,
而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,
也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
*/
private final Queue<Request> queue = new LinkedBlockingQueue();
@PostConstruct
public void init() {
//定时任务线程池,创建一个支持定时、周期性或延时任务的限定线程数目(这里传入的是1)的线程池
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleAtFixedRate(() -> {
int size = queue.size();
//如果队列没数据,表示这段时间没有请求,直接返回
if (size == 0) {
return;
}
List<Request> list = new ArrayList<>();
System.out.println("合并了 [" + size + "] 个请求");
//将队列的请求消费到一个集合保存
for (int i = 0; i < size; i++) {
// 后面的SQL语句是有长度限制的,所以还要做限制每次批量的数量,超过最大任务数,等下次执行
if (i < MAX_TASK_NUM) {
list.add(queue.poll());
}
}
//拿到我们需要去数据库查询的特征,保存为集合
List<Request> userReqs = new ArrayList<>();
for (Request request : list) {
userReqs.add(request);
}
//将参数传入service处理, 这里是本地服务,也可以把userService 看成RPC之类的远程调用
Map<String, Users> response = userService.queryUserByIdBatchQueue(userReqs);
for (Request userReq : userReqs) {
// 这里再把结果放到队列里
Users users = response.get(userReq.getRequestId());
userReq.usersQueue.offer(users);
}
}, 100, 10, TimeUnit.MILLISECONDS);
//scheduleAtFixedRate是周期性执行 schedule是延迟执行 initialDelay是初始延迟 period是周期间隔 后面是单位
//这里我写的是 初始化后100毫秒后执行,周期性执行10毫秒执行一次
}
public Users queryUser(Long userId) {
Request request = new Request();
// 这里用UUID做请求id
request.requestId = UUID.randomUUID().toString().replace("-", "");
request.userId = userId;
LinkedBlockingQueue<Users> usersQueue = new LinkedBlockingQueue<>();
request.usersQueue = usersQueue;
//将对象传入队列
queue.offer(request);
//取出元素时,如果队列为空,给定阻塞多少毫秒再队列取值,这里是3秒
try {
return usersQueue.poll(3000,TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
}
@Override
public Map<String, Users> queryUserByIdBatchQueue(List<UserWrapBatchQueueService.Request> userReqs) {
// 全部参数
List<Long> userIds = userReqs.stream().map(UserWrapBatchQueueService.Request::getUserId).collect(Collectors.toList());
QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
// 用in语句合并成一条SQL,避免多次请求数据库的IO
queryWrapper.in("id", userIds);
List<Users> users = usersMapper.selectList(queryWrapper);
Map<Long, List<Users>> userGroup = users.stream().collect(Collectors.groupingBy(Users::getId));
HashMap<String, Users> result = new HashMap<>();
// 数据分组
userReqs.forEach(val -> {
List<Users> usersList = userGroup.get(val.getUserId());
if (!CollectionUtils.isEmpty(usersList)) {
result.put(val.getRequestId(), usersList.get(0));
} else {
// 表示没数据 , 这里要new,不然加入队列会空指针
result.put(val.getRequestId(), new Users());
}
});
return result;
}
03.实现2:buffer-trigger
a.依赖
<dependency>
<groupId>com.github.phantomthief</groupId>
<artifactId>buffer-trigger</artifactId>
<version>0.2.9</version>
</dependency>
b.案例一:使用SimpleBufferTrigger
a.代码
/**
* 通用实现,适合大多数业务场景
* 消费触发策略会考虑消费回调函数的执行时间,实际执行间隔 = 理论执行间隔 - 消费回调函数执行时间;
* 如回调函数执行时间已超过理论执行间隔,将立即执行下一次消费任务.
*/
public class BufferTriggerDemo {
BufferTrigger<Long> bufferTrigger = BufferTrigger.<Long, Map<Long, AtomicInteger>> simple()
.maxBufferCount(10)
.interval(4, TimeUnit.SECONDS)
.setContainer(ConcurrentHashMap::new, (map, uid) -> {
map.computeIfAbsent(uid, key -> new AtomicInteger()).addAndGet(1);
return true;
})
.consumer(this::consumer)
.build();
public void consumer(Map<Long, AtomicInteger> map) {
System.out.println(map);
}
public void test() throws InterruptedException {
// 进程退出时手动消费一次
Runtime.getRuntime().addShutdownHook(new Thread(() -> bufferTrigger.manuallyDoTrigger()));
// 最大容量是10,这里尝试添加11个元素0-10
for (int i = 0; i < 5; i ++) {
for (long j = 0; j < 11; j ++) {
bufferTrigger.enqueue(j);
}
}
Thread.sleep(7000);
}
b.说明
maxBuffeCount(long count)
指定容器最大容量,比如这里指定了10,当在下次聚合前容器元素数量达到10就无法添加了,-1表示无限制
-------------------------------------------------------------------------------------------------
internal(longinterval, TimeUnit unit)
表示多久聚合一次,如果没达到时间那么consumer是不会输出的,聚合后容器就空了
-------------------------------------------------------------------------------------------------
consumer(ThrowableConsumer<? super C, Throwable> consumer)
表示如何消费聚合后的数据,标识我们如何去消费聚合后的数据,我这里就是简单打印,enqueue(E element): 添加元素
-------------------------------------------------------------------------------------------------
setContainer(Supplier<? extends C> factory, BiPredicate<? super C, ? super E> queueAdder)
第一个变量为factory,是个Supplier,获取容器用的,要求线程安全;第二个变量是缓存更新的方法BiPredicate<?
-------------------------------------------------------------------------------------------------
manuallyDoTrigger
主动触发一次消费,通常在java进程关闭的时候调用
c.案例2:使用BatchConsumeBlockingQueueTrigger
a.代码
/**
* 基于阻塞队列的批量消费触发器实现
* 该触发器适合生产者-消费者场景,缓存容器基于{@link LinkedBlockingQueue}队列实现
* 触发策略类似Kafka linger,批处理阈值与延迟等待时间满足其一即触发消费回调
*/
public class BufferTriggerDemo2 {
BufferTrigger<Long> bufferTrigger = BufferTrigger.<Long>batchBlocking()
.bufferSize(50)
.batchSize(10)
.linger(Duration.ofSeconds(1))
.setConsumerEx(this::consume)
.build();
private void consume(List<Long> nums) {
System.out.println(nums);
}
public void test() throws InterruptedException {
// 进程退出时手动消费一次
Runtime.getRuntime().addShutdownHook(new Thread(() -> bufferTrigger.manuallyDoTrigger()));
for (long j = 0; j < 60; j ++) {
bufferTrigger.enqueue(j);
}
Thread.sleep(7000);
}
b.说明
batchBlocking()
提供自带背压(back-pressure)的简单批量归并消费能力
-------------------------------------------------------------------------------------------------
bufferSize(intbufferSize)
缓存队列的最大容量
-------------------------------------------------------------------------------------------------
batchSize(int size):
批处理元素的数量阈值,达到这个数量后也会进行消费
-------------------------------------------------------------------------------------------------
linger(Duration duration)
多久消费一次
-------------------------------------------------------------------------------------------------
setConsumerEx(ThrowableConsumer<? super List, Exception> consumer):
消费函数,注入的对象为缓存队列中尚存的所有元素,非逐个元素消费
04.实现3:buffer-trigger整合到springboot
a.依赖
<dependency>
<groupId>com.github.phantomthief</groupId>
<artifactId>buffer-trigger</artifactId>
<version>${buffer.trigger.version}</version>
</dependency>
b.封装请求参数类
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DataExchange<T,R> {
private String bizNo;
private T request;
private CompletableFuture<Result<R>> response;
}
c.封装buffer-trigger处理类
@RequiredArgsConstructor
public class DelegateBatchConsumerTriggerHandler<T, R> implements BatchConsumerTriggerHandler<T, R>{
private final BufferTrigger<DataExchange<T, R>> bufferTrigger;
@SneakyThrows
@Override
public Result<R> handle(T request, String bizNo) {
DataExchange dataExchange = new DataExchange<>();
dataExchange.setBizNo(bizNo);
dataExchange.setRequest(request);
CompletableFuture<Result> response = new CompletableFuture<>();
dataExchange.setResponse(response);
bufferTrigger.enqueue(dataExchange);
return response.get();
}
@Override
public void closeBufferTrigger() {
// 触发该事件,关闭BufferTrigger,并将未消费的数据消费
if(bufferTrigger != null){
bufferTrigger.close();
}
}
}
d.封装buffer-trigger创建工厂
public interface BatchConsumerTriggerFactory {
default <T,R> BatchConsumerTriggerBuilder<DataExchange<T,R>> builder(){
return null;
}
default <T,R> BufferTrigger<DataExchange<T,R>> getTrigger(ThrowableConsumer<List<DataExchange<T,R>>, Exception> consumer, String bufferTriggerBizType){
if(!support(bufferTriggerBizType)){
return null;
}
return builder().setConsumerEx(consumer).build();
}
boolean support(String bufferTriggerBizType);
default <T,R> BatchConsumerTriggerHandler<T,R> getTriggerHandler(ThrowableConsumer<List<DataExchange<T,R>>, Exception> consumer, String bufferTriggerBizType){
BufferTrigger<DataExchange<T, R>> trigger = getTrigger(consumer, bufferTriggerBizType);
return new DelegateBatchConsumerTriggerHandler<>(trigger);
}
}
e.模拟用户注册dao
@Repository
public class UserDao {
private final Map<Long, User> userMap = new ConcurrentHashMap<>();
private final ThreadLocalRandom random = ThreadLocalRandom.current();
private final LongAdder idAdder = new LongAdder();
public User register(UserDTO userDTO){
mockExecuteCostTime();
return getUser(userDTO);
}
public List<User> batchRegister(List<UserDTO> userDTOs){
mockExecuteCostTime();
List<User> users = new ArrayList<>();
userDTOs.forEach(userDTO -> users.add(getUser(userDTO)));
return users;
}
f.模拟用户注册service
a.常规方式
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserDao userDao;
private final LongAdder count = new LongAdder();
@Override
public Result<User> register(UserDTO user) {
count.increment();
System.out.println("执行次数:" + count.sum());
return Result.success(userDao.register(user));
}
}
b.请求聚合方式
a.yml指定相关队列、定时器配置以及业务类别
lybgeek:
buffer:
trigger:
consume-queue-trigger-properties:
- bufferTriggerBizType: userReisgeter
config:
batchSize: 100
bufferSize: 1000
batchConsumeIntervalMills: 1000
b.代码
@Service
@RequiredArgsConstructor
public class UserServiceBufferTriggerImpl implements UserService, InitializingBean, DisposableBean {
public static final String BUFFER_TRIGGER_BIZ_TYPE = "userReisgeter";
private final UserDao userDao;
private final BatchConsumerTriggerFactory batchConsumerTriggerFactory;
private BatchConsumerTriggerHandler<UserDTO,User> batchConsumerTriggerHandler;
private final LongAdder count = new LongAdder();
@SneakyThrows
@Override
public Result<User> register(UserDTO user) {
return batchConsumerTriggerHandler.handle(user,BUFFER_TRIGGER_BIZ_TYPE + "-" + UUID.randomUUID());
}
@Override
public void afterPropertiesSet() throws Exception {
// key为业务属性唯一键,如果不存在业务属性唯一键,则可以取bizNo作为key,示例以username作为唯一键
Map<String, CompletableFuture<Result<User>>> completableFutureMap = new HashMap<>();
batchConsumerTriggerHandler = batchConsumerTriggerFactory.getTriggerHandler((ThrowableConsumer<List<DataExchange<UserDTO, User>>, Exception>) dataExchanges -> {
List<UserDTO> userDTOs = new ArrayList<>();
for (DataExchange<UserDTO, User> dataExchange : dataExchanges) {
UserDTO userDTO = dataExchange.getRequest();
completableFutureMap.put(userDTO.getUsername(),dataExchange.getResponse());
userDTOs.add(userDTO);
}
count.increment();
System.out.println("执行次数:" + count.sum());
List<User> users = userDao.batchRegister(userDTOs);
if(CollectionUtil.isNotEmpty(users)){
for (User user : users) {
CompletableFuture<Result<User>> completableFuture = completableFutureMap.remove(user.getUsername());
if(completableFuture != null){
completableFuture.complete(Result.success(user));
}
}
}
},BUFFER_TRIGGER_BIZ_TYPE);
}
@Override
public void destroy() throws Exception {
// 触发该事件,关闭BufferTrigger,并将未消费的数据消费
batchConsumerTriggerHandler.closeBufferTrigger();
}
}
h.分别开启20个线程,对常规方式以及聚合方式的service进行测试
a.常规方式
@Test
public void testRegisterUserByCommon() throws IOException {
new ConcurrentCall(20).run(()->{
UserDTO user = UserUtil.generateUser();
return userServiceImpl.register(user);
});
}
-------------------------------------------------------------------------------------------------
执行次数:1
执行次数:2
执行次数:7
执行次数:6
执行次数:10
执行次数:9
执行次数:5
执行次数:4
执行次数:11
执行次数:12
执行次数:3
执行次数:8
执行次数:17
执行次数:16
执行次数:15
执行次数:18
执行次数:14
执行次数:20
执行次数:13
执行次数:19
Result(code=200, msg=success, data=User(id=1, username=yangweize, fullname=杨伟泽, age=12, [email protected] , mobile=64294835455))
Result(code=200, msg=success, data=User(id=3, username=yaojinpeng, fullname=姚晋鹏, age=13, [email protected] , mobile=5381-03836251))
Result(code=200, msg=success, data=User(id=9, username=pengxiaoran, fullname=彭潇然, age=25, [email protected] , mobile=903-85787160))
Result(code=200, msg=success, data=User(id=9, username=guoweize, fullname=郭伟泽, age=9, [email protected] , mobile=57105382845))
Result(code=200, msg=success, data=User(id=8, username=huangjinyu, fullname=黄瑾瑜, age=29, [email protected] , mobile=449-27085386))
Result(code=200, msg=success, data=User(id=6, username=renkairui, fullname=任楷瑞, age=3, [email protected] , mobile=2777-67842072))
Result(code=200, msg=success, data=User(id=2, username=fuhaoran, fullname=傅昊然, age=15, [email protected] , mobile=332-47390793))
Result(code=200, msg=success, data=User(id=5, username=linmingxuan, fullname=林明轩, age=27, [email protected] , mobile=116-31209336))
Result(code=200, msg=success, data=User(id=5, username=shensicong, fullname=沈思聪, age=6, [email protected] , mobile=0532-05033168))
Result(code=200, msg=success, data=User(id=11, username=gongtianyu, fullname=龚天宇, age=4, [email protected] , mobile=9752-26976731))
Result(code=200, msg=success, data=User(id=13, username=xiongminghui, fullname=熊明辉, age=23, [email protected] , mobile=0049-21709250))
Result(code=200, msg=success, data=User(id=17, username=huzhize, fullname=胡志泽, age=0, [email protected] , mobile=760-85426527))
Result(code=200, msg=success, data=User(id=16, username=gaosiyuan, fullname=高思源, age=5, [email protected] , mobile=42452304656))
Result(code=200, msg=success, data=User(id=13, username=mojiaxi, fullname=莫嘉熙, age=2, [email protected] , mobile=7264-82263592))
Result(code=200, msg=success, data=User(id=18, username=caizimo, fullname=蔡子默, age=12, [email protected] , mobile=2653-82403850))
Result(code=200, msg=success, data=User(id=10, username=wancongjian, fullname=万聪健, age=10, [email protected] , mobile=954-37654583))
Result(code=200, msg=success, data=User(id=14, username=gongyuebin, fullname=龚越彬, age=0, [email protected] , mobile=77884047173))
Result(code=200, msg=success, data=User(id=15, username=fenghongtao, fullname=冯鸿涛, age=2, [email protected] , mobile=8832-09658213))
Result(code=200, msg=success, data=User(id=19, username=jiangyuanbo, fullname=江苑博, age=12, [email protected] , mobile=2132-90700641))
Result(code=200, msg=success, data=User(id=20, username=xiaoxinlei, fullname=萧鑫磊, age=13, [email protected] , mobile=02196775183))
b.聚合请求方式
@Test
public void testRegisterUserByBufferTrigger() throws IOException {
new ConcurrentCall(20).run(()->{
UserDTO user = UserUtil.generateUser();
return userServiceBufferTriggerImpl.register(user);
});
}
-------------------------------------------------------------------------------------------------
执行次数:1
Result(code=200, msg=success, data=User(id=1, username=heguo, fullname=何果, age=10, [email protected] , mobile=5725-06130005))
Result(code=200, msg=success, data=User(id=7, username=houwen, fullname=侯文, age=9, [email protected] , mobile=85830365362))
Result(code=200, msg=success, data=User(id=11, username=yangxiaoyu, fullname=杨笑愚, age=5, [email protected] , mobile=13776594491))
Result(code=200, msg=success, data=User(id=3, username=yusimiao, fullname=余思淼, age=5, [email protected] , mobile=070-18231344))
Result(code=200, msg=success, data=User(id=12, username=haotianyu, fullname=郝天宇, age=10, [email protected] , mobile=42693432247))
Result(code=200, msg=success, data=User(id=14, username=wangxinpeng, fullname=汪鑫鹏, age=1, [email protected] , mobile=59660609063))
Result(code=200, msg=success, data=User(id=15, username=tanzhichen, fullname=覃智宸, age=25, [email protected] , mobile=075-00624335))
Result(code=200, msg=success, data=User(id=4, username=lu:haoxuan, fullname=吕皓轩, age=14, email=lu:[email protected] , mobile=9548-30583153))
Result(code=200, msg=success, data=User(id=2, username=qiuyinxiang, fullname=邱胤祥, age=18, [email protected] , mobile=04148786960))
Result(code=200, msg=success, data=User(id=5, username=weiweicheng, fullname=魏伟诚, age=25, [email protected] , mobile=0960-77489940))
Result(code=200, msg=success, data=User(id=20, username=tanbin, fullname=谭彬, age=27, [email protected] , mobile=297-57401738))
Result(code=200, msg=success, data=User(id=18, username=husiyuan, fullname=胡思远, age=24, [email protected] , mobile=0809-08658163))
Result(code=200, msg=success, data=User(id=16, username=shishengrui, fullname=石晟睿, age=26, [email protected] , mobile=8205-70004359))
Result(code=200, msg=success, data=User(id=17, username=lu:zihan, fullname=吕子涵, age=0, email=lu:[email protected] , mobile=162-35081974))
Result(code=200, msg=success, data=User(id=19, username=xionghaoran, fullname=熊昊然, age=19, [email protected] , mobile=588-09693393))
Result(code=200, msg=success, data=User(id=13, username=jiangyuebin, fullname=姜越彬, age=19, [email protected] , mobile=472-74492380))
Result(code=200, msg=success, data=User(id=8, username=haoweicheng, fullname=郝伟诚, age=26, [email protected] , mobile=73205366322))
Result(code=200, msg=success, data=User(id=10, username=tanhongxuan, fullname=谭鸿煊, age=18, [email protected] , mobile=78536254981))
Result(code=200, msg=success, data=User(id=9, username=xielicheng, fullname=谢立诚, age=18, [email protected] , mobile=4364-05053591))
Result(code=200, msg=success, data=User(id=6, username=weiluyang, fullname=韦鹭洋, age=28, [email protected] , mobile=92876761170))
2 应用2
2.1 编码:6种
01.字符编码概述
a.定义
字符编码是计算机中用于将字符(如字母、数字、符号)转换为机器可读的数字的方式。
b.常见字符编码标准
a.ASCII
一个7位字符编码标准,用于表示英语字符。
b.UTF-8
一种变长的 Unicode 编码,能够表示世界上所有的字符,包括中文、日文、阿拉伯文等。
c.GBK
一个中文字符集,是 GB2312 的扩展,主要用于中文简体字的表示。
c.乱码问题
在开发中,中文乱码通常是由于不同字符编码间的转换不一致引起的。当文件或数据在不同编码格式间传输时,如果没有正确处理编码格式,就会导致乱码。
02.常见中文乱码情况
a.网页中文乱码
a.问题描述
当浏览器显示网页时,中文显示为乱码,尤其是含有中文字符的网页。
b.原因
网页未正确指定字符集,浏览器默认使用其他编码(如 ISO-8859-1)。
网页文件的编码格式和服务器响应头指定的编码格式不一致。
c.解决方案
在 HTML 页面的 <head> 标签中添加正确的字符集声明:
<meta charset="UTF-8">
-------------------------------------------------------------------------------------------------
确保 Web 服务器(如 Apache、Nginx)或应用服务器(如 Tomcat)正确设置了 Content-Type 和字符编码。
Content-Type: text/html; charset=UTF-8
b.控制台中文乱码
a.问题描述
在命令行或控制台中显示中文字符时,输出为乱码。
b.原因
控制台字符编码与程序输出的字符编码不一致。例如,程序使用 UTF-8 输出中文,而控制台使用 GBK 或其他编码。
c.解决方案
设置控制台编码为 UTF-8。
在 Linux 上,可以设置环境变量:
export LANG=en_US.UTF-8
-------------------------------------------------------------------------------------------------
在 Windows 控制台中,可以使用 chcp 命令将编码设置为 UTF-8:
chcp 65001
c.数据库中文乱码
a.问题描述
从数据库查询数据时,中文字符显示为乱码。
b.原因
数据库和数据库连接使用不同的字符编码。例如,数据库表使用 UTF-8 编码,而连接时使用了 ISO-8859-1 或 GBK。
c.解决方案
确保数据库、表和连接都使用 UTF-8 编码。
在数据库连接时明确指定字符编码:
对于 MySQL:
jdbc:mysql://localhost:3306/db_name?useUnicode=true&characterEncoding=UTF-8
-------------------------------------------------------------------------------------------------
在创建数据库时指定字符集:
CREATE DATABASE db_name CHARACTER SET utf8 COLLATE utf8_general_ci;
d.文件中文乱码
a.问题描述
当读取文件时,文件中的中文字符显示为乱码。
b.原因
文件的编码格式与读取时使用的编码格式不一致。比如文件使用 UTF-8 编码保存,而读取时用 GBK 编码解析。
c.解决方案
在读取文件时显式指定文件编码。例如,在 Java 中:
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("file.txt"), "UTF-8"));
e.JSON 或 XML 中的中文乱码
a.问题描述
JSON 或 XML 格式的数据中,中文字符显示为乱码。
b.原因
在处理 JSON 或 XML 数据时,字符编码未正确设置,导致中文字符无法正确解析。
c.解决方案
确保在发送和接收 JSON 或 XML 数据时都使用正确的编码(推荐使用 UTF-8)。
确保 Content-Type 设置正确:
Content-Type: application/json; charset=UTF-8
f.容器云环境中的中文乱码
a.问题描述
在容器云环境中运行的应用程序处理中文时,中文字符显示为乱码或不正确的字符。
b.原因
容器的系统字符集配置与应用程序期望的字符集不一致。例如,容器的默认字符集为 POSIX 或 C,而应用程序使用 UTF-8 来处理中文文本。
容器镜像中的操作系统环境未配置为支持 UTF-8,导致容器内的应用程序无法正确解析和处理中文字符。
c.解决方案
查看当前系统字符集:首先查看容器内部的系统字符集设置,使用命令:
locale
-------------------------------------------------------------------------------------------------
如果显示的是 POSIX 或其他不支持中文的字符集,可能会导致乱码问题。
设置容器的字符集为 UTF-8:
在容器中修改环境变量,设置 LANG 和 LC_CTYPE 为 en_US.UTF-8:
export LANG=en_US.UTF-8
export LC_CTYPE=en_US.UTF-8
-------------------------------------------------------------------------------------------------
如果希望此设置在容器每次启动时生效,可以在容器镜像的启动脚本中加入上述设置,或者修改容器内的 /etc/locale.conf 或 /etc/environment 文件:
echo "LANG=en_US.UTF-8" >> /etc/environment
-------------------------------------------------------------------------------------------------
安装所需的区域设置包:
某些容器镜像可能没有安装所需的语言包,导致 UTF-8 无法正常使用。可以在容器内安装 locales 包:
对于 Debian/Ubuntu 基础镜像:
apt-get update
apt-get install locales
dpkg-reconfigure locales
-------------------------------------------------------------------------------------------------
对于 CentOS/RHEL 基础镜像:
yum install glibc-common
localedef -v -c -i en_US -f UTF-8 en_US.UTF-8
-------------------------------------------------------------------------------------------------
重启容器:
修改字符集设置后,重启容器以使新的配置生效:
docker restart <container_name>
-------------------------------------------------------------------------------------------------
检查容器内应用程序的字符集配置:
确保应用程序在处理中文文本时,使用的是 UTF-8 编码。例如,在 Java 中:
new String(bytes, "UTF-8");
03.解决乱码问题的关键点
a.统一编码格式
确保数据的传输、存储和处理过程中的编码格式一致。推荐使用 UTF-8 编码,因为它支持全球所有语言字符,并且与 ASCII 向后兼容。
在跨平台开发中,特别是在 Linux、Windows 和 macOS 等不同系统间传递数据时,确保一致的编码格式非常重要。
b.显式设置编码
在处理文本文件、数据库、Web 页面时,明确指定使用 UTF-8 编码,而不是依赖于默认编码。
对于数据库连接、HTTP 请求和响应等,务必设置编码,确保不同系统和服务间的编码一致。
c.避免操作系统默认编码的差异
不同操作系统可能有不同的默认编码,Linux 和 macOS 通常使用 UTF-8,而 Windows 默认使用 GBK 或 Cp1252。确保在跨平台开发时,显式设置字符编码。
d.浏览器和服务器的配合
确保网页中的字符集声明与服务器响应头中的编码一致。浏览器会根据页面的 <meta> 标签或响应头来确定使用的字符编码。
2.2 密码:3种
00.汇总
a.回答
哈希算法
加盐技术
密码哈希算法(如 bcrypt 和 PBKDF2)
b.允许用户重置密码而不是查看原始密码
确保密码安全存储的必要手段
01.为什么我们只能重置密码而不是找回原密码?
a.问题引入
当我们忘记一个账号的登录密码并点击“忘记密码”时,系统总是让我们创建一个新密码,而不是告诉我们原来的密码。这是因为有非常合理的安全考量。
b.直接存储明文密码的风险
a.假设场景
假设一个用户在你的网站上注册了一个账号,并设置了密码为 abc654321。最直接的方式是将用户的密码以明文形式存储在数据库中:
username password
[email protected] abc654321
b.安全隐患
如果黑客获取了你的数据库访问权限,他不仅能看到这个用户的密码,还能轻易猜到用户在其他网站上使用的相同账号和密码。存储明文密码几乎没有任何保障。
c.哈希算法:密码不可逆存储
a.哈希存储
开发人员通常会将密码转换为不可逆的哈希值,然后将哈希值存储在数据库中:
username password
[email protected] 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8
b.安全性
即使黑客获取了这个哈希值,他们也不能直接通过哈希值反推原始密码。
c.原因
保护用户隐私:如果系统能够恢复或查看原始密码,系统本身就会有用户密码的明文副本,增加密码泄露的风险。
防止数据泄露后滥用:即使黑客侵入数据库,获得了密码的哈希值,也无法通过这些哈希值反向计算出原始密码。
d.加盐哈希:防止彩虹表攻击
a.彩虹表攻击
彩虹表攻击是一种通过预先计算大量常见密码及其哈希值的方式,试图快速破解哈希密码的技术。
b.加盐防御
每个密码都有独立的随机盐,即使彩虹表中包含了相同的密码,也无法匹配到哈希值。
c.加盐示例
username salt password
[email protected] 2dc7fcc... sha256("2dc7fcc..." + password_1)
[email protected] afadb2f... sha256("afadb2f..." + password_2)
d.加盐的作用
加盐就是密码里的独特调味料,让黑客破解起来更费劲,让你的密码更安全。
e.更安全的密码哈希算法:bcrypt 和 PBKDF2
a.算法特点
bcrypt 和 PBKDF2 这样的算法专为密码存储设计,它们的计算速度比常规哈希算法要慢得多,从而增加破解难度。
b.bcrypt 示例
import bcrypt
def hash_password_bcrypt(password: str) -> str:
# 生成盐并哈希密码
salt = bcrypt.gensalt() # 自动生成盐
hashed_password = bcrypt.hashpw(password.encode(), salt)
return hashed_password
def check_password_bcrypt(password: str, hashed_password: str) -> bool:
# 验证密码
return bcrypt.checkpw(password.encode(), hashed_password)
# 示例
password = "abc654321"
hashed_password = hash_password_bcrypt(password)
print("bcrypt 哈希后的密码:", hashed_password)
# 验证密码
is_valid = check_password_bcrypt(password, hashed_password)
print("密码验证结果:", is_valid)
02.密码重置:安全性考量
a.不可逆存储
正因为密码是以不可逆的方式存储的,当用户忘记密码时,系统无法直接告诉用户原来的密码。
b.重置密码的方式
系统会通过重置密码的方式来确保安全性。通过发送验证码或其他身份验证方式,确保只有合法用户能够重置密码。
2.3 日志链路:traceId
00.目的
有时候一个业务调用链场景,很长,调了各种各样的方法,看日志的时候,各个接口的日志穿插,确实让人头大
根据 TraceId 关键字进入服务器查询日志中是否有这个 TraceId,这样就把同一次的业务调用链上的日志串起来了
01.API说明
clear() 移除所有 MDC
get (String key) 获取当前线程 MDC 中指定 key 的值
getContext() 获取当前线程 MDC 的 MDC
put(String key, Object o) 往当前线程的 MDC 中存入指定的键值对
remove(String key) 删除当前线程 MDC 中指定的键值对
02.实现步骤
a.pom.xml 依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!--lombok配置-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
</dependencies>
b.logback-spring.xml
关键代码:[traceId:%X{traceId}], traceId 是通过拦截器里 MDC.put(traceId, tid) 添加
-----------------------------------------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!--日志存储路径-->
<property name="log" value="D:/test/log" />
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--输出格式化-->
<pattern>[traceId:%X{traceId}] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 按天生成日志文件 -->
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件名-->
<FileNamePattern>${log}/%d{yyyy-MM-dd}.log</FileNamePattern>
<!--保留天数-->
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>[traceId:%X{traceId}] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
<!--日志文件最大的大小-->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>10MB</MaxFileSize>
</triggeringPolicy>
</appender>
<!-- 日志输出级别 -->
<root level="INFO">
<appender-ref ref="console" />
<appender-ref ref="file" />
</root>
</configuration>
c.application.yml
server:
port: 8826
logging:
config: classpath:logback-spring.xml
d.自定义日志拦截器 LogInterceptor.java
用途:每一次链路,线程维度,添加最终的链路 ID traceId
MDC(Mapped Diagnostic Context) 诊断上下文映射,是 @Slf4j 提供的一个支持动态打印日志信息的工具
-----------------------------------------------------------------------------------------------------
import org.slf4j.MDC;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
/**
* 日志拦截器
*/
public class LogInterceptor implements HandlerInterceptor {
private static final String traceId = "traceId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String tid = UUID.randomUUID().toString().replace("-", "");
//可以考虑让客户端传入链路ID,但需保证一定的复杂度唯一性;如果没使用默认UUID自动生成
if (!StringUtils.isEmpty(request.getHeader("traceId"))){
tid=request.getHeader("traceId");
}
MDC.put(traceId, tid);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) {
// 请求处理完成后,清除MDC中的traceId,以免造成内存泄漏
MDC.remove(traceId);
}
}
e.WebConfigurerAdapter.java 添加拦截器
这个拦截的部分改为使用自定义注解 + AOP 也是很灵活的
-----------------------------------------------------------------------------------------------------
import javax.annotation.Resource;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
@Configuration
public class WebConfigurerAdapter extends WebMvcConfigurationSupport {
@Resource
private LogInterceptor logInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor);
//可以具体制定哪些需要拦截,哪些不拦截,其实也可以使用自定义注解更灵活完成
// .addPathPatterns("/**")
// .excludePathPatterns("/testxx.html");
}
}
f.测试接口
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@Api(tags = "测试接口")
@RequestMapping("/test")
@Slf4j
public class TestController {
@RequestMapping(value = "/log", method = RequestMethod.GET)
@ApiOperation(value = "测试日志")
public String sign() {
log.info("这是一行info日志");
log.error("这是一行error日志");
return "success";
}
}
02.异步场景
a.说明
使用线程的场景,写一个异步线程,加入这个调用里面。再次执行看开效果,我们会发现显然子线程丢失了 traceId
所以我们需要针对子线程使用情形,做调整,思路:将父线程的 traceId 传递下去给子线程即可
b.ThreadMdcUtil.java
import org.slf4j.MDC;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
/**
* @Description:
*/
public final class ThreadMdcUtil {
private static final String traceId = "traceId";
// 获取唯一性标识
public static String generateTraceId() {
return UUID.randomUUID().toString().replace("-", "");
}
public static void setTraceIdIfAbsent() {
if (MDC.get(traceId) == null) {
MDC.put(traceId, generateTraceId());
}
}
/**
* 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
*
* @param callable
* @param context
* @param <T>
* @return
*/
public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try {
return callable.call();
} finally {
MDC.clear();
}
};
}
/**
* 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
*
* @param runnable
* @param context
* @return
*/
public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try {
runnable.run();
} finally {
MDC.clear();
}
};
}
}
c.MyThreadPoolTaskExecutor.java重写方法
import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
public final class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
public MyThreadPoolTaskExecutor() {
super();
}
@Override
public void execute(Runnable task) {
super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public <T> Future<T> submit(Callable<T> task) {
return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public Future<?> submit(Runnable task) {
return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
}
d.ThreadPoolConfig.java定义线程池
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import java.util.concurrent.Executor;
@EnableAsync
@Configuration
public class ThreadPoolConfig {
/**
* 声明一个线程池
*/
@Bean("taskExecutor")
public Executor taskExecutor() {
MyThreadPoolTaskExecutor executor = new MyThreadPoolTaskExecutor();
//核心线程数5:线程池创建时候初始化的线程数
executor.setCorePoolSize(5);
//最大线程数5:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
executor.setMaxPoolSize(5);
//缓冲队列500:用来缓冲执行任务的队列
executor.setQueueCapacity(500);
//允许线程的空闲时间60秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
executor.setKeepAliveSeconds(60);
//线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
executor.setThreadNamePrefix("taskExecutor-");
executor.initialize();
return executor;
}
}
e.Service
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
/**
* 测试Service
*/
@Service("testService")
@Slf4j
public class TestService {
/**
* 异步操作测试
*/
@Async("taskExecutor")
public void asyncTest() {
try {
log.info("模拟异步开始......");
Thread.sleep(3000);
log.info("模拟异步结束......");
} catch (InterruptedException e) {
log.error("异步操作出错:"+e);
}
}
}
f.测试接口
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@Api(tags = "测试接口")
@RequestMapping("/test")
@Slf4j
public class TestController {
@Resource
private TestService testService;
@RequestMapping(value = "/log", method = RequestMethod.GET)
@ApiOperation(value = "测试日志")
public String sign() {
log.info("这是一行info日志");
log.error("这是一行error日志");
//异步操作测试
testService.asyncTest();
return "success";
}
}
03.定时任务
a.说明
如果使用了定时任务 @Scheduled, 这时候执行定时任务,不会走上面的拦截器逻辑
所以定时任务需要单独创建个 AOP 切面
b.创建个定时任务线程池
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import java.util.concurrent.Executors;
/**
* 定时任务线程池
*/
@EnableScheduling
@Configuration
public class SeheduleConfig implements SchedulingConfigurer{
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));
}
}
c.创建 AOP 切面
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.MDC;
import org.springframework.context.annotation.Configuration;
import java.util.UUID;
@Aspect //定义一个切面
@Configuration
public class SeheduleTaskAspect {
// 定义定时任务切点Pointcut
@Pointcut("@annotation(org.springframework.scheduling.annotation.Scheduled)")
public void seheduleTask() {
}
@Around("seheduleTask()")
public void doAround(ProceedingJoinPoint joinPoint) throws Throwable {
try {
String traceId = UUID.randomUUID().toString().replace("-", "");
//用于日志链路追踪,logback配置:%X{traceId}
MDC.put("traceId", traceId);
//执行定时任务方法
joinPoint.proceed();
} finally {
//请求处理完成后,清除MDC中的traceId,以免造成内存泄漏
MDC.remove("traceId");
}
}
}
d.创建定时任务测试
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.Date;
@Service
public class SeheduleTasks {
private Logger logger = LoggerFactory.getLogger(SeheduleTasks.class);
/**
* 1分钟执行一次
*/
@Scheduled(cron = "0 0/1 * * * ?")
public void testTask() {
logger.info("执行定时任务>"+new Date());
}
}
e.说明
服务启动的时候 traceId 是空的,这是正常的,因为还没到拦截器这一层
2.4 数据追踪:TimeTracker
01.概况
a.以前
a.代码
long start = System.currentTimeMillis();
try {
// 业务逻辑
} finally {
// 计算耗时
}
b.说明
每次都得写这种重复又啰嗦的代码,要不就得复制粘贴,还容易漏掉,CV大法固然好,但懒人总想要更懒的方式
b.进化:拥抱 try-with-resources
a.代码
try (TimeTracker ignored = new TimeTracker("数据库操作")) {
// 业务代码,耗时自动搞定!
}
b.说明
瞬间,代码变得清爽多了!资源自动管理,耗时自动计算,福音嘛这不是
新建一个 TimeTracker类,实现 AutoCloseable,简单鼓捣一番,重点在于,在 close() 中计算耗时,实现全自动化。于是就有了第一版
c.进化:函数式接口
a.方式1
TimeTracker.track("用户查询", () -> {
return userService.findById(123);
});
b.方式2:变1行
TimeTracker.track("操作", () -> riskyMethod());
c.方式3:返回值
String result = TimeTracker.track("简单任务", () -> {
Thread.sleep(1000);
return"完成";
});
02.完整代码
a.工具类
/**
* 性能跟踪工具类,用于测量代码执行时间并提供灵活的异常处理机制。
*
* <p>主要特性:
* <ul>
* <li>精确测量代码执行时间</li>
* <li>支持带返回值和无返回值的方法跟踪</li>
* <li>提供两种异常处理模式</li>
* <li>支持自动资源管理</li>
* </ul>
*
* <h2>使用示例:</h2>
*
* <h3> try-with-resources 手动跟踪</h3>
* <pre>{@code
* // 手动管理资源和性能跟踪
* try (TimeTracker tracker = new TimeTracker("数据库操作")) {
* database.connect();
* database.executeQuery();
* } // 自动关闭,并打印执行时间
*
* // 带返回值的try-with-resources
* try (TimeTracker tracker = new TimeTracker("复杂计算");
* Resource resource = acquireResource()) {
* return performComplexCalculation(resource);
* }
* }</pre>
*
* <h3>结合静态方法的try-with-resources</h3>
* <pre>{@code
* try (TimeTracker ignored = TimeTracker.of("网络请求")) {
* httpClient.sendRequest();
* httpClient.receiveResponse();
* }
* }</pre>
*
* <p>注意:使用try-with-resources可以确保资源正确关闭,
* 并自动记录执行时间。</p>
*
* <h3>lambda自动处理异常</h3>
* <pre>{@code
* // 无返回值方法
* TimeTracker.track("数据处理", () -> {
* processData(); // 可能抛出异常的方法
* });
*
* // 有返回值方法
* String result = TimeTracker.track("查询用户", () -> {
* return userService.findById(123);
* });
* }</pre>
*
* <h3>lambda显式异常处理</h3>
* <pre>{@code
* try {
* // 允许抛出原始异常
* String result = TimeTracker.trackThrows("复杂查询", () -> {
* return complexQuery(); // 可能抛出检查异常
* });
* } catch (SQLException e) {
* // 精确处理特定异常
* logger.error("数据库查询失败", e);
* }
* }</pre>
*
* <h3>lambda嵌套使用</h3>
* <pre>{@code
* TimeTracker.track("整体流程", () -> {
* // 子任务1
* TimeTracker.track("数据准备", () -> prepareData());
*
* // 子任务2
* return TimeTracker.track("数据处理", () -> processData());
* });
* }</pre>
*
* <p>注意:默认情况下会打印执行时间到控制台。对于生产环境,
* 建议根据需要自定义日志记录机制。</p>
*
* @author [Your Name]
* @version 1.0
* @since [版本号]
*/
public class TimeTracker implements AutoCloseable {
/** 操作名称 */
privatefinal String operationName;
/** 开始时间(纳秒) */
privatefinallong startTime;
/** 是否启用日志 */
privatefinalboolean logEnabled;
/**
* 创建一个新的TimeTracker实例。
*
* @param operationName 要跟踪的操作名称
*/
public TimeTracker(String operationName) {
this(operationName, true);
}
/**
* 私有构造函数,用于创建TimeTracker实例。
*
* @param operationName 操作名称
* @param logEnabled 是否启用日志输出
*/
private TimeTracker(String operationName, boolean logEnabled) {
this.operationName = operationName;
this.startTime = System.nanoTime();
this.logEnabled = logEnabled;
if (logEnabled) {
System.out.printf("开始执行: %s%n", operationName);
}
}
/**
* 创建一个新的TimeTracker实例的静态工厂方法。
*
* @param operationName 要跟踪的操作名称
* @return 新的TimeTracker实例
*/
public static TimeTracker of(String operationName) {
returnnew TimeTracker(operationName);
}
/**
* 跟踪带返回值的代码块执行时间,异常会被包装为RuntimeException。
*
* @param operationName 操作名称
* @param execution 要执行的代码块
* @param <T> 返回值类型
* @return 代码块的执行结果
* @throws RuntimeException 如果执行过程中发生异常
*/
publicstatic <T> T track(String operationName, ThrowableSupplier<T> execution) {
try {
return trackThrows(operationName, execution);
} catch (Exception e) {
thrownew RuntimeException("执行失败: " + operationName, e);
}
}
/**
* 跟踪带返回值的代码块执行时间,允许抛出异常。
*
* @param operationName 操作名称
* @param execution 要执行的代码块
* @param <T> 返回值类型
* @return 代码块的执行结果
* @throws Exception 如果执行过程中发生异常
*/
publicstatic <T> T trackThrows(String operationName, ThrowableSupplier<T> execution) throws Exception {
try (TimeTracker ignored = new TimeTracker(operationName, true)) {
return execution.get();
}
}
/**
* 跟踪无返回值的代码块执行时间,异常会被包装为RuntimeException。
*
* @param operationName 操作名称
* @param execution 要执行的代码块
* @throws RuntimeException 如果执行过程中发生异常
*/
public static void track(String operationName, ThrowableRunnable execution) {
try {
trackThrows(operationName, execution);
} catch (Exception e) {
thrownew RuntimeException("执行失败: " + operationName, e);
}
}
/**
* 跟踪无返回值的代码块执行时间,允许抛出异常。
*
* @param operationName 操作名称
* @param execution 要执行的代码块
* @throws Exception 如果执行过程中发生异常
*/
public static void trackThrows(String operationName, ThrowableRunnable execution) throws Exception {
try (TimeTracker ignored = new TimeTracker(operationName, true)) {
execution.run();
}
}
@Override
public void close() {
if (logEnabled) {
// 计算执行时间(转换为毫秒)
long timeElapsed = (System.nanoTime() - startTime) / 1_000_000;
System.out.printf("%s 执行完成,耗时: %d ms%n", operationName, timeElapsed);
}
}
/**
* 可抛出异常的Supplier函数式接口。
*
* @param <T> 返回值类型
*/
@FunctionalInterface
publicinterface ThrowableSupplier<T> {
/**
* 获取结果。
*
* @return 执行结果
* @throws Exception 如果执行过程中发生错误
*/
T get() throws Exception;
}
/**
* 可抛出异常的Runnable函数式接口。
*/
@FunctionalInterface
publicinterface ThrowableRunnable {
/**
* 执行操作。
*
* @throws Exception 如果执行过程中发生错误
*/
void run() throws Exception;
}
}
b.使用
import java.io.IOException;
publicclass TimeTrackerDemo {
public void demonstrateUsage() {
// 1. 使用不抛出检查异常的版本(异常被包装为RuntimeException)
TimeTracker.track("简单任务", () -> {
Thread.sleep(1000);
return"完成";
});
// 2. 使用可能抛出异常的版本
try {
TimeTracker.trackThrows("可能失败的任务", () -> {
if (Math.random() < 0.5) {
thrownew IOException("模拟IO异常");
}
return"成功";
});
} catch (Exception e) {
// 处理异常
e.printStackTrace();
}
// 3. 嵌套使用示例
try {
TimeTracker.trackThrows("复杂流程", () -> {
// 子任务1:使用不抛出异常的版本
TimeTracker.track("子任务1", () -> {
Thread.sleep(500);
});
// 子任务2:使用抛出异常的版本
return TimeTracker.trackThrows("子任务2", () -> {
Thread.sleep(500);
return"全部完成";
});
});
} catch (Exception e) {
// 处理异常
e.printStackTrace();
}
// 4. try-with-resources 示例
try (TimeTracker tracker = TimeTracker.of("资源管理演示")) {
// 模拟资源操作
performResourceIntensiveTask();
}
// 5. 多资源管理的try-with-resources
try (
TimeTracker tracker1 = TimeTracker.of("第一阶段");
TimeTracker tracker2 = TimeTracker.of("第二阶段");
// 可以同时管理其他资源
CustomResource resource = acquireResource()
) {
processResourcesSequentially(resource);
} catch (Exception e) {
// 异常处理
e.printStackTrace();
}
// 6. 忽略返回值的try-with-resources
try (TimeTracker ignored = TimeTracker.of("后台任务")) {
performBackgroundTask();
}
}
// 辅助方法(仅作示例)
private void performResourceIntensiveTask() {
Thread.sleep(1000);
System.out.println("资源密集型任务完成");
}
private CustomResource acquireResource() {
returnnew CustomResource();
}
private void processResourcesSequentially(CustomResource resource) {
// 处理资源的示例方法
resource.process();
}
private void performBackgroundTask() {
// 后台任务示例
System.out.println("执行后台任务");
}
// 模拟自定义资源类
privatestaticclass CustomResource implements AutoCloseable {
public void process() {
System.out.println("处理资源");
}
@Override
public void close() {
System.out.println("关闭资源");
}
}
}
c.改进建议
当然,这个类还有很大的改进空间,我简单列几个,列位看官可以根据自己的真实场景再逐步进行优化
集成日志框架,比如Slf4j,支持更灵活的输出方式
添加更多的时间统计维度(最大值、最小值、平均值等)
添加性能指标收集,支持监控数据统计
支持异步操作
2.5 自动注入:smart-di
01.开始
a.说明
Spring 动态依赖注入扩展,不再局限于简单的@Autowired
扩展注入注解:@SmartAutowired、@AutowiredProxySPI、@AutowiredSPI
b.依赖
<dependency>
<groupId>io.github.burukeyou</groupId>
<artifactId>spring-smart-di-all</artifactId>
<version>0.2.0</version>
</dependency>
c.使用
在spring配置类上标记 @EnableSmartDI 注解
02.@SmartAutowired注解
a.默认的注入逻辑
a.与@Autowired区别是
1.当依赖的具体的类为非接口时,而是具体类并且该具体类还多个子类,并且具体类本身是Bean,会将具体类注入。如果@Autowired 会抛出异常因为有多个实现类
2.可以根据指定环境变量属性的值进行注入。并且该属性值可以为 @BeanAliasName 配置的别名
b.比如有以下三个SpringBean
@Component
public class Weather {
}
@Component
@BeanAliasName("天气A服务商")
public class WeatherA extends Weather {
}
@Component
@BeanAliasName("天气B服务商")
public class WeatherB extends Weather {
}
-------------------------------------------------------------------------------------------------
然后使用@SmartAutowired注解进行依赖注入依然能够注入成功,注入的是Weather本身
当然如果Weather是非SpringBean,与@Autowired一样会抛出有多个实现类的异常无法自动注入
-------------------------------------------------------------------------------------------------
@SmartAutowired
private Weather weather;
-------------------------------------------------------------------------------------------------
为什么要加入这个注入逻辑呢,因为我们的做抽象设计的时候,往往会写出非常复杂的类图关系的,一个类往往有多个实现类
但我想注入那个类又不想搭配@Qualifier注解去硬编码指定注入的beanName(硬编码这对代码洁癖的来说非常重要)
也不想每次使用@Primary 去指定默认注入哪个,因为是确定的,所以"智能化"了一点去帮助我们的依赖注入
b.使用环境变量去配置注入的Bean
a.假如有以下配置
weather:
impl: 天气A服务商
b.说明
然后指定环境变量的key,则会使用该属性的具体值去执行依赖注入
这个属性值可以是beanName,也可以是全路径类名,也可以是使用@BeanAliasName注解标记的名字
-------------------------------------------------------------------------------------------------
@SmartAutowired("${weather.impl}")
private Weather weather;
03.AutowiredProxySPI注解
a.说明
当需要注入某个接口有多个实现类,可以根据该接口类标记的@ProxySPI注解、@EnvironmentProxySPI去标记动态注入的逻辑
让我们以一个场景去看如何使用,假如系统接入了多个短信服务商,然后用户可以在页面动态的切换不同的服务商
b.让我们手写会如何实现
a.说明
第一步先在某个位置(不管是nacos还是数据库)配置当前使用的服务商的对应值比如 sms.impl = "某腾短信"
第二步,在代码里执行发短信的时候,手动获取该sms.impl对应的服务商的实现类,伪代码可能如下:
b.代码
// 1、获取当前使用的服务商
String name = get("sms.impl");
// 2、获取对应的实现类
SmsService smsService = springContext.getBean(name);
// 3、使用smsService执行具体业务逻辑
smsService.sendMsg()
c.使用@AutowiredProxySPI
a.说明
但是现在让我们来 @AutowiredProxySPI 是如何自动屏蔽这些细节 需要搭配@ProxySPI来处理
假设我们的当前使用的服务商在环境变量中${sms.impl}, 便可以使用默认实现 @EnvironmentProxySPI
比如存在配置:
sms:
impl: 某移短信服务
b.说明
然后在接口类上标记@EnvironmentProxySPI即可,会根据该属性值去获取具体注入哪个实现类
然后就可以使用我们的 @AutowiredProxySPI注解进行依赖注入了,并且注入的是一个代理对象
代理的逻辑是每次执行SmsService的任何方法时,都会去重新获取一次 当前使用的实现类
所以即使你修改了${sms.impl}配置,也能实时生效而无需重启服务器
c.代码
@EnvironmentProxySPI("${sms.impl}")
public interface SmsService {
}
@BeanAliasName("某腾短信服务")
@Component
public class ASmsService implements SmsService {
}
@BeanAliasName("某移短信服务")
@Component
public class BSmsService implements SmsService {
}
// 依赖注入
@AutowiredProxySPI
private SmsService smsService;
d.说明
@EnvironmentProxySPI是用来配置环境变量相关注入逻辑,如果想要自定义配置比如在数据库中可实现自己的ProxySPI注解。
比如自定义DBProxySPI注解,并标记上@ProxySPI实现并指定具体AnnotationProxyFactory即可。然后DBProxySPI就可以像@EnvironmentProxySPI一样去使用了,下面是实现的伪代码:
e.代码
@Inherited
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@ProxySPI(DbProxyFactory.class)
public @interface DBProxySPI {
String value();
}
@Component
public class DbProxyFactory implements AnnotationProxyFactory<DBProxySPI> {
@Autowired
private SysConfigMapper sysConfigDao;
@Override
public Object getProxy(Class<?> targetClass,DBProxySPI spi) {
// todo 根据注解从数据库获取要注入的实现类
String configName = sysConfigDao.getConfig(spi.value());
return springContext.getBean(configName);
}
}
04.AutowiredSPI注解
a.说明
与@AutowiredProxySPI注解在使用上基本一致
b.区别
@AutowiredProxySPI注入的是代理对象
AutowiredSPI注入的当前使用的具体的实现类,只在服务器启动的时候注入,也就是说更改配置后需要重启服务切换实现类才会生效
2.6 请求调用:restclient
00.汇总
RestTemplate
WebClient
RestClient
01.RestTemplate
a.定义
RestTemplate 是 Spring 提供的一个同步 HTTP 客户端,用于与 RESTful 服务进行交互
b.原理
RestTemplate 提供了一种便捷的方式来发送 HTTP 请求和处理响应
它封装了底层的 HTTP 客户端库(如 Apache HttpClient 或 JDK 的 HttpURLConnection)
并提供了模板化的方法来执行 HTTP 操作
c.常用 API
getForObject(String url, Class<T> responseType, Object... uriVariables):发送 GET 请求并将响应体转换为指定类型的对象
postForObject(String url, Object request, Class<T> responseType, Object... uriVariables):发送 POST 请求并将响应体转换为指定类型的对象
exchange(String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables):执行指定 HTTP 方法的请求
d.使用步骤
1.创建 RestTemplate 实例
2.使用 RestTemplate 的方法发送 HTTP 请求
3.处理响应数据
e.示例代码
import org.springframework.web.client.RestTemplate;
public class RestTemplateExample {
public static void main(String[] args) {
RestTemplate restTemplate = new RestTemplate();
String url = "https://api.example.com/data";
// GET 请求
String result = restTemplate.getForObject(url, String.class);
System.out.println(result);
// POST 请求
MyRequest request = new MyRequest("value1", "value2");
MyResponse response = restTemplate.postForObject(url, request, MyResponse.class);
System.out.println(response);
}
}
02.WebClient
a.定义
WebClient 是 Spring 5 引入的一个非阻塞、响应式的 HTTP 客户端,适用于构建反应式应用程序
b.原理
WebClient 基于 Reactor 框架,利用其提供的 Mono 和 Flux 来处理异步数据流
它通过非阻塞 I/O 和反应式编程模型,能够高效地处理大量并发请求
c.常用 API
get(), post(), put(), delete():用于指定 HTTP 方法
uri(String uriTemplate, Object... uriVariables):设置请求的 URI
retrieve():执行请求并获取响应
bodyToMono(Class<T> elementClass):将响应体转换为 Mono
bodyToFlux(Class<T> elementClass):将响应体转换为 Flux
d.使用步骤
1.创建 WebClient 实例
2.构建请求并发送
3 处理响应数据
e.示例代码
import org.springframework.web.reactive.function.client.WebClient;
reactor.core.publisher.Mono;
public class WebClientExample {
public static void main(String[] args) {
WebClient webClient = WebClient.create("https://api.example.com");
// GET 请求
Mono<String> response = webClient.get()
.uri("/data")
.retrieve()
.bodyToMono(String.class);
response.subscribe(System.out::println);
}
}
03.RestClient
a.介绍
Spring Boot 3.2引入了RestClient,这是一个建立在WebClient之上的更高级抽象
RestClient通过提供更直观流畅的API并减少样板文件代码,进一步简化了HTTP请求的生成过程
它保留了WebClient的所有功能,同时提供了一个对开发人员更友好的界面
b.GET Request
a.RestTemplate
var response = restTemplate.getForObject("https://api.example.com/data", String.class);
b.RestClient
var response = restClient
.get()
.uri(cepURL)
.retrieve()
.toEntity(String.class);
c.POST Request
a.RestTemplate
ResponseEntity<String> response = restTemplate.postForEntity("https://api.example.com/data", request, String.class);
b.RestClient
var response = restClient
.post()
.uri("https://api.example.com/data")
.body(request)
.retrieve()
.toEntity(String.class);
d.错误处理
a.RestTemplate
try {
String response = restTemplate.getForObject("https://api.example.com/data", String.class);
} catch (RestClientException ex) {
// Handle exception
}
b.RestClient
String request = restClient.get()
.uri("https://api.example.com/this-url-does-not-exist")
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders())
})
.body(String.class);
2.7 请求调用:httpclient
00.汇总
二次封装httpclient为工具类
01.依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.32</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
02.配置类
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
@Data
@Configuration
@ConfigurationProperties("spring-core.http-client")
public class HttpClientProperties {
/**
* 是否启用http-client
*/
private boolean enable = false;
/**
* 连接超时时间,单位:毫秒
*/
private int connectTimeout = 3000;
/**
* 读取超时时间,单位:毫秒
*/
private int readTimeout = 5000;
/**
* 从连接池获取连接时间,单位:毫秒
*/
private int connectionRequestTimeout = 2000;
/**
* 最多同时连接请求数
*/
private int maxTotal = 150;
/**
* 每个路由最大连接数
*/
private int maxPerRoute = 30;
/**
* 字符集
*/
private Charset charset = StandardCharsets.UTF_8;
}
03.重写HttpEntityEnclosingRequestBase对象
import lombok.Getter;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.springframework.http.HttpMethod;
import java.net.URI;
public class HttpObject extends HttpEntityEnclosingRequestBase {
private final HttpMethod httpMethod;
@Getter
private final String url;
public HttpObject(HttpMethod httpMethod, String url) {
this.httpMethod = httpMethod;
this.setURI(URI.create(url));
this.url = url;
}
@Override
public String getMethod() {
return this.httpMethod.name();
}
}
04.初始化HttpClient交给IOC管理
import lombok.extern.slf4j.Slf4j;
import org.apache.http.NoHttpResponseException;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
/**
* HTTP连接池
**/
@Slf4j
@Configuration
@ConditionalOnClass(CloseableHttpClient.class)
@ConditionalOnProperty(prefix = "spring-core.http-client", name = "enable", havingValue = "true")
public class HttpClientConfigurer {
@Bean
public CloseableHttpClient httpClient(HttpClientProperties httpClientProperties) throws Exception {
int maxTotal = httpClientProperties.getMaxTotal();
int maxPerRoute = httpClientProperties.getMaxPerRoute();
int connectionRequestTimeout = httpClientProperties.getConnectionRequestTimeout();
int connectTimeout = httpClientProperties.getConnectTimeout();
int readTimeout = httpClientProperties.getReadTimeout();
// 创建和配置SSL连接工厂
SSLConnectionSocketFactory socketFactory = createSocketFactory();
// 创建注册表
Registry<ConnectionSocketFactory> registry = createSocketFactoryRegistry(socketFactory);
// HTTP连接池对象
PoolingHttpClientConnectionManager connectionManager = createConnectionManager(registry, maxTotal, maxPerRoute);
// 超时设置
RequestConfig config = createRequestConfig(connectionRequestTimeout, connectTimeout, readTimeout);
// 创建连接池
return HttpClients.custom().setRetryHandler(retryHandler()).setConnectionManager(connectionManager).setDefaultRequestConfig(config).build();
}
/**
* 重试机制
*
* @return
*/
private HttpRequestRetryHandler retryHandler() {
return (exception, executionCount, context) -> {
if (executionCount >= 3) {
return false;
}
if (exception instanceof NoHttpResponseException) {
log.info("第 {} 次, 发起重试机制", executionCount);
return true;
}
return false;
};
}
/**
* 创建和配置SSL连接工厂
*
* @return
* @throws NoSuchAlgorithmException
*/
private SSLConnectionSocketFactory createSocketFactory() throws NoSuchAlgorithmException {
// return new SSLConnectionSocketFactory(SSLContext.getDefault(), NoopHostnameVerifier.INSTANCE);
try {
SSLContext sc = SSLContext.getInstance("TLS");
X509TrustManager trustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
sc.init(null, new TrustManager[]{trustManager}, null);
return new SSLConnectionSocketFactory(sc, NoopHostnameVerifier.INSTANCE);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 创建注册表
*
* @param socketFactory SSL工厂对象
* @return
*/
private Registry<ConnectionSocketFactory> createSocketFactoryRegistry(SSLConnectionSocketFactory socketFactory) {
return RegistryBuilder.<ConnectionSocketFactory>create().register("https", socketFactory).register("http", PlainConnectionSocketFactory.INSTANCE).build();
}
/**
* 连接池管理器
*
* @param registry 注册表
* @param maxTotal 最多同时连接请求数
* @param maxPerRoute 每个路由最大连接数,路由指IP+PORT或者域名,例如连接池大小(MaxTotal)设置为300,路由连接数设置为200(DefaultMaxPerRoute),对于www.a.com与www.b.com两个路由来说,发起服务的主机连接到每个路由的最大连接数(并发数)不能超过200,两个路由的总连接数不能超过300。
* @return
*/
private PoolingHttpClientConnectionManager createConnectionManager(Registry<ConnectionSocketFactory> registry, int maxTotal, int maxPerRoute) {
PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(registry);
connManager.setMaxTotal(maxTotal);
connManager.setDefaultMaxPerRoute(maxPerRoute);
return connManager;
}
/**
* 超时设置
*
* @param connectionRequestTimeout 从连接池获取连接时间
* @param connectTimeout 创建连接时间
* @param readTimeout 数据传输时间
* @return
*/
private RequestConfig createRequestConfig(int connectionRequestTimeout, int connectTimeout, int readTimeout) {
return RequestConfig.custom().setConnectionRequestTimeout(connectionRequestTimeout).setConnectTimeout(connectTimeout).setSocketTimeout(readTimeout).build();
}
}
05.管理HttpClient的Builder对象
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.credlink.core.configurer.HttpClientConfigurer;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.http.HttpMethod;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* HTTP连接池客户端
**/
@Slf4j
@Component
@ConditionalOnBean(HttpClientConfigurer.class)
public class SpringHttpClient {
private final CloseableHttpClient httpClient;
public SpringHttpClient(CloseableHttpClient httpClient) {
this.httpClient = httpClient;
}
public Builder builder() {
return new Builder(httpClient);
}
public static class Builder {
// Http客户端
private final CloseableHttpClient httpClient;
// HTTP对象
private HttpObject httpBase;
// 表单内容
private Map<String, Object> formMap;
// JSON内容
private String bodyJson;
// 请求头
private Map<String, Object> headerMap;
// 字符集
private Charset charset;
// 请求配置,单独设置超时时间
private RequestConfig requestConfig;
// 请求开始时间
private final long millis;
// 是否打印响应内容
private boolean isLog;
public Builder(CloseableHttpClient httpClient) {
this.httpClient = httpClient;
this.millis = System.currentTimeMillis();
this.charset = StandardCharsets.UTF_8;
}
public Builder get(@NonNull String uri) {
this.httpBase = new HttpObject(HttpMethod.GET, uri);
return this;
}
public Builder post(@NonNull String uri) {
this.httpBase = new HttpObject(HttpMethod.POST, uri);
return this;
}
public Builder put(@NonNull String uri) {
this.httpBase = new HttpObject(HttpMethod.PUT, uri);
return this;
}
public Builder delete(@NonNull String uri) {
this.httpBase = new HttpObject(HttpMethod.DELETE, uri);
return this;
}
public Builder formMap(@NonNull Map<String, Object> formMap) {
if (Objects.isNull(this.formMap)) {
this.formMap = new HashMap<>();
}
this.formMap.putAll(formMap);
return this;
}
public Builder form(@NonNull String name, Object value) {
if (Objects.isNull(this.formMap)) {
this.formMap = new HashMap<>();
}
this.formMap.put(name, value);
return this;
}
public Builder bodyJson(@NonNull Object bodyJson) {
this.bodyJson = (bodyJson instanceof String) ? bodyJson.toString() : JSONObject.toJSONString(bodyJson);
return this;
}
public Builder headerMap(@NonNull Map<String, Object> headerMap) {
if (Objects.isNull(this.headerMap)) {
this.headerMap = new HashMap<>();
}
this.headerMap.putAll(headerMap);
return this;
}
public Builder header(@NonNull String name, Object value) {
if (Objects.isNull(this.headerMap)) {
this.headerMap = new HashMap<>();
}
this.headerMap.put(name, value);
return this;
}
public Builder charset(@NonNull Charset charset) {
this.charset = charset;
return this;
}
public Builder isLog() {
this.isLog = true;
return this;
}
public Builder channel(ChannelResult channelResult) {
this.channelResult = channelResult;
return this;
}
public Result build() {
HttpEntity entity = null;
Result result = new Result();
result.setLog(isLog);
result.setCharset(charset);
try {
// 设置表单请求参数
if (Objects.nonNull(formMap)) {
URIBuilder builder = new URIBuilder(httpBase.getUrl());
builder.setCharset(charset);
formMap.forEach((k, v) -> {
if (Objects.nonNull(v)) {
builder.setParameter(k, String.valueOf(v));
}
});
URI uri = builder.build();
httpBase.setURI(uri);
}
// 设置JSON请求参数
if (Objects.nonNull(bodyJson)) {
ByteArrayEntity byteArray = new ByteArrayEntity(bodyJson.getBytes(charset), ContentType.APPLICATION_JSON);
httpBase.setEntity(byteArray);
}
// 设置超时时间
if (Objects.nonNull(requestConfig)) {
httpBase.setConfig(requestConfig);
}
// 设置请求头
if (Objects.nonNull(headerMap)) {
for (Map.Entry<String, Object> entry : headerMap.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (Objects.nonNull(value)) {
httpBase.setHeader(key, String.valueOf(value));
}
}
}
// 发送请求
CloseableHttpResponse response = httpClient.execute(httpBase);
result.setCode(response.getStatusLine().getStatusCode());
entity = response.getEntity();
result.setContentType(ContentType.get(entity));
result.setBytes(EntityUtils.toByteArray(entity));
} catch (URISyntaxException | IOException e) {
throw new RuntimeException(e);
} finally {
try {
EntityUtils.consume(entity);
} catch (IOException e) {
log.info("释放连接异常\n", e);
}
log.info("请求地址 {}, 总耗时 {} 毫秒", httpBase.getUrl(), (System.currentTimeMillis() - millis));
}
return result;
}
}
@Getter
@Setter
public static class Result {
// HTTP状态码
private Integer code;
// 响应字节
private byte[] bytes;
// 内容类型
private ContentType contentType;
// 是否打印日志
private boolean isLog;
// 字符集
private Charset charset;
public boolean isSuccess() {
return Objects.nonNull(code) && code == 200;
}
public boolean isFile() {
return Objects.equals(ContentType.APPLICATION_OCTET_STREAM.getMimeType(), contentType.getMimeType());
}
public boolean isBytesNull() {
return Objects.isNull(bytes);
}
public String toJson() {
if (isBytesNull()) {
return null;
}
if (isFile()) {
throw new RuntimeException(String.format("内容类型 %s, 属于文件", contentType));
}
String body = new String(bytes, charset);
if (isLog) {
log.info("HTTP响应结果 {}", body);
}
return body;
}
public <T> T toBean(@NonNull Class<T> tClass) {
return JSONObject.parseObject(toJson(), tClass);
}
public <T> List<T> toList(@NonNull Class<T> tClass) {
return JSONArray.parseArray(toJson(), tClass);
}
public OutputStream toStream() {
if (isBytesNull()) {
return null;
}
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();) {
bos.write(bytes);
return bos;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public File toFile(File file) {
if (isBytesNull()) {
return null;
}
if (!isFile()) {
throw new RuntimeException(String.format("内容类型 %s, 属于文本, 其数据 %s", contentType.toString(), toJson()));
}
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(bytes);
return file;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public File toFile(String file) {
return toFile(new File(file));
}
}
}
06.使用方式
a.说明
只需要引入SpringHttpClient,调用里面的builder()方法,即可使用链式编程
支持post、get、put、delete的http请求和form-data、application/json的请求类型
打印响应日志以及http的耗时记录,其中toBean()方法可以转化成对应的实体类
b.代码
import com.alibaba.fastjson2.JSONObject;
import com.credlink.core.client.SpringHttpClient;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@Slf4j
@SpringBootTest
public class LinkApiServerTest {
@Autowired
private SpringHttpClient httpClient;
@Test
void test() {
JSONObject params = new JSONObject();
params.put("age", 18);
JSONObject result = httpClient.builder()
.post("http://127.0.0.1:8080/api/student/query")
.header("Authorization", "Basic xxxxxxxxxx")
.bodyJson(params)
.build()
.toBean(JSONObject.class);
log.info("响应结果 {}", result);
}
}
2.8 请求调用:uniapi-http
01.介绍
就像配置Spring的Controller那样简单,只不过相当于是反向配置而已
一个声明式的Http接口对接框架,能以极快的方式完成对一个第三方Http接口的对接和使用,之后就像调用本地方法一样自动去发起Http请求
不需要开发者去关注如何发送一个请求,如何去传递Http请求参数,以及如何对请求结果进行处理和反序列化,这些框架都帮你一一实现
该框架更注重于如何保持高内聚和可读性高的代码情况下与快速第三方渠道接口进行对接和集成
而非像传统编程式的Http请求客户端(比如HttpClient、Okhttp)那样专注于如何去发送Http请求,虽然底层也是用的Okhttp去发送请求
与其说的是对接的Http接口,不如说是对接的第三方渠道,UniHttp可支持自定义接口渠道方HttpAPI注解以及一些自定义的对接和交互行为
为此扩展了发送和响应和反序列化一个Http请求的各种生命周期钩子,开发者可自行去扩展实现
01.快速开始
a.引入依赖
<dependency>
<groupId>io.github.burukeyou</groupId>
<artifactId>uniapi-http</artifactId>
<version>0.0.4</version>
</dependency>
b.对接接口
a.说明
首先随便创建一个接口,然后在接口上标记@HttpApi注解,然后指定请求的域名url,然后就可以在方法上去配置对接哪个接口
比如下面两个方法的配置则对接了以下两个接口
GET http://localhost:8080/getUser
POST http://localhost:8080/addUser
方法返回值定义成Http响应body对应的类型即可,默认会使用fastjson反序列化Http响应body的值为该类型对象
b.代码
@HttpApi(url = "http://localhost:8080")
interface UserHttpApi {
@GetHttpInterface("/getUser")
BaseRsp<String> getUser(@QueryPar("name") String param,@HeaderPar("userId") Integer id);
@PostHttpInterface("/addUser")
BaseRsp<Add4DTO> addUser(@BodyJsonPar Add4DTO req);
}
c.说明
@QueryPar 表示将参数值放到Http请求的查询参数内
@HeaderPar 表示将参数值放到Http请求的请求头里
@BodyJsonPar 表示将参数值放到Http请求body内,并且content-type是application/json
d.结果
getUser方法最终构建的Http请求报文为
GET http://localhost:8080/getUser?name=param
Header:
userId: id
-------------------------------------------------------------------------------------------------
addUser最终构建的Http请求报文为
POST: http://localhost:8080/addUser
Header:
Content-Type: application/json
Body:
{"id":1,"name":"jay"}
c.声明定义的HttpAPI的包扫描路径
a.说明
在spring的配置类上使用@UniAPIScan注解标记定义的@HttpAPI的包扫描路径
会自动为标记了@HttpApi接口生成代理对象并且注入到Spring容器中
之后只需要像使用Spring的其他bean一样,依赖注入使用即可
b.代码
@UniAPIScan("com.xxx.demo.api")
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class,args);
}
}
d.依赖注入使用
@Service
class UserAppService {
@Autowired
private UserHttpApi userHttpApi;
public void doSomething(){
userHttpApi.getUser("jay",3);
}
}
02.说明介绍
a.@HttpApi注解接口
该接口上的方法会被代理到对应的Http请求接口,可指定请求的域名也可指定自定义的Http代理逻辑等等
b.@HttpInterface注解
a.说明
用于配置一个接口的参数,包括请求方式、请求路径、请求头、请求cookie、请求查询参数等等
并且内置了以下请求方式的@HttpInterface,不必再每次手动指定请求方式
b.说明
@PostHttpInterface
@PutHttpInterface
@DeleteHttpInterface
@GetHttpInterface
c.代码
@PostHttpInterface(
// 请求路径
path = "/getUser",
// 请求头
headers = {"clientType:sys-app","userId:99"},
// url查询参数
params = {"name=周杰伦","age=1"},
// url查询参数拼接字符串
paramStr = "a=1&b=2&c=3&d=哈哈&e=%E7%89%9B%E9%80%BC",
// cookie 字符串
cookie = "name=1;sessionId=999"
)
BaseRsp<String> getUser();
c.@Par注解
a.说明
以下各种Par后缀的注解,主要用于方法参数上,用于指定在发送请求时将参数值放到Http请求体的哪部分上。
为了方便描述,下文描述的普通值就是表示String,基本类型、基本类型的包装类型等类型.
b.@QueryPar注解
标记Http请求url的查询参数
支持以下方法参数类型的标记: 普通值、普通值集合、对象、Map
-------------------------------------------------------------------------------------------------
@PostHttpInterface
BaseRsp<String> getUser(@QueryPar("id") String id, // 普通值
@QueryPar("ids") List<Integer> idsList, // 普通值集合
@QueryPar User user, // 对象
@QueryPar Map<String,Object> map); // Map
-------------------------------------------------------------------------------------------------
如果类型是普通值或者普通值集合需要手动指定参数名,因为是当成单个查询参数传递
如果类型是对象或者Map是当成多个查询参数传递,字段名或者map的key名就是参数名,字段值或者map的value值就是参数值。
如果是对象,参数名默认是字段名,由于用的是fastjson序列化可以用@JSONField指定别名
c.@PathPar注解
标记Http请求路径变量参数,仅支持标记普通值类型
-------------------------------------------------------------------------------------------------
@PostHttpInterface("/getUser/{userId}/detail")
BaseRsp<String> getUser(@PathPar("userId") String id); // 普通值
d.@HeaderPar注解
标记Http请求头参数
支持以下方法参数类型:对象、Map、普通值
-------------------------------------------------------------------------------------------------
@PostHttpInterface
BaseRsp<String> getUser(@HeaderPar("id") String id, // 普通值
@HeaderPar User user, // 对象
@HeaderPar Map<String,Object> map); // Map
-------------------------------------------------------------------------------------------------
如果类型是普通值类型需要手动指定参数名,当成单个请求头参数传递. 如果是对象或者Map当成多个请求头参数。
e.@CookiePar注解
用于标记Http请求的cookie请求头
支持以下方法参数类型: Map、Cookie对象、字符串
-------------------------------------------------------------------------------------------------
@PostHttpInterface
BaseRsp<String> getUser(@CookiePar("id") String cookiePar, // 普通值 (指定name)当成单个cookie键值对处理
@CookiePar String cookieString, // 普通值 (不指定name),当成完整的cookie字符串处理
@CookiePar com.burukeyou.uniapi.http.support.Cookie cookieObj, // 单个Cookie对象
@CookiePar List<com.burukeyou.uniapi.http.support.Cookie> cookieList // Cookie对象列表
@CookiePar Map<String,Object> map); // Map
-------------------------------------------------------------------------------------------------
如果类型是字符串时,当指定参数名时,当成单个cookie键值对处理,如果不指定参数名时当成完整的cookie字符串处理比如a=1;b=2;c=3 这样
如果是Map当成多个cookie键值对处理。
如果类型是内置的 com.burukeyou.uniapi.http.support.Cookie对象当成单个cookie键值对处理
f.@BodyJsonPar注解
用于标记Http请求体内容为json形式: 对应content-type为 application/json
支持以下方法参数类型: 对象、对象集合、Map、普通值、普通值集合
-------------------------------------------------------------------------------------------------
@PostHttpInterface
BaseRsp<String> getUser(@BodyJsonPar String id, // 普通值
@BodyJsonPar String[] id // 普通值集合
@BodyJsonPar List<User> userList, // 对象集合
@BodyJsonPar User user, // 对象
@BodyJsonPar Map<String,Object> map); // Map
-------------------------------------------------------------------------------------------------
序列化和反序列化默认用的是fastjson,所以如果想指定别名,可以在字段上标记 @JSONField 注解取别名
e.@BodyFormPar注解
用于标记Http请求体内容为普通表单形式: 对应content-type为 application/x-www-form-urlencoded
支持以下方法参数类型:对象、Map、普通值
-------------------------------------------------------------------------------------------------
@PostHttpInterface
BaseRsp<String> getUser(@BodyFormPar("name") String value, // 普通值
@BodyFormPar User user, // 对象
@BodyFormPar Map<String,Object> map); // Map
-------------------------------------------------------------------------------------------------
如果类型是普通值类型需要手动指定参数名,当成单个请求表单键值对传递
f.@BodyMultiPartPar注解
用于标记Http请求体内容为复杂形式: 对应content-type为 multipart/form-data
支持以下方法参数类型: 对象、Map、普通值、File对象
-------------------------------------------------------------------------------------------------
@PostHttpInterface
BaseRsp<String> getUser(@BodyMultiPartPar("name") String value, // 单个表单文本值
@BodyMultiPartPar User user, // 对象
@BodyMultiPartPar Map<String,Object> map, // Map
@BodyMultiPartPar("userImg") File file); // 单个表单文件值
-------------------------------------------------------------------------------------------------
如果参数类型是普通值或者File类型,当成单个表单键值对处理,需要手动指定参数名。
如果参数类型是对象或者Map,当成多个表单键值对处理。如果字段值或者map的value参数值是File类型,则自动当成是文件表单字段传递处理
g.@BodyBinaryPar注解
用于标记Http请求体内容为二进制形式: 对应content-type为 application/octet-stream
支持以下方法参数类型: InputStream、File、InputStreamSource
-------------------------------------------------------------------------------------------------
@PostHttpInterface
BaseRsp<String> getUser(@BodyBinaryPar InputStream value,
@BodyBinaryPar File user,
@BodyBinaryPar InputStreamSource map);
-------------------------------------------------------------------------------------------------
h.@ComposePar注解
这个注解本身不是对Http请求内容的配置,仅用于标记一个对象,然后会对该对象内的所有标记了其他@Par注解的字段进行嵌套解析处理, 目的是减少方法参数数量,支持都内聚到一起传递
支持以下方法参数类型: 对象
-------------------------------------------------------------------------------------------------
@PostHttpInterface
BaseRsp<String> getUser(@ComposePar UserReq req);
-------------------------------------------------------------------------------------------------
比如UserReq里面的字段可以嵌套标记其他@Par注解,具体支持的标记类型和处理逻辑与前面一致
class UserReq {
@QueryPar
private Long id;
@HeaderPar
private String name;
@BodyJsonPar
private Add4DTO req;
@CookiePar
private String cook;
}
d.原始的HttpResponse
a.说明
HttpResponse表示Http请求的原始响应对象,如果业务需要关注拿到完整的Http响应,只需要在方法返回值包装返回即可
如下面所示,此时HttpResponse<Add4DTO>里的泛型Add4DTO才是代表接口实际返回的响应内容,后续可直接手动获取
b.代码
@PostHttpInterface("/user-web/get")
HttpResponse<Add4DTO> get();
通过它我们就可以拿到响应的Http状态码、响应头、响应cookie等等,当然也可以拿到我们的响应body的内容通过getBodyResult方法
e.处理文件下载接口
a.说明
对于若是下载文件的类型的接口,可将方法返回值定义为 HttpBinaryResponse、HttpFileResponse、HttpInputStreamResponse 的任意一种,这样就可以拿到下载后的文件
b.说明
HttpBinaryResponse: 表示下载的文件内容以二进制形式返回,如果是大文件请谨慎处理,因为会存放在内存中
HttpFileResponse: 表示下载的文件内容以File对象返回,这时文件已经被下载到了本地磁盘
HttpInputStreamResponse: 表示下载的文件内容输入流的形式返回,这时文件其实还没被下载到客户端,调用者可以自行读取该输入流进行文件的下载
f.HttpApiProcessor 生命周期钩子
a.说明
HttpApiProcessor是一个Http请求接口的各种生命周期钩子,开发者可以实现它在里面自定义编写各种对接逻辑。然后可以配置到@HttpApi注解或者@HttpInterface注解上, 然后框架内部默认会从SpringContext获取,获取不到则手动new一个
通常一个Http请求需要经历 构建请求参数、发送Http请求时,Http响应后获取响应内容、反序列化Http响应内容成具体对象
b.目前提供了4种钩子,执行顺序流程如下
postBeforeHttpMetadata (请求发送前)在发送请求之前,对Http请求体后置处理
|
V
postSendingHttpRequest (请求发送时)在Http请求发送时处理
|
V
postAfterHttpResponseBodyString (请求响应后)对响应body文本字符串进行后置处理
|
V
postAfterHttpResponseBodyResult (请求响应后)对响应body反序列化后的结果进行后置处理
|
V
postAfterMethodReturnValue (请求响应后)对代理的方法的返回值进行后置处理,类似aop的后置处理
c.说明
postBeforeHttpMetadata:可在发送http请求之前对请求体进行二次处理,比如加签之类
postSendHttpRequest:Http请求发送时会回调该方法,可以在该方法执行自定义的发送逻辑或者打印发送日志
postAfterHttpResponseBodyString:Http请求响应后,对响应body字符串进行进行后置处理,比如如果是加密数据可以进行解密
postAfterHttpResponseBodyResult:Http请求响应后,对响应body反序列化后的对象进行后置处理,比如填充默认返回值
postAfterMethodReturnValue:Http请求响应后,对代理的方法的返回值进行后置处理,类似aop的后置处理
d.回调参数说明
HttpMetadata:表示此次Http请求的请求体,包含请求url,请求头、请求方式、请求cookie、请求体、请求参数等等
HttpApiMethodInvocation:继承自MethodInvocation, 表示被代理的方法调用上下文,可以拿到被代理的类,被代理的方法,被代理的HttpAPI注解、HttpInterface注解等信息
g.配置自定义的Http客户端
a.说明
默认使用的是Okhttp客户端,如果要重新配置Okhttp客户端,注入spring的bean
b.代码
@Configuration
public class CusotmConfiguration {
@Bean
public OkHttpClient myOHttpClient(){
return new OkHttpClient.Builder()
.readTimeout(50, TimeUnit.SECONDS)
.writeTimeout(50, TimeUnit.SECONDS)
.connectTimeout(10, TimeUnit.SECONDS)
.connectionPool(new ConnectionPool(20,10, TimeUnit.MINUTES))
.build();
}
}
03.企业实战
a.案例背景
假设现在需要对接一个某天气服务的所有接口,需要在请求cookie带上一个token字段和sessionId字段,这两个字段的值需要每次接口调用前先手动调渠道方的一个特定的接口申请获取,token值在该接口返回值中返回,sessionId在该接口的响应头中返回
然后还需要在请求头上带上一个sign签名字段, 该sign签名字段生成规则需要用渠道方提供的公钥对所有请求体和请求参数进行加签生成
然后还需要在每个接口的查询参数上都带上一个渠道方分配的客户端appId
b.在application.yml中配置对接渠道方的信息
channel:
mtuan:
# 请求域名
url: http://127.0.0.1:8999
# 分配的渠道appId
appId: UUU-asd-01
# 分配的公钥
publicKey: fajdkf9492304jklfahqq
c.自定义该渠道方的HttpAPI注解
假设现在对接的是某团,所以自定义注解叫@MTuanHttpApi吧,然后需要在该注解上标记@HttpApi注解,并且需要配置processor字段,需要去自定义实现一个HttpApiProcessor这个具体实现后续讲
有了这个注解后就可以自定义该注解与对接渠道方相关的各种字段配置,当然也可以不定义
注意这里url的字段是使用 @AliasFor(annotation = HttpApi.class),这样构建的HttpMetadata中会默认解析填充要请求体,不标记则也可自行处理
-----------------------------------------------------------------------------------------------------
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@HttpApi(processor = MTuanHttpApiProcessor.class)
public @interface MTuanHttpApi {
/**
* 渠道方域名地址
*/
@AliasFor(annotation = HttpApi.class)
String url() default "${channel.mtuan.url}";
/**
* 渠道方分配的appId
*/
String appId() default "${channel.mtuan.appId}";
}
-----------------------------------------------------------------------------------------------------
@Slf4j
@Component
public class MTuanHttpApiProcessor implements HttpApiProcessor<MTuanHttpApi> {
}
-----------------------------------------------------------------------------------------------------
注意实现的HttpApiProcessor泛型要指定为刚才定义的注解@MTuanHttpApi类型
因为这个HttpApiProcessor配置到它上面如果需要通用处理可以定义为Annocation类型
d.对接接口
有了@MTuanHttpApi注解之后就可以开始对接接口了,比如假设有两个接口要对接。一个就是前面说的获取令牌的接口。一个是获取天气情况的接口
为什么getToken方法返回值是 HttpResponse,这是UniHttp内置的原始Http响应对象,方便我们去拿到原始Http响应体的一些内容(比如响应状态码、响应cookie)
其中的泛型BaseRsp才是实际的Http响应体反序列化后的内容。而getCityWeather方法没有使用HttpResponse包装,BaseRsp只是单纯Http响应体反序列化后的内容,这是两者的区别
前面介绍过 HttpResponse,其实大部份接口是不关注HttpResponse的可以不用去配置
-----------------------------------------------------------------------------------------------------
@MTuanHttpApi
public interface WeatherApi {
/**
* 根据城市名获取天气情况
*/
@GetHttpInterface("/getCityByName")
BaseRsp<WeatherDTO> getCityWeather(@QueryPar("city") String cityName);
/**
* 根据appId和公钥获取令牌
*/
@PostHttpInterface("/getToken")
HttpResponse<BaseRsp<TokenDTO>> getToken(@HeaderPar("appId") String appId, @HeaderPar("publicKey")String publicKey);
}
e.自定义HttpApiProcessor
在之前我们自定义了一个@MTuanHttpApi注解上指定了一个MTuanHttpApiProcessor
-----------------------------------------------------------------------------------------------------
@Slf4j
@Component
public class MTuanHttpApiProcessor implements HttpApiProcessor<MTuanHttpApi> {
/**
* 渠道方分配的公钥
*/
@Value("${channel.mtuan.publicKey}")
private String publicKey;
@Value("${channel.mtuan.appId}")
private String appId;
@Autowired
private Environment environment;
@Autowired
private WeatherApi weatherApi;
/** 实现-postBeforeHttpMetadata: 发送Http请求之前会回调该方法,可对Http请求体的内容进行二次处理
*
* @param httpMetadata 原来的请求体
* @param methodInvocation 被代理的方法
* @return 新的请求体
*/
@Override
public HttpMetadata postBeforeHttpMetadata(HttpMetadata httpMetadata, HttpApiMethodInvocation<MTuanHttpApi> methodInvocation) {
/**
* 在查询参数中添加提供的appId字段
*/
// 获取MTuanHttpApi注解
MTuanHttpApi apiAnnotation = methodInvocation.getProxyApiAnnotation();
// 获取MTuanHttpApi注解的appId,由于该appId是环境变量所以我们从environment中解析取出来
String appIdVar = apiAnnotation.appId();
appIdVar = environment.resolvePlaceholders(appIdVar);
// 添加到查询参数中
httpMetadata.putQueryParam("appId",appIdVar);
/**
* 生成签名sign字段
*/
// 获取所有查询参数
Map<String, Object> queryParam = httpMetadata.getHttpUrl().getQueryParam();
// 获取请求体参数
HttpBody body = httpMetadata.getBody();
// 生成签名
String signKey = createSignKey(queryParam,body);
// 将签名添加到请求头中
httpMetadata.putHeader("sign",signKey);
return httpMetadata;
}
private String createSignKey(Map<String, Object> queryParam, HttpBody body) {
// todo 伪代码
// 1、将查询参数拼接成字符串
String queryParamString = queryParam.entrySet()
.stream().map(e -> e.getKey() + "="+e.getValue())
.collect(Collectors.joining(";"));
// 2、将请求体参数拼接成字符串
String bodyString = "";
if (body instanceof HttpBodyJSON){
// application/json 类型的请求体
bodyString = body.toStringBody();
}elseif (body instanceof HttpBodyFormData){
// application/x-www-form-urlencoded 类型的请求体
bodyString = body.toStringBody();
}elseif (body instanceof HttpBodyMultipart){
// multipart/form-data 类型的请求体
bodyString = body.toStringBody();
}
// 使用公钥publicKey 加密拼接起来
String sign = publicKey + queryParamString + bodyString;
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(sign.getBytes());
return new String(digest);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
/**
* 实现-postBeforeHttpMetadata: 发送Http请求时,可定义发送请求的行为 或者打印请求和响应日志。
*/
@Override
public HttpResponse<?> postSendHttpRequest(HttpSender httpSender, HttpMetadata httpMetadata) {
// 忽略 weatherApi.getToken的方法回调,否则该方法也会回调此方法会递归死循环。 或者该接口指定自定义的HttpApiProcessor重写postSendingHttpRequest
Method getTokenMethod = ReflectionUtils.findMethod(WeatherServiceApi.class, "getToken",String.class,String.class);
if (getTokenMethod == null || getTokenMethod.equals(methodInvocation.getMethod())){
return httpSender.sendHttpRequest(httpMetadata);
}
// 1、动态获取token和sessionId
HttpResponse<String> httpResponse = weatherApi.getToken(appId, publicKey);
// 从响应体获取令牌token
String token = httpResponse.getBodyResult();
// 从响应头中获取sessionId
String sessionId = httpResponse.getHeader("sessionId");
// 把这两个值放到此次的请求cookie中
httpMetadata.addCookie(new Cookie("token",token));
httpMetadata.addCookie(new Cookie("sessionId",sessionId));
log.info("开始发送Http请求 请求接口:{} 请求体:{}",httpMetadata.getHttpUrl().toUrl(),httpMetadata.toHttpProtocol());
// 使用框架内置工具实现发送请求
HttpResponse<?> rsp = httpSender.sendHttpRequest(httpMetadata);
log.info("开始发送Http请求 响应结果:{}",rsp.toHttpProtocol());
return rsp;
}
/**
* 实现-postAfterHttpResponseBodyResult: 反序列化后Http响应体的内容后回调,可对该结果进行二次处理返回
* @param bodyResult Http响应体反序列化后的结果
* @param rsp 原始Http响应对象
* @param method 被代理的方法
* @param httpMetadata Http请求体
*/
@Override
public Object postAfterHttpResponseBodyResult(Object bodyResult, HttpResponse<?> rsp, Method method, HttpMetadata httpMetadata) {
if (bodyResult instanceof BaseRsp){
BaseRsp baseRsp = (BaseRsp) bodyResult;
// 设置
baseRsp.setCode(999);
}
return bodyResult;
}
}
-----------------------------------------------------------------------------------------------------
上面我们分别重写了postBeforeHttpMetadata、postSendHttpRequest、postAfterHttpResponseBodyResult三个生命周期的钩子方法去完成我们的需求
在发送请求前对请求体进行加签、在发送请求时动态获取令牌重新构建请求体和打印日志、在发送请求后给响应对象设置code为999
2.9 权限注解:sa-token
01.常见的自定义注解鉴权
a.定义自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ApiAccess {
/**
* 交易码
*
* @return 交易码
*/
String transCode();
}
b.实现拦截器
@Slf4j
@Component
public class ApiAccessInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("ApiAccessInterceptor preHandle");
if (handler instanceof HandlerMethod) {
Method method = ((HandlerMethod) handler).getMethod();
if (method.isAnnotationPresent(ApiAccess.class)) {
ApiAccess apiAccess = method.getAnnotation(ApiAccess.class);
String transCode = apiAccess.transCode();
String transCodeHeader = request.getHeader("transCode");
String token = request.getHeader("token");
if (!StringUtils.hasText(transCodeHeader) || !StringUtils.hasText(token)) {
throw new RuntimeException("transCode or token is empty");
}
log.info("transCode: {}, transCodeHeader:{}, token:{}", transCode, transCodeHeader, token);
if (!transCode.equals(transCodeHeader)) {
throw new RuntimeException("transCode not match");
}
ApiAccessUtil.valid(transCode, token);
}
}
return true;
}
}
c.辅助验证的方法,真实生产上应该是查数据库或其他
public class ApiAccessUtil {
public static final List<ApiAccessPO> API_ACCESS_LIST = new ArrayList<>();
static {
API_ACCESS_LIST.add(new ApiAccessPO("wnhyang01", "123456"));
API_ACCESS_LIST.add(new ApiAccessPO("wnhyang02", "234567"));
API_ACCESS_LIST.add(new ApiAccessPO("wnhyang03", "888888"));
API_ACCESS_LIST.add(new ApiAccessPO("wnhyang04", "666666"));
}
public static void valid(String transCode, String token) {
for (ApiAccessPO apiAccessPO : API_ACCESS_LIST) {
if (apiAccessPO.getTransCode().equals(transCode) && apiAccessPO.getToken().equals(token)) {
return;
}
}
throw new RuntimeException("invalid access");
}
@Data
@NoArgsConstructor
@AllArgsConstructor
private static class ApiAccessPO {
private String transCode;
private String token;
}
}
d.配置拦截器
@Slf4j
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final ApiAccessInterceptor apiAccessInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiAccessInterceptor);
}
}
e.验证接口
@Slf4j
@RestController
@RequestMapping("/api")
public class ApiAccessController {
@GetMapping("/wnhyang01")
@ApiAccess(transCode = "wnhyang01")
public String wnhyang01() {
return "wnhyang01";
}
@GetMapping("/wnhyang02")
@ApiAccess(transCode = "wnhyang02")
public String wnhyang02() {
return "wnhyang02";
}
@GetMapping("/wnhyang03")
@ApiAccess(transCode = "wnhyang03")
public String wnhyang03() {
return "wnhyang03";
}
@GetMapping("/wnhyang04")
@ApiAccess(transCode = "wnhyang04")
public String wnhyang04() {
return "wnhyang04";
}
}
-----------------------------------------------------------------------------------------------------
测试可知,只有 Header 的 transCode 和 token 都不为空
并且 Header 的 transCode 与接口配置的唯一 transCode 一致前提下
transCode 和 token 都能通过验证才能通过拦截器
02.使用新版Sa-Token完成
a.自定义注解
与前面一致就行
b.创建注解处理器
实现 SaAnnotationHandlerInterface 接口的两个抽象方法就好,checkMethod 放鉴权逻辑
与前面的拦截器方法一致就好,将拦截器里使用的 request 替换为 SaHolder.getRequest()
注意使用 @Component 将类注册为 IOC Bean 就省去了手动注册了
SaAnnotationStrategy.instance.registerAnnotationHandler(new CheckAccountHandler());
-----------------------------------------------------------------------------------------------------
@Slf4j
@Component
public class ApiAccessHandler implements SaAnnotationHandlerInterface<ApiAccess> {
@Override
public Class<ApiAccess> getHandlerAnnotationClass() {
return ApiAccess.class;
}
@Override
public void checkMethod(ApiAccess apiAccess, Method method) {
log.info("checkMethod");
String transCode = apiAccess.transCode();
String transCodeHeader = SaHolder.getRequest().getHeader("transCode");
String token = SaHolder.getRequest().getHeader("token");
if (!StringUtils.hasText(transCodeHeader) || !StringUtils.hasText(token)) {
throw new RuntimeException("transCode or token is empty");
}
log.info("transCode: {}, transCodeHeader:{}, token:{}", transCode, transCodeHeader, token);
if (!transCode.equals(transCodeHeader)) {
throw new RuntimeException("transCode not match");
}
ApiAccessUtil.valid(transCode, token);
}
}
c.配置拦截器
与前面一样,删除自己的拦截器配置,加上 SaInterceptor 就好
-----------------------------------------------------------------------------------------------------
@Slf4j
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final ApiAccessInterceptor apiAccessInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 删除 registry.addInterceptor(apiAccessInterceptor);
registry.addInterceptor(new SaInterceptor());
}
}
d.验证
试过你就会发现,前后效果一样
2.10 缓存注解:simple-cache
00.介绍
添加springboot项目
框架中去除了redisconfig类,避免了redis的单机和集群问题
用户可以自定义使用自己项目中的redisTemplate的bean,只需要配置redisTemplate的名称
RedisCache注解添加了返回类型和添加了TimeUnit
01.使用
引入simple-cache-spring-boot-starter依赖
在启动类加上@EnableSimpleCache注解
在配置文件中填写属于你自己项目的redistemplate的bean的名称
在你的业务类方法上添加注解RedisCache
02.代码
a.依赖
<dependency>
<groupId>io.gitee.antopen</groupId>
<artifactId>simple-cache-spring-boot-starter</artifactId>
<version>1.2.0</version>
</dependency>
b.在启动类加上注解
@EnableSimpleCache
c.在配置文件中填写属于你自己项目的redistemplate的bean的名称
如果你redistemplate没有配置bean的名称,则可以不填写,框架会自动拿名称为redisTemplate的bean
如果你配置的redistemplate的bean的名称,如上面的前置条件里面一样,则需要配置redisTemplateName
simplecache.redisTemplateName=myRedisTemplate
d.业务类方法上添加注解RedisCache
a.代码
@RedisCache(key = "testParams",expire = 100,resultClass = Result.class)
public Result testParams(String name, String sex) {
JSONObject jsonObject = new JSONObject();
jsonObject.put(name, UUID.randomUUID());
jsonObject.put(sex, new Random().nextInt(2));
return Result.buildSuccess(jsonObject);
}
b.@RedisCache可选值
属性 类型 必须指定 默认值 描述
key string 是 无 缓存的key,可以自定义
expire long 否 1(一天) 缓存的时间,可以修改
unit TimeUnit 否 TimeUnit.DAYS(一天) 缓存的单位,可以修改
resultClass Class<?> 否 JSONObject.class 返回类的class,方便序列化,可以修改
2.11 消息中间件:FolkMQ
00.介绍
FolkMQ 是一个 “纯血国产” 的消息中间件。支持内嵌、单机、集群、多重集群等多种部署方式
内嵌版,就相当于 H2 或 SQLite 数据库一样
大项目,则可以使用独立部署的单机版”或集群版
01.内嵌版
FolkMQ 内嵌版(带 Web 控制台界面的),体积增加 7Mb,就可以附加完整消息中间件的能力喽
比如你在用 “诺依” 开发个小项目,需要消息中间件,但是又不想独立部署。这就很适合呢!
内嵌版与单机板,功能一模一样
控制台界面是基于 Solon 框架 开发的,非常小巧。(可以用宿主项目的端口,也可以独立端口)
02.代码
a.依赖
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon.web.servlet.jakarta</artifactId>
<version>2.8.3</version>
</dependency>
<dependency>
<groupId>org.noear</groupId>
<artifactId>folkmq-broker-embedded</artifactId>
<version>1.7.1</version>
</dependency>
b.配置类FolkMqConfig:专门安排它一个包名folkmq,可以缩小 solon 的扫描范围
package demoapp.folkmq;
@Configuration
public class FolkMqConfig {
@PostConstruct
public void start() {
//启动 solon
Solon.start(FolkMqConfig.class, new String[]{});
}
@PreDestroy
public void stop() {
if (Solon.app() != null) {
//停止 solon(根据配置,可支持两段式安全停止)
Solon.stopBlock(false, Solon.cfg().stopDelay());
}
}
@Bean
public FilterRegistrationBean folkmqAdmin(){
//通过 Servlet Filter 实现 http 能力对接
FilterRegistrationBean<SolonServletFilter> filter = new FilterRegistrationBean<>();
filter.setName("SolonFilter");
filter.addUrlPatterns("/folkmq/*");
filter.setFilter(new SolonServletFilter());
return filter;
}
}
c.配置文件folkmq.yml
# 如果使用 servelt 则使用与 sprongboot 相同的等口
server.port: 8080
# 避免与其它 token 冲突
server.session.state.jwt.name: FOLKMQ-TOKEN
# 消息控制台账号密码
folkmq.admin: admin
# 消息传输协议(tcp 或 ws)
folkmq.schema: tcp
# 消息传输端口(默认为 server.port + 10000)
folkmq.transport.port: 0
d.测试类
public class ClientTest {
public static void main(String[] args) throws Exception {
MqClient client = FolkMQ.createClient("folkmq://localhost:18080")
.nameAs("demoapp")
.connect();
//订阅消息
client.subscribe("demo.topic", message -> {
System.out.println(message);
});
for (int i = 0; i < 10; i++) {
//发布消息
client.publish("demo.topic", new MqMessage("hello" + i));
}
}
}
e.web页面
http://127.0.0.1:8080/folkmq
2.12 列式数据库:ClickHouse
00.介绍
ClickHouse是一个高性能的面向列的 SQL 数据库管理系统 (DBMS),用于在线分析处理 (OLAP)
它既有 开源软件 版本,也有 云服务 版本
01.依赖
<dependency>
<groupId>com.clickhouse</groupId>
<artifactId>clickhouse-jdbc</artifactId>
<version>0.6.0</version>
<classifier>all</classifier>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
02.配置
spring:
# 数据库配置
datasource:
dynamic:
# 设置默认的数据源或者数据源组,默认值为 master
primary: mysql
datasource:
mysql:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/xxx?serverTimezone=Asia/Shanghai
username: root
password: 111111
clickhouse:
driver-class-name: com.clickhouse.jdbc.ClickHouseDriver
url: jdbc:ch://localhost/default?serverTimezone=Asia/Shanghai
03.整合MyBatis-Plus
a.mapper文件
/**
* @author pine
*/
@DS("clickhouse")
public interface ClickHouseTestMapper extends BaseMapper<Table> {
}
b.说明
ClickHouseTestMapper就拥有基础的增删改查能力了
04.测试
@SpringBootTest
public class MapperTest {
@Resource
private BackupMapper backupMapper;
@Resource
private ClickHouseTestMapper clickHouseTestMapper;
@Test
void mapperTest() {
List<Table> o = clickHouseTestMapper.selectList(
Wrappers.<Table>lambdaQuery()
.select(Table::getMessage)
);
System.out.println(o);
}
}
2.13 函数式接口:FunctionalUtils
01.功能
a.异步处理网络请求
在处理网络请求时,异步执行可以避免I/O操作阻塞主线程
使用FunctionalUtils的supplyAsync方法,可以轻松地将网络请求任务提交给线程池,并在请求完成时处理结果
b.数据流处理
对于数据流的处理,FunctionalUtils提供了map和filter方法,它们可以对集合进行转换和过滤
这些方法利用Java的流API,允许开发者以声明式的方式处理数据
c.事务性文件操作
在执行一系列文件操作时,如创建、写入和删除文件,可能需要这些操作要么全部成功要么全部失败
FunctionalUtils的performTransaction方法可以保证这种一致性
d.性能考虑
尽管FunctionalUtils增加了编程的便利性,但在使用异步和多线程功能时,性能和资源管理仍然是关键考虑因素
FunctionalUtils使用了一个缓存线程池来优化任务的执行
但需要在应用程序关闭时调用shutdownExecutorService方法来释放线程池资源,避免潜在的资源泄露
02.工具类
package com.dereksmart.crawling.fuc;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.Collection;
import java.util.function.Predicate;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.*;
/**
* @Author derek_smart
* @Date 2024/9/20 8:08
* @Description FunctionalUtils工具类
*/
public class FunctionalUtils {
// 共享线程池,用于异步操作
private static final ExecutorService executorService = Executors.newCachedThreadPool();
/**
* 从Supplier获取值,并返回它。
*/
public static <T> T get(Supplier<T> supplier) {
return supplier.get();
}
/**
* 处理对象T,使用Consumer进行消费。
*/
public static <T> void consume(T item, Consumer<T> consumer) {
consumer.accept(item);
}
/**
* 应用函数到输入值,并返回结果。
*/
public static <T, R> R apply(T item, Function<T, R> function) {
return function.apply(item);
}
/**
* 重复执行Supplier提供的操作,直到满足条件。
*/
public static <T> void repeatUntil(Supplier<Boolean> condition, Supplier<T> operation) {
while (!condition.get()) {
operation.get();
}
}
/**
* 对象转换,将输入对象T使用转换函数转换为R。
*/
public static <T, R> R transform(T input, Function<T, R> transformer) {
return transformer.apply(input);
}
/**
* 安全地执行一个可能抛出异常的操作,异常被捕获并处理。
*/
public static void runSafely(Runnable operation, Consumer<Exception> exceptionHandler) {
try {
operation.run();
} catch (Exception e) {
exceptionHandler.accept(e);
}
}
/**
* 创建并返回一个新对象,通过Supplier接口。
*/
public static <T> T create(Supplier<T> supplier) {
return supplier.get();
}
/**
* 执行一个条件检查,如果条件为真,则执行操作。
*/
public static void doIf(Supplier<Boolean> condition, Runnable action) {
if (condition.get()) {
action.run();
}
}
/**
* 执行一个操作,并返回一个状态值,通常用于链式操作中的状态检查。
*/
public static boolean tryPerform(Runnable action) {
try {
action.run();
return true;
} catch (Exception e) {
return false;
}
}
/**
* 对集合中的每个元素执行给定的操作。
*/
public static <T> void forEach(Collection<T> collection, Consumer<T> action) {
for (T item : collection) {
action.accept(item);
}
}
/**
* 对集合中的元素进行过滤,并返回一个新的集合。
*/
public static <T> Collection<T> filter(Collection<T> collection, Predicate<T> predicate) {
Collection<T> result = createCollectionInstance(collection);
for (T item : collection) {
if (predicate.test(item)) {
result.add(item);
}
}
return result;
}
/**
* 创建与给定集合类型相同的空集合实例。
*/
private static <T> Collection<T> createCollectionInstance(Collection<T> collection) {
try {
return collection.getClass().newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException("Could not create a new instance of the collection class", e);
}
}
/**
* 对集合中的元素应用转换函数,并返回一个新的集合。
*/
public static <T, R> Collection<R> map(Collection<T> collection, Function<T, R> mapper) {
Collection<R> result = (Collection<R>) createCollectionInstance(collection);
for (T item : collection) {
result.add(mapper.apply(item));
}
return result;
}
/**
* 异步执行一个操作,返回CompletableFuture。
*/
public static CompletableFuture<Void> runAsync(Runnable runnable) {
return CompletableFuture.runAsync(runnable, executorService);
}
/**
* 异步执行一个有返回值的操作,返回CompletableFuture。
*/
public static <T> CompletableFuture<T> supplyAsync(Supplier<T> supplier) {
return CompletableFuture.supplyAsync(supplier, executorService);
}
/**
* 异步执行一个操作,并在操作完成时使用Consumer处理结果。
*/
public static <T> void runAsync(T item, Consumer<T> consumer) {
CompletableFuture.runAsync(() -> consumer.accept(item), executorService);
}
/**
* 异步执行一个有返回值的操作,并应用Function处理结果。
*/
public static <T, R> CompletableFuture<R> applyAsync(T item, Function<T, R> function) {
return CompletableFuture.supplyAsync(() -> function.apply(item), executorService);
}
/**
* 使用CompletableFuture执行一个操作,并在操作完成时进行回调。
*/
public static <T> void whenCompleteAsync(Supplier<T> supplier, BiConsumer<? super T, ? super Throwable> action) {
CompletableFuture.supplyAsync(supplier, executorService).whenCompleteAsync(action, executorService);
}
/**
* 执行一个事务性操作,确保所有步骤都成功完成,否则回滚。
*/
public static <T> boolean performTransaction(Supplier<Boolean>... operations) {
boolean success = true;
for (Supplier<Boolean> operation : operations) {
if (!operation.get()) {
success = false;
break;
}
}
if (!success) {
// Rollback logic if necessary
}
return success;
}
/**
* 关闭工具类使用的线程池资源,应当在应用程序关闭时调用。
*/
public static void shutdownExecutorService() {
executorService.shutdown();
}
}
---------------------------------------------------------------------------------------------------------
get(Supplier<T> supplier) 从Supplier获取值
consume(T item, Consumer<T> consumer) 对象消费,执行Consumer的操作
apply(T item, Function<T, R> function) 对象转换,将输入对象转换为另一种类型
repeatUntil(Supplier<Boolean> condition, Supplier<T> operation) 重复执行操作,直到条件满足
transform(T input, Function<T, R> transformer) 对象转换的另一种形式
runSafely(Runnable operation, Consumer<Exception> exceptionHandler) 安全执行操作,异常通过Consumer处理
create(Supplier<T> supplier) 创建对象实例
doIf(Supplier<Boolean> condition, Runnable action) 条件执行,如果条件为真,则执行操作
tryPerform(Runnable action) 尝试执行操作,返回执行成功或失败的布尔值
forEach(Collection<T> collection, Consumer<T> action) 遍历集合,对每个元素执行操作
filter(Collection<T> collection, Predicate<T> predicate) 过滤集合,根据条件返回满足条件的元素集合
map(Collection<T> collection, Function<T, R> mapper) 转换集合,对集合中的每个元素应用转换函数,返回转换后的元素集合
createCollectionInstance(Collection<T> collection) 创建与给定集合类型相同的空集合实例,用于filter和map方法
runAsync(Runnable runnable) 异步执行一个无返回值的操作
supplyAsync(Supplier<T> supplier) 异步执行一个有返回值的操作
runAsync(T item, Consumer<T> consumer) 异步执行一个消费者操作
applyAsync(T item, Function<T, R> function) 异步执行一个函数操作并返回结果
whenCompleteAsync(Supplier<T> supplier, BiConsumer<? super T, ? super Throwable> action) 异步执行一个操作并在操作完成时进行回调
performTransaction(Supplier<Boolean>... operations) 执行一个事务性操作,确保所有步骤都成功完成,否则执行回滚逻辑
shutdownExecutorService() 关闭线程池资源,当应用程序关闭时应当调用此方法
03.测试类
package com.dereksmart.crawling.fuc;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* @Author derek_smart
* @Date 2024/9/20 8:28
* @Description FunctionalUtils测试类
*/
public class FunctionalUtilsExample {
public static void main(String[] args) {
// 异步执行无返回值的操作
CompletableFuture<Void> future = FunctionalUtils.runAsync(() -> {
performSomeLongRunningOperation();
System.out.println("Async operation performed on thread: " + Thread.currentThread().getName());
});
// 异步执行有返回值的操作,并处理结果
CompletableFuture<Integer> futureWithResult = FunctionalUtils.supplyAsync(() -> {
System.out.println("Async operation with result performed on thread: " + Thread.currentThread().getName());
return calculateSomeValue();
});
// 在futureWithResult操作完成后,对结果进行消费处理
futureWithResult.thenAccept(result -> System.out.println("Result of async operation: " + result));
// 异步执行消费者操作
FunctionalUtils.runAsync("Hello, World!", message -> {
System.out.println("Async consumer executed with message: " + message);
});
// 异步执行函数操作并返回结果
CompletableFuture<String> futureTransformed = FunctionalUtils.applyAsync(42, number -> {
System.out.println("Async function executed with number: " + number);
return "Transformed number: " + (number * 2);
});
// 异步执行操作,并在操作完成时进行回调
FunctionalUtils.whenCompleteAsync(() -> "Operation completed", (result, throwable) -> {
if (throwable == null) {
System.out.println("Async operation completed with result: " + result);
} else {
System.out.println("Async operation failed with exception: " + throwable);
}
});
// 执行事务性操作
boolean transactionResult = FunctionalUtils.performTransaction(
() -> performStep1(),
() -> performStep2(),
() -> performStep3()
);
System.out.println("Transaction result: " + transactionResult);
// 等待异步操作完成,这里只是为了示例需要
// 在实际应用中应该以更合适的方式等待异步操作的完成
awaitCompletionOfFutures(future, futureWithResult, futureTransformed);
// 应用程序关闭时,关闭线程池资源
FunctionalUtils.shutdownExecutorService();
}
private static void performSomeLongRunningOperation() {
// 模拟长时间运行的操作
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private static int calculateSomeValue() {
// 模拟计算操作
return 42;
}
private static boolean performStep1() {
// 模拟事务步骤1
return true; // 假设步骤成功
}
private static boolean performStep2() {
// 模拟事务步骤2
return true; // 假设步骤成功
}
private static boolean performStep3() {
// 模拟事务步骤3
return true; // 假设步骤成功
}
@SafeVarargs
private static void awaitCompletionOfFutures(CompletableFuture<?>... futures) {
CompletableFuture.allOf(futures).join();
}
}
---------------------------------------------------------------------------------------------------------
使用FunctionalUtils类来异步执行操作、处理返回结果、执行消费者操作、执行函数操作并返回结果、在操作完成时进行回调以及执行事务性操作
runAsync: 异步执行长时间运行的操作
supplyAsync: 异步执行有返回值的操作
runAsync with consumer: 异步执行消费者操作
applyAsync: 异步执行函数操作并返回结果
whenCompleteAsync: 异步执行操作并在操作完成时进行回调
performTransaction: 执行一系列事务性操作,如果所有步骤都成功,则返回true,否则返回false
awaitCompletionOfFutures: 等待所有异步操作完成,这是为了示例需要,实际应用应该有更合适的异步操作等待策略
shutdownExecutorService: 关闭线程池资源,应在应用程序关闭时调用
2.14 内部聚合流+异步处理:Flink
01.引入maven的配置依赖
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<flink.version>1.7.0</flink.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-core</artifactId>
<version>1.13.2</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients_2.12</artifactId>
<version>1.13.2</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-runtime-web_2.12</artifactId>
<version>1.13.2</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java_2.12</artifactId>
<version>1.13.2</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka_2.12</artifactId>
<version>1.13.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.67</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
</dependency>
</dependencies>
02.编写主要的flink代码
package org.idea.flink.api;
import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.tuple.Triple;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.streaming.api.datastream.AsyncDataStream;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer011;
import org.apache.flink.util.Collector;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.idea.flink.api.constants.KafkaProperties;
import org.idea.flink.api.model.UserClickMsg;
import org.idea.flink.api.model.UserMsgFlatMapFunction;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
/**
* @author idea
* @create 2024/3/16 17:46
* @description 接收kafka数据源
*/
public class KafkaSourceMain {
private static StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
public static void main(String[] args) throws Exception {
System.out.println("开启flink程序接收kafka数据源");
env.enableCheckpointing(60000);
Properties properties = new Properties();
properties.setProperty("bootstrap.servers", KafkaProperties.KAFKA_BROKER_ADDRESS);
properties.setProperty("group.id", "test_group_id");
FlinkKafkaConsumer userCenterSource = new FlinkKafkaConsumer(KafkaProperties.USER_SERVICE_TOPIC, new SimpleStringSchema(), properties);
DataStream<UserClickMsg> userDataStream = env.addSource(userCenterSource).name("用户消息").flatMap(new UserMsgFlatMapFunction());
FlinkKafkaConsumer orderCenterSource = new FlinkKafkaConsumer(KafkaProperties.ORDER_SERVICE_TOPIC, new SimpleStringSchema(), properties);
DataStream<UserClickMsg> orderDataStream = env.addSource(orderCenterSource).name("订单中心").flatMap(new UserMsgFlatMapFunction());
DataStream<UserClickMsg> unionDataStream = userDataStream;
unionDataStream.union(orderDataStream);
unionDataStream.print();
SingleOutputStreamOperator<UserClickMsg> singleOutputStreamOperator = unionDataStream.
keyBy(userClickMsg -> userClickMsg.getUserId() + ":" + userClickMsg.getGoodId() + ":" + userClickMsg.getAction() + ":" + userClickMsg.getPlatform())
.process(new KeyedProcessFunction<String, UserClickMsg, UserClickMsg>() {
@Override
public void processElement(UserClickMsg value, KeyedProcessFunction<String, UserClickMsg, UserClickMsg>.Context ctx, Collector<UserClickMsg> out) throws Exception {
out.collect(value);
}
}).name("旁路上报kafka消息");
singleOutputStreamOperator.flatMap(new FlatMapFunction<UserClickMsg, String>() {
@Override
public void flatMap(UserClickMsg userClickMsg, Collector<String> collector) throws Exception {
collector.collect(JSON.toJSONString(userClickMsg));
}
}).addSink(getSinkOutProducer()).name("去重后上报kafka消息");
DataStream<String> asyncStream = AsyncDataStream.unorderedWait(singleOutputStreamOperator,new AsyncHandler(),5, TimeUnit.SECONDS);
asyncStream.addSink(getAsyncProducer()).name("异步流处理结果");
asyncStream.print();
env.execute("test-flink-consumer");
}
public static FlinkKafkaProducer<String> getAsyncProducer() {
Properties rptDataSinkProp = new Properties();
rptDataSinkProp.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, KafkaProperties.KAFKA_BROKER_ADDRESS);
rptDataSinkProp.setProperty(ProducerConfig.TRANSACTION_TIMEOUT_CONFIG, 1000 * 700 + "");
rptDataSinkProp.setProperty(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, "1");
rptDataSinkProp.setProperty(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true");
return new FlinkKafkaProducer<String>( "async_handle_result", new SimpleStringSchema(),rptDataSinkProp);
}
public static FlinkKafkaProducer<String> getSinkOutProducer() {
Properties rptDataSinkProp = new Properties();
rptDataSinkProp.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, KafkaProperties.KAFKA_BROKER_ADDRESS);
rptDataSinkProp.setProperty(ProducerConfig.TRANSACTION_TIMEOUT_CONFIG, 1000 * 700 + "");
rptDataSinkProp.setProperty(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, "1");
rptDataSinkProp.setProperty(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true");
return new FlinkKafkaProducer<String>( "sink_out_topic", new SimpleStringSchema(),rptDataSinkProp);
}
}
03.UserClickMsg消息对象
/**
* @author idea
* @create 2024/3/16 19:54
* @description
*/
public class UserClickMsg {
private String platform;
private Long userId;
private int action;
private Long goodId;
public String getPlatform() {
return platform;
}
public void setPlatform(String platform) {
this.platform = platform;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public int getAction() {
return action;
}
public void setAction(int action) {
this.action = action;
}
public Long getGoodId() {
return goodId;
}
public void setGoodId(Long goodId) {
this.goodId = goodId;
}
@Override
public String toString() {
return "UserClickMsg{" +
"platform='" + platform + ''' +
", userId=" + userId +
", action=" + action +
", goodId=" + goodId +
'}';
}
}
04.用于格式转换的FlatMapFunction
package org.idea.flink.api.model;
import com.alibaba.fastjson.JSON;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.util.Collector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author idea
* @create 2024/3/16 19:55
* @description 用户消息转换函数
*/
public class UserMsgFlatMapFunction implements FlatMapFunction<String, UserClickMsg> {
private static final Logger LOGGER = LoggerFactory.getLogger(UserMsgFlatMapFunction.class);
@Override
public void flatMap(String msg, Collector<UserClickMsg> collector) throws Exception {
try {
UserClickMsg userMsg = JSON.parseObject(msg, UserClickMsg.class);
System.out.println(userMsg);
collector.collect(userMsg);
} catch (Exception e) {
LOGGER.error("msg content error:{}", msg);
}
}
}
05.异步处理
package org.idea.flink.api;
import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.RandomUtils;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.async.ResultFuture;
import org.apache.flink.streaming.api.functions.async.RichAsyncFunction;
import org.idea.flink.api.model.UserClickMsg;
import java.util.Collections;
import java.util.concurrent.*;
/**
* @author idea
* @create 2024/3/17 08:35
* @description 异步处理handler
*/
public class AsyncHandler extends RichAsyncFunction<UserClickMsg, String> {
private transient ThreadPoolExecutor threadPool;
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
threadPool = new ThreadPoolExecutor(10, 100, 3000, TimeUnit.MICROSECONDS,
new ArrayBlockingQueue<>(1000), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("async-handle-" + RandomUtils.nextInt(0,100));
return thread;
}
});
}
@Override
public void close() throws Exception {
super.close();
threadPool.shutdown();
}
@Override
public void asyncInvoke(UserClickMsg input, ResultFuture<String> resultFuture) throws Exception {
CompletableFuture.runAsync( ()->{
System.out.println("异步处理数据:" + Thread.currentThread().getName() + "|" + JSON.toJSONString(input));
resultFuture.complete(Collections.singleton("success"));
},threadPool);
}
}
06.代码中的操作解释
a.flatmap
你可以理解为类似jdk8的lambda表达式中的扁平化处理,将对象的输入格式A转换为输出格式B
b.union
聚合流,你可以理解为将多个输入流合并成一个流,这样对于我们的代码处理会更简单一些,但是要确保多个流的格式统一才行
c.addSink
添加输出流,一般我们会把flink处理的结果sink出去,例如投递到下一个Kafka主题上
d.AsyncDataStream.unorderedWait
异步处理,一般我们会将一些比较复杂的业务操作,例如es查询,mongodb查询等等
07.运行效果
我们往 user_service_topic,order_service_topic 两个主题的投递Kafka消息,然后监听
async_handle_result,sink_out_topic 主题 可以分别看到flink异步处理后的结果,以及所有投递过来的消息转换格式后的内容
3 扩展
3.1 工具
01.Hutool工具类
类型转换工具类-Convert
日期时间工具-DateUtil
IO流相关 新建、删除、复制、移动、改名
线程和并发 ThreadUtil.execAsync
工具类:StrUtil、ObjectUtil、ArrayUtil | XmlUtil、NumberUtil、MathUtil、ReUtil | ReflectUtil sub、format | equals、contains | addAll、filter、contains、zip、join | findAll、isMatch | ReflectUtil.newInstance(ExamInfoDict.class).run();
语言特性:Dict、Console、Validator、StrFormatter、TreeUtil Console.log、Console.error | isEmail、isMatchRegex
JavaBean:BeanUtil、Opt BeanUtil.copyProperties(p1, p2);
集合类:CollUtil、ListUtil | ConcurrentHashMap、ConcurrentHashSet、LineIter join、append、addAll、zip(变map) | split、sub、indexOfAll、sortByProperty
Map:MapUtil、BiMap、TableMap、MapBuilder toListMap、toMapList、reverse、sort | 双向查找BiMap、可重复键值TableMap、流式构建器-MapBuilder
文本操作:CsvUtil、UnicodeUtil、StrBuilder、StrSplitter StrBuilder.create(); builder.reset(); | split、splitByRegex、splitByLength
比较器:CompareUtil
JSON:JSONUtil、JSONObject、JSONArray
数据库:SqlExecutor、Session
HTTP客户端:HttpUtil、HtmlUtil、HttpResponse、HttpRequest
定时任务:CronUtil
图形验证码:Captcha
Office文档操作:ExcelUtil、ExcelWriter、ExcelReader
JWT:JWTUtil、JWTSignerUtil、JWTValidator
扩展:ServletUtil、QrCodeUtil、MailUtil、FTP、Jsch封装、ZipUtil、CompressUtil、PinyinUtil ServletUtil.getClientIP(request)
02.JDK自带
Collections:sort、reverse、shuffle、min、max、synchronizedList、unmodifiableList
Arrays:sort、binarySearch、copyOf、fill、asList
Objects:isNull、nonNull、equals、hash、requireNonNull
Optional:of、empty、isPresent、ifPresent、orElse
time:LocalDate、LocalTime、LocalDateTime、ZonedDateTime、Duration、Period
Stream:filter、map、collect、forEach、reduce
function:Predicate、Function、Consumer、Supplier
03.common-lang3
ArrayUtils:isEmpty、isNotEmpty、add、removeElement、contains、toObject
StringUtils:isEmpty、isNotEmpty、isBlank、isNotBlank、join、split、contains、substring
ObjectUtils:isEmpty、isNotEmpty、defaultIfNull、equals、hashCode
DateUtils:addDays、addMonths、addYears、truncate、isSameDay
04.Hutool升级到6.0后
包名修改,包名变更为org.dromara.hutool
重新整理规整,内部工具类重新整理规整,减少无用模块,工具类归类到对应package中
重构Http模块,被人诟病的http模块做重构,采用门面模式,支持包括HttpUrlConnection、HttpClient4、HttpClient5、OkHttp3等库
性能优化,在能力范围之内尽量做性能优化,不跟其他高性能库“攀比”
做减法,相比5.x版本做减法,大部分工作是删掉一些重复代码和无用的重载,使用上可能会增加代码量,但是相比减少了歧义
统一构造方法,构建一种对象不再使用混乱的createXXX、newXXX、ofXXX等名字,而是统一使用of或者ofXXX
3.2 定时:Cron
01.定义
Cron表达式是一种用于定义时间调度的字符串格式
它由5到7个字段组成,每个字段用空格分隔。每个字段代表不同的时间单位,如秒、分钟、小时等
02.语法格式
a.字段及其可用字符
econds (秒): 允许使用 , - * /,范围为 0-59
Minutes (分钟): 允许使用 , - * /,范围为 0-59
Hours (小时): 允许使用 , - * /,范围为 0-23
Day of Month (日期): 允许使用 , - * / ? L W C,范围为 1-31
Month (月份): 允许使用 , - * /,范围为 1-12 或 JAN-DEC
Day of Week (星期): 允许使用 , - * / ? L C #,范围为 1-7 或 SUN-SAT,其中 1 表示星期天
Year (年): 允许使用 , - * /,范围为 1970-2099
b.特殊字符
*:匹配任意值。例如,* 在分钟字段表示每分钟
?:仅用于 Day of Month 和 Day of Week,表示不指定具体值
-:表示范围。例如,5-20 在分钟字段表示从5到20分钟
/:表示步长。例如,5/20 在分钟字段表示从第5分钟开始,每隔20分钟
,:表示枚举值。例如,5,20 在分钟字段表示第5和第20分钟
L:表示最后一个,仅用于 Day of Month 和 Day of Week
W:表示最近的工作日,仅用于 Day of Month
LW:表示某月的最后一个工作日
#:用于指定每月的第几个星期几,仅用于 Day of Week
03.示例
0 0 2 1 * ? *:每月1日凌晨2点执行任务。
0 15 10 ? * MON-FRI:周一到周五每天上午10:15执行任务。
0 15 10 ? 6L 2002-2006:2002到2006年每月的最后一个星期五上午10:15执行任务。
3.3 定时:Quartz
01.概念
a.介绍
Quartz 是一个功能丰富开放源码的任务调度框架。
几乎可以集成到任何Java应用程序中——从最小的独立应用程序到最大的电子商务系统。
可以创建简单或者复杂的调度用来执数十个甚至成百上万个作业;
Quartz功能强大可以让你的程序在指定时间执行,也可以按照某一个频度执行,支持数据库、监听器、插件、集群等特性。
-----------------------------------------------------------------------------------------------------
Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,完全由Java开发,可以用来执行定时任务,
类似于java.util.Timer。但是相较于Timer, Quartz增加了很多功能:
持久性作业 - 就是保持调度定时的状态;
作业管理 - 对调度作业进行有效的管理;
b.使用场景
定时消息推送、定时抢购、定时发送邮件、定时统计
c.注解:@DisallowConcurrentExecution
如果项目中出现定时任务并发执行的情况可以在job类上加上注解 @DisallowConcurrentExecution 来解决。
注意:@DisallowConcurrentExecution是对JobDetail实例生效,如果一个job类被不同的jobdetail引用,这样是可以并发执行。
如果是Spring可以将job的concurrent属性设置为false。默认是true
-----------------------------------------------------------------------------------------------------
<bean id="scheduleJob" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="scheduleBusinessObject"/>
<property name="targetMethod" value="doIt"/>
<property name="concurrent" value="false"/>
</bean>
d.监听器
监听器顾名思义,就是对事件进行监听并且加入自己相应的业务逻辑,
主要有以下三个监听器分别对Job,Trigger,Scheduler进行监听。
-----------------------------------------------------------------------------------------------------
JobListener
TriggerListener
SchedulerListener
e.表说明
| Table Name | Description
|--------------------------|--------------------------------------
| QRTZ_CALENDARS | 存储Quartz的Calendar信息
| QRTZ_CRON_TRIGGERS | 存储CronTrigger,包括Cron表达式和时区信息
| QRTZ_FIRED_TRIGGERS | 存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息
| QRTZ_PAUSED_TRIGGER_GRPS | 存储已暂停的Trigger组的信息
| QRTZ_SCHEDULER_STATE | 存储少量的有关Scheduler的状态信息,和别的Scheduler实例
| QRTZ_LOCKS | 存储程序的悲观锁的信息
| QRTZ_JOB_DETAILS | 存储每一个已配置的Job的详细信息
| QRTZ_JOB_LISTENERS | 存储有关已配置的JobListener的信息
| QRTZ_SIMPLE_TRIGGERS | 存储简单的Trigger,包括重复次数、间隔、以及已触的次数
| QRTZ_BLOG_TRIGGERS | Trigger作为Blob类型存储
| QRTZ_TRIGGER_LISTENERS | 存储已配置的TriggerListener的信息
| QRTZ_TRIGGERS | 存储已配置的Trigger的信息
f.默认使用内存存储任务,可以改为JDBC
Quartz 默认使用的是内存的方式来存储任务,为了持久化,我们这里改为 JDBC 的形式,
并且指定 spring.quartz.jdbc.initialize-schema=never,这样我们可以手动创建数据表。
-----------------------------------------------------------------------------------------------------
因为该值的另外两个选项ALWAYS和EMBEDDED都不太符合我们的要求:
ALWAYS:每次都初始化
EMBEDDED:只初始化嵌入式数据库,比如说 H2、HSQL
g.第一次使用开启always
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: xxx
username: xxx
password: xxx
quartz:
job-store-type: jdbc # 定时任务的数据保存到jdbc即数据库中
jdbc:
# embedded:默认
# always:启动的时候初始化表,我们只在第一次启动的时候用它来自动创建表,然后改回embedded即可,不然数据每次都会被清空
# never:启动的时候不初始化表,也不知道和embedded有什么不同
initialize-schema: embedded
-----------------------------------------------------------------------------------------------------
第一次启动的时候请把上面的initialize-schema设置为always,这会在数据库里面自动建表,
然后第二次启动时改回embedded即可。
如果不需要定时任务的持久化就可以不管。
02.重要组成1
a.三要素
Scheduler、Trigger、JobDetail & Job
调度器 :Quartz框架的核心是调度器。调度器负责管理Quartz应用运行时环境。调度器不是靠自己做所有的工作,而是依赖框架内一些非常重要的部件。Quartz不仅仅是线程和线程池管理。为确保可伸缩性,Quartz采用了基于多线程的架构。启动时,框架初始化一套worker线程,这套线程被调度器用来执行预定的作业。这就是Quartz怎样能并发运行多个作业的原理。Quartz依赖一套松耦合的线程池管理部件来管理线程环境。
任务:这个很简单,就是我们自己编写的业务逻辑,交给quartz帮我们执行 。
触发器:简单的讲就是调度作业,什么时候开始执行,什么时候结束执行。
b.Job
是一个接口,接口中只定义了一个execute方法
void execute(JobExecutionContext var1) throws JobExecutionException;
通过实现接口中的execute方法,在方法中编写所需要定时执行的Job(任务),JobExecutionContext类提供了调度应用的一些信息。
Job运行时的信息保存在JobDataMap实例中。
c.JobDetail
JobDetail定义的是任务数据,而真正的执行逻辑是在Job中。
scheduler每次执行, 都会根据JobDetail创建一个新的job实例。
为什么需要有个JobDetai来作为job的定义,为什么不直接使用job?
解释:如果使用jobdetail来定义,那么每次调度都会创建一个new job实例,这样带来的好处就是任务并发执行的时候,互不干扰,不会对临界资源造成影响。
-----------------------------------------------------------------------------------------------------
quartz每次都会直接创建一个JobDetail,同时创建一个Job实例,它不直接接受一个Job的实例,但是它接受一个Job的实现类,
通过new instance()的反射方式来实例一个Job,在这里Job是一个接口,我们需要自己编写类去实现这个接口。
d.Scheduler
org.quartz.Scheduler非常重要是Quartz 调度程序的主要接口,用于组装jobDetail和trigger开始任务调度。
Scheduler维护了一个JobDetails 和Triggers的注册表。一旦在Scheduler注册过了,当定时任务触发时间一到,调度程序就会负责执行预先定义的Job。
调度程序Scheduler实例是通过SchedulerFactory工厂来创建的。
SchedulerFactory调度程序工厂,有两个默认的实现类:
DirectSchedulerFactory和StdSchedulerFactory。
-----------------------------------------------------------------------------------------------------
DirectSchedulerFactory:
DirectSchedulerFactory是一个org.quartz.SchedulerFactory的单例实现。
-----------------------------------------------------------------------------------------------------
StdSchedulerFactory:
是基于Quartz属性文件创建Quartz Scheduler 调度程序的,一般我们用这个偏多。
默认情况下是加载当前工作目录下的”quartz.properties”属性文件。如果加载失败,会去加载org/quartz包下的”quartz.properties”属性文件。
e.JobBulider
用于定义/构建已经定义了Job实例的JobDetail实例
f.TriggerBuilder
用于定义/构建Trigger实例。
g.Trigger1
a.介绍
是一个类,描述触发]ob执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个子类。
当且仅当需调度一次或者以固定时间间隔周期执行调度, SimpleTrigger是最适合的选择;
而CronTrigger则可以通过Cron表达式定义出各种复杂时间规则的调度方案:如工作日周一到周
五的15: 00~16: 00执行调度等
-------------------------------------------------------------------------------------------------
它由SimpleTrigger和CronTrigger组成,SimpleTrigger实现类似Timer的定时调度任务,CronTrigger可以通过cron表达式实现更复杂的调度逻辑·。
Scheduler:调度器,JobDetail和Trigger可以通过Scheduler绑定到一起。
b.Trigger常用方法
withIdentity:定义触发器一些属性 比如名字,组名。
usingJobData():给具体job传递参数
startNow():触发器立即执行
withSchedule():以哪种触发器出发
startAt() : 开始时间(java.util.Date)
endAt() : 结束时间,设置了结束时间则在这之后,不再触发
c.Trigger的重点内容就是在**withSchedule()**这个方法,一共有四种具体实现方法,分别是:
1、SimpleScheduleBuilder
2、DailyTimeIntervalScheduleBuilder
3、CalendarIntervalScheduleBuilder
4、CronScheduleBuilder
h.Trigger2
a.方式1:SimpleScheduleBuilder
最简单的触发器,表示从某一时刻开始,以一定的时间间隔执行任务。
-------------------------------------------------------------------------------------------------
属性:
repeatInterval 重复间隔。
repeatCount 重复次数。
-------------------------------------------------------------------------------------------------
例如:从现在开始,每分钟执行一次。
//定义Trigger触发器
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger", "group1")
//立即执行
.startNow()
//定时,每隔一秒钟执行一次
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(1)
//重复执行
.repeatForever())
.build();
b.方式2:DailyTimeIntervalScheduleBuilder
每一天的某一个时间段内,以一定的时间间隔执行任务,可以指定具体的某一天(星期一、星期二~~)
-------------------------------------------------------------------------------------------------
属性:
withIntervalInHours 重复间隔(秒、分钟、小时。。。)。
onDaysOfTheWeek 具体的星期。 默认 周一到周日
startingDailyAt 每天开始时间 默认 0.0
endingDailyAt 每天结束时间,默认 23.59.59
repeatCount 重复次数。 默认是-1 不限次数
interval 每次执行间隔
-------------------------------------------------------------------------------------------------
例如:比如每周一到周五中午12点开始,下午16点结束,每次执行间隔1 小时。
//定义Trigger触发器
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger", "group1")
//立即执行
.startNow()
.withSchedule(dailyTimeIntervalSchedule()
.startingDailyAt(TimeOfDay.hourAndMinuteOfDay(12, 00)) //每天12:00开始
.endingDailyAt(TimeOfDay.hourAndMinuteOfDay(16, 0)) //16:00 结束
.onDaysOfTheWeek(SUNDAY,MONDAY,TUESDAY,WEDNESDAY,THURSDAY) //周一至周五执行
.withIntervalInHours(1)) //每间隔1小时执行一次
.build();
c.方式3:CalendarIntervalScheduleBuilder
和SimpleScheduleBuilder类似,都是表示从某一时刻开始,以一定时间间隔执行任务。
但是SimpleScheduleBuilder无法指定一些特殊情况,比如每月执行一次,每周执行一次、每年执行一次
-------------------------------------------------------------------------------------------------
属性:
interval 执行间隔
intervalUnit 执行间隔的单位(秒,分,小,天,月,年,周)
-------------------------------------------------------------------------------------------------
例如:
//定义Trigger触发器
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger", "group1")
//立即执行
.startNow()
.withSchedule(calendarIntervalSchedule()
.withIntervalInWeeks(1) //每周执行一次
).build();
f.方式4:CronScheduleBuilder
通过cron表达式可以定义任意规则,实现以上的任意需求
-------------------------------------------------------------------------------------------------
//定义Trigger触发器
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger", "group1")
//立即执行
.startNow()
.withSchedule(cronSchedule("0 0/1 8-17 * * ?") // 每天8:00-17:00,每隔2分钟执行一次
).build();
03.重要组成2
a.Job接口
可以通过实现该就接口来实现我们自己的业务逻辑,该接口只有execute()一个方法,
我们可以通过下面的方式来实现Job接口来实现我们自己的业务逻辑
-----------------------------------------------------------------------------------------------------
public class HelloJob implements Job{
public void execute(JobExecutionContext context) throws JobExecutionException {
//编写我们自己的业务逻辑
}
}
b.JobDetail
每次都会直接创建一个JobDetail,同时创建一个Job实例,它不直接接受一个Job的实例,
但是它接受一个Job的实现类,通过new instance()的反射方式来实例一个Job。
可以通过下面的方式将一个Job实现类绑定到JobDetail中
-----------------------------------------------------------------------------------------------------
JobDetail jobDetail=JobBuilder.newJob(HelloJob.class).
withIdentity("myJob", "group1")
.build();
c.JobBuiler
主要是用来创建jobDeatil实例
d.JobStore:
绑定了Job的各种数据
e.trigger:
前文讲到它主要用来执行Job实现类的业务逻辑的,我们可以通过下面的代码来创建一个Trigger实例
-----------------------------------------------------------------------------------------------------
CronTrigger trigger = (CronTrigger) TriggerBuilder
.newTrigger()
.withIdentity("myTrigger", "group1") //创建一个标识符
.startAt(date)//什么时候开始触发
//每秒钟触发一次任务
.withSchedule(CronScheduleBuilder.cronSchedule("* * * * * ? *"))
.build();
f.Scheduler
a.总结
Scheduler 配置参数一般存储在quartz.properties中,我们可以修改参数来配置相应的参数。
通过调用getScheduler()方法就能创建和初始化调度对象。
-------------------------------------------------------------------------------------------------
Scheduler函数介绍:
Date schedulerJob(JobDetail,Trigger trigger);返回最近触发的一次时间
void standby()暂时挂起
void shutdown()完全关闭,不能重新启动了
shutdown(true)表示等待所有正在执行的job执行完毕之后,再关闭scheduler
shutdown(false)即直接关闭scheduler
b.创建Scheduler方式1:通过StdSchedulerFactory来创建
SchedulerFactory sfact=new StdSchedulerFactory();
Scheduler scheduler=sfact.getScheduler();
c.创建Scheduler方式2:通过DirectSchedulerFactory来创建
DiredtSchedulerFactory factory=DirectSchedulerFactory.getInstance();
Scheduler scheduler=factory.getScheduler();
04.配置流程
a.介绍
quartz.properties这个资源文件,在org.quartz这个包下,当我们程序启动的时候,
它首先会到我们的根目录下查看是否配置了该资源文件,如果没有就会到该包下读取相应信息,
当我们咋实现更复杂的逻辑时,需要自己指定参数的时候,可以自己配置参数来实现。
b.quartz.properties
a.介绍
org.quartz.scheduler.instanceName: DefaultQuartzScheduler
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 10
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
org.quartz.jobStore.misfireThreshold: 60000
org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
b.该资源文件主要组成部分
①调度器属性
②线程池属性
③作业存储设置
④插件设置
c.调度器属性
org.quartz.scheduler.instanceName属性用来区分特定的调度器实例,可以按照功能用途来给调度器起名。
org.quartz.scheduler.instanceId属性和前者一样,也允许任何字符串,但这个值必须是在所有调度器实例中是唯一的,尤其是在一个集群当中,作为集群的唯一key,假如你想quartz帮你生成这个值的话,可以设置我Auto
d.线程池属性
threadCount设置线程的数量
threadPriority设置线程的优先级
org.quartz.threadPool.class 线程池的实现
e.作业存储设置
描述了在调度器实例的声明周期中,job和trigger信息是怎么样存储的
f.插件配置
满足特定需求用到的quartz插件的配置
05.Java使用1
a.依赖
<!--quartz相关依赖-->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.3.0</version>
</dependency>
b.定义需要执行的任务内容,例如:输入当前系统时间
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.util.Date;
/**
* 定义要执行的任务内容
*/
public class MyQuartzJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
//输出系统当前时间
System.err.println("系统当前时间为+"+new Date());
}
}
c.构建调度任务1
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
public class QuartzTest{
public static void main(String[] args) {
try {
//获取调度器
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
//包装任务内容JobDetail
JobDetail jobDetail = JobBuilder.newJob(MyQuartzJob.class)
//定义name和group
.withIdentity("job1", "group1")
.usingJobData("name","今天天气不错")
.build();
//定义Trigger触发器
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger", "group1")
//立即执行
.startNow()
//定时,每隔一秒钟执行一次
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(1)
//重复执行
.repeatForever())
.build();
//组装任务
scheduler.scheduleJob(jobDetail,trigger);
//启动调度器 开始调度
scheduler.start();
//运行一段时间后(10秒)关闭任务
Thread.sleep(10000);
scheduler.shutdown();
} catch (SchedulerException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
d.构建调度任务2
public class QuartzManager{
private static final SimpleTrigger CronTrigger = null;
public static void main(String[] args){
}
public void simpleDemo(){
//通过SchedulerFactory来获取一个调度器
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler;
try {
scheduler = schedulerFactory.getScheduler();
//引进作业程序
JobDetail jobDetail =
new JobDetail("jobDetail-s1", "jobDetailGroup-s1", QuartzDemo.class);
//new一个触发器
SimpleTrigger simpleTrigger =
new SimpleTrigger("simpleTrigger", "triggerGroup-s1");
//设置作业启动时间
long ctime = System.currentTimeMillis();
simpleTrigger.setStartTime(new Date(ctime));
//设置作业执行间隔
simpleTrigger.setRepeatInterval(1000);
//设置作业执行次数
simpleTrigger.setRepeatCount(10);
//设置作业执行优先级默认为5
//simpleTrigger.setPriority(10);
//作业和触发器设置到调度器中
scheduler.scheduleJob(jobDetail, simpleTrigger);
//启动调度器
scheduler.start();
} catch (SchedulerException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void cronDemo(){
try {
SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();
Scheduler sched = schedFact.getScheduler();
sched.start();
JobDetail jobDetail = new JobDetail( " Income Report " ,
" Report Generation " , QuartzDemo.class );
jobDetail.getJobDataMap().put( " type " , " FULL " );
CronTrigger trigger = new CronTrigger( " Income Report " ,
" Report Generation " );
/**/ /* 每1分钟执行一次 */
trigger.setCronExpression( "0 33 16 * * ?" );
sched.scheduleJob(jobDetail, trigger);
} catch (Exception e) {
e.printStackTrace();
}
}
public void caledarDemo(){
//通过SchedulerFactory来获取一个调度器
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler;
try {
scheduler = schedulerFactory.getScheduler();
//引进作业程序
JobDetail jobDetail =
new JobDetail("jobDetail-s1", "jobDetailGroup-s1", QuartzDemo.class);
//new一个触发器
CronTrigger simpleTrigger =
new CronTrigger("trigger", "group", "job", "group", "16 26/1 8-17 * * ?");
// new SimpleTrigger("simpleTrigger", "triggerGroup-s1");
//设置作业启动时间
//Calendar excelCal = Calendar.getInstance();
//excelCal.add(Calendar.DAY_OF_MONTH, 1);
///excelCal.set(Calendar.HOUR_OF_DAY, 16);
//excelCal.set(Calendar.SECOND, 0);
//excelCal.add(Calendar.MINUTE, 9);
// long ctime = System.currentTimeMillis();
// simpleTrigger.setStartTime(excelCal.getTime());
//设置作业执行间隔
// simpleTrigger.setRepeatInterval(1000);
//设置作业执行次数
// simpleTrigger.setRepeatCount(10);
//设置作业执行优先级默认为5
//simpleTrigger.setPriority(10);
//作业和触发器设置到调度器中
scheduler.scheduleJob(jobDetail, simpleTrigger);
//启动调度器
scheduler.start();
} catch (SchedulerException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
-----------------------------------------------------------------------------------------------------
1)Job类必须有默认的无参构造方法,当然不覆盖的话类本身就是无参的构造方法
2)Job的scope必须是Public类型的,因为quartz根据反射机制实例化类,如果不是public的,无法对其暴露
3) Job类不能是内部类,原因同上,所以最好单独建类
e.构建调度任务3
/**
* Package Name:nc.xyzq.common.task
*
*/
package nc.xyzq.common.task;
import org.quartz.CronTrigger;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.impl.StdSchedulerFactory;
//任务管理器
public class QuartzManager {
private static SchedulerFactory gSchedulerFactory = new StdSchedulerFactory();
private static String JOB_GROUP_NAME = "EXTJWEB_JOBGROUP_NAME";
private static String TRIGGER_GROUP_NAME = "EXTJWEB_TRIGGERGROUP_NAME";
/**
* 添加一个定时任务,使用默认的任务组名,触发器名,触发器组名
*
* @param jobName 任务名
* @param jobClass 任务
* @param time 时间设置,参考quartz说明文档
*
*/
public static void addJob(String jobName, String jobClass, String time) {
try {
//System.out.println("addJob>>>1111>>Apache Tomcat v6.0.32 at localhost:"+jobName+" jobClass:"+jobClass+" time:"+time);
//通过SchedulerFactory来获取一个调度器
Scheduler sched = gSchedulerFactory.getScheduler();
//引进作业程序
JobDetail jobDetail = new JobDetail(jobName, JOB_GROUP_NAME, Class
.forName(jobClass));// 任务名,任务组,任务执行类
//new一个触发器
CronTrigger trigger = new CronTrigger(jobName, TRIGGER_GROUP_NAME);
// 触发器时间设定
trigger.setCronExpression(time);
sched.scheduleJob(jobDetail, trigger);
// 启动
if (!sched.isShutdown()) {
sched.start();
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* 添加一个定时任务
*
* @param jobName 任务名
* @param jobGroupName 任务组名
* @param triggerName 触发器名
* @param triggerGroupName 触发器组名
* @param jobClass 任务
* @param time 时间设置,参考quartz说明文档
*/
public static void addJob(String jobName, String jobGroupName,
String triggerName, String triggerGroupName, String jobClass,
String time) {
try {
//通过SchedulerFactory来获取一个调度器
Scheduler sched = gSchedulerFactory.getScheduler();
//引进作业程序
JobDetail jobDetail = new JobDetail(jobName, jobGroupName, Class
.forName(jobClass));// 任务名,任务组,任务执行类
//触发器
CronTrigger trigger = new CronTrigger(triggerName, triggerGroupName);
// 触发器时间设定
trigger.setCronExpression(time);
sched.scheduleJob(jobDetail, trigger);
// 启动
if (!sched.isShutdown()) {
sched.start();
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
/**
* 修改一个任务的触发时间(使用默认的任务组名,触发器名,触发器组名)
*
* @param jobName
* @param time
*/
public static void modifyJobTime(String jobName, String time) {
try {
Scheduler sched = gSchedulerFactory.getScheduler();
CronTrigger trigger = (CronTrigger) sched.getTrigger(jobName,
TRIGGER_GROUP_NAME);
if (trigger == null) {
return;
}
String oldTime = trigger.getCronExpression();
if (!oldTime.equalsIgnoreCase(time)) {
JobDetail jobDetail = sched.getJobDetail(jobName,
JOB_GROUP_NAME);
Class objJobClass = jobDetail.getJobClass();
String jobClass = objJobClass.getName();
removeJob(jobName);
addJob(jobName, jobClass, time);
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
/**
* 修改一个任务的触发时间
*
* @param triggerName
* @param triggerGroupName
* @param time
*/
public static void modifyJobTime(String triggerName,
String triggerGroupName, String time) {
try {
Scheduler sched = gSchedulerFactory.getScheduler();
CronTrigger trigger = (CronTrigger) sched.getTrigger(triggerName,
triggerGroupName);
if (trigger == null) {
return;
}
String oldTime = trigger.getCronExpression();
if (!oldTime.equalsIgnoreCase(time)) {
CronTrigger ct = (CronTrigger) trigger;
// 修改时间
ct.setCronExpression(time);
// 重启触发器
sched.resumeTrigger(triggerName, triggerGroupName);
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
/**
* 移除一个任务(使用默认的任务组名,触发器名,触发器组名)
*
* @param jobName
*/
public static void removeJob(String jobName) {
try {
Scheduler sched = gSchedulerFactory.getScheduler();
// 停止触发器
sched.pauseTrigger(jobName, TRIGGER_GROUP_NAME);
// 移除触发器
sched.unscheduleJob(jobName, TRIGGER_GROUP_NAME);
// 删除任务
sched.deleteJob(jobName, JOB_GROUP_NAME);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
/**
* 移除一个任务
*
* @param jobName
* @param jobGroupName
* @param triggerName
* @param triggerGroupName
*/
public static void removeJob(String jobName, String jobGroupName,
String triggerName, String triggerGroupName) {
try {
Scheduler sched = gSchedulerFactory.getScheduler();
// 停止触发器
sched.pauseTrigger(triggerName, triggerGroupName);
// 移除触发器
sched.unscheduleJob(triggerName, triggerGroupName);
// 删除任务
sched.deleteJob(jobName, jobGroupName);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
/**
* 启动所有定时任务
*/
public static void startJobs() {
try {
Scheduler sched = gSchedulerFactory.getScheduler();
if (sched.isShutdown()) {
sched.start();
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
/**
* 关闭所有定时任务
*/
public static void shutdownJobs() {
try {
Scheduler sched = gSchedulerFactory.getScheduler();
if (!sched.isShutdown()) {
sched.shutdown();
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
/***
* 停止触发器
* @param triggerName
* @param triggerGroupName
*/
public static void pauseTrigger(){
try {
Scheduler sched = gSchedulerFactory.getScheduler();
sched.pauseTriggerGroup(TRIGGER_GROUP_NAME);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
/***
* 重启触发器
* @param triggerName
* @param triggerGroupName
* @param time
*/
public static void resumeTrigger() {
try {
Scheduler sched = gSchedulerFactory.getScheduler();
// 重启触发器
sched.resumeTriggerGroup(TRIGGER_GROUP_NAME);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
06.Java使用2
a.依赖
<dependencies>
<!--Quartz任务调度-->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
</dependencies>
b.定义Job
/**
* 工作类的具体实现,即需要定时执行的“某件事”
* */
public class MyJob implements Job {
//执行
public void execute(JobExecutionContext context) throws JobExecutionException {
//创建工作详情
JobDetail jobDetail=context.getJobDetail();
//获取工作的名称
String jobName = jobDetail.getKey().getName();//任务名
String jobGroup = jobDetail.getKey().getGroup();//任务group
System.out.println("job执行,job:"+jobName+" group:"+jobGroup);
System.out.println(new Date());
}
}
c.API测试
package com.qf;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.util.GregorianCalendar;
public class TestQuartz {
public static void main(String[] args) throws Exception{
//创建scheduler,调度器
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
//定义一个Trigger,触发条件类
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1") //定义name/group
.startNow()//设置开始时间,一旦加入scheduler,表示立即生效,即开始计时
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(2) //每隔2秒执行一次
//.repeatForever()) //一直执行,直到结束时间
.withRepeatCount(3))//设置执行次数
//设置结束时间(注:月份默认从0开始)
.endAt(new GregorianCalendar(2020,5,27,17,30,10).getTime())
.build();
//定义一个JobDetail
JobDetail job = JobBuilder.newJob(MyJob.class)
.withIdentity("job1","group1") //定义name/group
.build();
//调度器 中加入 任务和触发器
scheduler.scheduleJob(job, trigger);
//启动任务调度
scheduler.start();
}
}
d.默认配置
# 名为:quartz.properties,放置在classpath下,如果没有此配置则按默认配置启动
# 指定调度器名称,非实现类
org.quartz.scheduler.instanceName = DefaultQuartzScheduler
# 指定线程池实现类
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
# 线程池线程数量
org.quartz.threadPool.threadCount = 10
# 优先级,默认5
org.quartz.threadPool.threadPriority = 5
# 非持久化job
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
e.核心类说明
Scheduler:调度器。所有的调度都是由它控制,是Quartz的大脑,所有任务都是由它来管理
Job:任务,想定时执行的事情(定义业务逻辑)
JobDetail:基于Job,进一步包装。其中关联一个Job,并为Job指定更详细的属性,比如标识等
Trigger:触发器。可以指定给某个任务,指定任务的触发机制。
f.SimpleTrigger
以一定的时间间隔(单位是毫秒)执行的任务。
指定起始和截止时间(时间段)
指定时间间隔、执行次数
-----------------------------------------------------------------------------------------------------
SimpleTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1")
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(1) //每秒执行一次
.repeatForever())// 不限执行次数
.endAt(new GregorianCalendar(2020, 4, 7, 2, 24, 0).getTime())
.build();
-----------------------------------------------------------------------------------------------------
SimpleTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1")
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInMinutes(3) // 每3分钟执行一次
.withRepeatCount(3)) // 执行次数不超过3次
.endAt(new GregorianCalendar(2020, 4, 7, 2, 24, 0).getTime())
.build();
g.CronTrigger【重点】
适合于更复杂的任务,它支持类型于Linux Cron的语法(并且更强大)。
指定Cron表达式即可
-----------------------------------------------------------------------------------------------------
// 每天10:00-12:00,每隔2秒钟执行一次
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger2", "group2")
.withSchedule(CronScheduleBuilder.cronSchedule("*/2 * 10-12 * * ?"))
.build();
07.Spring整合Quartz
a.依赖
<dependencies>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
</dependencies>
b.定义Job
public class MyJob implements Job {
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.err.println("job 执行"+new Date());
}
}
c.配置applicationContext.xml
调度器 SchedulerFactoryBean
触发器 CronTriggerFactoryBean
JobDetail JobDetailFactoryBean
-----------------------------------------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--
Spring整合Quartz进行配置遵循下面的步骤:
1:定义工作任务的Job
2:定义触发器Trigger,并将触发器与工作任务绑定
3:定义调度器,并将Trigger注册到Scheduler
-->
<!-- 1:定义任务的bean ,这里使用JobDetailFactoryBean,也可以使用MethodInvokingJobDetailFactoryBean ,配置类似-->
<bean name="lxJob" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<!-- 指定job的名称 -->
<property name="name" value="job1"/>
<!-- 指定job的分组 -->
<property name="group" value="job_group1"/>
<!-- 指定具体的job类 -->
<property name="jobClass" value="com.qf.quartz.MyJob"/>
</bean>
<!-- 2:定义触发器的bean,定义一个Cron的Trigger,一个触发器只能和一个任务进行绑定 -->
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<!-- 指定Trigger的名称 -->
<property name="name" value="trigger1"/>
<!-- 指定Trigger的名称 -->
<property name="group" value="trigger_group1"/>
<!-- 指定Tirgger绑定的JobDetail -->
<property name="jobDetail" ref="lxJob"/>
<!-- 指定Cron 的表达式 ,当前是每隔5s运行一次 -->
<property name="cronExpression" value="*/5 * * * * ?" />
</bean>
<!-- 3.定义调度器,并将Trigger注册到调度器中 -->
<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="cronTrigger"/>
</list>
</property>
<!-- 添加 quartz 配置,如下两种方式均可 -->
<!--<property name="configLocation" value="classpath:quartz.properties"></property>-->
<property name="quartzProperties">
<value>
# 指定调度器名称,实际类型为:QuartzScheduler
org.quartz.scheduler.instanceName = MyScheduler
# 指定连接池
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
# 连接池线程数量
org.quartz.threadPool.threadCount = 11
# 优先级
org.quartz.threadPool.threadPriority = 5
# 不持久化job
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
</value>
</property>
</bean>
</beans>
d.启动任务
工厂启动,调度器启动,任务调度开始
-----------------------------------------------------------------------------------------------------
public static void main(String[] args) throws InterruptedException, SchedulerException {
// 工厂启动,任务启动,工厂关闭,任务停止
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
}
e.任务操作
a.删除任务
public static void main(String[] args) throws InterruptedException, SchedulerException {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
System.out.println("=============");
StdScheduler scheduler = (StdScheduler) context.getBean("scheduler");
System.out.println(scheduler.getClass());
Thread.sleep(3000);
// 删除Job
scheduler.deleteJob(JobKey.jobKey("job1","job_group1"));
}
b.暂停、恢复
public static void main(String[] args) throws InterruptedException, SchedulerException {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
System.out.println("=============");
StdScheduler scheduler = (StdScheduler) context.getBean("scheduler");
System.out.println(scheduler.getClass());
Thread.sleep(3000);
// 暂停,恢复工作
scheduler.pauseJob(JobKey.jobKey("job1","job_group1"));// 暂停工作
Thread.sleep(3000);
scheduler.resumeJob(JobKey.jobKey("job1","job_group1"));// 恢复工作
}
c.批量操作
public static void main(String[] args) throws InterruptedException, SchedulerException {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
System.out.println("=============");
StdScheduler scheduler = (StdScheduler) context.getBean("scheduler");
System.out.println(scheduler.getClass());
Thread.sleep(3000);
GroupMatcher<JobKey> group1 = GroupMatcher.groupEquals("job_group1");
scheduler.pauseJobs(group1); // 暂停组中所有工作
Thread.sleep(2000);
scheduler.resumeJobs(group1); // 恢复组中所有工作
}
08.SpringBoot整合Quartz,无持久化
a.依赖
<!-- 实现对 Spring MVC 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 实现对 Quartz 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
b.配置文件
spring:
# Quartz 的配置,对应 QuartzProperties 配置类
quartz:
job-store-type: memory # Job 存储器类型。默认为 memory 表示内存,可选 jdbc 使用数据库。
auto-startup: true # Quartz 是否自动启动
startup-delay: 0 # 延迟 N 秒启动
wait-for-jobs-to-complete-on-shutdown: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true
overwrite-existing-jobs: false # 是否覆盖已有 Job 的配置
properties: # 添加 Quartz Scheduler 附加属性
org:
quartz:
threadPool:
threadCount: 25 # 线程池大小。默认为 10 。
threadPriority: 5 # 线程优先级
class: org.quartz.simpl.SimpleThreadPool # 线程池类型
# jdbc: # 这里暂时不说明,使用 JDBC 的 JobStore 的时候,才需要配置
c.创建Job
@Slf4j
public class FirstJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
String now = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now());
log.info("当前的时间: " + now);
}
}
-----------------------------------------------------------------------------------------------------
@Slf4j
public class SecondJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
String now = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now());
log.info("SecondJob执行, 当前的时间: " + now);
}
}
d.调度器Scheduler绑定
a.方式1:使用bean的自动配置
@Configuration
public class QuartzConfig {
private static final String ID = "SUMMERDAY";
@Bean
public JobDetail jobDetail1() {
return JobBuilder.newJob(FirstJob.class)
.withIdentity(ID + " 01")
.storeDurably()
.build();
}
@Bean
public Trigger trigger1() {
// 简单的调度计划的构造器
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(5) // 频率
.repeatForever(); // 次数
return TriggerBuilder.newTrigger()
.forJob(jobDetail1())
.withIdentity(ID + " 01Trigger")
.withSchedule(scheduleBuilder)
.build();
}
}
b.方式2:Scheduler手动配置
@Component
public class JobInit implements ApplicationRunner {
private static final String ID = "SUMMERDAY";
@Autowired
private Scheduler scheduler;
@Override
public void run(ApplicationArguments args) throws Exception {
JobDetail jobDetail = JobBuilder.newJob(FirstJob.class)
.withIdentity(ID + " 01")
.storeDurably()
.build();
CronScheduleBuilder scheduleBuilder =
CronScheduleBuilder.cronSchedule("0/5 * * * * ? *");
// 创建任务触发器
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(jobDetail)
.withIdentity(ID + " 01Trigger")
.withSchedule(scheduleBuilder)
.startNow() //立即執行一次任務
.build();
// 手动将触发器与任务绑定到调度器内
scheduler.scheduleJob(jobDetail, trigger);
}
}
e.主启动类
@SpringBootApplication
public class DemoSpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(DemoSpringBootApplication.class, args);
}
}
f.测试
启动程序,FirstJob每5s执行一次,SecondJob每10s执行一次。
09.SpringBoot整合Quartz,持久化1
a.Quartz持久化配置提供了两种存储器
a.RAMJobStore
不要外部数据库,配置容易,运行速度快
因为调度程序信息是存储在被分配给 JVM 的内存里面,所以,当应用程序停止运行时,所有调度信息将被丢失。另外因为存储到JVM内存里面,所以可以存储多少个 Job 和 Trigger 将会受到限制
b.JDBC作业存储
支持集群,因为所有的任务信息都会保存到数据库中,可以控制事物,还有就是如果应用服务器关闭或者重启,任务信息都不会丢失,并且可以恢复因服务器关闭或者重启而导致执行失败的任务
运行速度的快慢取决与连接数据库的快慢
b.创建数据库表
a.介绍
为了测试Quartz的持久化配置,我们事先在mysql中创建一个数据库quartz,并执行脚本,
脚本藏在org\quartz-scheduler\quartz\2.3.2\quartz-2.3.2.jar!\org\quartz\impl\jdbcjobstore\tables_mysql_innodb.sql,
jdbcjobstore中有支持许多种数据库的脚本,可以按需执行。
b.操作
mysql> use quartz;
Database changed
mysql> show tables;
+--------------------------+
| Tables_in_quartz |
+--------------------------+
| qrtz_blob_triggers | blog类型存储triggers
| qrtz_calendars | 以blog类型存储Calendar信息
| qrtz_cron_triggers | 存储cron trigger信息
| qrtz_fired_triggers | 存储已触发的trigger相关信息
| qrtz_job_details | 存储每一个已配置的job details
| qrtz_locks | 存储悲观锁的信息
| qrtz_paused_trigger_grps | 存储已暂停的trigger组信息
| qrtz_scheduler_state | 存储Scheduler状态信息
| qrtz_simple_triggers | 存储simple trigger信息
| qrtz_simprop_triggers | 存储其他几种trigger信息
| qrtz_triggers | 存储已配置的trigger信息
+--------------------------+
c.说明
所有的表中都含有一个SCHED_NAME字段,对应我们配置的scheduler-name,相同 Scheduler-name的节点,形成一个 Quartz 集群。
c.引入mysql相关依赖
<dependencies>
<!-- 实现对 Spring MVC 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 实现对 Quartz 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
</dependencies>
d.配置yml
spring:
datasource:
quartz:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/quartz?serverTimezone=GMT%2B8
username: root
password: 123456
quartz:
job-store-type: jdbc # 使用数据库存储
scheduler-name: hyhScheduler # 相同 Scheduler 名字的节点,形成一个 Quartz 集群
wait-for-jobs-to-complete-on-shutdown: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true
jdbc:
initialize-schema: never # 是否自动使用 SQL 初始化 Quartz 表结构。这里设置成 never ,我们手动创建表结构。
properties:
org:
quartz:
# JobStore 相关配置
jobStore:
dataSource: quartzDataSource # 使用的数据源
class: org.quartz.impl.jdbcjobstore.JobStoreTX # JobStore 实现类
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: QRTZ_ # Quartz 表前缀
isClustered: true # 是集群模式
clusterCheckinInterval: 1000
useProperties: false
# 线程池相关配置
threadPool:
threadCount: 25 # 线程池大小。默认为 10 。
threadPriority: 5 # 线程优先级
class: org.quartz.simpl.SimpleThreadPool # 线程池类型
e.配置数据源
@Configuration
public class DataSourceConfiguration {
private static HikariDataSource createHikariDataSource(DataSourceProperties properties) {
// 创建 HikariDataSource 对象
HikariDataSource dataSource = properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
// 设置线程池名
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
/**
* 创建 quartz 数据源的配置对象
*/
@Primary
@Bean(name = "quartzDataSourceProperties")
@ConfigurationProperties(prefix = "spring.datasource.quartz")
// 读取 spring.datasource.quartz 配置到 DataSourceProperties 对象
public DataSourceProperties quartzDataSourceProperties() {
return new DataSourceProperties();
}
/**
* 创建 quartz 数据源
*/
@Bean(name = "quartzDataSource")
@ConfigurationProperties(prefix = "spring.datasource.quartz.hikari")
@QuartzDataSource
public DataSource quartzDataSource() {
// 获得 DataSourceProperties 对象
DataSourceProperties properties = this.quartzDataSourceProperties();
// 创建 HikariDataSource 对象
return createHikariDataSource(properties);
}
}
f.创建任务
@Component
public class JobInit implements ApplicationRunner {
private static final String ID = "SUMMERDAY";
@Autowired
private Scheduler scheduler;
@Override
public void run(ApplicationArguments args) throws Exception {
JobDetail jobDetail = JobBuilder.newJob(SecondJob.class)
.withIdentity(ID + " 02")
.storeDurably()
.build();
CronScheduleBuilder scheduleBuilder =
CronScheduleBuilder.cronSchedule("0/10 * * * * ? *");
// 创建任务触发器
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(jobDetail)
.withIdentity(ID + " 02Trigger")
.withSchedule(scheduleBuilder)
.startNow() //立即執行一次任務
.build();
Set<Trigger> set = new HashSet<>();
set.add(trigger);
// boolean replace 表示启动时对数据库中的quartz的任务进行覆盖。
scheduler.scheduleJob(jobDetail, set, true);
}
}
g.主启动类
@SpringBootApplication
public class DemoSpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(DemoSpringBootApplication.class, args);
}
}
h.测试
启动测试之后,我们的quartz任务相关信息就已经成功存储到mysql中了。
mysql> select * from qrtz_simple_triggers;
+--------------+---------------------+---------------+--------------+-----------------+-----------------+
| SCHED_NAME | TRIGGER_NAME | TRIGGER_GROUP | REPEAT_COUNT | REPEAT_INTERVAL | TIMES_TRIGGERED |
+--------------+---------------------+---------------+--------------+-----------------+-----------------+
| hyhScheduler | SUMMERDAY 01Trigger | DEFAULT | -1 | 5000 | 812 |
+--------------+---------------------+---------------+--------------+-----------------+-----------------+
1 row in set (0.00 sec)
-----------------------------------------------------------------------------------------------------
mysql> select * from qrtz_cron_triggers;
+--------------+---------------------+---------------+------------------+---------------+
| SCHED_NAME | TRIGGER_NAME | TRIGGER_GROUP | CRON_EXPRESSION | TIME_ZONE_ID |
+--------------+---------------------+---------------+------------------+---------------+
| hyhScheduler | SUMMERDAY 02Trigger | DEFAULT | 0/10 * * * * ? * | Asia/Shanghai |
+--------------+---------------------+---------------+------------------+---------------+
10.SpringBoot整合Quartz,持久化2
a.依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
<version>2.6.7</version>
</dependency>
b.配置
spring:
quartz:
job-store-type: jdbc # 默认为内存 memory 的方式,这里我们使用数据库的形式
wait-for-jobs-to-complete-on-shutdown: true # 关闭时等待任务完成
overwrite-existing-jobs: true # 可以覆盖已有的任务
jdbc:
initialize-schema: never # 是否自动使用 SQL 初始化 Quartz 表结构
properties: # quartz原生配置
org:
quartz:
scheduler:
instanceName: scheduler # 调度器实例名称
instanceId: AUTO # 调度器实例ID自动生成
# JobStore 相关配置
jobStore:
class: org.quartz.impl.jdbcjobstore.JobStoreTX # JobStore 实现类
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate # 使用完全兼容JDBC的驱动
tablePrefix: QRTZ_ # Quartz 表前缀
useProperties: false # 是否将JobDataMap中的属性转为字符串存储
# 线程池相关配置
threadPool:
threadCount: 25 # 线程池大小。默认为 10 。
threadPriority: 5 # 线程优先级
class: org.quartz.simpl.SimpleThreadPool # 指定线程池实现类,对调度器提供固定大小的线程池
-----------------------------------------------------------------------------------------------------
Quartz 默认使用的是内存的方式来存储任务,为了持久化,我们这里改为 JDBC 的形式,
并且指定 spring.quartz.jdbc.initialize-schema=never,这样我们可以手动创建数据表。
-----------------------------------------------------------------------------------------------------
因为该值的另外两个选项ALWAYS和EMBEDDED都不太符合我们的要求:
ALWAYS:每次都初始化
EMBEDDED:只初始化嵌入式数据库,比如说 H2、HSQL
c.创建任务调度的接口 IScheduleService,定义三个方法,分别是通过 Cron 表达式来调度任务、指定时间来调度任务,以及取消任务。
public interface IScheduleService {
/**
* 通过 Cron 表达式来调度任务
*/
String scheduleJob(Class<? extends Job> jobBeanClass, String cron, String data);
/**
* 指定时间来调度任务
*/
String scheduleFixTimeJob(Class<? extends Job> jobBeanClass, Date startTime, String data);
/**
* 取消定时任务
*/
Boolean cancelScheduleJob(String jobName);
}
d.创建任务调度业务实现类 ScheduleServiceImpl,通过Scheduler、CronTrigger、JobDetail的API来实现对应的方法。
@Slf4j
@Service
public class ScheduleServiceImpl implements IScheduleService {
private String defaultGroup = "default_group";
@Autowired
private Scheduler scheduler;
@Override
public String scheduleJob(Class<? extends Job> jobBeanClass, String cron, String data) {
String jobName = UUID.fastUUID().toString();
JobDetail jobDetail = JobBuilder.newJob(jobBeanClass)
.withIdentity(jobName, defaultGroup)
.usingJobData("data", data)
.build();
//创建触发器,指定任务执行时间
CronTrigger cronTrigger = TriggerBuilder.newTrigger()
.withIdentity(jobName, defaultGroup)
.withSchedule(CronScheduleBuilder.cronSchedule(cron))
.build();
// 调度器进行任务调度
try {
scheduler.scheduleJob(jobDetail, cronTrigger);
} catch (SchedulerException e) {
log.error("任务调度执行失败{}", e.getMessage());
}
return jobName;
}
@Override
public String scheduleFixTimeJob(Class<? extends Job> jobBeanClass, Date startTime, String data) {
//日期转CRON表达式
String startCron = String.format("%d %d %d %d %d ? %d",
DateUtil.second(startTime),
DateUtil.minute(startTime),
DateUtil.hour(startTime, true),
DateUtil.dayOfMonth(startTime),
DateUtil.month(startTime) + 1,
DateUtil.year(startTime));
return scheduleJob(jobBeanClass, startCron, data);
}
@Override
public Boolean cancelScheduleJob(String jobName) {
boolean success = false;
try {
// 暂停触发器
scheduler.pauseTrigger(new TriggerKey(jobName, defaultGroup));
// 移除触发器中的任务
scheduler.unscheduleJob(new TriggerKey(jobName, defaultGroup));
// 删除任务
scheduler.deleteJob(new JobKey(jobName, defaultGroup));
success = true;
} catch (SchedulerException e) {
log.error("任务取消失败{}", e.getMessage());
}
return success;
}
}
e.定义好要执行的任务,继承 QuartzJobBean 类,实现 executeInternal 方法,这里只定义一个定时发布文章的任务。
@Slf4j
@Component
public class PublishPostJob extends QuartzJobBean {
@Autowired
private IScheduleService scheduleService;
@Autowired
private IPostsService postsService;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
Trigger trigger = jobExecutionContext.getTrigger();
JobDetail jobDetail = jobExecutionContext.getJobDetail();
JobDataMap jobDataMap = jobDetail.getJobDataMap();
Long data = jobDataMap.getLong("data");
log.debug("定时发布文章操作:{}",data);
// 获取文章的 ID后获取文章,更新文章为发布的状态,还有发布的时间
boolean success = postsService.updatePostByScheduler(data);
//完成后删除触发器和任务
if (success) {
log.debug("定时任务执行成功,开始清除定时任务");
scheduleService.cancelScheduleJob(trigger.getKey().getName());
}
}
}
f.发布文章的接口里 PostsServiceImpl 添加定时发布的任务调度方法。
@Service
public class PostsServiceImpl extends ServiceImpl<PostsMapper, Posts> implements IPostsService {
private void handleScheduledAfter(Posts posts) {
// 文章已经保存为草稿了,并且拿到了文章 ID
// 调用定时任务
String jobName = scheduleService.scheduleFixTimeJob(PublishPostJob.class, posts.getPostDate(), posts.getPostsId().toString());
LOGGER.debug("定时任务{}开始执行", jobName);
}
}
g.启动服务,通过Swagger 来测试一下,注意设置文章的定时发布时间。
查看 Quartz 的数据表 qrtz_cron_triggers,发现任务已经添加进来了。
qrtz_job_details 表里也可以查看具体的任务详情。
文章定时发布的时间到了之后,在日志里也可以看到 Quartz 的执行日志。
-----------------------------------------------------------------------------------------------------
再次查看 Quartz 数据表 qrtz_cron_triggers 和 qrtz_job_details 的时候,也会发现定时任务已经清除了。
整体上来说,Spring Boot 整合 Quartz还是非常丝滑的,配置少,步骤清晰,比 Spring Task 更强大,既能针对内存也能持久化,所以大家在遇到定时任务的时候完全可以尝试一把。
11.SpringBoot整合Quartz,持久化3(定)
a.依赖
<!--Quartz定时任务-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
b.配置文件中增加Quartz的支持
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: xxx
username: xxx
password: xxx
quartz:
job-store-type: jdbc # 定时任务的数据保存到jdbc即数据库中
jdbc:
# embedded:默认
# always:启动的时候初始化表,我们只在第一次启动的时候用它来自动创建表,然后改回embedded即可,不然数据每次都会被清空
# never:启动的时候不初始化表,也不知道和embedded有什么不同
initialize-schema: embedded
-----------------------------------------------------------------------------------------------------
第一次启动的时候请把上面的initialize-schema设置为always,这会在数据库里面自动建表,
然后第二次启动时改回embedded即可。
如果不需要定时任务的持久化就可以不管。
c.方式1:使用Quartz写一个定时任务的步骤,很简单,问题是配置写死了,没有办法动态调整
a.写一个测试用的任务类
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;
@Component
public class QuartzTestJob extends QuartzJobBean {
@Override
protected void executeInternal(org.quartz.JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("Quartz Test Job");
}
}
b.为这个任务类写一个配置类
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class QuartzTestJobConfig {
@Bean
public JobDetail quartzTestJobDetail() {
return JobBuilder.newJob(QuartzTestJob.class)
.withIdentity(QuartzTestJob.class.getSimpleName())
.storeDurably()
.usingJobData("data", "test")
.build();
}
@Bean
public Trigger quartzTestJobTrigger() {
// 0/1 * * * * ?
return TriggerBuilder.newTrigger()
.forJob(QuartzTestJob.class.getSimpleName())
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1).repeatForever())
.build();
}
}
d.方式2:写接口,把上面这个任务配置类去掉
a.业务层
public interface QuartzService {
/**
* 添加定时任务
*/
void addJob(QuartzCreateParam param) throws SchedulerException;
/**
* 修改定时任务
*/
void updateJob(QuartzUpdateParam param) throws SchedulerException;
/**
* 暂停定时任务
*/
void pauseJob(QuartzDetailParam param) throws SchedulerException;
/**
* 恢复定时任务
*/
void resumeJob(QuartzDetailParam param) throws SchedulerException;
/**
* 删除定时任务
*/
void deleteJob(QuartzDetailParam param) throws SchedulerException;
/**
* 定时任务列表
* @return
*/
List<QuartzJobDetailDto> jobList() throws SchedulerException;
/**
* 定时任务详情
*/
QuartzJobDetailDto jobDetail(QuartzDetailParam param) throws SchedulerException;
}
b.业务层实现
@Service
public class QuartzServiceImpl implements QuartzService {
@Autowired
private Scheduler scheduler;
@Autowired
private SchedulerFactoryBean schedulerFactoryBean;
@Override
public void addJob(QuartzCreateParam param) throws SchedulerException {
String clazzName = param.getJobClazz();
String jobName = param.getJobName();
String jobGroup = param.getJobGroup();
String cron = param.getCron();
String description = param.getDescription();
JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
checkJobExist(jobKey);
Class<? extends Job> jobClass = null;
try {
jobClass = (Class<? extends Job>) Class.forName(clazzName);
} catch (ClassNotFoundException e) {
throw new BaseException("找不到任务类:" + clazzName);
}
JobDataMap jobDataMap = new JobDataMap();
if (param.getJobDataMap() != null) {
param.getJobDataMap().forEach(jobDataMap::put);
}
Scheduler scheduler = schedulerFactoryBean.getScheduler();
JobDetail jobDetail = JobBuilder.newJob(jobClass)
.withIdentity(jobName, jobGroup)
.usingJobData(jobDataMap)
.build();
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cron);
String triggerId = jobKey.getGroup() + jobKey.getName();
Trigger trigger = TriggerBuilder.newTrigger()
.withSchedule(scheduleBuilder)
.withIdentity(triggerId)
.withDescription(description)
.build();
scheduler.scheduleJob(jobDetail, trigger);
if (!scheduler.isShutdown()) {
scheduler.start();
}
}
@Override
public void updateJob(QuartzUpdateParam param) throws SchedulerException {
String jobName = param.getJobName();
String jobGroup = param.getJobGroup();
String cron = param.getCron();
JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
String triggerId = jobKey.getGroup() + jobKey.getName();
checkJobExist(jobKey);
TriggerKey triggerKey = TriggerKey.triggerKey(triggerId);
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cron);
TriggerBuilder<?> triggerBuilder = TriggerBuilder.newTrigger()
.withSchedule(scheduleBuilder)
.withIdentity(triggerId);
Trigger trigger = triggerBuilder.build();
scheduler.rescheduleJob(triggerKey, trigger);
}
@Override
public void pauseJob(QuartzDetailParam param) throws SchedulerException {
String jobName = param.getJobName();
String jobGroup = param.getJobGroup();
JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
checkJobExist(jobKey);
scheduler.pauseJob(jobKey);
}
@Override
public void resumeJob(QuartzDetailParam param) throws SchedulerException {
String jobName = param.getJobName();
String jobGroup = param.getJobGroup();
JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
checkJobExist(jobKey);
scheduler.resumeJob(jobKey);
}
@Override
public void deleteJob(QuartzDetailParam param) throws SchedulerException {
String jobName = param.getJobName();
String jobGroup = param.getJobGroup();
JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
checkJobExist(jobKey);
// 先暂停再删除
scheduler.pauseJob(jobKey);
scheduler.deleteJob(jobKey);
}
@Override
public List<QuartzJobDetailDto> jobList() throws SchedulerException {
GroupMatcher<JobKey> matcher = GroupMatcher.anyJobGroup();
List<QuartzJobDetailDto> jobDtoList = new ArrayList<>();
for (JobKey jobKey : scheduler.getJobKeys(matcher)) {
QuartzJobDetailDto jobDto = getJobDtoByJobKey(jobKey);
jobDtoList.add(jobDto);
}
return jobDtoList;
}
@Override
public QuartzJobDetailDto jobDetail(QuartzDetailParam param) throws SchedulerException {
String jobName = param.getJobName();
String jobGroup = param.getJobGroup();
JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
return getJobDtoByJobKey(jobKey);
}
/*************** 私有方法 ***************/
private void checkJobExist(JobKey jobKey) throws SchedulerException {
if (!scheduler.checkExists(jobKey)) {
throw new BaseException("该定时任务不存在:" + jobKey.getName());
}
}
public QuartzJobDetailDto getJobDtoByJobKey(JobKey jobKey) throws SchedulerException {
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
List<Trigger> triggerList = (List<Trigger>) scheduler.getTriggersOfJob(jobKey);
QuartzJobDetailDto jobDto = new QuartzJobDetailDto();
jobDto.setJobClazz(jobDetail.getJobClass().toString());
jobDto.setJobName(jobKey.getName());
jobDto.setJobGroup(jobKey.getGroup());
jobDto.setJobDataMap(jobDetail.getJobDataMap());
List<QuartzTriggerDetailDto> triggerDtoList = new ArrayList<>();
for (Trigger trigger : triggerList) {
QuartzTriggerDetailDto triggerDto = new QuartzTriggerDetailDto();
triggerDto.setTriggerName(trigger.getKey().getName());
triggerDto.setTriggerGroup(trigger.getKey().getGroup());
triggerDto.setDescription(trigger.getDescription());
if (trigger instanceof CronTriggerImpl) {
CronTriggerImpl cronTriggerImpl = (CronTriggerImpl) trigger;
String cronExpression = cronTriggerImpl.getCronExpression();
triggerDto.setCron(cronExpression);
// 最近10次的触发时间
List<Date> dates = TriggerUtils.computeFireTimes(cronTriggerImpl, null, 10);
triggerDto.setRecentFireTimeList(dates);
}
Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
triggerDto.setTriggerState(triggerState.toString());
triggerDtoList.add(triggerDto);
}
jobDto.setTriggerDetailDtoList(triggerDtoList);
return jobDto;
}
}
c.接口层
@RestController
public class QuartzController {
@Autowired
private QuartzServiceImpl quartzService;
@PostMapping("/quartz/addJob")
public void addJob(@RequestBody QuartzCreateParam param) throws SchedulerException {
quartzService.addJob(param);
}
@PostMapping("/quartz/updateJob")
public void updateJob(@RequestBody QuartzUpdateParam param) throws SchedulerException {
quartzService.updateJob(param);
}
@PostMapping("/quartz/pauseJob")
public void pauseJob(@RequestBody QuartzDetailParam param) throws SchedulerException {
quartzService.pauseJob(param);
}
@PostMapping("/quartz/resumeJob")
public void resumeJob(@RequestBody QuartzDetailParam param) throws SchedulerException {
quartzService.resumeJob(param);
}
@PostMapping("/quartz/deleteJob")
public void deleteJob(@RequestBody QuartzDetailParam param) throws SchedulerException {
quartzService.deleteJob(param);
}
@PostMapping("/quartz/jobList")
public List<QuartzJobDetailDto> jobList() throws SchedulerException {
return quartzService.jobList();
}
@PostMapping("/quartz/jobDetail")
public QuartzJobDetailDto jobDetail(@RequestBody QuartzDetailParam param) throws SchedulerException {
return quartzService.jobDetail(param);
}
}
d.接口请求参数
@ApiModel(value = "Quartz任务添加请求参数")
public class QuartzCreateParam extends BaseParam {
@NotBlank(message = "任务类不能为空")
@ApiModelProperty(value = "任务类路径", required = true)
private String jobClazz;
@NotBlank(message = "任务类名不能为空")
@ApiModelProperty(value = "任务类名", required = true)
private String jobName;
/**
* 组名+任务类key组成唯一标识,所以如果这个参数为空,那么默认以任务类key作为组名
*/
@ApiModelProperty(value = "任务组名,命名空间")
private String jobGroup;
@ApiModelProperty(value = "任务数据")
private Map<String, Object> jobDataMap;
@ApiModelProperty(value = "cron表达式")
private String cron;
@ApiModelProperty(value = "描述")
private String description;
}
-------------------------------------------------------------------------------------------------
@ApiModel(value = "Quartz任务更新请求参数")
public class QuartzUpdateParam extends BaseParam {
@NotBlank(message = "任务类名不能为空")
@ApiModelProperty(value = "任务类名", required = true)
private String jobName;
@ApiModelProperty(value = "任务组名,命名空间")
private String jobGroup;
@ApiModelProperty(value = "cron表达式")
private String cron;
}
-------------------------------------------------------------------------------------------------
@ApiModel(value = "Quartz任务详情请求参数")
public class QuartzDetailParam extends BaseParam {
@NotBlank(message = "任务类名不能为空")
@ApiModelProperty(value = "任务类名", required = true)
private String jobName;
@ApiModelProperty(value = "任务组名,命名空间")
private String jobGroup;
}
e.接口返回结果类
@ApiModel(value = "Quartz定时任务详情类")
public class QuartzJobDetailDto {
@ApiModelProperty(value = "任务类路径")
private String jobClazz;
@ApiModelProperty(value = "任务类名")
private String jobName;
@ApiModelProperty(value = "任务组名,命名空间")
private String jobGroup;
@ApiModelProperty(value = "任务数据")
private Map<String, Object> jobDataMap;
@ApiModelProperty(value = "触发器列表")
private List<QuartzTriggerDetailDto> triggerDetailDtoList;
}
-------------------------------------------------------------------------------------------------
@ApiModel(value = "Quartz定时任务触发器详情类")
public class QuartzTriggerDetailDto {
private String triggerName;
private String triggerGroup;
private String cron;
private String description;
private String triggerState;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private List<Date> recentFireTimeList;
}
f.调用接口进行测试
a.添加任务接口:/quartz/addJob
{
"jobClazz": "com.cc.job.QuartzTestJob",
"jobName": "QuartzTestJob",
"cron": "1/2 * * * * ? ",
"description": "测试定时任务",
"jobDataMap": {
"key": "value"
}
}
b.修改任务接口:/quartz/updateJob
{
"jobName": "QuartzTestJob",
"cron": "0/2 * * * * ?"
}
修改任务只能修改cron时间,如果想要修改其他内容,只能删除任务后重新添加。
c.删除任务接口:/quartz/updateJob
{
"jobName": "QuartzTestJob"
}
暂停、恢复、详情接口同删除任务接口的请求参数,就不赘述了。
f.任务列表:/quartz/jobList
{}
---------------------------------------------------------------------------------------------
{
"code": 10000,
"msg": "请求成功",
"data": [
{
"jobClazz": "class com.cc.job.QuartzTestJob",
"jobName": "QuartzTestJob",
"jobGroup": "DEFAULT",
"jobDataMap": {
"key": "value"
},
"triggerDetailDtoList": [
{
"triggerName": "DEFAULTQuartzTestJob",
"triggerGroup": "DEFAULT",
"cron": "0/2 * * * * ?",
"description": null,
"triggerState": "NORMAL",
"recentFireTimeList": [
"2023-07-19 09:23:16",
"2023-07-19 09:23:18",
"2023-07-19 09:23:20",
"2023-07-19 09:23:22",
"2023-07-19 09:23:24",
"2023-07-19 09:23:26",
"2023-07-19 09:23:28",
"2023-07-19 09:23:30",
"2023-07-19 09:23:32",
"2023-07-19 09:23:34"
]
}
]
}
],
"traceId": null,
"success": true
}
3.4 版本:Flyway
01.概念
a.介绍
Flyway是一款开源的数据库版本管理工具,可以实现管理并跟踪数据库变更,支持数据库版本自动升级,而且不需要复杂的配置,能够帮助团队更加方便、合理的管理数据库变更。
例:创建两个sql变更文件,项目启动后会将两个文件中的sql语句全部执行。
-----------------------------------------------------------------------------------------------------
①社区版目前不支持版本回退
②社区版本没有任务队列和异步任务的支持,所以在大量变更时略有风险,尽量拆分
b.支持性
目前支持mysql5.7的社区版为7.15.0,支持mysql8.0的版本是8.2.0,8.2.1移除了mysql支持,
flyway的8.2.1版本移除mysql的解决方案,增加依赖:
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
c.主要特性
普通 SQL:纯 SQL 脚本(包括占位符替换)没有专有的XML格式,没有锁定
无限制:使用 Java 代码来进行一些高级数据操作
零依赖:只需运行在 Java6(及以上)和数据库所需的 JDBC 驱动
约定优于配置:迁移时,自动查找系统文件和类路径中的 SQL 文件或 Java 类
高可靠性:在集群环境下进行数据库升级是安全可靠的
云支持:完全支持 Microsoft SQL Azure, Google Cloud SQL & App Engine、Heroku Postgres 和 Amazon RDS
自动迁移:使用 Flyway 提供的 API,让应用启动和迁移同时工作
快速失败:损坏的数据库或失败的迁移可以防止应用程序启动
数据库清理:在一个数据库中删除所有的表、视图、触发器,而不是删除数据库本身
d.运行原理
当 Flyway 连接数据库中的 schema 后,会先检查是否已存在 flyway_schema_history 表,
如果没有则创建,该表用于跟踪数据库的状态,如数据迁移的版本,迁移成功状态等信息。
当 flyway_schema_history 存在后,Flyway 会扫描文件系统或应用中的 classpath 目录的数据迁移文件,然后根据它们的版本号进行按序迁移
由于 flyway_schema_history 表中记录了迁移的版本号,如果文件的版本号小于或等于标记为当前版本的版本号,则忽略它们不执行。
e.总结
按脚本命名规范置入SQL脚本,启动项目
将变更的SQL脚本,按照脚本命名规范创建sql文件,置入flyway/sql/mysql目录(可配置)下,启动项目;
项目启动后系统会自动创建flyway_schema_history表,执行SQL变更脚本,并插入脚本执行记录至该表。
-----------------------------------------------------------------------------------------------------
脚本命名
1.仅需要被执行一次的SQL命名以大写的"V"开头,后面跟上"0~9"数字的组合,数字之间可以用“.”或者下划线"_"分割开,然后再以两个下划线分割,其后跟文件名称,最后以.sql结尾
V[年月日]_[序号]__[模块名缩写]_[操作类型]_[业务描述].sql
例如:
V20240104_1__easyoa_add_field_attendance.sql
2.可重复运行的SQL,则以大写的“R”开头,后面再以两个下划线分割,其后跟文件名称。
R__[模块名缩写]_[业务描述].sql
例如:
R__easyoa_update_user.sql
R__202402_drag_update_template.sql
3.注意事项
3.1 版本号需要唯一,否则Flyway执行会报错;如果V__脚本.sql,已经执行过了,不能修改里面的内容,再次执行Flyway就会报错。
3.2 R开头的脚本.sql,允许脚本内容的修改,如有变化可以执行多次。
3.3 V开头的SQL执行优先级要比R开头的SQL优先级高。
-----------------------------------------------------------------------------------------------------
V3.7.13__add_test.sql
INSERT INTO `jeecg-boot_20241201_online`.`vilgo_leave`(`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `name`, `start_time`, `end_time`, `type`, `reason`, `info`) VALUES ('1862431748885483526', 'admin', '2024-11-30 15:07:58', 'admin', '2024-12-01 09:36:19', 'A01', '李紫轩', '2024-11-06', '2024-11-30', 'rj', '广西壮族自治区玉林市玉州区名山街道', '广西壮族自治区玉林市玉州区名山街道');
02.总结
a.背景
开发时,如果A开发和B开发都对同一数据库进行了修改,那么如何进行数据同步呢?
假如多个开发人员都修改了sql脚本,怎么同步到测试环境和生产环境?
b.工作原理
Flyway在第一次执行时,会创建一个默认名为flyway_schema_history的历史记录表,这张表会用来跟踪或记录数据库的状态,
然后每次项目启动时都会自动扫描在resources/db/migration下的文件的版本号并且通过查询flyway_schema_history来判断是否有新增文件,从而判断是否进行迁移。
-----------------------------------------------------------------------------------------------------
默认的查找 migration 的路径为 classpath:db/migration ,对应 SQL 文件可放置在src/main/resources/db/migration 下,
Java 类可放置在 src/main/java/db/migration 下。
-----------------------------------------------------------------------------------------------------
Flyway在第一次执行时,会创建一个默认名为flyway_schema_history的历史记录表,这张表会用来跟踪或记录数据库的状态,
然后每次启动时都会自动扫描在resources/db/migration下的文件并且通过查询flyway_schema_history来判断是否为新增文件,从而判断是否进行迁移。
-----------------------------------------------------------------------------------------------------
默认的查找 migration 的路径为 classpath:db/migration ,对应 SQL 文件可放置在
src/main/resources/db/migration 下,Java 类可放置在 src/main/java/db/migration 下
c.校验版本号算法
flyway在升级数据库时会先计算之前已经升级过的脚本的checksum值和数据库的checkSum值进行比对,
如果老脚本发生了变化后checkSum校验就会失败,从而抛出异常,checkSum计算算法为(CRC32 (循环冗余校验码)算法)。
新增的脚本则会和数据库中的版本号进行比较,如果小于数据库存储的最后一个版本号,也不会继续执行。
d.Flyway的锁机制
Flyway使用数据库锁机制(locking technology of your database)来协调多个节点,
从而保证多套应用程序可同时执migration,而且集群控制也可做配置。
基于数据库锁机制实现分布式锁有两种,基于数据库表和基于数据库排他锁,Flyway采用的是基于数据库排他锁。
-----------------------------------------------------------------------------------------------------
排他锁(Exclusive Locks,简称X锁),又称为写锁、独占锁,在数据库管理上,是锁的基本类型之一。若
事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到T释放
A上的锁。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A
e.Flyway的启动速度
耗时主要来源两个方面:Flyway依次读取脚本中内容时的IO开销、Flyway计算脚本checksum值的算法开销
对于IO开销而言,每个脚本如果不是涉及大量的数据变更,只是表结构的变更,脚本的大小都非常小,可以不考虑。
03.Java使用
a.添加依赖
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>6.5.7</version>
</dependency>
b.添加配置
spring:
# 数据库连接配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm-demo?characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
username: xxx
password: xxx
flyway:
# 是否启用flyway
enabled: true
# 编码格式,默认UTF-8
encoding: UTF-8
# 迁移sql脚本文件存放路径,默认db/migration
locations: classpath:db/migration
# 迁移sql脚本文件名称的前缀,默认V
sql-migration-prefix: V
# 迁移sql脚本文件名称的分隔符,默认2个下划线__
sql-migration-separator: __
# 迁移sql脚本文件名称的后缀
sql-migration-suffixes: .sql
# 迁移时是否进行校验,默认true
validate-on-migrate: true
# 当迁移发现数据库非空且存在没有元数据的表时,自动执行基准迁移,新建schema_version表
baseline-on-migrate: true
c.命令行
flyway migrate:把数据库默认数据库迁移到最新版本
flyway clean:清除指定库下所有的对象,包括table、view、triggers…,让数据库变成空的状态
flyway info:用于打印所有Migrations的详细和状态信息
flyway Validate:是指验证已经应用的Migrations是否有变更,Flyway是默认是开启验证的
flyway Repair:修复Metadata表,该操作在Metadata表出现错误时是非常有用的。
Repair会修复Metadata表的错误,通常有两种用途:
①移除失败的Migration记录,该问题只是针对不支持DDL事务的数据库。
②重新调整已经应用的Migratons的Checksums值,比如:某个Migratinon已经被应用,但本地进行了修改,又期望重新应用并调整Checksum值,不过尽量不要这样操作,否则可能造成其它环境失败。
d.配置行
flyway.baseline-description对执行迁移时基准版本的描述.
flyway.baseline-on-migrate当迁移时发现目标schema非空,而且带有没有元数据的表时,是否自动执行基准迁移,默认false.
flyway.baseline-version开始执行基准迁移时对现有的schema的版本打标签,默认值为1.
flyway.check-location检查迁移脚本的位置是否存在,默认false.
flyway.clean-on-validation-error当发现校验错误时是否自动调用clean,默认false.
flyway.enabled是否开启flywary,默认true.
flyway.encoding设置迁移时的编码,默认UTF-8.
flyway.ignore-failed-future-migration当读取元数据表时是否忽略错误的迁移,默认false.
flyway.init-sqls当初始化好连接时要执行的SQL.
flyway.locations迁移脚本的位置,默认db/migration.
flyway.out-of-order是否允许无序的迁移,默认false.
flyway.password目标数据库的密码.
flyway.placeholder-prefix设置每个placeholder的前缀,默认${.
flyway.placeholder-replacementplaceholders是否要被替换,默认true.
flyway.placeholder-suffix设置每个placeholder的后缀,默认}.
flyway.placeholders.[placeholder name]设置placeholder的value
flyway.schemas设定需要flywary迁移的schema,大小写敏感,默认为连接默认的schema.
flyway.sql-migration-prefix迁移文件的前缀,默认为V.
flyway.sql-migration-separator迁移脚本的文件名分隔符,默认__
flyway.sql-migration-suffix迁移脚本的后缀,默认为.sql
flyway.tableflyway使用的元数据表名,默认为schema_version
flyway.target迁移时使用的目标版本,默认为latest version
flyway.url迁移时使用的JDBC URL,如果没有指定的话,将使用配置的主数据源
flyway.user迁移数据库的用户名
flyway.validate-on-migrate迁移时是否校验,默认为true.
e.sql脚本命名规则
1.仅需要执行一次的,以大写“V”开头,V+版本后(版本号间的数字以“.” 或者“ _ ”分隔开,“ _ ”会自动编译成 “ . ” )+" __"+文件描述+后缀名
2.需要执行多次的,以大写“R”开头,命名如R__clean.sql ,R的脚本只要改变了就会执行,R不带版本号
3.V开头的比R开头的优先级要高。
-----------------------------------------------------------------------------------------------------
前缀:用于版本控制(可配置)、撤消(可配置)和可重复迁移(可配置)VUR)
版本:带有点或下划线的版本可根据需要分隔任意数量的部分(不适用于可重复的迁移)
分隔符:(两个下划线)(可配置)__)
说明:下划线或空格分隔单词
后缀:(可配置.sql)
f.开发时注意事项
1.报错后需要删除flyway_schema_history中记录,否则启动失败
2.V的优先级高于R,假如三个V迁移脚本和一个R(无论新建还是修改)一起执行,其中一个V报错,则V会全部执行完成且记录到flyway_schema_history中,而R不执行且不记录,删除表中报错记录后,查询启动,则执行原错误V和未执行的R
3.多个要执行的R中,如果出现了其中一个出现了错误,则在其后的R都不执行
4.R的执行顺序根据命名来进行排序
5.一个文件中ddl并不由一个事务管理,比如创建三个表,中间创建表语句报错,则第一个表还是会创建成功并且提交事务
6.已经执行过的迁移文件(V)不能修改,否则报错。
7.同一个迁移文件下同表内DDL无法回滚,DML可回滚, 从报错点开始不往下执行,Flyway使用数据库锁机制
8.版本号相同会报错(Found more than one migration with version 1.0.0.9)
9.同一个迁移文件下假设都是dml,那么如果中间出现错误,所有的dml语句都会回滚
10.删除sql文件后启动会报错,报错如下:If you removed this migration intentionally, run repair to mark the migration as deleted.
3.5 格式:Jackjson
00.概述
a.优点
Jackjson 所依赖的 jar 包较少,简单易用
其他 Java 的 json 的框架 Gson 等相比, Jackjson 解析大的 json 文件速度比较快
Jackjson 运行时占用内存比较低,性能比较好
Jackjson 有灵活的 API,可以很容易进行扩展和定制
b.3个模块
Jackjson-core:用于JSON解析和生成
Jackjson-databind:用于数据绑定
Jackjson-annotations:用于注解支持
01.快速上手
a.假设我们有一个简单的 User 类
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String name;
private int age;
}
b.Java对象转JSON字符串
@Test
public void testSerializationExample() throws JsonProcessingException {
User user = new User("小凡", 18);
ObjectMapper objectMapper = new ObjectMapper();
String userstr = objectMapper.writeValueAsString(user);
System.out.println(userstr);
}
//输出
{"name":"小凡","age":18}
c.JSON字符串转Java对象
@Test
public void testDeserializationExample() throws JsonProcessingException {
String json = "{\"name\":\"小凡\",\"age\":18}";
ObjectMapper objectMapper = new ObjectMapper();
User user = objectMapper.readValue(json, User.class);
System.out.println(user);
}
//输出
User(name=小凡, age=18)
02.序列化
a.普通Java对象序列化
@Test
public void testObjectToJson() throws JsonProcessingException {
User user = new User();
user.setName("小凡");
user.setAge(18);
ObjectMapper mapper = new ObjectMapper();
String userstr = mapper.writeValueAsString(user);
System.out.println(userstr);
}
//输出
{"name":"小凡","age":18}
b.复杂对象序列化
a.构造作者出版信息对象
@Data
public class Book {
private String bookName;
private String publishDate;
private String publishHouse;
private Double price;
}
@Data
public class Address {
private String city;
private String street;
}
@Data
public class PublishInfo {
private String name;
private String sex;
private Integer age;
private Address addr;
private List<Book> books;
}
b.复杂对象转JSON字符串
@Test
public void testComplexObjectToJson() throws JsonProcessingException {
ArrayList<Book> books = new ArrayList<Book>();
Book book1 = new Book();
book1.setBookName("Java从入门到放弃");
book1.setPublishDate("2004-01-01");
book1.setPublishHouse("小凡出版社");
book1.setPrice(66.66);
Book book2 = new Book();
book2.setBookName("Spring从入门到入土");
book2.setPublishDate("2024-01-01");
book2.setPublishHouse("小凡出版社");
book2.setPrice(88.88);
books.add(book1);
books.add(book2);
Address addr = new Address();
addr.setCity("昆明");
addr.setStreet("xxx区xxx路xxx号");
PublishInfo publishInfo = new PublishInfo();
publishInfo.setName("小凡");
publishInfo.setSex("男");
publishInfo.setAge(18);
publishInfo.setAddr(addr);
publishInfo.setBooks(books);
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(publishInfo);
System.out.println(json);
}
//返回
{"name":"小凡","sex":"男","age":18,"addr":{"city":"昆明","street":"xxx区xxx路xxx号"},"books":[{"bookName":"Java从入门到放弃","publishDate":"2004-01-01","publishHouse":"小凡出版社","price":66.66},{"bookName":"Spring从入门到入土","publishDate":"2024-01-01","publishHouse":"小凡出版社","price":88.88}]}
c.List集合序列化
@Test
public void testListToJson() throws JsonProcessingException {
User user1 = new User();
user1.setName("小凡001");
user1.setAge(18);
User user2 = new User();
user2.setName("小凡002");
user2.setAge(30);
ArrayList<User> users = new ArrayList<>();
users.add(user1);
users.add(user2);
ObjectMapper mapper = new ObjectMapper();
String userstr = mapper.writeValueAsString(users);
System.out.println(userstr);
}
//输出
[{"name":"小凡001","age":18},{"name":"小凡002","age":30}]
d.Map集合序列化
a.示例1
@Test
public void testMapToJson() throws JsonProcessingException {
User user = new User();
user.setName("小凡");
user.setAge(18);
List<String> asList = Arrays.asList("抽烟", "喝酒", "烫头发");
HashMap<String, Object> map = new HashMap<>();
map.put("user", user);
map.put("hobby",asList);
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(map);
System.out.println(json);
}
//输出
{"user":{"name":"小凡","age":18},"hobby":["抽烟","喝酒","烫头发"]}
b.示例2
@Test
public void testMapToJsonSup() throws JsonProcessingException {
User user1 = new User();
user1.setName("小凡001");
user1.setAge(18);
User user2 = new User();
user2.setName("小凡002");
user2.setAge(30);
ArrayList<User> users = new ArrayList<>();
users.add(user1);
users.add(user2);
List<String> asList = Arrays.asList("抽烟", "喝酒", "烫头发");
HashMap<String, Object> map = new HashMap<>();
map.put("users", users);
map.put("hobby",asList);
map.put("name","张三");
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(map);
System.out.println(json);
}
//输出
{"name":"张三","users":[{"name":"小凡001","age":18},{"name":"小凡002","age":30}],"hobby":["抽烟","喝酒","烫头发"]}
e.日期处理
a.说明
默认情况下,jackson会将日期类型属性序列化成long型值(自1970年1月1日以来的毫秒数)
显然这样格式的数据不符合人类直观查看
b.假设我们有个Person对象
@Data
public class Person {
private String name;
private Date birthday;
}
c.我们先来看看默认转换的结果
@Test
public void testDateToJsonDefault() throws JsonProcessingException {
Person person = new Person();
person.setName("小凡");
person.setBirthday(new Date());
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(person);
System.out.println(json);
}
//输出
{"name":"小凡","birthday":1712220896407}
d.通过SimpleDateFormat 将日期格式化成人类可看格式显示
@Test
public void testDateToJson() throws JsonProcessingException {
Person person = new Person();
person.setName("小凡");
person.setBirthday(new Date());
ObjectMapper mapper = new ObjectMapper();
//进行一下日期转换
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
mapper.setDateFormat(dateFormat);
String json = mapper.writeValueAsString(person);
System.out.println(json);
}
//输出 这个格式就人性化多了
{"name":"小凡","birthday":"2024-04-04"}
03.反序列化
a.说明
Jackson通过将JSON字段的名称与Java对象中的getter和setter方法进行匹配,将JSON对象的字段映射到Java对象中的属性
Jackson删除了getter和setter方法名称的“ get”和“ set”部分,并将其余名称的第一个字符转换为小写
b.普通JSON字符串反序列化
a.JSON字符串->Java对象
@Test
public void testStrToObject() throws JsonProcessingException {
String json = "{\"name\":\"小凡\",\"age\":18}";
ObjectMapper mapper = new ObjectMapper();
User user = mapper.readValue(json, User.class);
System.out.println(user);
}
//输出
User(name=小凡, age=18)
b.字符输入流-->Java对象
@Test
public void testReaderToObject() throws JsonProcessingException {
String json = "{\"name\":\"小医仙\",\"age\":18}";
Reader reader = new StringReader(json);
ObjectMapper mapper = new ObjectMapper();
User user = mapper.readValue(json, User.class);
System.out.println(user);
}
//输出
User(name=小医仙, age=18)
c.字节输入流->Java对象
@Test
public void testInputStreamToObject() throws IOException {
FileInputStream inputStream = new FileInputStream("F:\\vueworkspace\\jackjson-demo\\jackjson-demo\\src\\json\\user001.json");
ObjectMapper mapper = new ObjectMapper();
User user = mapper.readValue(inputStream, User.class);
System.out.println(user);
}
//输出
User(name=萧炎, age=18)
d.JSON 文件反->Java对象
@Test
public void testJsonfileToObject() throws IOException {
File file = new File("F:\\vueworkspace\\jackjson-demo\\jackjson-demo\\src\\json\\user.json");
ObjectMapper mapper = new ObjectMapper();
User user = mapper.readValue(file, User.class);
System.out.println(user);
}
//输出
User(name=萧炎, age=18)
e.URL文件->Java对象
@Test
public void testUrlToObject() throws IOException {
String url ="https://files.cnblogs.com/files/blogs/685650/user.json";
URL url1 = new URL(url);
ObjectMapper mapper = new ObjectMapper();
User user = mapper.readValue(url1, User.class);
System.out.println(user);
}
//输出
User(name=紫妍, age=18)
f.字节数组-> java对象
@Test
public void testByteToObject() throws IOException {
String json = "{\"name\":\"韩雪\",\"age\":18}";
byte[] bytes = json.getBytes();
ObjectMapper mapper = new ObjectMapper();
User user = mapper.readValue(bytes,
3.6 格式:Fastjson
00.关于 Fastjson
a.简介
Fastjson 是由阿里巴巴集团开发的一个高性能的 JSON 处理库,它支持 Java 对象与 JSON 字符串之间的互相转换
Fastjson 自 2011 年发布以来,以其卓越的性能和丰富的功能在 Java 社区中获得了广泛的应用
Alibaba Fastjson: 目前在人类已知范围内,这个星球跑的最快的 Java JSON 库
在过去的十年里,fastjson v1 作为国内 github star 最多和最受欢迎的 json 解析库
如今 fastjson v2 重磅来袭,性能炸裂
b.发展
a.初始阶段
2010 年:FastJSON 的起源可以追溯到 2010 年左右。当时,阿里巴巴的技术专家温绍锦(花名:高铁)在寻找一个高效、易用的 JSON 解析库时发现现有的解决方案并不完全满足需求,于是决定自己动手编写一个
早期版本:最初的 FastJSON 版本是基于一些基本的需求设计的,目标是提供快速且易于使用的 JSON 处理功能
b.成长与发展
开源与社区支持:随着 FastJSON 的性能和功能逐渐得到认可,阿里巴巴决定将其开源,以便更多开发者能够使用并贡献代码。这标志着 FastJSON 开始走向更广泛的社区支持和发展
版本迭代:FastJSON 经历了多个版本的迭代,每个新版本都会修复已知问题,并引入新的特性和优化。例如,v1.2.x 系列版本中就增加了许多重要的改进和安全特性
c.安全性与漏洞
安全性问题:尽管 FastJSON 在性能上表现出色,但它也经历了几次严重的安全漏洞事件。特别是在 v1.2.43 及之前的版本中,存在一些潜在的安全风险,如反序列化攻击等
响应与改进:面对这些挑战,FastJSON 团队积极应对,发布了多个补丁和更新来解决已知的安全问题,并增强了整体的安全性
d.新一代 FastJSON
FastJSON 2.0:为了从根本上解决历史遗留的问题,并进一步提升性能和安全性,阿里巴巴推出了 FastJSON 2.0。这个新版本重新设计了架构,提供了更好的兼容性和扩展性,并且更加注重安全性
持续改进:FastJSON 2.0 之后,团队继续致力于提高库的稳定性和性能,同时增加了一些新的功能和优化,以适应不断变化的技术环境
e.当前状态
广泛采用:FastJSON 已经成为国内外非常流行的 JSON 处理库之一,被大量企业级应用和服务所采用
持续发展:FastJSON 仍在不断发展和完善中,定期发布新的版本以修复 bug、添加新功能和支持最新的 Java 版本
FASTJSON v2 是 FASTJSON 项目的重要升级,目标是为下一个十年提供一个高性能的 JSON 库。通过同一套 API 支持 JSON/JSONB 两种协议,JSONPath 是一等公民,支持全量解析和部分解析,支持 Java 服务端、客户端 Android、大数据场景
c.特点
高性能: FASTJSON 在序列化和反序列化方面表现出色,速度通常比其他 JSON 处理库(如 Jackson 和 Gson)更快
易用性: 提供简单易用的 API,使得 JSON 的处理变得直观,开发者可以轻松地将 Java 对象转化为 JSON 字符串,或从 JSON 字符串中解析出 Java 对象
支持复杂数据结构: 能够处理复杂的数据结构,包括嵌套对象、数组等
灵活的配置: 提供多种特性配置,允许用户根据需求定制序列化和反序列化的行为,比如日期格式、字段过滤等
快速解析: 支持从字符串流或输入流快速解析 JSON 数据,提高了性能和内存使用效率
d.应用场景
Web 开发:常用于 RESTful API 的 JSON 数据处理
数据交换:在分布式系统中,作为不同服务之间的数据交换格式
配置文件解析:可以用作应用程序的配置文件格式,方便读取和写入
01.基本用法
a.添加依赖
a.v1版本
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
b.v2版本
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.x.x</version>
</dependency>
c.如果原来使用 fastjson 1.2.x 版本,可以使用兼容包
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.x.x</version>
</dependency>
b.序列化(Java 对象转 JSON 字符串)
import com.alibaba.fastjson.JSON;
public class Example {
public static void main(String[] args) {
User user = new User("Alice", 30);
String jsonString = JSON.toJSONString(user);
System.out.println(jsonString); // 输出: {"name":"Alice","age":30}
}
}
class User {
private String name;
private int age;
// 构造函数和 getter、setter 略
}
c.反序列化(JSON 字符串转 Java 对象)
String jsonString = "{\"name\":\"Alice\",\"age\":30}";
User user = JSON.parseObject(jsonString, User.class);
System.out.println(user.getName()); // 输出: Alice
02.主要核心类
a.JSON类
a.简介
FastJSON 提供了几个核心类来处理 JSON 数据
在包中可以发现主要的 3 个类,JSON,JSONArray,JSONObject,三者之间的关系如下,JSONObject 和 JSONArray 继承 JSON
JSON 类是 Fastjson 中最核心的类,提供了用于 JSON 数据处理的静态方法
b.常用方法
toJSON(Object obj):将 Java 对象转换为 JSON 对象
toJSONString(Object obj):将 Java 对象序列化为 JSON 字符串
parse(String text):将 JSON 字符串解析为 JSON 对象
parseObject(String text, Class<T> clazz):将 JSON 字符串解析为指定类型的 Java 对象
c.toJSONString(Object o)
这个方法平时最常见了,将 JavaBean 序列化成 JSON 文本
通过传入一个对象,便可以将对象转成 JSON 字符串,这里我们传入的不仅仅是 JavaBean 还可以是一个 Map 对象
传入一个 Map 对象我们同样可以获取到一个 JSON 字符串
List 对象也很适用:结果是一个标准的 JSONArray 的字符串
d.parseArray(String text)
这是一个将 JSON 字符串转为 JSONArray 的方法
e.parseObject(String text)
上面说到的是序列化,那么对应的便是反序列化
反序列化就是把 JSON 格式的字符串转化为 Java Bean 对象
用法十分简单,可以将一个标准的 JSON 字符串转为一个 JSONObject 对象
由于 JSONObject 类实现了 Map 接口,因此我们可以通过 get() 来获取到值
我们已经知道了 Map 的致命不足,所以我们更希望能得到一个 JavaBean 对象
当然也是可以的!我们通过传入我们想要转换的对象类型,就可以得到我们想要的 JavaBean
b.JSONArray类
a.简介
JSONArray 是 Fastjson 库中用于表示 JSON 数组的类。它提供了多种方法来操作和管理 JSON 数组的数据
b.常用方法
add(Object value):向数组中添加一个元素
get(int index):获取指定索引位置的元素
size():返回数组的长度
toJSONString():将 JSONArray 转换为 JSON 字符串
toArray(Class<T> componentType):将 JSONArray 转换为指定类型的 Java 数组
toList(Class<T> clazz):将 JSONArray 转换为指定类型的 Java 列表
c.创建 JSONArray
import com.alibaba.fastjson.JSONArray;
public class JSONArrayExample {
public static void main(String[] args) {
// 创建一个空的 JSONArray
JSONArray jsonArray = new JSONArray();
// 添加元素
jsonArray.add("Hello");
jsonArray.add(123);
jsonArray.add(true);
System.out.println(jsonArray); // 输出: ["Hello",123,true]
}
}
d.从 JSON 字符串解析 JSONArray
import com.alibaba.fastjson.JSONArray;
public class ParseJSONArrayExample {
public static void main(String[] args) {
// 从 JSON 字符串解析 JSONArray
String jsonString = "[\"Apple\", \"Banana\", \"Cherry\"]";
JSONArray jsonArray = JSONArray.parseArray(jsonString);
System.out.println(jsonArray); // 输出: ["Apple","Banana","Cherry"]
}
}
e.访问 JSONArray 中的元素
import com.alibaba.fastjson.JSONArray;
public class AccessJSONArrayExample {
public static void main(String[] args) {
String jsonString = "[\"Apple\", \"Banana\", \"Cherry\"]";
JSONArray jsonArray = JSONArray.parseArray(jsonString);
// 获取元素
String firstElement = jsonArray.getString(0); // "Apple"
String secondElement = jsonArray.getString(1); // "Banana"
System.out.println(firstElement); // 输出: Apple
System.out.println(secondElement); // 输出: Banana
}
}
f.遍历 JSONArray
import com.alibaba.fastjson.JSONArray;
public class IterateJSONArrayExample {
public static void main(String[] args) {
String jsonString = "[\"Apple\", \"Banana\", \"Cherry\"]";
JSONArray jsonArray = JSONArray.parseArray(jsonString);
// 遍历 JSONArray
for (int i = 0; i < jsonArray.size(); i++) {
System.out.println(jsonArray.getString(i));
}
// 输出:
// Apple
// Banana
// Cherry
}
}
g.修改 JSONArray 中的元素
import com.alibaba.fastjson.JSONArray;
public class ModifyJSONArrayExample {
public static void main(String[] args) {
String jsonString = "[\"Apple\", \"Banana\", \"Cherry\"]";
JSONArray jsonArray = JSONArray.parseArray(jsonString);
// 修改元素
jsonArray.set(1, "Blueberry"); // 将 "Banana" 修改为 "Blueberry"
System.out.println(jsonArray); // 输出: ["Apple","Blueberry","Cherry"]
}
}
h.删除 JSONArray 中的元素
import com.alibaba.fastjson.JSONArray;
public class RemoveFromJSONArrayExample {
public static void main(String[] args) {
String jsonString = "[\"Apple\", \"Banana\", \"Cherry\"]";
JSONArray jsonArray = JSONArray.parseArray(jsonString);
// 删除元素
jsonArray.remove(1); // 删除 "Banana"
System.out.println(jsonArray); // 输出: ["Apple","Cherry"]
}
}
c.JSONObject类
a.简介
JSONObject 类实现了 Map 接口,代表了一个 JSON 对象。它允许你通过键值对的方式存储和检索
b.常用方法
get(String key):获取指定键的值
put(String key, Object value):添加或替换指定键的值
getString(String key)、getIntValue(String key) 等:获取特定类型的值
fluentPut(String key, Object value):链式调用的方式添加键值对
toJSONString():将 JSONObject 转换为 JSON 字符串
c.构造方法
JSONObject() : 创建一个空的 JSON 对象
JSONObject(Map<?, ?> map) : 根据给定的映射创建 JSON 对象
d.添加和设置键值对
a.put(String key, Object value) : 添加或更新键值对
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", "Alice");
jsonObject.put("age", 30);
b.putAll(Map<? extends String, ?> map) : 将一个映射中的所有键值对添加到 JSON 对象中
Map<String, Object> map = new HashMap<>();
map.put("city", "Beijing");
map.put("country", "China");
jsonObject.putAll(map);
e.获取值
a.get(String key) : 根据键获取对应的值
Object name = jsonObject.get("name");
b.getString(String key) : 根据键获取字符串类型的值
String name = jsonObject.getString("name");
getInteger(String key) : 根据键获取整数类型的值。
Integer age = jsonObject.getInteger("age");
c.getJSONObject(String key) : 根据键获取 JSON 对象
JSONObject address = jsonObject.getJSONObject("address");
d.getJSONArray(String key) : 根据键获取 JSON 数组
JSONArray hobbies = jsonObject.getJSONArray("hobbies");
f.删除键值对
a.remove(String key) : 删除指定键的键值对
jsonObject.remove("city");
b.clear() : 清空 JSON 对象中的所有键值对
jsonObject.clear();
g.查询方法
a.containsKey(String key) : 检查 JSON 对象中是否包含指定的键
boolean hasName = jsonObject.containsKey("name");
b.size() : 返回 JSON 对象中键值对的数量
int size = jsonObject.size();
h.转换
a.toJSONString() : 将 JSON 对象转换为 JSON 字符串
String jsonString = jsonObject.toJSONString();
b.toJavaObject(Class<T> clazz) : 将 JSON 对象转换为 Java 对象
Person person = jsonObject.toJavaObject(Person.class);
i.其他方法
a.keySet() : 返回 JSON 对象中所有键的集合
Set<String> keys = jsonObject.keySet();
b.clone() : 克隆一个新的 JSONObject
JSONObject clonedObject = jsonObject.clone();
j.示例
import com.alibaba.fastjson.JSONObject;
public class FastjsonExample {
public static void main(String[] args) {
// 创建一个 JSON 对象
JSONObject jsonObject = new JSONObject();
// 添加键值对
jsonObject.put("name", "Alice");
jsonObject.put("age", 30);
jsonObject.put("city", "Beijing");
// 打印 JSON 对象
System.out.println("JSON Object: " + jsonObject.toJSONString());
// 访问值
String name = jsonObject.getString("name");
Integer age = jsonObject.getInteger("age");
System.out.println("Name: " + name);
System.out.println("Age: " + age);
// 修改值
jsonObject.put("age", 31);
System.out.println("Updated JSON Object: " + jsonObject.toJSONString());
// 删除键值对
jsonObject.remove("city");
System.out.println("After removal: " + jsonObject.toJSONString());
// 查询大小
System.out.println("Size of object: " + jsonObject.size());
}
}
3.7 队列:Disruptor
01.概述
a.定义
Disruptor 是一个高性能、低延迟的无锁队列替代方案,最初由LMAX公司开发,专为处理高吞吐量和低延迟的消息传递系统而设计
它利用环形缓冲区(RingBuffer)和无锁的生产者-消费者模型,大幅提升并发性能
b.对比
相比传统的基于 java.util.concurrent 的队列(如 ArrayBlockingQueue、LinkedBlockingQueue)
Disruptor 通过避免锁竞争、减少 CPU 缓存行无效(cache invalidation)等方式提高吞吐量
02.核心概念
a.RingBuffer(环形缓冲区)
Disruptor 的核心数据结构是环形缓冲区(RingBuffer),它类似于一个固定大小的数组,数据结构如下
+----+----+----+----+----+----+----+----+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+----+----+----+----+----+----+----+----+
RingBuffer 通过索引递增的方式循环使用元素,避免内存分配和垃圾回收的开销
b.Sequence(序列号)
在 Disruptor 中,所有读写操作都基于 Sequence,用于跟踪当前生产和消费的位置
1.Cursor:指向 RingBuffer 中最后一个被写入的位置。
2.SequenceBarrier:用于协调生产者和消费者的进度,确保消费者不会读取尚未发布的数据。
3.Sequencer:用于管理 RingBuffer 的序列。
c.Producer(生产者)
生产者向 RingBuffer 写入数据,通常采用 ClaimStrategy 申请空间,然后写入数据并发布
d.Consumer(消费者)
消费者从 RingBuffer 读取数据,并可以设置多个消费者进行并行处理,支持 WorkerPool 模式
e.WaitStrategy(等待策略)
Disruptor 通过 WaitStrategy 来决定消费者如何等待新的数据到达
1.BusySpinWaitStrategy:自旋等待,适用于低延迟应用,但 CPU 开销较大
2.SleepingWaitStrategy:适当休眠,减少 CPU 占用
3.YieldingWaitStrategy:让出 CPU 时间片,适用于高吞吐场景
03.优势
a.无锁设计
传统队列使用 ReentrantLock 或 synchronized 来保证线程安全
而 Disruptor 通过 CAS(Compare-And-Swap)机制更新 Sequence,避免锁的开销
b.高效的 CPU 缓存利用
Disruptor 采用 伪共享(False Sharing)避免CPU缓存行竞争
并使用 缓存行填充(Cache Line Padding)来减少缓存行失效
c.生产者-消费者模型优化
单消费者:一个消费者处理所有数据
多消费者并行消费:多个消费者共同消费数据,提高吞吐量
菱形依赖消费:一个消费者的输出作为另一个消费者的输入
04.使用示例
a.引入依赖
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.2</version>
</dependency>
b.创建事件类
public class LongEvent {
private long value;
public void set(long value) {
this.value = value;
}
public long getValue() {
return value;
}
}
c.定义事件工厂
import com.lmax.disruptor.EventFactory;
public class LongEventFactory implements EventFactory<LongEvent> {
@Override
public LongEvent newInstance() {
return new LongEvent();
}
}
d.事件处理器
import com.lmax.disruptor.EventHandler;
public class LongEventHandler implements EventHandler<LongEvent> {
@Override
public void onEvent(LongEvent event, long sequence, boolean endOfBatch) {
System.out.println("Event: " + event.getValue());
}
}
e.配置Disruptor
import com.lmax.disruptor.*;
import com.lmax.disruptor.dsl.Disruptor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class DisruptorExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
LongEventFactory factory = new LongEventFactory();
int bufferSize = 1024;
Disruptor<LongEvent> disruptor = new Disruptor<>(
factory, bufferSize, executor, ProducerType.SINGLE, new YieldingWaitStrategy());
disruptor.handleEventsWith(new LongEventHandler());
disruptor.start();
RingBuffer<LongEvent> ringBuffer = disrupt
05.应用场景
a.事件处理
a.描述
Disruptor 适用于事件驱动架构,实现高效的事件处理
b.示例代码
public class EventData {
private String data;
public void setData(String data) { this.data = data; }
public String getData() { return data; }
}
public class EventFactory implements EventFactory<EventData> {
@Override
public EventData newInstance() { return new EventData(); }
}
public class EventHandler implements EventHandler<EventData> {
@Override
public void onEvent(EventData event, long sequence, boolean endOfBatch) {
System.out.println("Processing event: " + event.getData());
}
}
public class EventProcessingSystem {
public static void main(String[] args) {
Disruptor<EventData> disruptor = new Disruptor<>(
new EventFactory(),
1024,
Executors.defaultThreadFactory(),
ProducerType.SINGLE,
new SleepingWaitStrategy()
);
disruptor.handleEventsWith(new EventHandler());
disruptor.start();
RingBuffer<EventData> ringBuffer = disruptor.getRingBuffer();
ringBuffer.publishEvent((event, sequence) -> event.setData("Sample Event"));
}
}
b.日志记录
a.描述
Disruptor 适合用作高性能日志队列,避免传统阻塞队列的性能瓶颈
b.示例代码
public class LogEvent {
private String message;
public void setMessage(String message) { this.message = message; }
public String getMessage() { return message; }
}
public class LogEventFactory implements EventFactory<LogEvent> {
@Override
public LogEvent newInstance() { return new LogEvent(); }
}
public class LogEventHandler implements EventHandler<LogEvent> {
@Override
public void onEvent(LogEvent event, long sequence, boolean endOfBatch) {
System.out.println("Log: " + event.getMessage());
}
}
public class DisruptorLogSystem {
public static void main(String[] args) {
Disruptor<LogEvent> disruptor = new Disruptor<>(
new LogEventFactory(),
1024,
Executors.defaultThreadFactory(),
ProducerType.SINGLE,
new SleepingWaitStrategy()
);
disruptor.handleEventsWith(new LogEventHandler());
disruptor.start();
RingBuffer<LogEvent> ringBuffer = disruptor.getRingBuffer();
ringBuffer.publishEvent((event, sequence) -> event.setMessage("Test Log"));
}
}
c.消息传递
a.描述
Disruptor 适用于高吞吐量的消息传递系统,例如消息队列,以及实时消息等
b.示例代码
public class MessageEvent {
private String message;
public void setMessage(String message) { this.message = message; }
public String getMessage() { return message; }
}
public class MessageEventHandler implements EventHandler<MessageEvent> {
@Override
public void onEvent(MessageEvent event, long sequence, boolean endOfBatch) {
System.out.println("Received message: " + event.getMessage());
}
}
public class DisruptorMessageQueue {
public static void main(String[] args) {
Disruptor<MessageEvent> disruptor = new Disruptor<>(
MessageEvent::new,
1024,
Executors.defaultThreadFactory(),
ProducerType.SINGLE,
new YieldingWaitStrategy()
);
disruptor.handleEventsWith(new MessageEventHandler());
disruptor.start();
RingBuffer<MessageEvent> ringBuffer = disruptor.getRingBuffer();
ringBuffer.publishEvent((event, sequence) -> event.setMessage("Hello Disruptor"));
}
}
d.实时数据分析
a.描述
Disruptor 可用于高并发环境下的实时数据流处理
b.示例代码
public class DataEvent {
private double value;
public void setValue(double value) { this.value = value; }
}
public class DataAnalyzer implements EventHandler<DataEvent> {
@Override
public void onEvent(DataEvent event, long sequence, boolean endOfBatch) {
System.out.println("Analyzing data: " + event.value);
}
}
e.并发任务调度
a.描述
在高并发环境下,使用 Disruptor 可以构建高效的异步任务调度系统
b.示例代码
public class TaskEvent {
private Runnable task;
public void setTask(Runnable task) { this.task = task; }
}
public class TaskHandler implements EventHandler<TaskEvent> {
@Override
public void onEvent(TaskEvent event, long sequence, boolean endOfBatch) {
event.task.run();
}
}
3.8 工具:Spring
00.汇总
BeanInfo
PropertyDescriptor
BeanWrapper
ResolvableType
ConversionService
Spring AOP三件套:AopContext、AopUtils、ReflectionUtils
01.BeanInfo
a.定义
BeanInfo 是 JavaBeans 规范的一部分,用于描述 JavaBean 的属性、事件和方法
b.原理
通过反射机制,BeanInfo 提供了对 JavaBean 的元数据访问
c.常用 API
getPropertyDescriptors():获取属性描述符数组
getMethodDescriptors():获取方法描述符数组
d.使用步骤
1.使用 Introspector.getBeanInfo(Class<?> beanClass) 获取 BeanInfo 对象
2.通过 BeanInfo 获取属性和方法描述符
e.示例代码
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
public class BeanInfoExample {
public static void main(String[] args) throws Exception {
BeanInfo beanInfo = Introspector.getBeanInfo(MyBean.class);
for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {
System.out.println(pd.getName());
}
}
}
class MyBean {
private String name;
private int age;
// getters and setters
}
02.PropertyDescriptor
a.定义
PropertyDescriptor 用于描述 JavaBean 的一个属性,包括属性的读写方法
b.原理
通过反射获取属性的读写方法,提供对属性的访问
c.常用 API
getReadMethod():获取属性的读取方法
getWriteMethod():获取属性的写入方法
d.使用步骤
1.创建 PropertyDescriptor 对象,指定属性名和对应的读写方法
2.使用 getReadMethod() 和 getWriteMethod() 访问属性
e.示例代码
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
public class PropertyDescriptorExample {
public static void main(String[] args) throws Exception {
PropertyDescriptor pd = new PropertyDescriptor("name", MyBean.class);
Method readMethod = pd.getReadMethod();
Method writeMethod = pd.getWriteMethod();
MyBean bean = new MyBean();
writeMethod.invoke(bean, "John");
System.out.println(readMethod.invoke(bean));
}
}
03.BeanWrapper
a.定义
BeanWrapper 是 Spring 提供的一个接口,用于操作 JavaBean 的属性
b.原理
通过反射和 PropertyEditor,BeanWrapper 提供了对 JavaBean 属性的动态访问和修改
c.常用 API
setPropertyValue(String propertyName, Object value):设置属性值
getPropertyValue(String propertyName):获取属性值
d.使用步骤
1.创建 BeanWrapper 实例,传入目标 JavaBean
2.使用 setPropertyValue 和 getPropertyValue 操作属性
e.示例代码
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
public class BeanWrapperExample {
public static void main(String[] args) {
MyBean bean = new MyBean();
BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean);
wrapper.setPropertyValue("name", "Alice");
System.out.println(wrapper.getPropertyValue("name"));
}
}
04.ResolvableType
a.定义
ResolvableType 是 Spring 提供的一个类,用于解析泛型类型信息
b.原理
通过反射,ResolvableType 提供了对泛型类型的解析和操作
c.常用 API
forClass(Class<?> clazz):获取类的 ResolvableType
getGeneric(int... indexes):获取泛型参数类型
d.使用步骤
1.使用 ResolvableType.forClass 获取类的 ResolvableType
2.使用 getGeneric 获取泛型参数类型
e.示例代码
import org.springframework.core.ResolvableType;
import java.util.List;
public class ResolvableTypeExample {
public static void main(String[] args) {
ResolvableType resolvableType = ResolvableType.forClass(MyGenericClass.class);
ResolvableType genericType = resolvableType.getGeneric(0);
System.out.println(genericType.resolve());
}
}
class MyGenericClass<T> {
}
05.ConversionService
a.定义
ConversionService 是 Spring 提供的一个接口,用于类型转换
b.原理
通过注册转换器,ConversionService 提供了灵活的类型转换机制
c.常用 API
convert(Object source, Class<T> targetType):将源对象转换为目标类型
d.使用步骤
1.创建 ConversionService 实例
2.使用 convert 方法进行类型转换
e.示例代码
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
public class ConversionServiceExample {
public static void main(String[] args) {
ConversionService conversionService = new DefaultConversionService();
Integer number = conversionService.convert("123", Integer.class);
System.out.println(number);
}
}
06.Spring AOP三件套:AopContext、AopUtils、ReflectionUtils
a.AopContext
a.说明
AopContext 是 Spring 框架中的一个类,它提供了对当前 AOP 代理对象的访问,以及对目标对象的引用
AopContext 主要用于获取当前代理对象的相关信息,以及在 AOP 代理中进行一些特定的操作
b.方法
getTargetObject(): 获取当前代理的目标对象
currentProxy(): 获取当前的代理对象
c.代码
public void noTransactionTask(String keyword){ // 注意这里 调用了代理类的方法
((YourClass) AopContext.currentProxy()).transactionTask(keyword);
}
@Transactional
void transactionTask(String keyword) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) { //logger
//error tracking
}
System.out.println(keyword);
}
d.说明
同一个类中两个方法,noTransactionTask 方法调用 transactionTask 方法,为了使事务注解不失效
就可以使用 AopContext.currentProxy() 去获取当前代理对象
b.AopUtils
a.说明
AopUtils 提供了一些静态方法来处理与 AOP 相关的操作,如获取代理对象、获取目标对象、判断代理类型等
b.方法
getTargetObject(): 从代理对象中获取目标对象
isJdkDynamicProxy(Object obj): 判断是否是 JDK 动态代理
isCglibProxy(Object obj): 判断是否是 CGLIB 代理
c.代码
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
public class AopUtilsExample {
public static void main(String[] args) {
MyService myService = ...
// 假设 myService 已经被代理
if (AopUtils.isCglibProxy(myService)) {
System.out.println("这是一个 CGLIB 代理对象");
}
}
}
c.ReflectionUtils
a.说明
ReflectionUtils 提供了一系列反射操作的便捷方法,如设置字段值、获取字段值、调用方法等
这些方法封装了 Java 反射 API 的复杂性,使得反射操作更加简单和安全
b.方法
makeAccessible(Field field): 使私有字段可访问
getField(Field field, Object target): 获取对象的字段值
invokeMethod(Method method, Object target, Object... args): 调用对象的方法
c.代码
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
import java.util.Map;
public class ReflectionUtilsExample {
public static void main(String[] args) throws Exception {
ExampleBean bean = new ExampleBean();
bean.setMapAttribute(new HashMap<>());
Field field = ReflectionUtils.findField(ExampleBean.class, "mapAttribute");
ReflectionUtils.makeAccessible(field);
Object value = ReflectionUtils.getField(field, bean);
System.out.println(value);
}
static class ExampleBean {
private Map<String, String> mapAttribute;
public void setMapAttribute(Map<String, String> mapAttribute) {
this.mapAttribute = mapAttribute;
}
}
}
3.9 拷贝:BeanUtils
00.汇总
a.总结
01.属性类型不一致导致拷贝失败 当源对象和目标对象的同一属性类型不一致时,拷贝会失败。例如,一个类中的属性是 Long 类型,而另一个类中的相同属性是 String 类型
02.属性名不一致 当源对象和目标对象的属性名不一致时,BeanUtils.copyProperties 无法进行属性拷贝
03.null值覆盖导致数据异常 当源对象的某些字段为 null 时,使用 BeanUtils.copyProperties 会将这些 null 值覆盖到目标对象的相应字段,导致原有数据丢失
04.导包错误导致拷贝数据异常 如果项目中同时引入了Spring的beans包和Apache的beanutils包,导入错误的包可能导致数据拷贝失败。Spring的 BeanUtils 和 Apache的 BeanUtils 方法签名不同,容易混淆
05.查找不到字段引用,修改内容难以溯源 使用 BeanUtils.copyProperties 进行数据拷贝时,难以通过代码搜索快速定位到字段的赋值位置,增加了排查问题的难度
06.内部类数据无法成功拷贝 即使内部类的类型和字段名相同,BeanUtils.copyProperties 也无法成功拷贝内部类的数据,因为它们被认为是不同的属性
07.BeanUtils.copyProperties是浅拷贝 BeanUtils.copyProperties 进行的是浅拷贝,对于引用类型的属性,源对象和目标对象共享相同的引用。修改源对象的引用属性会影响目标对象
08.底层实现为反射拷贝效率低 BeanUtils.copyProperties 底层通过反射实现,效率较低。与直接使用 set 方法赋值相比,性能差距明显
09.boolean类型加is属性开头 当属性名以 is 开头且类型为 boolean 时,BeanUtils.copyProperties 可能无法正确识别和拷贝这些属性
10.对象属性没有get/set方法赋值失败 如果对象的属性没有对应的 get 或 set 方法,BeanUtils.copyProperties 无法进行属性拷贝
b.源码分析
1.Spring的BeanUtils拷贝,使用的是反射机制
2.先获取target中所有字段以及它们的getter和setter方法
3.遍历target的字段,如果字段有setter方法或者不是忽略对象则进行下一步操作,否则忽略
4.用target的字段去source中获取对应的值(通过getter方法),有值则进行下一步,否则忽略
5.获source和target中同一个字段的类型,并且判断类型是否相同,相同则继续下一步,否则忽略
6.如果source和target的字段是非public,则通过反射修改权限
7.最后,通过反射完成赋值
-----------------------------------------------------------------------------------------------------
org.springframework.beans.BeanUtils的拷贝屏蔽了很多的异常,总结如下:
1.source和target的字段缺少getter和setter方法,拷贝失败
2.source和target的字段名称不同,拷贝失败,即字段名相同才可以拷贝
3.ource和target的字段类型不同,拷贝失败,即类型相同才可以拷贝
4.对于Map类型,无法拷贝
01.属性类型不一致导致拷贝失败
a.描述
当源对象和目标对象的同一属性类型不一致时,拷贝会失败。例如,一个类中的属性是 Long 类型,而另一个类中的相同属性是 String 类型
b.示例
public class BeanUtilsTest {
public static void main(String[] args) {
SourcePoJo sourcePoJo = new SourcePoJo("jingdong", 35711L);
TargetPoJo targetPoJo = new TargetPoJo();
BeanUtils.copyProperties(sourcePoJo, targetPoJo);
System.out.println(targetPoJo);
}
}
@Data
@AllArgsConstructor
class SourcePoJo {
private String username;
private Long id;
}
@Data
class TargetPoJo {
private String username;
private String id; // 类型不一致
}
-----------------------------------------------------------------------------------------------------
id 字段由于类型不一致,导致拷贝后的值为 null
02.属性名不一致
a.描述
当源对象和目标对象的属性名不一致时,BeanUtils.copyProperties 无法进行属性拷贝。
b.示例
public class BeanUtilsTest {
public static void main(String[] args) {
SourcePoJo sourcePoJo = new SourcePoJo("jingdong", 35711L);
TargetPoJo targetPoJo = new TargetPoJo();
BeanUtils.copyProperties(sourcePoJo, targetPoJo);
System.out.println(targetPoJo);
}
}
@Data
@AllArgsConstructor
class SourcePoJo {
private String username;
private Long id;
}
@Data
class TargetPoJo {
private String userName; // 属性名不一致
private Long id;
}
-----------------------------------------------------------------------------------------------------
username 属性无法拷贝到 userName,导致目标对象的 userName 属性值为默认值 null
03.null值覆盖导致数据异常
a.描述
当源对象的某些字段为 null 时,使用 BeanUtils.copyProperties 会将这些 null 值覆盖到目标对象的相应字段,导致原有数据丢失
b.示例
public class BeanUtilsTest {
public static void main(String[] args) {
SourcePoJo sourcePoJo = new SourcePoJo();
sourcePoJo.setId("35711");
TargetPoJo targetPoJo = new TargetPoJo();
targetPoJo.setUsername("Joy");
BeanUtils.copyProperties(sourcePoJo, targetPoJo);
System.out.println(targetPoJo);
}
}
@Data
class SourcePoJo {
private String username; // 为 null
private String id;
}
@Data
class TargetPoJo {
private String username; // 原有值将被覆盖为 null
private String id;
}
-----------------------------------------------------------------------------------------------------
username字段被覆盖成了null
04.导包错误导致拷贝数据异常
a.描述
如果项目中同时引入了Spring的beans包和Apache的beanutils包,导入错误的包可能导致数据拷贝失败
b.示例
// 正确的导包方式
import org.springframework.beans.BeanUtils;
// 错误的导包方式
import org.apache.commons.beanutils.BeanUtils;
-----------------------------------------------------------------------------------------------------
注意:Spring的 BeanUtils 和 Apache的 BeanUtils 方法签名不同,容易混淆。
c.Spring的 BeanUtils
a.包名
org.springframework.beans.BeanUtils
b.方法签名
public static void copyProperties(Object source, Object target) throws BeansException
c.特点
更加简洁,直接传入源对象和目标对象
与Spring框架紧密集成,适用于Spring应用程序
d.代码
import org.springframework.beans.BeanUtils;
public class SpringBeanUtilsExample {
public static void main(String[] args) {
SourcePoJo sourcePoJo = new SourcePoJo("Alice", "123");
TargetPoJo targetPoJo = new TargetPoJo();
BeanUtils.copyProperties(sourcePoJo, targetPoJo);
System.out.println(targetPoJo);
}
}
@Data
@AllArgsConstructor
class SourcePoJo {
private String username;
private String id;
}
@Data
class TargetPoJo {
private String username;
private String id;
}
d.Spring的 BeanUtils
a.包名
org.apache.commons.beanutils.BeanUtils
b.方法签名
public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException
c.特点
需要处理 IllegalAccessException 和 InvocationTargetException 异常
提供了更多的实用工具方法,但在Spring项目中使用较少
d.代码
import org.apache.commons.beanutils.BeanUtils;
public class ApacheBeanUtilsExample {
public static void main(String[] args) {
SourcePoJo sourcePoJo = new SourcePoJo("Alice", "123");
TargetPoJo targetPoJo = new TargetPoJo();
try {
BeanUtils.copyProperties(targetPoJo, sourcePoJo);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
System.out.println(targetPoJo);
}
}
@Data
@AllArgsConstructor
class SourcePoJo {
private String username;
private String id;
}
@Data
class TargetPoJo {
private String username;
private String id;
}
05.查找不到字段引用,修改内容难以溯源
a.描述
使用 BeanUtils.copyProperties 进行数据拷贝时,
难以通过代码搜索快速定位到字段的赋值位置,增加了排查问题的难度。
b.示例
public class BeanUtilsTest {
public static void main(String[] args) {
SourcePoJo sourcePoJo = new SourcePoJo("Alice", "123");
TargetPoJo targetPoJo = new TargetPoJo();
BeanUtils.copyProperties(sourcePoJo, targetPoJo);
// 难以通过搜索定位到 username 的赋值
}
}
06.内部类数据无法成功拷贝
a.描述
即使内部类的类型和字段名相同,BeanUtils.copyProperties 也无法成功拷贝内部类的数据
b.示例
public class BeanUtilsTest {
public static void main(String[] args) {
SourcePoJo sourcePoJo = new SourcePoJo();
sourcePoJo.setUsername("joy");
SourcePoJo.InnerClass innerClass = new SourcePoJo.InnerClass("sourceInner");
sourcePoJo.innerClass = innerClass;
TargetPoJo targetPoJo = new TargetPoJo();
BeanUtils.copyProperties(sourcePoJo, targetPoJo);
System.out.println(targetPoJo);
}
}
@Data
class SourcePoJo {
private String username;
public InnerClass innerClass;
@Data
@AllArgsConstructor
public static class InnerClass {
public String innerName;
}
}
@Data
class TargetPoJo {
private String username;
public InnerClass innerClass;
@Data
public static class InnerClass {
public String innerName;
}
}
-----------------------------------------------------------------------------------------------------
innerClass 数据未被拷贝
07.BeanUtils.copyProperties是浅拷贝
a.描述
BeanUtils.copyProperties 进行的是浅拷贝,对于引用类型的属性,源对象和目标对象共享相同的引用。修改源对象的引用属性会影响目标对象
b.示例
public class BeanUtilsTest {
public static void main(String[] args) {
Person sourcePerson = new Person("sunyangwei", new Card("123456"));
Person targetPerson = new Person();
BeanUtils.copyProperties(sourcePerson, targetPerson);
sourcePerson.getCard().setNum("35711");
System.out.println(targetPerson);
}
}
@Data
@AllArgsConstructor
class Card {
private String num;
}
@NoArgsConstructor
@AllArgsConstructor
@Data
class Person {
private String name;
private Card card;
}
-----------------------------------------------------------------------------------------------------
修改 sourcePerson 的 card 属性后,targetPerson 的 card 属性也被修改,因为它们共享同一个 Card 对象
08.底层实现为反射拷贝效率低
a.描述
BeanUtils.copyProperties 底层通过反射实现,效率较低。与直接使用 set 方法赋值相比,性能差距明显
b.示例
public class BeanUtilsTest {
public static void main(String[] args) {
long copyStartTime = System.currentTimeMillis();
User sourceUser = new User("sunyangwei");
User targetUser = new User();
for (int i = 0; i < 10000; i++) {
BeanUtils.copyProperties(sourceUser, targetUser);
}
System.out.println("copy方式:" + (System.currentTimeMillis() - copyStartTime));
long setStartTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
targetUser.setUserName(sourceUser.getUserName());
}
System.out.println("set方式:" + (System.currentTimeMillis() - setStartTime));
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class User {
private String userName;
}
-----------------------------------------------------------------------------------------------------
BeanUtils.copyProperties 的执行时间明显长于直接使用 set 方法
09.boolean类型加is属性开头
a.描述
当属性名以 is 开头且类型为 boolean 时,BeanUtils.copyProperties 可能无法正确识别和拷贝这些属性
b.示例
public class BeanUtilsTest {
public static void main(String[] args) {
SourcePoJo sourcePoJo = new SourcePoJo();
sourcePoJo.setSuccess(true);
TargetPoJo targetPoJo = new TargetPoJo();
BeanUtils.copyProperties(sourcePoJo, targetPoJo);
System.out.println(targetPoJo.isSuccess());
}
}
@Data
class SourcePoJo {
private boolean isSuccess;
}
@Data
class TargetPoJo {
private boolean isSuccess;
}
-----------------------------------------------------------------------------------------------------
isSuccess 属性可能无法正确拷贝,导致目标对象的 isSuccess 属性值为默认值 false
10.对象属性没有get/set方法赋值失败
a.描述
如果对象的属性没有对应的 get 或 set 方法,BeanUtils.copyProperties 无法进行属性拷贝
b.示例
public class BeanUtilsTest {
public static void main(String[] args) {
SourcePoJo sourcePoJo = new SourcePoJo();
sourcePoJo.setUsername("joy");
TargetPoJo targetPoJo = new TargetPoJo();
BeanUtils.copyProperties(sourcePoJo, targetPoJo);
System.out.println(targetPoJo.getUsername());
}
}
@Data
class SourcePoJo {
private String username;
}
class TargetPoJo {
private String username; // 没有 get/set 方法
public String getUsername() {
return username;
}
}
-----------------------------------------------------------------------------------------------------
username 属性无法拷贝,导致目标对象的 username 属性值为默认值 null
3.10 反射:ReflectionUtils
01.定义
ReflectionUtils是Spring框架中的一个工具类,封装了Java反射的常用操作
提供了一些静态方法来访问类的字段、方法、构造函数等
02.原理
ReflectionUtils通过Java反射机制,允许在运行时动态地访问和修改类的属性和方法
它提供了一些安全检查和异常处理,使得反射操作更加安全和简洁
03.常用API
findField(Class<?> clazz, String name): 查找指定名称的字段
setField(Field field, Object target, Object value): 设置字段的值
getField(Field field, Object target): 获取字段的值
findMethod(Class<?> clazz, String name, Class<?>... paramTypes): 查找指定名称和参数类型的方法
invokeMethod(Method method, Object target, Object... args): 调用方法
doWithFields(Class<?> clazz, FieldCallback fc): 对类的所有字段执行回调操作
doWithMethods(Class<?> clazz, MethodCallback mc): 对类的所有方法执行回调操作
04.使用步骤
引入Spring框架
使用ReflectionUtils提供的静态方法进行反射操作
处理可能的异常,如IllegalAccessException和InvocationTargetException
05.场景及代码示例
a.场景1:访问私有字段
import org.springframework.util.ReflectionUtils;
public class ReflectionExample {
private String secret = "hidden";
public static void main(String[] args) {
ReflectionExample example = new ReflectionExample();
Field field = ReflectionUtils.findField(ReflectionExample.class, "secret");
ReflectionUtils.makeAccessible(field);
String secretValue = (String) ReflectionUtils.getField(field, example);
System.out.println("Secret value: " + secretValue);
}
}
说明:使用ReflectionUtils访问私有字段secret并获取其值
b.场景2:调用私有方法
import org.springframework.util.ReflectionUtils;
public class ReflectionExample {
private void hiddenMethod() {
System.out.println("Hidden method called!");
}
public static void main(String[] args) {
ReflectionExample example = new ReflectionExample();
Method method = ReflectionUtils.findMethod(ReflectionExample.class, "hiddenMethod");
ReflectionUtils.makeAccessible(method);
ReflectionUtils.invokeMethod(method, example);
}
}
说明:使用ReflectionUtils调用私有方法hiddenMethod
c.场景3:对所有字段执行操作
import org.springframework.util.ReflectionUtils;
public class ReflectionExample {
private String field1 = "value1";
private int field2 = 42;
public static void main(String[] args) {
ReflectionUtils.doWithFields(ReflectionExample.class, field -> {
ReflectionUtils.makeAccessible(field);
Object value = ReflectionUtils.getField(field, new ReflectionExample());
System.out.println(field.getName() + ": " + value);
});
}
}
说明:使用ReflectionUtils对类的所有字段执行操作,打印字段名称和值
3.11 拷贝:MapStructPlus
00.汇总
01.简单映射
02.映射嵌套对象
03.自定义映射方法
04.映射集合
05.映射不同字段名
06.映射带有默认值的字段
00.mapStruct踩坑指南
a.基础类型定制方法慎用
a.代码
// 基础类型定义了一个特殊方法。当 customerId(long) 为0的时候,转换成null。
default String mapCustomerId(Long customerId) {
return customerId != null && customerId != 0L ? String.valueOf(customerId) : null;
}
b.代码
@Mappings({
@Mapping(target = "customerId", expression = "java(mapCustomerId(mdto.getcustomerId()))")
})
CustomerVO mdtoToVO(CustomerMDTO mdto);
c.效果
所有 Long 类型的字段,都采用了这个方法。影响面被大大扩大!
比如:long 类型在执行映射的时候也采用了这个方法
customerVO.setId( mapCustomerId( mdto.getId() ) );
b.Long类型默认值0
a.代码
CustomerCreateParam createRequestToParam(CustomerCreateRequest request);
@Mappings({
@Mapping(source = "customerId", target = "customerId", defaultValue = "0L")
})
CustomerUpdateParam updateRequestToParam(CustomerUpdateRequest request);
b.生成代码
Long.parseLong( request.getcustomerId() )
c.执行代码
Exception in thread "main" java.lang.NumberFormatException: For input string: "0L"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.parseLong(Long.java:631)
d.如果设置0,编译通不过!
/Users/uzong/IdeaProjects/uzong-crm/uzong-crm-web/src/main/java/com/uzong/crm/web/converter/CustomerEndPointConverter.java:56:25
java: Can't map "0" to "Long customerId". Reason: L/l mandatory for long types.
01.简单映射
a.定义源和目标对象
@Data
@AllArgsConstructor
@NoArgsConstructor
class SourcePoJo {
private String username;
private Long id;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class TargetPoJo {
private String username;
private String id;
}
b.定义映射接口
import io.github.mapstruct.plus.Mapper;
import io.github.mapstruct.plus.Mapping;
import io.github.mapstruct.plus.factory.Mappers;
@Mapper
public interface PoJoMapper {
PoJoMapper INSTANCE = Mappers.getMapper(PoJoMapper.class);
@Mapping(source = "id", target = "id", numberFormat = "#")
TargetPoJo sourceToTarget(SourcePoJo source);
}
c.使用映射接口
public class MapStructPlusExample {
public static void main(String[] args) {
SourcePoJo sourcePoJo = new SourcePoJo("Alice", 123L);
TargetPoJo targetPoJo = PoJoMapper.INSTANCE.sourceToTarget(sourcePoJo);
System.out.println(targetPoJo);
}
}
02.映射嵌套对象
a.定义源和目标对象
@Data
@AllArgsConstructor
@NoArgsConstructor
class SourcePoJo {
private String username;
private Long id;
private InnerClass innerClass;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class InnerClass {
private String innerName;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class TargetPoJo {
private String username;
private String id;
private InnerClass innerClass;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class InnerClass {
private String innerName;
}
}
b.定义映射接口
@Mapper
public interface PoJoMapper {
PoJoMapper INSTANCE = Mappers.getMapper(PoJoMapper.class);
@Mapping(source = "id", target = "id", numberFormat = "#")
TargetPoJo sourceToTarget(SourcePoJo source);
@Mapping(source = "innerClass.innerName", target = "innerClass.innerName")
TargetPoJo.InnerClass mapInnerClass(SourcePoJo.InnerClass innerClass);
}
c.使用映射接口
public class MapStructPlusExample {
public static void main(String[] args) {
SourcePoJo.InnerClass innerClass = new SourcePoJo.InnerClass("sourceInner");
SourcePoJo sourcePoJo = new SourcePoJo("Alice", 123L, innerClass);
TargetPoJo targetPoJo = PoJoMapper.INSTANCE.sourceToTarget(sourcePoJo);
System.out.println(targetPoJo);
}
}
03.自定义映射方法
a.定义源和目标对象
@Data
@AllArgsConstructor
@NoArgsConstructor
class SourcePoJo {
private String username;
private Long id;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class TargetPoJo {
private String username;
private String id;
}
b.定义映射接口
import io.github.mapstruct.plus.Mapper;
import io.github.mapstruct.plus.Mapping;
import io.github.mapstruct.plus.Named;
import io.github.mapstruct.plus.factory.Mappers;
@Mapper
public interface PoJoMapper {
PoJoMapper INSTANCE = Mappers.getMapper(PoJoMapper.class);
@Mapping(source = "id", target = "id", qualifiedByName = "longToString")
TargetPoJo sourceToTarget(SourcePoJo source);
@Named("longToString")
default String longToString(Long id) {
return id != null ? id.toString() : null;
}
}
c.使用映射接口
public class MapStructPlusExample {
public static void main(String[] args) {
SourcePoJo sourcePoJo = new SourcePoJo("Alice", 123L);
TargetPoJo targetPoJo = PoJoMapper.INSTANCE.sourceToTarget(sourcePoJo);
System.out.println(targetPoJo);
}
}
04.映射集合
a.定义源和目标对象
@Data
@AllArgsConstructor
@NoArgsConstructor
class SourcePoJo {
private String username;
private Long id;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class TargetPoJo {
private String username;
private String id;
}
b.定义映射接口
import io.github.mapstruct.plus.Mapper;
import io.github.mapstruct.plus.Mapping;
import io.github.mapstruct.plus.factory.Mappers;
import java.util.List;
@Mapper
public interface PoJoMapper {
PoJoMapper INSTANCE = Mappers.getMapper(PoJoMapper.class);
@Mapping(source = "id", target = "id", numberFormat = "#")
TargetPoJo sourceToTarget(SourcePoJo source);
List<TargetPoJo> sourceListToTargetList(List<SourcePoJo> sourceList);
}
c.使用映射接口
import java.util.Arrays;
import java.util.List;
public class MapStructPlusExample {
public static void main(String[] args) {
SourcePoJo sourcePoJo1 = new SourcePoJo("Alice", 123L);
SourcePoJo sourcePoJo2 = new SourcePoJo("Bob", 456L);
List<SourcePoJo> sourceList = Arrays.asList(sourcePoJo1, sourcePoJo2);
List<TargetPoJo> targetList = PoJoMapper.INSTANCE.sourceListToTargetList(sourceList);
targetList.forEach(System.out::println);
}
}
05.映射不同字段名
a.定义源和目标对象
@Data
@AllArgsConstructor
@NoArgsConstructor
class SourcePoJo {
private String userName;
private Long id;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class TargetPoJo {
private String username;
private String id;
}
b.定义映射接口
import io.github.mapstruct.plus.Mapper;
import io.github.mapstruct.plus.Mapping;
import io.github.mapstruct.plus.factory.Mappers;
@Mapper
public interface PoJoMapper {
PoJoMapper INSTANCE = Mappers.getMapper(PoJoMapper.class);
@Mapping(source = "userName", target = "username")
@Mapping(source = "id", target = "id", numberFormat = "#")
TargetPoJo sourceToTarget(SourcePoJo source);
}
c.使用映射接口
public class MapStructPlusExample {
public static void main(String[] args) {
SourcePoJo sourcePoJo = new SourcePoJo("Alice", 123L);
TargetPoJo targetPoJo = PoJoMapper.INSTANCE.sourceToTarget(sourcePoJo);
System.out.println(targetPoJo);
}
}
06.映射带有默认值的字段
a.定义源和目标对象
@Data
@AllArgsConstructor
@NoArgsConstructor
class SourcePoJo {
private String username;
private Long id;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class TargetPoJo {
private String username;
private String id;
private String defaultField = "defaultValue";
}
b.定义映射接口
import io.github.mapstruct.plus.Mapper;
import io.github.mapstruct.plus.Mapping;
import io.github.mapstruct.plus.factory.Mappers;
@Mapper
public interface PoJoMapper {
PoJoMapper INSTANCE = Mappers.getMapper(PoJoMapper.class);
@Mapping(source = "id", target = "id", numberFormat = "#")
TargetPoJo sourceToTarget(SourcePoJo source);
}
c.使用映射接口
public class MapStructPlusExample {
public static void main(String[] args) {
SourcePoJo sourcePoJo = new SourcePoJo("Alice", 123L);
TargetPoJo targetPoJo = PoJoMapper.INSTANCE.sourceToTarget(sourcePoJo);
System.out.println(targetPoJo);
}
}
3.12 搜索:RedisSearch
00.介绍
RedisSearch 是一个基于 Redis 的搜索引擎模块,它提供了全文搜索、索引和聚合功能
通过 RedisSearch,可以为 Redis 中的数据创建索引,执行复杂的搜索查询
并实现高级功能,如自动完成、分面搜索和排序
01.依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>RedisSearch</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.redis.om</groupId>
<artifactId>redis-om-spring</artifactId>
<version>0.8.2</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
02.控制层
package com.et.controller;
import com.et.redis.document.Student;
import com.et.redis.document.StudentRepository;
import com.et.redis.hash.Person;
import com.et.redis.hash.PersonRepository;
import jakarta.websocket.server.PathParam;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
public class WebController {
private PersonRepository personRepository;
private StudentRepository studentRepository;
public WebController(PersonRepository personRepository, StudentRepository studentRepository) {
this.personRepository = personRepository;
this.studentRepository = studentRepository;
}
@PostMapping("/person")
public Person save(@RequestBody Person person) {
return personRepository.save(person);
}
@GetMapping("/person")
public Person get(@PathParam("name") String name, @PathParam("searchLastName") String searchLastName) {
if (name != null)
return this.personRepository.findByName(name)
.orElseThrow(() -> new RuntimeException("person not found"));
if (searchLastName != null)
return this.personRepository.searchByLastName(searchLastName)
.orElseThrow(() -> new RuntimeException("person not found"));
return null;
}
// ---- Student Endpoints
@PostMapping("/student")
public Student saveStudent(@RequestBody Student student) {
return studentRepository.save(student);
}
@GetMapping("/student")
public Student getStudent(@PathParam("name") String name, @PathParam("searchLastName") String searchLastName) {
if (name != null)
return this.studentRepository.findByName(name)
.orElseThrow(() -> new RuntimeException("Student not found"));
if (searchLastName != null)
return this.studentRepository.searchByLastName(searchLastName)
.orElseThrow(() -> new RuntimeException("Student not found"));
return null;
}
@ExceptionHandler(value = RuntimeException.class)
public ResponseEntity handleError(RuntimeException e) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(e.getMessage());
}
}
03.@RedisHash方式
package com.et.redis.hash;
import com.redis.om.spring.annotations.Indexed;
import com.redis.om.spring.annotations.Searchable;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
@RedisHash
public class Person {
@Id
private String id;
@Indexed
private String name;
@Searchable
private String lastName;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
package com.et.redis.hash;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface PersonRepository extends CrudRepository<Person, String> {
Optional<Person> findByName(String name);
Optional<Person> searchByLastName(String name);
}
04.@Document方式
package com.et.redis.document;
import com.redis.om.spring.annotations.Document;
import com.redis.om.spring.annotations.Indexed;
import com.redis.om.spring.annotations.Searchable;
import org.springframework.data.annotation.Id;
@Document
public class Student {
@Id
private String id;
@Indexed
private String name;
@Searchable
private String lastName;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
package com.et.redis.document;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface StudentRepository extends CrudRepository<Student, String> {
Optional<Student> findByName(String name);
Optional<Student> searchByLastName(String name);
}
05.启动类
package com.et;
import com.redis.om.spring.annotations.EnableRedisDocumentRepositories;
import com.redis.om.spring.annotations.EnableRedisEnhancedRepositories;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@EnableRedisDocumentRepositories(basePackages = "com.et.redis.document")
@EnableRedisEnhancedRepositories(basePackages = "com.et.redis.hash")
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
06.配置文件
server:
port: 8088
spring:
redis:
host: localhost
port: 6379
3.13 ID生成:Snowflake
01.概述
a.定义
雪花算法是一种分布式ID生成算法,由Twitter开源。它用于生成全局唯一的ID,具有高性能和高可用性,适用于分布式系统中需要唯一标识的场景
b.原理
雪花算法生成的ID是一个64位的整数,通常由以下几个部分组成:
符号位(1位):始终为0,因为生成的ID是正数
时间戳(41位):表示当前时间的毫秒数,可以使用约69年
数据中心ID(5位):标识数据中心,可以有32个不同的数据中心
机器ID(5位):标识同一数据中心内的机器,可以有32台不同的机器
序列号(12位):在同一毫秒内生成的序列号,支持每毫秒产生4096个不同的ID
c.常用API
在Java中,雪花算法通常通过自定义实现或使用第三方库来生成ID
02.使用步骤
a.定义常量
定义符号位、时间戳、数据中心ID、机器ID和序列号的位数
b.初始化参数
设置数据中心ID和机器ID
c.实现ID生成逻辑
根据当前时间戳和序列号生成唯一ID
03.每个场景对应的代码示例
public class SnowflakeIdGenerator {
// 常量定义
private static final long EPOCH = 1609459200000L; // 自定义起始时间戳(2021-01-01)
private static final long DATA_CENTER_ID_BITS = 5L;
private static final long MACHINE_ID_BITS = 5L;
private static final long SEQUENCE_BITS = 12L;
private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);
private static final long MAX_MACHINE_ID = ~(-1L << MACHINE_ID_BITS);
private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);
private static final long MACHINE_ID_SHIFT = SEQUENCE_BITS;
private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS;
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS + DATA_CENTER_ID_BITS;
// 实例变量
private final long dataCenterId;
private final long machineId;
private long sequence = 0L;
private long lastTimestamp = -1L;
// 构造函数
public SnowflakeIdGenerator(long dataCenterId, long machineId) {
if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
throw new IllegalArgumentException(String.format("DataCenter ID can't be greater than %d or less than 0", MAX_DATA_CENTER_ID));
}
if (machineId > MAX_MACHINE_ID || machineId < 0) {
throw new IllegalArgumentException(String.format("Machine ID can't be greater than %d or less than 0", MAX_MACHINE_ID));
}
this.dataCenterId = dataCenterId;
this.machineId = machineId;
}
// 生成ID方法
public synchronized long nextId() {
long timestamp = currentTimeMillis();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0) {
timestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - EPOCH) << TIMESTAMP_SHIFT)
| (dataCenterId << DATA_CENTER_ID_SHIFT)
| (machineId << MACHINE_ID_SHIFT)
| sequence;
}
// 获取当前时间戳
private long currentTimeMillis() {
return System.currentTimeMillis();
}
// 等待下一个毫秒
private long waitNextMillis(long lastTimestamp) {
long timestamp = currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = currentTimeMillis();
}
return timestamp;
}
public static void main(String[] args) {
SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1);
for (int i = 0; i < 10; i++) {
System.out.println(idGenerator.nextId());
}
}
}
04.使用场景
a.分布式系统
需要生成全局唯一ID的场景,如订单号、用户ID等
b.高并发环境
需要快速生成唯一ID,且不依赖于数据库的自增ID
c.数据中心和多机房部署
支持多数据中心和多机器的ID生成,避免ID冲突