2020-05-05~至今
积累,成长
目前分支规划存在一定的不合理性,将在后期进行完善优化.所有的架构都是基于业务进行演化的.
工具类 > 工具包 > 功能模块 > 场景启动器 > 功能服务 > 分布式
master
微服务架构 业务场景模拟,学习案例
SpringBoot 2.3.2.RELEASE
SpringCloud Hoxton.SR8
Nacos
OpenFeign 2.2.5.RELEASE
Hystrix 2.2.5.RELEASE
gateway
study
单体架构、学习案例
SpringBoot 2.3.12.RELEASE
scene
单体架构、最佳实践、插件化、模块化、配置化
SpringBoot 2.7.5
Spring Security 2.7.5
cloud
规划中
SpringBoot 2.4.2
SpringCloud 2020.0.6
eureka 3.0.6
hystrix 2.2.10.RELEASE
openfeign 3.0.7
ribbon 2.2.10.RELEASE
cloud alibaba
规划中
nacos
gateway
sentinel
Treasure是一个Java技术生态项目,涵盖了单体、微服务、DDD等架构实践,以兴趣、学习目的、技术积累为理念,逐步完善迭代。主要包含学习成长过程中一些技术点、工作中积累的一些心得,面试中一些业务场景模拟及解决方案一些常见、通用业务的解决方案、合理应用设计模式进行一些业务代码的重构优化、常用小轮子的积累、一些更优雅的编码实现、通用场景封装等内容
scene
分支为了方便随取随用,没有做过多的聚合,各个模块相对独立
com.dingwen
├── scene[综合场景启动器使用示例]
│ └── pom.xml[ Maven依赖]
├── starter[启动器集]
│ └── base-spring-boot-starter[基础场景启动器]
│ └── api-docs-spring-boot-starter[API文档再封装场景启动器]
│ └── screw-spring-boot-starter[数据库文档生成场景启动器]
│ └── webplus-spring-boot-starter[Web再封装场景启动器 ]
│ └── redis-spring-boot-starter[Redis场景启动器]
│ └── caffeine-spring-boot-starter[本地缓存启动器]
│ └── async-spring-boot-starter[异步场景启动器]
│ └── enums-spring-boot-starter[枚举场景启动器]
│ └── mybatisplus-spring-boot-starter[MybatisPlus场景启动器]
│ └── log-spring-boot-starter[日志场景启动器]
│ └── logv-spring-boot-starter[日志查看启动器]
│ └── email-spring-boot-starter[电子邮件场景启动器]
│ └── mongo-spring-boot-starter[MongoDB场景启动器]
│ └── xxl-job-spring-boot-starter[xxljob场景启动器]
│ └── translate-spring-boot-starter[翻译场景启动器]
│ └── oss-spring-boot-starter[文件存储场景启动器]
│ └── pipeline-spring-boot-starter[责任链Pipeline场景启动器]
│ └── bar-spring-boot-starter[进度条场景启动器]
│ └── dic-spring-boot-starter[字典场景启动器]
│ └── excel-spring-boot-starter[Excel场景启动器]
│ └── config-spring-boot-starter[系统配置场景启动器]
│ └── quartz-spring-boot-starter[Quartz定时任务场景启动器]
│ └── file-spring-boot-starter[文件场景启动器]
│ └── change-log-spring-boot-starter[变更记录场景启动器]
│ └── db-backup-spring-boot-starter[数据归档场景启动器]
│ └── event-spring-boot-starter[事件场景启动器]
│ └── 规划中[数据脱敏场景启动器]
│ └── jwt-spring-boot-starter[JWT场景启动器]
│ └── security-plus-spring-boot-starter[Security场景启动器]
│ └── auth-spring-boot-starter[认证场景启动器]
│ └── dcache-spring-boot-starter[二级缓存启动器]
│ └── kkFile-docker[文件预览场景启动器]
│ └── 规划中[监控场景启动器]
│ └── 规划中[审核场景启动器]
│ └── 规划中[Flowable启动器]
│ └── 规划中[Camunda启动器]
│ └── 规划中[微信公众号开发启动器]
│ └── 规划中[钉钉开发启动器]
│ └── 规划中[重要表单变更日志启动器]
│ └── 规划中[elasticsearch启动器]
│ └── 规划中[Dingger消息告警二次封装]
├── pom.xml[Maven依赖]
study
分支com.dingwen
├── treasure-canal-client[canal客户端 [80]]
├── treasure-kettle[kettle集成企业级解决方案 [9999]]
├── treasure-websocket[websocket方案 [8081] [8080]]
├── treasure-sms4j[通用短信解决方案 [8080]]
├── treasure-poi-tl[word模板渲染解决方案 [8080]]
├── treasure-jimu-report[开源报表解决方案 [8080]]
├── treasure-dingtalk-ger[钉钉,企业微信预警机器人解决方案 [8080]]
├── 规划中[认证解决方案 [8080]]
├── 规划中[单点登录解决方案 [8080]]
├── 规划中[动态表单解决方案 [8080]]
├── treasure-gof[大话设计模式]
├── pom.xml[Maven依赖]
master
分支com.cdn
com.dingwen
├── treasure-auth[认证服务 [20902]]
├── treasure-business[业务服务 [20903]]
├── treasure-admin[监控服务 [20901]]
├── treasure-common[通用模块]
│ └── common-pom[依赖管理模块]
│ └── common-base[基础模块]
│ └── common-beansearcher[对象搜索]
│ └── common-config[基础配置]
│ └── common-core[ 核心模块]
│ └── common-jpa[持久层JPA]
│ └── common-jwt[JWT令牌]
│ └── common-knifej[接口文档]
│ └── common-model[通用MODEL]
│ └── common-mongodb[MongoDB]
│ └── common-mybatisplus[持久层Mybatisplus]
│ └── common-rabbitmq[RabbitMQ]
│ └── common-redis[Redis]
│ └── common-security[安全模块]
│ └── common-sensitive[自定义注解实现数据脱敏]
│ └── common-web[WEB模块]
│ └── common-tkmybatis[tkmybatis模块]
│ └── common-minio[minio文件存储]
│ └── common-easyexcel[excel 文件导入导出]
│ └── common-influxdb[时序数据库案例]
│ └── common-open-api[open api 案例]
│ └── open-api-baidu-map[百度地图]
│ └── open-api-sms[阿里云短信]
│ └── open-api-tx[天行数据基础服务]
│ └── open-api-wechat-pub[微信公众号]
│ └── open-api-baidu-map[百度地图]
│ └── open-api-tianxing-rainbow[天行数据彩虹屁]
├── treasure-gateway[网关服务 [20904]]
├── treasure-log[日志服务 [20905]]
├── treasure-manage[后台管理 [20906]]
├── treasure-file-generate[ 文件生成服务 [20907]]
├── treasure-task-quartz[定时任务(Quarzt实现) [20908]]
├── treasure-file[文件服务 [20909]]
├── treasure-code-generate[代码生成服务 [20910]]
├── treasure-slow-sql[慢SQL [20911]]
├── treasure-xxl-job-admin[xxl-job-admin [20933]]
├── logs[日志]
├── sql[sql]
├── img[图片]
├── pom.xml[公共依赖]
cloud
SpringCloud 实践
alibaba_cloud
阿里系微服务生态实践
ddd
DDD架构实践
参考若依
规划中
Redis |
Mysql |
MongoDB |
Canal |
Postgresql |
ElasticSearch |
Alibaba Druid | |||
Spring |
SpringMVC |
SpringBoot |
Spring Security |
Spring-retry |
mybatis-plus-join-boot |
SpringBoot Admin | |||
Mybatis |
MybatisPlus |
TK Mybatis |
JPA | ||||||
Mapstruct |
MapstructPlus |
Hutool |
Screw |
BeanSearcher |
EasyExcel |
knife4j | p6spy | ||
ip2region |
Guava |
commons-lang3 |
Lombok |
Maven | |||||
xxl-job |
Quartz | ||||||||
Nacos |
SpringCloud Alibaba |
GateWay |
Feign |
Hystrix |
Ribbon | ||||
RabbitMQ |
RocketMQ |
Kafka | |||||||
Kettle |
ELK | ||||||||
Thymeleaf |
Layui | ||||||||
Minio |
Aliyun OSS |
JWT | |||||||
Websocket |
sms4j | ||||||||
Bistoury |
poi-tl |
jimu-report |
dingtalk-spring-boot-starter |
Arthas |
接口文档地址: https://apifox.com/apidoc/shared-8d3d332d-b936-4ec1-8d14-516d89a8f85d
Maven增加一下配置即可
<servers>
<!-- 阿里云私服开始 -->
<server>
<id>rdc-releases</id>
<username>62c3e2f6a908b6a4db54fa26</username>
<password>5V8N7KJ]rvtt</password>
</server>
<server>
<id>rdc-snapshots</id>
<username>62c3e2f6a908b6a4db54fa26</username>
<password>5V8N7KJ]rvtt</password>
</server>
<!-- 阿里云私服结束 -->
</servers>
<!-- 相当于拦截器访问改地址是映射的配置 -->
<mirrors>
<!-- 阿里云制品仓库 -->
<mirror>
<id>mirror</id>
<mirrorOf>central,jcenter,!rdc-releases,!rdc-snapshots</mirrorOf>
<name>mirror</name>
<url>https://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
</mirrors>
<profiles>
<!-- 阿里云制品仓库 -->
<profile>
<id>rdc</id>
<properties>
<altReleaseDeploymentRepository>
rdc-releases::default::https://packages.aliyun.com/maven/repository/2380560-release-WXL1gl/
</altReleaseDeploymentRepository>
<altSnapshotDeploymentRepository>
rdc-snapshots::default::https://packages.aliyun.com/maven/repository/2380560-snapshot-vKCASA/
</altSnapshotDeploymentRepository>
</properties>
</profile>
</profiles>
<!-- 激活配置 -->
<activeProfiles>
<activeProfile>rdc</activeProfile>
</activeProfiles>
基于
SpringBoot2.1.7
和JDK1.8
封装的基础场景启动器
logback
配置优化,异步日志优化 <!--基础场景启动器-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>base-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableBase
注解以开启基础场景功能
@EnableBase
AspectUtils
: 切面工具类
BeanCopyUtils
: bean深拷贝工具(基于 cglib 性能优异)
CheckerUtils
: Lambda形式对象校验包
DateUtils
: 日期工具类
LaExUtils
: Lambdas受检异常封装处理
AddressUtils
: 地址工具类
RegionUtils
: IP离线定位
MessageUtils
: 国际化消息
ReflectUtils
: 反射工具
SqlUtils
: sql操作工具类
StringUtils
: 字符串操作工具
StreamUtils
: stream工具类
distinct()
List复杂类型去重+简单类型去重group2Map(Collection<E> collection, Integer groupSize)
List任务分组IdUtils
: ID工具类
Validatetils
: 编程式灵活校验工具
SpringUtils
: Spring工具类(基于Hutool拓展,获取代理对象)+发布事件持久化拓展
OptimizeUtils
: 优化工具类
PinYin4jUtils
: 文字转汉语拼音
MapstructUtils
: 对象映射拷贝工具再封装
CheckerUtils
Checker<SysUser> checker = Checkers.<SysUser>lambdaCheck()
.notNull(SysUser::getName)
.ne(SysUser::getAge, 0)
.custom(item -> item.getAge() > queryByDb(item.getId()), "年龄异常");
checker.check(sysUser);
OptimizeUtils
参考Spring StopWatch 的拓展优化,精确计算执行耗时,执行次数,方便进行优化
OptimizeUtilController
/**
* OptimizeUtilController: 优化工具测试
* @author dingwen
* @since 2022/8/28
*/
@Api(tags = "优化工具API")
@RestController
@Slf4j
@RequestMapping("optimize")
@RequiredArgsConstructor
public class OptimizeUtilController {
@ApiOperation(value = "API使用测试")
@GetMapping
public void test() {
optimizeApi();
}
@SneakyThrows(Throwable.class)
private void optimizeApi() {
OptimizeUtil.start("任务1");
TimeUnit.SECONDS.sleep(1);
OptimizeUtil.stop("任务1");
for (int i = 0; i < 100; i++) {
OptimizeUtil.start("任务2");
for (int j = 0; j < 1000; j++) {
OptimizeUtil.start("任务2-1");
OptimizeUtil.stop("任务2-1");
}
OptimizeUtil.stop("任务2");
}
OptimizeUtil.print("任务2");
OptimizeUtil.print();
}
}
SpringUtils
publishEvent(event)
,发布订阅事件拓展,提供基础事件对象BaseEvent
,同一发布事件入口,进行事件持久化方便进行监控和重发.当然监听实现方需要考虑幂等实现
将提供
event-spring-boot-starter
实现IEvent
接口实现事件对象持久化,重发等功能
/**
* 发布事件,将指定的应用事件发布到Spring事件传播机制中。
* @param event 应用事件对象,代表一个具体的事件实例,将被Spring应用上下文中的事件监听器处理。
*/
public static void publishEvent(BaseEvent event){
// 将事件对象持久化到数据库,方便实现重发
Map<String, IEvent> events = SpringUtil.getBeansOfType(IEvent.class);
events.forEach((eName,ev) -> ev.saveEvent(event));
// 将事件发布到Spring应用上下文中
SpringUtil.publishEvent(event);
}
参考: 事件场景启动器
ICurrentUserService
提供顶层接口,供外部实现.从而实现与权限模块解耦. 注意: 次接口仅仅支持单实现,不支持策略
SpringBoot Web
的二次封装
序列化反序列化配置
Date
LocalDateTime
Long
解决大数字前端精度丢失问题Debug
方法级别调试日志
静态资源映射配置化实现
Xss
防护配置化实现
构建可重复读取inputStream的request
分页工具 PageUtils
支持国际化的统一异常处理 GlobalExceptionHandler
支持国际化的统一接口返回 ResultVOGenerator
tranceId
返回: 实现每次请求可溯源优雅校验实现
允许的值集校验 AllowableValues
禁止的值集校验 BanValues
中文字符校验 Chinese
身份证号码校验 IdCard
枚举值校验 EnumValues
多个字段必须有一个不为空 ChooseRequired
手机号码校验 Mobile
数字校验 Numbers
当指定字段满足某值时当前字段不能为空 WhenRequired
分组校验 ValidGroup
编程式灵活校验 ValidateUtils
ServletUtils
若依拓展Servlet
工具
Controller
基础接口抽象
BaseCrudController
BaseViewController
基础查询对象封装,支持数据权限
全局日志请求耗时过滤器GlobalLogFilter
自适应浏览器的国际化方案
<!--webplus场景-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>webplus-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableWebplus
注解以开启web场景功能场景功能
@EnableWebplus
配置webplus
dingwen:
treasure:
# webplus
webplus:
# 开启Debug
debug: true
# 静态资源映射处理
handlers:
- handler: "doc.html"
locations: "classpath:/META-INF/resources/"
- handler: "swagger-ui.html"
locations: "classpath:/META-INF/resources/"
- handler: "/webjars/**"
locations: "classpath:/META-INF/resources/webjars/"
基于过滤器+AOP+
InheritableThreadLocal
实现的可配置的链路追踪日志方案,后期可对接ELK加工处理
区分不同的环境,通过全局异常捕获,统一返回国际化的友好的异常消息.
GlobalLogFilter
RepeatableFilter
TraceIdFilter
XSS
攻击XssFilter
支持异常消息和
valid
校验.I18nConfig
,ValidateConfig
@ApiOperation("测试国际化校验消息")
@ApiImplicitParam(name = "msg", value = "消息", dataTypeClass = String.class)
@GetMapping("/valid")
public ResultVO<String> testInternationalization(@RequestParam @Size(min = 1,max = 6,message = "{treasure.webplus.valid.test.msg.size}") String msg) {
return success(msg);
}
GlobalExceptionMsgSender
使用方只需要实现
GlobalExceptionMsgSender
接口即可,当发生异常是会自动处理异常消息发送.业务系统可以自定义消息告警的方式.首推飞书的机器人告警.
成熟的开源解决方案: https://gitee.com/jaemon/dingtalk-spring-boot-starter.git
WebPlusView
继承自
org.springframework.web.servlet.view.AbstractView
实现自定义文件视图
TODO
基于
Knif4j
封装的API文档场景启动器
<!--api文档生成启动器-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>api-docs-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableApiDocs
注解,以开启接口文档功能
@EnableApiDocs
配置
dingwen:
treasure:
# API接口文档生成
api:
docs:
# 标题
title: "Ding Wen Service Api"
# 描述
description: "This is Interface Desc"
# 服务地址
url: "http://127.0.0.1"
# 版本号
version: "1.0.0"
# 分组名称
group: prod
# 内部API请求头值
headervalue: "DINGWEN-API-VALUE"
# 内部APi请求名称
headername: "DINGWEN-API-NAME"
# 文档联系人名称
contactname: "dingwen"
# 文档联系人站点
contacturl: "https://treasure.dingwen.top"
# 文档联系人邮箱
contactemail: "dingwen0314@163.com"
实现SpringBoot中线程池动态化配置
<!--异步场景-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>async-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableApiDocs
注解,以开启异步场景功能,注意: 包名称是:com.dingwen.treasure.async.annotation
下的
@EnableAsync
配置线程池
dingwen:
treasure:
# async
async:
# 是否开启线程池实时日志打印
logPrint: true
pool:
# 核心线程数
- core: 8
# 最大线程数
max: 16
# 线程空闲时间
keepAliveTime: 60
# 缓冲队列大小
queueCapacity: 2000
# 线程池前缀
poolNamePrefix: treasure-async-
# 线程池对象名称
poolBeanName: logvExecutor
# 线程池拒绝策略
poolPolicy: CallerRunsPolicy
// 注解式
@Resource(name = "fileExecutor")
@Lazy
private ThreadPoolTaskExecutor fileExecutor;
// 编程式
ThreadPoolTaskExecutor logVExecutor = SpringUtil.getBean("logvExecutor", ThreadPoolTaskExecutor.class);
实现方法级别的日志,请求级别的日志,业务级别的日志配置化,拓展化,可视化以及代码定位
方法日志细分为整体日志
MeLog
,参数日志ParamMeLog
,返回结果日志ResultMeLog
,方法异常日志ThrowingMeLog
可灵活配置实现 可基于日志格式化接口定制实现,已提供默认实现 提供回调接口可灵活拓展处理日志
可整体配置所有请求的日志记录规则,提供回调接口可进行定制化
基于注解实现的操作日志,依附于Spring事件监听机制解耦,监听对应日志实现及可实现定制化
Spring事件监听机制解耦,监听对应日志实现及可实现定制化
<!--日志场景-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>log-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableLog
注解以开启日志通用场景功能
@EnableLog
日志配置
dingwen:
treasure:
# 请求日志
log:
request:
# 是否开启请求日志
reEnable: true
# 是否记录请求体内容
reEnableBody: false
method:
global-log-level: debug
# 全局综合日志代码定位
global-log-position: unknown
# 全局综合日志格式化
global-log-formatter: com.dingwen.treasure.log.method.format.DefaultMeLogFormatter
# 全局综合日志回调
global-log-callback: com.dingwen.treasure.log.method.callback.DefaultMeLogCallback
# 全局参数日志级别
global-param-log-level: debug
# 全局参数日志代码定位
global-param-log-position: unknown
# 全局参数日志格式化
global-param-log-formatter: com.dingwen.treasure.log.method.format.DefaultMeParamLogFormatter
# 全局参数日志回调
global-param-log-callback: com.dingwen.treasure.log.method.callback.DefaultMeLogCallback
# 全局结果日志级别
global-result-log-level: debug
# 全局结果日志代码定位
global-result-log-position: unknown
# 全局结果日志格式化
global-result-log-formatter: com.dingwen.treasure.log.method.format.DefaultMeResultLogFormatter
# 全局结果日志回调
global-result-log-callback: com.dingwen.treasure.log.method.callback.DefaultMeLogCallback
# 全局异常日志回调
global-throwing-log-callback: com.dingwen.treasure.log.method.callback.DefaultMeLogCallback
/**
* 测试方法日志
*
* @param name 名字
* @return {@link ResultVO}<{@link String}>
*/
@GetMapping("/method-test")
@ApiOperation(value = "测试方法日志")
@ApiImplicitParam(name = "name", value = "名称", dataTypeClass = String.class)
@MeLog(value = "方法日志测试业务")
public ResultVO<String> testMeLog(@RequestParam String name) {
return success();
}
/**
* 测试操作日志
*
* @param name 名称
* @return {@link ResultVO}<{@link String}>
*/
@GetMapping("/operate-test")
@ApiOperation(value = "测试操作日志")
@ApiImplicitParam(name = "name", value = "名称", dataTypeClass = String.class)
@OperateLogAnnotation(module = "场景", desc = "test")
public ResultVO<String> testOperateLog(@RequestParam String name) {
return success();
}
基于Websocket、SpringBoot2.x、layui实现的可配置的Web版日志查看器
<!--日志查看场景-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>logv-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableLogv
注解以开启日志查看场景功能
@EnableLogv
配置接口文档放行、线程池、监控项
dingwen:
treasure:
# webplus
webplus:
debug: true
handlers:
- handler: "doc.html"
locations: "classpath:/META-INF/resources/"
- handler: "swagger-ui.html"
locations: "classpath:/META-INF/resources/"
- handler: "/webjars/**"
locations: "classpath:/META-INF/resources/webjars/"
# async
async:
logPrint: true
pool:
- core: 8
max: 16
keepAliveTime: 60
queueCapacity: 2000
poolNamePrefix: treasure-async-
poolBeanName: logvExecutor
poolPolicy: CallerRunsPolicy
# 监控项
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
shutdown:
enabled: true
默认启动端口:
2023
从前端到服务乃至数据库到枚举解决方案,零代码,开箱即用 已加本地缓存,速度杆杆滴
大道至简
/common/enums
/common/enums/page
/common/enums/{enumClassName}
枚举持久化依赖各自持久层实现。 MybatisPlus 3.5.2后版本只需要在实体类中存储数据库值的字段标注
@EnumValue
即可
<!--枚举场景-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>enums-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableEnums
注解以开启枚举功能
/**
* 逻辑删除
* @author dingwen
* @since 2022/6/14
*/
@Getter
@AllArgsConstructor
public enum LogicDelete implements IBaseEnum<Integer> {
/**
* 已删除
*/
DELETED(0, "删除"),
/**
*存在
*/
EXISTED(1, "存在");
/**
* 状态值
*/
@EnumValue
private final Integer code;
/**
* 状态描述
*/
private final String desc;
}
dingwen:
treasure:
# enums
enums:
# 枚举类扫描包
packagepath: "com.dingwen"
# 枚举类所在类路径
classpath: "/**/*.class"
IBaseEnum中已提供枚举比较方法以及转换方法可直接使用
以空间换时间的方式实现.
枚举场景初始化时会将所有枚举缓存到
map
,使用时再通过key
获取即可,省去了循环查找的过程.在枚举数量较多的场景下效率较高.
package com.dingwen.treasure.scene.enums;
import com.dingwen.treasure.enums.core.IBaseEnum;
/**
* 配置环境简单枚举
*
* @author dingwen
* @since 2024/1/18 15:22
*/
public enum ProfileSimpleEnum implements IBaseEnum<String> {
/**
* 生产环境
*/
PROD,
/**
* 开发环境
*/
DEV,
/**
* 测试环境
*/
TEST
}
IBaseEnum
核心方法eq(T code)
: 判断枚举值是否相等from(Class<E> eClass, Object code)
: 通过枚举code枚举转换fastFrom(Class<E> eClass, Object code)
: 更高效的通过枚举code枚举转换fastDescFrom(Class<E> eClass, Object code)
: 更高效的通过枚举code枚举转换获取描述信息基于Screw、Freemarker实现的支持word,markdown,html文件生成的数据库文档生器
<!--数据库文档生成场景启动器-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>screw-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableScrew
注解以开启数据库表文档生成功能
@EnableScrew
配置文档参数
dingwen:
treasure:
# 数据库文档生成
screw:
# 数据源连接地址
jdbcUrl: ${spring.datasource.dynamic.datasource.master.url}
# 数据源用户名
username: ${spring.datasource.dynamic.datasource.master.username}
# 数据源连接密码
password: ${spring.datasource.dynamic.datasource.master.password}
# 驱动类名称
driverClassName: ${spring.datasource.dynamic.datasource.master.driver-class-name}
# 是否读取表注释信息
tableRemark: true
# 文档版本号
docVersion: 1.0.0
# 文件输出目录
fileOutputDir: @project.name@/src/main/resources/static
基于
MongoTemplate
分装的类似MybatisPLus
的lambda
形式的增删改查API
<!--mongo场景启动器-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>mongo-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableScrew
注解以开启mongo场景功能
@EnableMongo
/**
* 请求日志
*
* @author dingwen
* @since 2023/7/24 13:12
*/
@ApiModel(value = "RequestLog", description = "请求日志实体")
@Document("tre_c_request_log")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = true)
public class RequestLog extends BaseMongoEntity {
@ApiModelProperty(value = "请求日志id")
@Id
private String reLogId;
@ApiModelProperty(value = "请求时间")
@Field("reTime")
private LocalDateTime reTime;
@ApiModelProperty(value = "请求IP")
@Field("reIp")
@Indexed
private String reIp;
@ApiModelProperty(value = "IP属地")
@Field("reAddress")
private String reAddress;
@ApiModelProperty(value = "耗时")
@Field("consumeTime")
private Long consumeTime;
@ApiModelProperty(value = "请求体信息")
@Field("resBody")
private String resBody;
@ApiModelProperty(value = "响应体信息")
@Field("respBody")
private String respBody;
@ApiModelProperty(value = "请求地址")
@Field("reUrl")
@Indexed
private String reUrl;
@ApiModelProperty(value = "请求头信息")
@Field("reqHeaders")
private String reqHeaders;
}
/**
* 请求日志服务
*
* @author dingwen
* @since 2023/7/24 13:36
*/
public interface IRequestLogService extends MongoService<RequestLog> {
}
/**
* 请求日志服务
*
* @author dingwen
* @since 2023/7/24 13:38
*/
@Service
public class RequestLogServiceImpl extends MongoServiceImpl<RequestLog> implements IRequestLogService {
}
/**
* MongoAPI
*
* @author dingwen
* @since 2023/7/24 13:42
*/
@Api(tags = "MongoDB API")
@RestController
@Slf4j
@RequestMapping("common/mongo")
public class MongoController implements BaseViewController {
@Resource
private IRequestLogService requestLogService;
/**
* 请求日志列表
*
*/
@ApiOperation(value = "请求日志列表")
@ApiImplicitParams({
@ApiImplicitParam(name = "reIp", value = "请求IP", dataTypeClass = String.class),
@ApiImplicitParam(name = "reAddress", value = "IP属地", dataTypeClass = String.class),
@ApiImplicitParam(name = "reUrl", value = "请求地址", dataTypeClass = String.class)
})
@GetMapping("request-logs")
public ResultVO<PageVO<RequestLog>> getRequestLogPage(@RequestParam(required = false) String reIp,
@RequestParam(required = false) String reAddress,
@RequestParam(required = false) String reUrl) {
LambdaQueryWrapper<RequestLog> query = Wrappers.<RequestLog>lambdaQuery()
.like(StrUtil.isNotBlank(reIp), RequestLog::getReIp, reIp)
.like(StrUtil.isNotBlank(reAddress), RequestLog::getReAddress, reAddress)
.like(StrUtil.isNotBlank(reUrl), RequestLog::getReUrl, reUrl)
.orderByDesc(RequestLog::getCreateTime);
Page<RequestLog> page = requestLogService.page(query, PageUtils.getPageNum(), PageUtils.getPageSize());
return page(page.getRecords(), Convert.toInt(page.getTotal()));
}
}
eq
: 等于ne
: 不等于le
: 小于等于lt
: 小于ge
: 大于等于gt
: 大于bw
: 在...之间in
: 包含nin
: 不包含like
: 全模糊查询left_like
: 左模糊查询right_like
: 右模糊查询基于
xxl-job
v2.4.0版本的执行器封装,实现配置化、插件化使用
参考官网 xxl-job
<!--xxl-job场景启动器-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>xxl-job-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableXxlJob
注解以开启xxl-job
定时任务场景功能
@EnableXxlJob
配置xxl-job参数
dingwen:
treasure:
#xxl-job定时任务场景
xxljob:
# 执行器开关
enabled: true
adminAddresses: http://127.0.0.1:8003
#调度中心应用名
adminAppName: treasure-xxl-job-admin
# 执行器通讯TOKEN
accessToken: xxl-job-access-token
# 执行器配置
executor:
appName: scene
#执行器日志文件保存天数:大于3生效
logRetentionDays: 10
# 执行器运行日志文件存储磁盘路径 【注意不要和项目本身log路径冲突】
logPath: logs/xxljob
# 执行器端口号
port: 9999
基于
caffeine
实现的配置化的本地缓存
Spring cache
实现
@Cacheable
: 将方法的结果缓存起来,下一次方法执行参数相同时,将不执行方法,返回缓存中的结果@CacheEvict
: 移除指定缓存@CachePut
: 更新缓存@Caching
: 可以指定相同类型的多个缓存注解,例如根据不同的条件@CacheConfig
: 类级别注解,可以设置一些共通的配置,@CacheConfig(cacheNames=""), 代表该类下的方法均使用这个cacheNamesCacheHelper
VisualCaffeineCache
<!--caffeine场景-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>caffeine-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableCaffeine
注解以开启caffeine
本地缓存场景功能
@EnableCaffeine
dingwen:
treasure:
# caffeine
caffeine:
caches:
# 缓存Spring Bean名称
- name: testCache
# 有效时间,单位秒
invalidTime: 60
# 最大缓存数量
maximumSize: 100
- name: fileCache
invalidTime: 60
maximumSize: 1000
package com.dingwen.treasure.scene.controller.caffeine;
/**
* Caffeine
*
* @author dingwen
* @since 2023/5/22 16:20
*/
@Api(tags = "本地缓存API")
@RestController
@Slf4j
@RequestMapping("common/caffeine")
@CacheConfig(cacheManager = "caffeineCacheManager")
public class CaffeineController implements BaseViewController {
@Resource(name = "testCache")
@Lazy
private CaffeineCache testCache;
/**
* 缓存测试
*
* @return {@link ResultVO}<{@link String}>
*/
@ApiOperation("本地缓存测试")
@Cacheable(cacheNames = "testCache")
@GetMapping("/test")
public ResultVO<String> cache() {
testCache.putIfAbsent("test", "test");
String test = testCache.get("test", String.class);
log.info("test:{}", test);
return success(test);
}
CacheHelper
封装同一的现查询本地缓存,若存在则返回,不存在则查询数据库返回且同时加入本地缓存
package com.dingwen.treasure.caffeine.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.cache.Cache;
import org.springframework.cache.caffeine.CaffeineCache;
import java.lang.reflect.Field;
import java.util.*;
import java.util.function.Function;
/**
* 缓存工具类
*
* @author dingwen
* @since 2023/5/30 16:01
*/
public class CacheHelper {
/**
* 实体初始化
*/
private static final CacheHelper CACHEHELPER = new CacheHelper();
/**
* 构造器私有
*/
private CacheHelper() {
}
/**
* 获取实例
*
* @return {@link CacheHelper}
*/
public static CacheHelper getInstance() {
return CACHEHELPER;
}
/**
* 缓存Map
*
* @param cache 缓存
* @return {@link Map}<{@link String}, {@link Object}>
*/
public <T> Map<String, T> cacheToMap(Cache cache) {
Object obj = cache.getNativeCache();
Map<String, Object> map = new HashMap<>(16);
Field[] fields = obj.getClass().getDeclaredFields();
try {
for (Field field : fields) {
field.setAccessible(true);
map.put(field.getName(), field.get(obj));
}
} catch (Exception e) {
return null;
}
return (Map<String, T>) map.get("cache");
}
/**
* 本地缓存通用逻辑处理
*
* @param key 缓存key id
* @param keyPrefix 缓存key 前缀
* @param keySuffix 缓存key 后缀
* @param clazz 缓存值字节码
* @param func 实际执行函数
* @param caffeineCache 缓存池
* @return {@link V}
*/
public <K, V> V cacheAndGet(K key, String keyPrefix, String keySuffix,
Class<V> clazz, Function<K, V> func,
CaffeineCache caffeineCache) {
if (Objects.isNull(key)
|| Objects.isNull(keyPrefix)
|| Objects.isNull(keySuffix)
|| Objects.isNull(clazz)
|| Objects.isNull(caffeineCache)) {
if (Objects.isNull(func)) {
return null;
}
return func.apply(key);
}
String cacheKey = StrUtil.format("{}_{}_{}", keyPrefix, key, keySuffix);
V vCache = caffeineCache.get(cacheKey, clazz);
if (Objects.nonNull(vCache)) {
return vCache;
}
V v = func.apply(key);
caffeineCache.put(cacheKey, v);
return v;
}
/**
* caches and gets
*
* @param keys 缓存keys ids
* @param keyPrefix 缓存key 前缀
* @param keySuffix 缓存key 后缀
* @param clazz 缓存值字节码
* @param func 实际执行函数
* @param caffeineCache 缓存池
* @return {@link V}
*/
public <K, V> List<V> cachesAndGets(List<K> keys, String keyPrefix, String keySuffix,
Class<V> clazz, Function<List<K>, Map<K, V>> func,
CaffeineCache caffeineCache) {
if (CollUtil.isEmpty(keys)
|| Objects.isNull(keyPrefix)
|| Objects.isNull(keySuffix)
|| Objects.isNull(clazz)
|| Objects.isNull(caffeineCache)) {
if (Objects.isNull(func)) {
return null;
}
return new ArrayList<>(func.apply(keys).values());
}
// 缓存的结果
List<V> cacheValues = new ArrayList<>(keys.size());
// 没有在缓存中的key
List<K> noCacheKeys = new LinkedList<>();
for (K key : keys) {
String cacheKey = buildCacheKey(key, keyPrefix, keySuffix);
V vCache = caffeineCache.get(cacheKey, clazz);
if (Objects.nonNull(vCache)) {
cacheValues.add(vCache);
} else {
// 缓存未命中
noCacheKeys.add(key);
}
}
// 查询参数完全命中,直接返回
if (CollUtil.isNotEmpty(noCacheKeys)) {
return cacheValues;
}
// 未命中缓存的数据
Map<K, V> noCacheValues = func.apply(noCacheKeys);
if (CollUtil.isEmpty(noCacheValues)) {
return cacheValues;
}
// 添加新查询到的数据到缓存中
noCacheValues.forEach((K key, V value) -> {
String cacheKey = buildCacheKey(key, keyPrefix, keySuffix);
caffeineCache.put(cacheKey, value);
cacheValues.add(value);
});
return cacheValues;
}
/**
* 构建缓存key
*
* @param key key
* @param keyPrefix key prefix
* @param keySuffix key suffix
* @return {@link String}
*/
public <K> String buildCacheKey(K key, String keyPrefix, String keySuffix) {
return StrUtil.format("{}_{}_{}", keyPrefix, key, keySuffix);
}
}
VisualCaffeineCache
显示更丰富的内容
基于
RedisTemplate
封装的启动器
List
,Set
,Map
缓存操作void removeZset(final String key, Double scoreStart, Double scoreEnd)
<T> Set<T> getZsetCacheZet(final String key,Double scoreStart,Double scoreEnd)
Redis
信息RedisCache
RedisShareLockComponent
zset
实现延迟队列CAS
基于lua
脚本实现DelayRemoves
<!--redis场景-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>redis-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableRedis
注解以开启redis
场景功能
@EnableRedis
dingwen:
treasure:
# redis
redis:
easy:
# 开启easy cache
cache: true
rate:
# 开启限流
limiter: true
re:
# 开启防重复提交
submit: true
# 缓存项配置
caches:
- keyPrefix: "re_submit:"
remark: "防止重复提交"
- keyPrefix: "common_lock:"
remark: "通用锁"
package com.dingwen.treasure.scene.controller.redis;
/**
* Redis场景API
*
* @author dingwen
* @since 2023/5/22 11:11
*/
@Api(tags = "Redis缓存API")
@RestController
@Slf4j
@RequestMapping("common/redis")
public class RedisController implements BaseViewController {
/**
* redis 服务
*/
@Resource
private RedisService redisService;
@Resource
private RedisProperties redisProperties;
/**
* 得到Redis信息
*
* @return {@link ResultVO}
*/
@ApiOperation("获取Redis信息")
@GetMapping("/info")
public ResultVO getInfo() {
return success(redisService.getInfo());
}
/**
* 获取缓存项
*
* @return {@link ResultVO}<{@link List}<{@link CacheVO}>>
*/
@ApiOperation(value = "获取缓存项")
@GetMapping
public ResultVO<List<CacheVO>> getCaches() {
List<CacheBO> caches = redisProperties.getCaches();
List<CacheVO> cacheVOS = BeanCopyUtils.copyList(caches, CacheVO.class);
return success(cacheVOS);
}
/**
* 获取指定前缀的所有key
*
* @param keyPrefix 关键前缀
* @return {@link ResultVO}<{@link Collection}<{@link String}>>
*/
@ApiOperation(value = "获取指定前缀的所有key")
@ApiImplicitParam(name = "keyPrefix", value = "key前缀", dataTypeClass = String.class)
@GetMapping("/{keyPrefix}")
public ResultVO<Collection<String>> getKeys(@PathVariable("keyPrefix") String keyPrefix) {
return success(redisService.getKeys(keyPrefix + RedisConstant.KEY_ALL));
}
/**
* 获取缓存值
*
* @param keyPrefix 关键前缀
* @param key 关键
* @return {@link ResultVO}<{@link CacheVO}>
*/
@ApiOperation(value = "获取指定key的缓存值")
@ApiImplicitParams({
@ApiImplicitParam(name = "keyPrefix", value = "key前缀", dataTypeClass = String.class),
@ApiImplicitParam(name = "key", value = "key", dataTypeClass = String.class)
})
@GetMapping("/{keyPrefix}/{key}")
public ResultVO<CacheVO> getCache(@PathVariable("keyPrefix") String keyPrefix, @PathVariable("key") String key) {
String cacheValue = redisService.getCacheObject(key);
CacheVO cacheVO = CacheVO.builder()
.keyPrefix(keyPrefix)
.cacheKey(key)
.cacheValue(cacheValue)
.build();
return success(cacheVO);
}
/**
* 清除指定key前缀的所有缓存
*
* @param keyPrefix 关键前缀
* @return {@link ResultVO}<{@link String}>
*/
@ApiOperation(value = "清除指定key前缀的所有缓存")
@ApiImplicitParam(name = "keyPrefix", value = "key前缀", dataTypeClass = String.class)
@PutMapping("/{keyPrefix}")
public ResultVO<String> cleanCaches(@PathVariable("keyPrefix") String keyPrefix) {
redisService.removeKeys(keyPrefix + RedisConstant.KEY_ALL);
return success();
}
/**
* 清除指定key的缓存
*
* @param key 关键
* @return {@link ResultVO}<{@link String}>
*/
@ApiOperation(value = "清除指定key的缓存")
@ApiImplicitParam(name = "key", value = "key", dataTypeClass = String.class)
@DeleteMapping("/{key}")
public ResultVO<String> cleanCache(@PathVariable("key") String key) {
redisService.deleteObject(key);
return success();
}
/**
* 清洗所有缓存
*
* @return {@link ResultVO}<{@link String}>
*/
@ApiOperation(value = "清除所有缓存")
@DeleteMapping
public ResultVO<String> cleanAllCache() {
redisService.removeKeys(RedisConstant.KEY_ALL);
return success();
}
/**
* 轻松缓存
*
* @param easyCacheSubmitVO 轻松缓存提交内容
* @return {@link ResultVO}<{@link String}>
*/
@PostMapping("/easy-cache")
@ApiOperation("轻松缓存测试")
@EasyCache(keyParams = {
"#easyCacheSubmitVO.getId()",
"#easyCacheSubmitVO.getName()"},
time = 100,
returnType = ResultVO.class)
public ResultVO<EasyCacheSubmitVO> easyCache(@RequestBody EasyCacheSubmitVO easyCacheSubmitVO) {
return success(easyCacheSubmitVO);
}
/**
* 速率限制
*
* @return {@link ResultVO}<{@link String}>
*/
@ApiOperation("redis限流测试")
@GetMapping("/rate-limit")
@RateLimiter(time = 1, count = 2)
public ResultVO<String> rateLimit() {
return success();
}
/**
* 防止重复提交测试
*/
@ApiOperation("防止重复提交测试")
@GetMapping("/re-submit")
@ReSubmit(message = "登录重复提交请求", isDeleteKey = false, time = 90)
public void resubmit() {
}
/**
* 锁 </br>
* redisson方案;<a href="https://redisson.org">...</a>
*
* @return {@link ResultVO}<{@link String}>
*/
@ApiOperation("lock测试")
@GetMapping("/lock")
public ResultVO<String> lock() {
boolean ifAbsent = redisService.setIfAbsent(RedisKeyConstant.LOCK_PREFIX.concat("test"), "lock test",
1L, TimeUnit.MINUTES);
// 剩余时间
long expire = redisService.getExpire(RedisKeyConstant.LOCK_PREFIX.concat("test"), TimeUnit.SECONDS);
if (ObjectUtil.isNotNull(ifAbsent) && ifAbsent) {
return success();
}
String message = StrUtil.format("频繁的操作,请{}秒后重试", expire);
return failure(message);
}
}
继承抽象类
AbstractRedisCache
完成自身预热逻辑,由统一预热组件进行调用
RedisShareLockComponent
/**
* redis 分布式锁组件
*
* @author dingwen
* @since 2023/12/5 14:57
*/
@Component
@Slf4j
public class RedisShareLockComponent {
@Resource
private RedisProperties redisProperties;
@Resource
private RedisService redisService;
/**
* 加锁å
*
* @param lockKey 锁key
* @param requestId 请求id
* @param time 时间
* @param timeUnit 时间单位
* @return boolean
*/
public boolean lock(String lockKey, String requestId, Long time, TimeUnit timeUnit) {
// 参数检查
lockParamsCheck(lockKey, requestId, time, timeUnit);
// 当前时间
long currentTime = System.currentTimeMillis();
// 超时时间
long outTime = currentTime + redisProperties.getShareLockTimeOut();
boolean lockResult = false;
// 加锁
while (currentTime < outTime) {
lockResult = redisService.setIfAbsent(lockKey, requestId, time, timeUnit);
if (lockResult) {
log.info("[Redis模块]\t[分布式锁],加锁成功:lockKey:{},requestId:{},time:{},timeUnit:{}",
lockKey, requestId, time, timeUnit);
return true;
}
ThreadUtil.sleep(100);
currentTime = System.currentTimeMillis();
}
return lockResult;
}
/**
* 加锁参数检查
*
* @param lockKey lock key
* @param requestId request id
* @param time time
* @param timeUnit time unit
*/
private void lockParamsCheck(String lockKey, String requestId, Long time, TimeUnit timeUnit) {
if (StrUtil.isBlank(lockKey) || StrUtil.isBlank(requestId) || time <= 0 || Objects.isNull(timeUnit)) {
log.error("[Redis模块]\t[分布式锁],加锁参数错误:lockKey:{},requestId:{},time:{},timeUnit:{}",
lockKey, requestId, time, timeUnit);
throw new ShareLockException("加锁参数异常");
}
}
/**
* 解锁
*
* @param lockKey 锁keu
* @param requestId 请求id
* @return boolean
*/
public boolean unLock(String lockKey, String requestId) {
if (StrUtil.isBlank(lockKey) || StrUtil.isBlank(requestId)) {
throw new ShareLockException("解锁参数异常");
}
try {
String cacheRequestId = redisService.getCacheObject(lockKey);
if (requestId.equals(cacheRequestId)) {
redisService.deleteObject(cacheRequestId);
return true;
}
} catch (Exception e) {
log.error("[Redis模块]\t[分布式锁],解锁失败。lockKey:{},requestId:{}", lockKey, requestId, e);
}
return false;
}
/**
* 尝试加锁方法
*
* @param lockKey 锁key
* @param requestId 请求id
* @param time 时间
* @param timeUnit 时间单位
* @return boolean
*/
public boolean tryLock(String lockKey, String requestId, Long time, TimeUnit timeUnit) {
// 参数检查
lockParamsCheck(lockKey, requestId, time, timeUnit);
return redisService.setIfAbsent(lockKey, requestId, time, timeUnit);
}
}
zset
实现延迟队列 DelayTaskComponent
当我们有期望一个任务再某一个时间点再去执行,此时业务相对比较简单,不想引入Mq组件时可以考虑实用Redis实现延迟队列。
基于redis的zset实现,zset天生具有score的特性。可以根据score放入,而且可以通过range进行排序获取,以及删除指定的值。从业务上,我们可以再新增任务的时候放入,再通过定时任务进行拉取,要注意的一点就是拉取的时候要有分布式锁,保证不进行重复拉取就可以了。
CAS
基于lua
脚本实现DelayRemoves
使用延时双删,保证缓存数据库一致性问题
基于
sun.mail
实现的可配置的电子邮件使用场景
<!--邮件场景启动器-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>email-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableEmail
注解以开启电子邮件场景功能
@EnableEmail
dingwen:
treasure:
# 电子邮件
email:
enabled: true
# SMTP服务器域名
host: smtp.163.com
port: 465
# 是否需要用户名密码验证
auth: true
# 发送方,遵循RFC-822标准
from: dingwen0314@163.com
# 用户名(注意:如果使用foxmail邮箱,此处user为qq号)
user: dingwen0314@163.com
# 密码(注意,某些邮箱需要为SMTP服务单独设置密码,详情查看相关帮助)
pass: TODO
# 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。
starttlsEnable: true
# 使用SSL安全连接
sslEnable: true
# SMTP超时时长,单位毫秒,缺省值不超时
timeout: 0
# Socket连接超时值,单位毫秒,缺省值不超时
connectionTimeout: 0
/**
* 发送电子邮件
*/
@Test
public void sendEmail() {
String messageId = MailUtils.sendText("1981723769@qq.com", "test", "测试邮件内容");
System.out.println(messageId);
}
基于
Redis
实现的多线程任务调度的进度条任务场景
<!--进度条场景-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>bar-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableBar
注解以开启进度条场景功能
@EnableBar
工厂模式 + 策略模式 + 门面模式 + 模板方法 实现的定制化配置化规则引擎
PipelineContext
<!--pipeline场景-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>pipeline-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnablePipeline
注解以开启pipeLine
场景功能
@EnablePipeline
dingwen:
treasure:
# pipeline
pipeline:
routes:
# 管道上下文名称 key
testContext:
# 第一个执行的管道
- onePipeLine
# 第二个执行的管道
- twoPipeLine
// 配置管道过滤器
@PipeFilters(filters = {
@PipeFilter(beanName = "beforeOneFilter",exePoint = PipelineFilterExePoint.ALL),
@PipeFilter(beanName = "beforeTwoFilter",exePoint = PipelineFilterExePoint.BEFORE),
})
PipelineContext
管道上下文Pipeline
管道接口PipelineFilter
管道过滤器接口@PipeFilter
过滤器注解PipelineFactory
管道执行工厂PipelineExecutor
管道执行器基于亚马逊
S3
封装的支持阿里云,Minio
等多种存储方式的对象存储通用业务场景启动器
<!--对象存储场景-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>oss-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableOss
注解以开启oss
场景功能
@EnableOss
dingwen:
treasure:
# oss
oss:
# 对象存储服务的URL
endpoint: http://127.0.0.1:9000
# key
access-key: admin
# 密钥
secret-key: 1234567890
# 路径风格
path-style-access: true
# 最大线程数
max-connections: 100
# 区域
region:
OssTemplate
操作API基于
MybatisPlus
的二次封装
统一实体
统一查询对象
场景异常封装处理
逻辑删除
枚举处理
通用字段填充
多租户
数据权限
通用选项查询组件
完整SQL
日志打印
SQL
执行耗时
通用字符串字段长度校验组件
同一数据唯一性校验组件
获取表信息工具
字段比较工具
通用查询组件 QueryUtils
[支持数据权限]
FULL
: 全模糊查询EQ
: 全等于查询LEFT
: 全以什么结尾模糊查询查询RIGHT
: 全以什么开头模糊查询查询IN
: 在集合查询NOT_IN
: 不在集合查询RANGE
: 范围内查询DESC
: 降序ASC
: 升序数据冗余解决方案IRedundancyMaintainService
<!--mybatisplus场景启动器-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>mybatisplus-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableMybatisPlus
注解以开启mybatisplus
场景功能
@EnableMybatisPlus
当你需要查询某个数据作为下拉选项时可使用此组件,支持四级选项、数据权限、中间表关联。已实现本地缓存优化。
@Test
void testOpForLevel3() {
List<OpParam> opParams = new ArrayList<>();
OpParam unitOpParam = OpParam
.builder()
.labelField("unit_name")
.valueField("unit_id")
.tableName("hom_u_unit")
.build();
OpParam deptOpParam = OpParam
.builder()
.labelField("t1.dept_name")
.valueField("t1.dept_id")
.tableName("sys_dept t1")
.leftJoinField(", t2.dept_id")
.leftJoinSql("left join hom_u_unit_lnk_dept t2 on t1.dept_id = t2.dept_id")
.parentField("t2.unit_id")
.build();
OpParam usertOpParam = OpParam
.builder()
.labelField("user_name")
.valueField("user_id")
.tableName("sys_user")
.parentField("dept_id")
.build();
opParams.add(unitOpParam);
opParams.add(deptOpParam);
opParams.add(usertOpParam);
List<Option> options = OptionHelper.get(opParams);
log.info(JSONUtil.toJsonPrettyStr(options));
}
@Test
void testOpForLevel2() {
List<OpParam> opParams = new ArrayList<>();
OpParam deptOpParam = OpParam
.builder()
.labelField("dept_name")
.valueField("dept_id")
.tableName("sys_dept")
.build();
OpParam usertOpParam = OpParam
.builder()
.labelField("user_name")
.valueField("user_id")
.tableName("sys_user")
.parentField("dept_id")
.build();
opParams.add(deptOpParam);
opParams.add(usertOpParam);
List<Option> options = OptionHelper.get(opParams);
log.info(JSONUtil.toJsonPrettyStr(options));
}
@Test
void testOpForLevel1() {
List<OpParam> opParams = new ArrayList<>();
OpParam deptOpParam = OpParam
.builder()
.labelField("dept_name")
.valueField("dept_id")
.tableName("sys_dept")
.build();
opParams.add(deptOpParam);
List<Option> options = OptionHelper.get(opParams);
log.info(JSONUtil.toJsonPrettyStr(options));
}
CRUD
package com.dingwen.web.mybatisplus;
/**
* BaseCrudController MybatisPlus实现 </br>
* <p> P: 持久化对象</p>
*
* @author dingwen
* @since 2023/3/19 15:55
*/
public abstract class AbstractBaseControllerMybatisPlusImpl<P extends BaseEntity>
implements BaseCrudController<P>, BaseViewController {
/**
* 服务
*/
@Autowired
private IService<P> iService;
/**
* 根据Id查询,返回单个实体
*
* @param id 数据表主键
* @return {@link ResultVO} 结果
*/
@ApiOperation("根据唯一键查询,返回单个对象")
@ApiOperationSupport(author = "dingwen")
@ApiImplicitParam(value = "唯一键", name = "id", dataTypeClass = Long.class)
@GetMapping("/default/{id}")
@Override
public ResultVO find(@PathVariable Serializable id) {
return success(iService.getById(id));
}
/**
* 分页查询 </br>
* 分页参数由分页工具自动从Servlet上下文中获取
*
* @param p 查询实体
* @return {@link ResultVO}<{@link PageVO}
*/
@ApiOperation("分页查询,返回PageVO")
@ApiOperationSupport(author = "dingwen")
@GetMapping("/default/page")
@Override
public ResultVO<PageVO<P>> findPage(@ModelAttribute P p) {
PageUtils.startPage();
return page(iService.list(new QueryWrapper<>(p)));
}
/**
* 根据唯一键集查询数据对象
*
* @param ids 唯一键集
* @return {@link ResultVO}<{@link List}
*/
@ApiOperation("根据唯一键集查询,返回对象列表")
@ApiOperationSupport(author = "dingwen")
@ApiImplicitParam(value = "唯一键集", name = "ids", dataTypeClass = List.class)
@GetMapping("/default/{ids}")
@Override
public ResultVO<List<P>> find(@PathVariable List<Serializable> ids) {
return success(iService.listByIds(ids));
}
/**
* 获取数据表总记录条数
*
* @return {@link ResultVO}<{@link Long}>
*/
@ApiOperation("获取数据表总记录条数,返回统计数量")
@ApiOperationSupport(author = "dingwen")
@GetMapping("/default/count")
@Override
public ResultVO<Long> count() {
return success(iService.count());
}
/**
* 通过唯一键查询是否存在 </br>
* <ol>
* <li>true: 存在</li>
* <li>false: 不存在</li>
* </ol>
*
* @param id 唯一键
* @return {@link ResultVO}
*/
@ApiOperation("通过唯一键查询是否存在,返回布尔值")
@ApiOperationSupport(author = "dingwen")
@GetMapping("/default/exists/{id}")
@Override
public ResultVO<Boolean> exists(@PathVariable Serializable id) {
return success(ObjectUtil.isEmpty(iService.getById(id)));
}
/**
* 通过对象唯一键进行修改
*
* @param p 参数对象
* @return {@link ResultVO}<{@link Boolean}>
*/
@ApiOperation("通过对象唯一键进行修改,返回布尔值")
@ApiOperationSupport(author = "dingwen")
@PutMapping("/default")
@Override
public ResultVO<Boolean> modify(@RequestBody P p) {
return genResult(iService.updateById(p));
}
/**
* 保存
*
* @param p 数据对象
* @return {@link ResultVO}
*/
@ApiOperation("保存一个对象到数据库,返回布尔值")
@ApiOperationSupport(author = "dingwen")
@PostMapping("/default")
@Override
public ResultVO create(@RequestBody P p) {
return genResult(iService.save(p));
}
/**
* 批量保存
*
* @param ps 对象集
* @return {@link ResultVO}
*/
@ApiOperation("批量添加,返回布尔值")
@ApiOperationSupport(author = "dingwen")
@PostMapping("/default/batch")
@Override
public ResultVO<Boolean> create(@RequestBody List<P> ps) {
return genResult(iService.saveBatch(ps));
}
/**
* 删除单条记录
*
* @param id 唯一键
* @return {@link ResultVO}
*/
@ApiOperation("删除单条记录,返回布尔值")
@ApiOperationSupport(author = "dingwen")
@DeleteMapping("/default/{id}")
@Override
public ResultVO<Boolean> remove(@PathVariable Serializable id) {
return genResult(iService.removeById(id));
}
/**
* 根据唯一键集批量删除
*
* @param ids 唯一键集
* @return {@link ResultVO}<{@link Boolean}>
*/
@ApiOperation("批量删除记录,返回布尔值")
@ApiOperationSupport(author = "dingwen")
@ApiImplicitParam(name = "ids", value = "唯一键集", dataTypeClass = List.class)
@DeleteMapping("/default/batch/{ids}")
@Override
public ResultVO<Boolean> remove(@PathVariable List<Serializable> ids) {
return genResult(iService.removeByIds(ids));
}
/**
* 保存或更新
*
* @param p p
* @return {@link ResultVO}<{@link Boolean}>
*/
@ApiOperation("批量删除记录,返回布尔值")
@ApiOperationSupport(author = "dingwen")
@PostMapping("/default/sa-mos")
@Override
public ResultVO<Boolean> saveOrUpdate(P p) {
return genResult(iService.saveOrUpdate(p));
}
}
要求: 足够灵活,足够高效,足够优雅
TODO
SQL
日志及耗时日志基于拦截器实现,核心类:
SQL
拼接拦截器MybatisPlusSqlLogInterceptor
,SQL
耗时拦截器MybatisSqlStatementInterceptor
添加以下配置启动:
# mybatisplus 场景启动器
mybatisplus:
# 是否开启多租户插件
tenant: false
# 是否开启SQL拦截器日志
sqlLog: true
# sql耗时统计(毫秒) 低档
consumeLow: 999
# sql耗时统计(毫秒) 中档
consumeMiddle: 5000
# sql耗时统计(毫秒) 高档
consumeHeight: 10000
druid
连接加密使用
DruidEncryptUtils
生产密码,公钥,私钥,再增加对应配置即可
多数据源示例配置
spring:
datasource:
dynamic:
# 性能分析插件(有性能损耗 不建议生产环境使用)
p6spy: false
primary: master
datasource:
master:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://127.0.0.1:3306/treasure?characterEncoding=utf-8&useSSL=true&serverTimezone=GMT
username: root
password: ENC([TODO 密码])
driver-class-name: com.mysql.cj.jdbc.Driver
public-key: [TODO publicKey]
slave:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://127.0.0.1:3306/treasure_slave?characterEncoding=utf-8&useSSL=true&serverTimezone=GMT
username: root
password: ENC([TODO 密码])
driver-class-name: com.mysql.cj.jdbc.Driver
# 公钥
public-key: [TODO publicKey]
druid:
filter:
config:
enabled: true
# 加密公钥
connection-properties: config.decrypt=true;config.decrypt.key=${spring.datasource.druid.publicKey};
TableFieldLengthValidHelper
对于一些不为空的亦或是规则的校验
Valid API
已经很方便了,但是对于字符串类型长度的校验,若数据表发生了改变需要同步维护实体中的字段校验信息。通用字符串字段长度校验组件就是为了解决这一问题,通过查询数据表的方式来进行字符串的长度校验,提供丰富的日志,确保不会出现因为字段过长倒是的数据库操作错误
当你需要保证表中某一字段唯一而又不想依赖数据库的唯一性约束,在数据提交的时候就完成校验。直接过滤掉这一类错误数据的数据库访问时实用此组件
TableInfoUtils
通过
SqlRunner
获取数据库表字段定义,注释等信息及表注释等信息。目前只支持Mysql
。已做本地缓存优化
getTableName(T ojb)
:获取表名称getTableComment(String tableName,String database)
:获取表注释信息getTableFieldInfos(T obj)
: 获取表字段信息CompareUtils
为实现关键数据的变更历史提供支持。例如:名称:【】,描述:【】,发生了变更。由【】变更为【】。
QueryUtil
查询对象继承
BaseQuery
,标注对应注解
/**
* 字典查询对象
*
* @author dingwen
* @since 2023/6/8 10:04
*/
@ApiModel(value = "字典查询对象")
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class DictQuery extends BaseQuery implements Serializable {
@ApiModelProperty(value = "字典名称")
@QueryType(value = QueryMode.FULL)
private String dictName;
@ApiModelProperty(value = "字典类型")
@QueryType(value = QueryMode.EQ)
private String dictType;
@ApiModelProperty(value = "字典状态")
@QueryType(value = QueryMode.EQ)
private DicStatus status;
@ApiModelProperty(value = "字典值标签")
@QueryType(value = QueryMode.EQ)
private String dicLabel;
@ApiModelProperty(value = "字典值父id")
@QueryType(value = QueryMode.EQ)
private Long dictDataParentId;
@ApiModelProperty("字典所属模块")
@QueryType(value = QueryMode.EQ)
private String dictModule;
@ApiModelProperty("创建时间")
@QueryType(value = QueryMode.DESC)
private Date createTime;
private static final long serialVersionUID = -6210670520489664106L;
}
支持的查询类型
/**
* 查询方式
* @author dingwen
* @since 2022/6/14
*/
@Getter
@AllArgsConstructor
public enum QueryMode implements IBaseEnum<String> {
/**
* 全模糊
*/
FULL("FULL", "包含模糊查询"),
/**
* 等于
*/
EQ("EQ", "等于"),
/**
* 以什么结尾模糊查询
*/
LEFT("LEFT", "以什么结尾模糊查询"),
/**
* 以什么开头模糊查询
*/
RIGHT("RIGHT", "以什么开头模糊查询"),
/**
* 在集合
*/
IN("IN", "在集合查询"),
/**
* 不在集合
*/
NOT_IN("NOT_IN", "不在集合查询"),
/**
* 范围内查询
*/
RANGE("RANGE", "范围内查询"),
/**
* 降序
*/
DESC("DESC", "降序"),
/**
* 升序
*/
ASC("ASC", "升序");
/**
* 状态值
*/
@EnumValue
private final String code;
/**
* 状态描述
*/
private final String desc;
}
使用
/**
* 字典分页查询
*
* @param dictQuery dict类型查询
* @return {@link PageVO}<{@link DictVO}>
*/
@ApiOperation("字典分页查询")
@ApiImplicitParams(
{
@ApiImplicitParam(name = "dictName", value = "字典名称", dataTypeClass = String.class),
@ApiImplicitParam(name = "dictType", value = "字典类型", dataTypeClass = String.class),
@ApiImplicitParam(name = "dictModule", value = "字典所属模块", dataTypeClass = String.class),
@ApiImplicitParam(name = "status", value = "字典状态", dataTypeClass = Enum.class)
})
@GetMapping("/pages")
public ResultVO<PageVO<DictVO>> dictPage(@ModelAttribute DictQuery dictQuery) {
QueryWrapper<Dict> queryWrapper = QueryUtils.buildQueryWrapper(dictQuery);
Page<Dict> dictPage = dictManager.queryDictPage(queryWrapper);
return page(converter.convert(dictPage,DictVO.class), dictPage.getTotal());
}
IRedundancyMaintainService
表设计时,为了保证性能会考虑冗余一些字段从而提升性能.但是这样带来了当冗余的原始数据更新的时候还显示旧数据的问题.本章节所述及解决这一问题.
不冗余字段,使用本地缓存+Redis缓存等方式提升性能.也可参考本项目中的
translate-spring-boot-starter
翻译场景的使用.
设计冗余字段,在原始数据发生修改的时候进行同步更新.更新的逻辑相对通用,使用
Spring
的事件机制进行抽象复用.
当冗余字段发生变更的时候发布事件
/**
* 数据冗余维护事件
*
* @author dingwen
* @since 2024/3/11 11:06
*/
@Getter
@Setter
public class RedundancyMaintainEvent extends ApplicationEvent {
/**
* 更新的条件字段获取方法
*/
private SFunction<?, ?> conditionFieldFunc;
/**
* 更新条件值
*/
private Object conditionVal;
/**
* 实际更新的字段获取方法
*/
private SFunction<?, ?> updateFieldFunc;
/**
* 更新条件值
*/
private Object updateVal;
/**
* 变更的实体对应的全类名称
*/
private String updateFullClassName;
private static final long serialVersionUID = -4069028139785256372L;
/**
* Create a new {@code ApplicationEvent}.
*
* @param source the object on which the event initially occurred or with
* which the event is associated (never {@code null})
*/
public RedundancyMaintainEvent(Object source) {
super(source);
}
}
依赖冗余数据的服务需要实现
IRedundancyMaintainService
服务,然后由监听器RedundancyMaintainListener
去统一匹配调用对应的修改服务进行冗余数据的同步处理
BaseEntity
统一实体提供统一的通用字段
Lombok
注解@FieldNameConstants
: 可以通过常量访问属性
<!--通用字典场景-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>dic-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableDic
注解以开启字典场景功能
@EnableDic
DictHelper
Redis + CaffeineCache 专注提升效率
package com.dingwen.treasure.dic.utils;
import cn.hutool.extra.spring.SpringUtil;
import com.dingwen.treasure.dic.service.impl.DictDataServiceImpl;
import java.util.Objects;
import java.util.Optional;
/**
* 字典工具类
*
* @author dingwen
* @since 2023/8/12 17:35
*/
public class DictHelper {
/**
* 获取字典值翻译
*
* @param dictType 字典类型
* @param dictValue 字典值
* @return 翻译
*/
public static Optional<String> getDictLabel(String dictType, String dictValue) {
DictDataServiceImpl dictDataService = SpringUtil.getBean(DictDataServiceImpl.class);
if (Objects.isNull(dictDataService)) {
return Optional.empty();
}
return Optional.ofNullable(dictDataService.translate(dictType, dictValue));
}
/**
* 获取字典值
*
* @param dictType 字典类型
* @param dictLabel 字典标签
* @return 翻译
*/
public static Optional<String> getDictValue(String dictType, String dictLabel) {
DictDataServiceImpl dictDataService = SpringUtil.getBean(DictDataServiceImpl.class);
if (Objects.isNull(dictDataService)) {
return Optional.empty();
}
return Optional.ofNullable(dictDataService.revertTranslate(dictType, dictLabel));
}
}
基于Jackson反序列化进行的字典翻译组件
ITranslateService
TranslateResult
形式的方法返回值自动翻译 <!--翻译场景启动器-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>translate-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableTranslate
注解以开启通用翻译场景功能
@EnableTranslate
// 默认翻译实现,使用自定义名称新字段
@ApiModelProperty(value = "name")
@Translate(
service = "defaultTranslateServiceImpl", // 翻译实现接口
mapper = "id", // 翻译依据值
key = "file_name", // 翻译值对应表字段
keyExtend = "tre_c_file", // 翻译对应表名称
param = "file_id" // 翻译依据值对应表字段名称
)
private String name;
// 自定义翻译实现,例: 字典翻译实现,使用已有字段名称
@Translate(
service = "dictTranslateServiceImpl",// 翻译实现接口
key = "d_field_type", // 翻译值对应表字段
mapper = "dictLabel" // 翻译依据值
)
@ApiModelProperty(value = "dictLabel")
private String dictLabel;
ITranslateService
package com.dingwen.treasure.translate.core.service;
/**
* 翻译接口
*
* @author dingwen
* @since 2023/6/9 15:39
*/
public interface ITranslateService<T, P1, P2, P3, P4> {
/**
* 翻译
*
* @param key 关键
* @param keyExtend 关键扩展
* @param param 参数
* @param paramExtend 参数扩展
* @return {@link T}
*/
T translate(P1 key, P2 keyExtend, P3 param, P4 paramExtend);
}
Translate
package com.dingwen.treasure.translate.annotation;
import com.dingwen.treasure.translate.core.handler.TranslationHandler;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.*;
/**
* 通用翻译注解
*
* @author dingwen
* @date 2023/06/09
*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
@Documented
@JacksonAnnotationsInside
@JsonSerialize(using = TranslationHandler.class)
public @interface Translate {
/**
* 执行翻译的服务组件Bean名称
*/
String service() default "defaultTranslateServiceImpl";
/**
* 映射字段 </br>
* 默认取当前字段的值 如果设置了 {@link Translate#mapper()} 则取映射字段的值
*/
String mapper() default "";
/**
* key
*/
String key() default "";
/**
* key扩展
*/
String keyExtend() default "";
/**
* 参数
*/
String param() default "";
/**
* 参数扩展
*/
String paramExtend() default "";
}
TranslateResult
在返回方式使用该注解,配合
Translate
可以实现自动翻译
/**
* 系统配置管理组件
* @author dingwen
* @since 2024/2/1 13:19
*/
@Component
public class ConfigManagerImpl implements IConfigManager {
@Resource
private IConfigService configService;
@Resource
private Converter converter;
@Override
@TranslateResult
public List<ConfigVO> getAllConfigs() {
return converter.convert(configService.list(),ConfigVO.class);
}
}
基于数据库悲观锁实现分布式场景下的定时任务不漏跑,不重复执行问题
<!--Quartz定时任务场景-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>quartz-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableQuartz
注解以开启quartz
定时任务场景功能
@EnableQuartz
AbstractJob
实现自定义任务/**
* TestJob </br>
* <p> Quartz禁止并发执行 DisallowConcurrentExecution</p>
* @author dingwen
* @date 2022/5/11
*/
@Slf4j
@DisallowConcurrentExecution
public class TestJob extends AbstractJob {
/**
* 执行
*/
@Override
protected void exactExecution(){
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("TestJob execute ...");
}
}
<!--通用系统配置场景-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>config-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableConfig
注解以开启系统通用配置场景功能
@SpringBootApplication(scanBasePackages = "com.dingwen")
ConfigHelper
Redis + CaffeineCache 专注提升效率
package com.dingwen.treasure.config.utils;
import cn.hutool.extra.spring.SpringUtil;
import com.dingwen.treasure.config.entity.Config;
import com.dingwen.treasure.config.service.impl.ConfigServiceImpl;
import java.util.Objects;
import java.util.Optional;
/**
* 系统配置工具
*
* @author dingwen
* @since 2023/8/12 17:35
*/
public class ConfigHelper {
/**
* 获取配置值
*
* @param configKey 配置关键
* @return {@link String}
*/
public static Optional<String> getVal(String configKey) {
Optional<Config> configOp = getConfig(configKey);
return configOp.map(Config::getConfigVal);
}
/**
* 获取配置
*
* @param configKey 配置关键
* @return {@link Config}
*/
public static Optional<Config> getConfig(String configKey) {
ConfigServiceImpl configService = SpringUtil.getBean(ConfigServiceImpl.class);
if (Objects.isNull(configService)) {
return Optional.empty();
}
return Optional.ofNullable(configService.getOneByConfigKey(configKey));
}
}
基于阿里开源
EasyExcel
进行二次封装,开箱即用
translate-spring-boot-starter
ExcelExpProperty
ExcelAutoMergeHandler
一套包含前后端的一条龙的通用的文件场景,业务数据基于
MybatisPlus
存储,文件数据可存储与系统本地或任何一直OSS存储
适用场景: 一张表中有1000w的数据,但是可能其中有800w是历史数据(冷数据),我们可能在业务上已经不再使用这些数据,如果放在业务表中,可能会影响我们业务的效率,所以我们可以将其归档到另一张表中,将其变成冷数据
**流程:**数据归档的流程大概是: 1)从原数据表获取需要归档的数据;2)将这部分数据插入归档的表中;3)将元数据表中这部分数据删除
**注意点:**我们数据归档中,事务的提交应该采用手动事务提交,如果使用大事务的情况下,可能会导致事务超时等一系列的问题!还有,我们需要实现可控归档,需要达到我们可以手动控制是否归档、停止,并且还能动态配置归档范围
<!--数据归档场景-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>db-backup-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableDbBackup
注解以开启场景功能
@EnableDbBackup
dingwen:
treasure:
db:
backup:
maxLoopCount: TODO // 备份最大循环次数
backUpDataRules: TODO // 备份规则: 开始归档的id;结束归档的id;一次查询的条数
IBackupDataService
接口/**
* 测试用户表备份
* @author dingwen
* @since 2024/1/3 14:42
*/
@Service
@Slf4j
public class TreUserBackupData extends AbstractBackupData<Object, BackUpDataRule> {
@Resource
private DbBackupProperties dbBackupProperties;
@Override
public BackupDataScene getScene() {
return BackupDataScene.TRE_USER_FORWARD;
}
/**
* back up data rule
*/
private BackUpDataRule backUpDataRule;
@Override
public Boolean needStop() {
return backUpDataRule.getStopFlag();
}
@Override
public BackUpDataRule getRule() {
Map<String, BackUpDataRule> stringBackUpDataRuleMap = Optional.ofNullable(dbBackupProperties).map(DbBackupProperties::getBackUpDataRules).orElse(null);
if(CollUtil.isEmpty(stringBackUpDataRuleMap)){
return null;
}
backUpDataRule = stringBackUpDataRuleMap.get(BackupDataScene.TRE_USER_FORWARD);
return backUpDataRule;
}
@Override
public BackUpDataRule changeOffSet(BackUpDataRule backupDataRule) {
backupDataRule.setBeginId(backupDataRule.getEndId());
Long endId = backupDataRule.getBeginId() + backupDataRule.getQuerySize();
backupDataRule.setEndId(endId);
return backupDataRule;
}
@Override
public List<Object> queryData(BackUpDataRule backUpDataRule) {
log.info("[数据归档模块]\t[用户数据查询]");
return Collections.emptyList();
}
@Override
public void insertData(List<Object> datas) {
log.info("[数据归档模块]\t[用户数据插入]");
}
@Override
public void deleteData(List<Object> datas) {
log.info("[数据归档模块]\t[用户数据删除]");
}
}
BackupDataFactory.BACKUP_DATA_SERVICE.get(TODO).exeBackUpData();
针对字段更新的变更日志的通用实现
<!--变更日志场景-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>change-log-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableChangeLog
注解以开启场景功能
@EnableChangeLog
配置数据表信息获取数据库名称以及忽略的字段
dingwen:
treasure:
change:
log:
dataBase: TODO //数据库
ignores: TODO // 忽略的字段
在需要使用的地方组装事件对象进行发布即可
@Test
public void testChangelog(){
DictData newData = new DictData();
newData.setDictType("c_config_type");
newData.setStatus(DicStatus.ENABLED.getCode());
DictData oldData = new DictData();
oldData.setDictType("d_field_type");
oldData.setStatus(DicStatus.DISABLED.getCode());
ChangeLogMetricEvent event = new ChangeLogMetricEvent("testChangelog");
event.setChangeLogType(ChangeLogType.UPDATE);
event.setDataInfo("用户基础信息");
event.setDataIdName("dict_data_id");
event.setDataId(newData.getDictDataId());
event.setDataIdDescription("主键-字典值id");
event.setNewData(newData);
event.setOldData(oldData);
Map<String, Function<Object, Object>> fieldConverts = new HashMap<>(2);
fieldConverts.put("dictType", dictService::convert);
fieldConverts.put("status", o -> IBaseEnum.fastDescFrom(DicStatus.class,((IBaseEnum)o).getCode()));
event.setFieldConverts(fieldConverts);
List<TableFieldInfo> extFields = new ArrayList<>(2);
extFields.add(TableFieldInfo.builder().columnName("dictModule").columnComment("字典所属模块").build());
event.setExtFields(extFields);
SpringUtil.publishEvent(event);
}
常规的
Spring
事件订阅机制实现存在不能重发,可靠性,幂等性等问题.故对事件机制进行拓展解决上述问题.
业务方只需要继承
BaseEvent
事件及BaseListener
再通过SpringUtils
发布事件就可以实现该功能
采用持久化的方式保证重试可靠功能,当
Spring
环境中没有找到对应的实现时则会按照默认的方式进行.初次之外也提供了基于
Redis
分布式锁方式的幂等不重复执行可靠性实现.
/**
* 抽象事件监听器 </br>
* <p>
* TODO 事务控制
* TODO 分布式锁实现
* </p>
* @author dingwen
* @since 2024/3/27 11:12
*/
@Slf4j
public abstract class BaseListener<T extends BaseEvent> implements ApplicationListener<T> {
/**
* 处理业务方法
*
* @param baseEvent base event
*/
abstract protected void handler(T baseEvent);
/**
* 当应用程序发生事件时调用此方法 </br>
* <p>
* 此方法并不能保证幂等,也不能保证多线程条件下不重复执行.
* 若有此需求请考虑使用分布式锁控制的实现
* </p>
*
* @param event event
*/
@Override
public void onApplicationEvent(T event) {
log.info("[base]抽象事件监听器,开始执行,事件对象:{}", JSONUtil.toJsonStr(event));
Map<String, IEvent> eventServices = SpringUtils.getBeansOfType(IEvent.class);
if(CollUtil.isEmpty(eventServices) || Objects.isNull(event.getEventId())){
log.warn("[base]抽象事件监听器,缺失事件对象id或未找到事件服务,将已普通方式运行,不能保证监听执行成功以及重发功能");
handler(event);
return;
}
eventServices.forEach((eName,eService)->{
if(eService.isNeedExecute(event.getEventId())){
log.info("[base]抽象事件监听器,开始执行,eventId:{}", event.getEventId());
handler(event);
log.info("[base]抽象事件监听器,执行成功,进行状态修复,eventId:{}", event.getEventId());
eService.succeed(event.getEventId());
}
});
}
}
并发安全的实现
/**
* 安全的,能保证幂等的,不重复执行的,并发安全的监听器实现 </br>
* <p>
* 后期可采用动态代理优化
* </p>
*
* @author dingwen
* @since 2024/3/27 15:10
*/
@Slf4j
public abstract class AbstractSafeBaseListener<T extends BaseEvent> extends AbstractBaseListener<T> {
@Override
public void onApplicationEvent(T event) {
log.info("[base]并发安全的抽象事件监听器,开始执行,事件对象:{}", JSONUtil.toJsonStr(event));
EventProperties eventProperties = SpringUtils.getBean(EventProperties.class);
Assert.notNull(eventProperties, "事件场景启动器关键配置缺失");
RedisShareLockComponent shareLockComponent = SpringUtils.getBean(RedisShareLockComponent.class);
if (Objects.isNull(shareLockComponent)) {
log.warn("[event] [安全的监听器],关键组件缺失,将使用不具备安全功能的监听器实现");
super.onApplicationEvent(event);
return;
}
String lockKey = EventConstant.LOCK_EVENT_PREFIX.concat(Convert.toStr(event.getEventId()));
String requestId = IdUtils.fastUUID();
Long lockTime = ObjectUtil.defaultIfNull(eventProperties.getLockTime(), 10L);
TimeUnit lockTimeUnit = ObjectUtil.defaultIfNull(eventProperties.getLockTimeUnit(), TimeUnit.SECONDS);
try {
boolean lock = shareLockComponent.lock(lockKey, requestId, lockTime, lockTimeUnit);
if (Boolean.FALSE.equals(lock)) {
log.warn("[event] [安全的监听器],资源抢占,取消执行,lockKey:{},eventId:{}", lockKey, event.getEventId());
return;
}
super.onApplicationEvent(event);
} catch (Exception e) {
log.error("[event] [安全的监听器],执行失败,错误消息:{},lockKey:{},eventId:{}", e.getMessage(), lockKey,
event.getEventId(), e);
} finally {
shareLockComponent.unLock(lockKey, requestId);
}
}
}
<!--事件场景启动器-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>event-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableEvent
注解以开启场景功能
@EnableEvent
若是并发安全的实现则需要配置锁相关参数
dingwen:
treasure:
# 事件
event:
# 时间
lock-time: 10
# 时间单位: seconds
lock-time-unit: seconds
boolean retry(Long eventId)
Page<Event> queryEventPage(QueryWrapper<Event> queryWrapper)
通过灵活的配置,实现Jwt的生成,校验,刷新等.
<!--jwt场景启动器-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>jwt-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableJwt
注解以开启场景功能
@EnableJwt
dingwen:
treasure:
# jwt
jwt:
# 头信息: 默认值: authorization
header: "Authorization"
# 令牌前缀: 默认值 Bearer
token-prefix: "Bearer "
# 令牌密钥: 最少长度 32
secret-key: "38329cc9d1b1496da21700d02ecd0690c348930073f"
# App端过期时间 (单位分钟)
app-expire-time: 5
# Web端过期时间 (单位分钟)
web-expire-time: 20
# App 刷新时间 秒
app-refresh-time: 10
# Web 刷新事件 秒
web-refresh-time: 120
# 令牌签发者
issuer: "treasure"
# 令牌签发主题
subject: "jwt"
Jwt
JwtController
GET
[生成App端JwtToken]: common/jwt/apps
GET
[生成Web端JwtToken]: common/jwt/webs
GET
[判断是否需要刷新]: common/jwt/needs
POST
[验证JwtToken]: common/jwt/verifies
POST
[刷新JwtToken]: common/jwt/refresh
若令牌异常(包括未携带令牌,或者非法的令牌又或是过期的令牌)都会抛出
JwtVerifyException
异常,并返回401
错误
security-plus-spring-boot-starter
Spring Security
的二次封装
<!--安全场景启动器-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>security-plus-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableSecurityPlus
注解以开启场景功能
@EnableSecurityPlus
dingwen:
treasure:
# 安全
security:
plus:
# 是否开启安全管控
enable-security-plus: true
# 是否开启jwt安全管控
enable-jwt-filter: true
# 是否开启接口基本动态权限控制
enable-dynamic-security: true
matchers:
# 允许进行匿名访问的url
anonymous:
- /
- /common/auths/webs/logins
- /common/auths/captcha
login:
# 退出登录地址
logoutProcessingUrl: common/auths/logout
Jwt
令牌实现ExcludesAuthorityStrategy
IncludesAuthorityStrategy
IncludeAuthorityStrategy
Caffeine
+ Redis
二级缓存TokenService
SecurityPlusGrantedAuthority
, SecurityPlusUtils
AbstractDynamicAttributeService
package com.dingwen.treasure.auth.support.security;
import cn.hutool.core.collection.CollUtil;
import com.dingwen.treasure.auth.manager.IAuthResManager;
import com.dingwen.treasure.auth.model.bo.AuthResBO;
import com.dingwen.treasure.auth.model.po.AuthRes;
import com.dingwen.treasure.auth.service.IAuthResService;
import com.dingwen.treasure.security.plus.enums.AuthorityStrategy;
import com.dingwen.treasure.security.plus.support.dynamic.AbstractDynamicAttributeService;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.stream.Collectors;
/**
* DynamicAttributeServiceImpl : 动态资源权限服务
*
* @author dingwen
* @since 2024/4/22 14:21
*/
@Component
public class DynamicAttributeServiceImpl extends AbstractDynamicAttributeService {
@Resource
private IAuthResManager authResManager;
@Resource
private IAuthResService authResService;
@Override
public Map<String, List<ConfigAttribute>> loadSourceAttributes() {
List<AuthResBO> authResList = authResManager.getEnabledAuthRes();
if (CollUtil.isEmpty(authResList)) {
return Collections.emptyMap();
}
return buildAttributes(authResList);
}
@Override
public AuthorityStrategy getAuthorityStrategy(FilterInvocation filterInvocation) {
HttpServletRequest httpRequest = filterInvocation.getHttpRequest();
String requestURI = httpRequest.getRequestURI();
String method = httpRequest.getMethod();
// 多级缓存处理
AuthRes authRes = authResService.queryOne(requestURI,method);
if (Objects.nonNull(authRes)) {
return authRes.getAuthorityStrategy();
}
return AuthorityStrategy.INCLUDES;
}
/**
* 构建权限map
*
* @param authResList 资源权限业务对象
* * @return 权限信息
*/
private Map<String, List<ConfigAttribute>> buildAttributes(List<AuthResBO> authResList) {
Map<String, List<ConfigAttribute>> result = new HashMap<>(authResList.size());
for (AuthResBO ar : authResList) {
List<ConfigAttribute> configAttributes = ar
.getAttributes()
.stream()
.map(SecurityConfig::new)
.collect(Collectors.toList());
result.put(authResService.buildSourceKey(ar.getRequestUri(), ar.getRequestMethod()), configAttributes);
}
return result;
}
}
AuthController
POST
[刷新令牌]: common/auths/refresh
auth-spring-boot-starter
基于安全场景启动器完成认证(但不局限于此方式),兼容
SaToken
等
<!--认证场景启动器-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>auth-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableAuth
注解以开启场景功能
@EnableAuth
dingwen:
treasure:
# 认证
auth:
# 登录密码相关配置
password-properties:
# 是否开启密码加密传输 (sm2)
enabled: true
# 私钥
privateKey: "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQga+98CaPB0t83RhgbzPSxNCbwhluKaOcWSWXMJ9mHi7KgCgYIKoEcz1UBgi2hRANCAAQYqj8QyJqBOTHfb0orFU7I4wlg/FGzLEdTjMvz1UjDosEZ/8RHv0VQHsulvaQFkmoUnq1rsaLpW0vgzsCdmza+"
# 公钥
publicKey: "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEGKo/EMiagTkx329KKxVOyOMJYPxRsyxHU4zL89VIw6LBGf/ER79FUB7Lpb2kBZJqFJ6ta7Gi6VtL4M7AnZs2vg=="
# 验证码相关配置
captcha-properties:
# 验证码开关
enabled: false
# 验证干扰类型
captchaDisturbType: LINE_CAPTCHA
# 默认验证码宽度 200
width: 200
# 默认验证码高度 100
height: 100
# 默认验证码字符个数 5
codeCount: 5
# 默认验证码干扰数 15
disturbCount: 15
# 验证码有效期: 默认两分钟
time: 2
# 验证码有效期: 默认分钟
unit: MINUTES
# 登录配置
login-properties:
# 是否开启错误次数限制
enableErrLimit: true
# 最大重试次数: 默认值 5
maxRetries: 5
# 账户锁定时间
lockTime: 5
# 账户锁定单位
lockTimeUnit: MINUTES
# 登录错误锁定前缀
loginErrorPrefix: "account:login:err:{}"
ICaptchaManager
DynamicAttributeServiceImpl
支持
Spring Security
动态权限
ILoginStrategy
AuthController
GET
[获取验证码]: common/auths/captcha
[默认1分钟只能调用10次]POST
[Web端登录]: common/auths/webs/logins
基于本地缓存场景启动器和Redis场景启动器,Caffeine + Redis + Spring Cache 实现的二级缓存
<!--二级缓存场景启动器-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>dcache-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在启动类上添加
@EnableDCache
注解以开启场景功能
@EnableDCache
dingwen:
treasure:
# 二级缓存
dcache
JVM
进程刷新com.dingwen.treasure.dcache.core.DoubleCache
: 二级缓存com.dingwen.treasure.dcache.core.DoubleCacheManager
: 缓存管理器com.dingwen.treasure.dcache.core.RefreshCacheListener
: 字典缓存监听通过redis订阅发布实现缓存刷新 @Cacheable(cacheNames = "authResCache",key = "#requestUri+':'+#requestMethod",cacheManager = "doubleCacheManager")
@Override
public AuthRes queryOne(String requestUri, String requestMethod) {
return queryOne(AuthRes.builder().requestUri(requestUri).requestMethod(requestMethod).build());
}
业务场景模拟,最佳实践
MessageController
MarketingController
JavaScriptController
RateLimiterController
ProgressBarTaskController
AreaUtilController
AsyncController
BatchSaveController
CompletableFuture
案例CompletableFutureController
CorsController
CustomValidateController
DesensitizationController
DeserializerController
DictionaryController
EasyCacheController
EnumConvertController
ExceptionController
LocaleController
MarketingController
MybatisPlusController
OperationLogRecordController
OrderController
OweFeeController
PayController
ReSubmitController
setnx
版分布式锁案例ResponseBodyAdvice
实现统一返回 ResponseBodyAdviceController
SmsController
feign
调用案例TaskFeignController
TaskGenerateController
EnumController
SensitiveController
WechatPubController
API入口:
MessageController
参考Spring StopWatch 的拓展优化,精确计算执行耗时,执行次数,方便进行优化
OptimizeUtilController
/**
* OptimizeUtilController: 优化工具测试
* @author dingwen
* @since 2022/8/28
*/
@Api(tags = "优化工具API")
@RestController
@Slf4j
@RequestMapping("optimize")
@RequiredArgsConstructor
public class OptimizeUtilController {
@ApiOperation(value = "API使用测试")
@GetMapping
public void test() {
optimizeApi();
}
@SneakyThrows(Throwable.class)
private void optimizeApi() {
OptimizeUtil.start("任务1");
TimeUnit.SECONDS.sleep(1);
OptimizeUtil.stop("任务1");
for (int i = 0; i < 100; i++) {
OptimizeUtil.start("任务2");
for (int j = 0; j < 1000; j++) {
OptimizeUtil.start("任务2-1");
OptimizeUtil.stop("任务2-1");
}
OptimizeUtil.stop("任务2");
}
OptimizeUtil.print("任务2");
OptimizeUtil.print();
}
}
Redis + JVM 双缓存
// 测试js
function add(op1, op2) {
return op1 + op2
}
add(a, b)
通过Java调用JavaScript进行规则校验,实现复杂且灵活可配置的规则校验功能
JavaScriptController
名称 | 类型 | 长度 | 备注 |
rule_id | bigint | 主键自增雪花id | |
rule_name | varchar | 100 | 校验规则名称 |
rule_description | varchar | 200 | 规则描述 |
rule_state | smallint | 规则状态:0禁用 1启用 | |
rule_content | varchar | 255 | 规则内容(JavaScript) |
field_name | varchar | 255 | 校验字段名称(所需要多个字段逗号分隔) |
rule_code | varchar | 100 | 规则Code(保留字段) |
rule_type | smallint | 规则类型:0必填 1长度 2必填+长度 3敏感词 4正则 | |
business_id | bigint | 业务Id | |
create_time | datetime | 创建时间(由MybatisPlus自动填充) | |
update_time | datetime | 修改时间(由MybatisPlus自动填充) | |
deleted | smallint | 逻辑删除标识,1:存在,2:已删除 | |
version | smallint | 版本号(乐观锁) | |
create_by | varchar | 100 | 创建者(也可基于Security、MybatisPlus实现自动填充) |
update_by | varchar | 100 | 更新者(也可基于Security、MybatisPlus实现自动填充) |
remark | varchar | 255 | 备注(保留字段) |
基于
setnx
,通过自定义注解+脚本实现限流(参考若依实现)
/**
* redis限流实现API
*
* @author dingwen
* @since 2022/11/17
*/
@Api(tags = "redis限流实现API")
@RestController
@RequestMapping("redis")
public class RateLimiterController {
@ApiOperation("redis限流测试")
@RateLimiter(time = 1, count = 2)
@GetMapping("/rate")
public Result<String> rateLimiterSimpleTest() {
return ResultGenerator.genSuccessResult();
}
}
基于
CompletableFuture
和redis
的多线程任务进度条实现,后端异步进行任务,前端轮询调用进度条进度查询
redis
>
setnx
检查任务key是否存在,任务是否在进行中
hash
存储任务进度信息 >
increment
CompletableFuture
>
whenComplete
子任务完成时更新任务进度
exceptionally
发生异常时更新任务进度AbstractProgressBarTask
抽象任务进度条组件,囊括进度条功能,子类继承实现业务逻辑即可
/**
* 进度条任务API
*
* @author dingwen
* @since 2022/12/07
*/
@Api(tags = "进度条任务API")
@RestController
@RequestMapping("bar")
public class ProgressBarTaskController {
@Resource(name = "testProgressBarTask")
private TestProgressBarTask testProgressBarTask;
@ApiOperation(value = "提交进度条任务")
@ApiImplicitParams({
@ApiImplicitParam(name = "taskId", value = "任务id"),
@ApiImplicitParam(name = "taskType", value = "任务类型")
})
@PutMapping()
public Result<TaskVo> submit(@RequestParam("taskId") String taskId, @RequestParam("taskType") String taskType) {
TaskType taskTypeByCode = EnumUtil.getEnumByCode(TaskType.class, taskType);
return ResultGenerator.genSuccessResult(
testProgressBarTask.execute(
taskId,
taskTypeByCode,
100,
100
)
);
}
@ApiOperation(value = "查询任务进度")
@ApiImplicitParams({
@ApiImplicitParam(name = "taskId", value = "任务id"),
@ApiImplicitParam(name = "taskType", value = "任务类型")
})
@GetMapping("/{taskId}")
public Result<TaskVo> queryProgress(@PathVariable("taskId") String taskId,
@RequestParam("taskType") String taskType) {
TaskType taskTypeByCode = EnumUtil.getEnumByCode(TaskType.class, taskType);
return ResultGenerator.genSuccessResult(testProgressBarTask.queryProcess(taskId, taskTypeByCode));
}
}
SensitiveController
SensitiveEnum
SensitiveEntity
SmsController
WechatPubController
开放平台对接(基于Spring提供定时任务实现):
- 天行数据
- 百度地图
- 微信公众号平台
时序数据库案例
- 官网
- 接口
IotDataService
以上的流程没有问题,当数据变更的时候,如何能保证将缓存同步到最新呢?
假设数据库更新成功,缓存更新失败,在缓存过期失效之前,读取到的缓存数据都是旧的
假设缓存更新成功,数据库更新失败,那读取到的数据都是错误的
假设删除缓成功,此时A线程正在更新数据库,同时B线程也来了查询数据,发现缓存中没有,就查询数据库。此科查询到的数据任然是旧数据。
若此时做延迟删除缓存,根据业务时间灵活调整,确保修改数据线程已提交,延迟删除之后再查询就能保证数据是正确的了
若删除缓存失败,可加入消息队列,做删除重试
cacal
方案开发独立的服务,监控数据库的改变,同步对于的缓存
- CPU 密集型的程序 - 核心数 + 1
- I/O 密集型的程序 - 核心数 * 2
CPU利用率: 如果指令需要不断的执行,则CPU的利用率为100%,此时CPU将不能做除执行次指令之外的任何事情
线程上下文切换的代价: 现代CPU基本都是多核心的,可以同时做核心数件事情互不打扰.如果要执行的线程大于核心数,那么就需要通过操作系统的调度了。操作系统给每个线程分配CPU时间片资源,然后不停的切换,从而实现“并行”执行的效果.每次切换会伴随着寄存器数据更新,内存页表更新等操作,就会导致CPU资源过多的浪费在上下文切换上,而不是在执行程序,得不偿失.
高效利用:多程序在运行时都会有一些 I/O操作,可能是读写文件,网络收发报文等,这些 I/O 操作在进行时时需要等待反馈的。比如网络读写时,需要等待报文发送或者接收到,在这个等待过程中,线程是等待状态,CPU没有工作。此时操作系统就会调度CPU去执行其他线程的指令,这样就完美利用了CPU这段空闲期,提高了CPU的利用率。
- 一个极端的线程(不停执行“计算”型操作时),就可以把单个核心的利用率跑满,多核心CPU最多只能同时执行等于核心数的“极端”线程数
- 如果每个线程都这么“极端”,且同时执行的线程数超过核心数,会导致不必要的切换,造成负载过高,只会让执行更慢
- I/O 等暂停类操作时,CPU处于空闲状态,操作系统调度CPU执行其他线程,可以提高CPU利用率,同时执行更多的线程
- I/O 事件的频率频率越高,或者等待/暂停时间越长,CPU的空闲时间也就更长,利用率越低,操作系统可以调度CPU执行更多的线程
引用自《Java 并发编程实战》
如果我期望目标利用率为90%(多核90),那么需要的线程数为:
核心数12 * 利用率0.9 * (1 + 50(sleep时间)/50(循环50_000_000耗时)) ≈ 22
通过线程数来计算CPU利用率
线程数22 / (核心数12 * (1 + 50(sleep时间)/50(循环50_000_000耗时))) ≈ 0.9
虽然公式很好,但在真实的程序中,一般很难获得准确的等待时间和计算时间,因为程序很复杂,不只是“计算”。一段代码中会有很多的内存读写,计算,I/O 等复合操作,精确的获取这两个指标很难,所以光靠公式计算线程数过于理想化。
没有固定答案,先设定预期,比如我期望的CPU利用率在多少,负载在多少,GC频率多少之类的指标后,再通过测试不断的调整到一个合理的线程数
比如一个普通的,SpringBoot 为基础的业务系统,默认Tomcat容器+HikariCP连接池+G1回收器,如果此时项目中也需要一个业务场景的多线程(或者线程池)来异步/并行执行业务流程。
此时我按照上面的公式来规划线程数的话,误差一定会很大。因为此时这台主机上,已经有很多运行中的线程了,Tomcat有自己的线程池,HikariCP也有自己的后台线程,JVM也有一些编译的线程,连G1都有自己的后台线程。这些线程也是运行在当前进程、当前主机上的,也会占用CPU的资源。
- 分析当前主机上,有没有其他进程干扰
- 分析当前JVM进程上,有没有其他运行中或可能运行的线程
- 设定目标
- 目标CPU利用率 - 我最高能容忍我的CPU飙到多少?
- 目标GC频率/暂停时间 - 多线程执行后,GC频率会增高,最大能容忍到什么频率,每次暂停时间多少?
- 执行效率 - 比如批处理时,我单位时间内要开多少线程才能及时处理完毕
- ……
- 梳理链路关键点,是否有卡脖子的点,因为如果线程数过多,链路上某些节点资源有限可能会导致大量的线程在等待资源(比如三方接口限流,连接池数量有限,中间件压力过大无法支撑等)
- 不断的增加/减少线程数来测试,按最高的要求去测试,最终获得一个“满足要求”的线程数
注意:不同场景下的线程数理念也有所不同
- Tomcat中的maxThreads,在Blocking I/O和No-Blocking I/O下就不一样
- Dubbo 默认还是单连接呢,也有I/O线程(池)和业务线程(池)的区分,I/O线程一般不是瓶颈,所以不必太多,但业务线程很容易称为瓶颈
- Redis 6.0以后也是多线程了,不过它只是I/O 多线程,“业务”处理还是单线程
很多的内部业务系统,并不需要啥性能,稳定好用符合需求就可以了。推荐的线程数是:CPU核心数
Runtime.getRuntime().availableProcessors()//获取逻辑核心数,如6核心12线程,那么返回的是12
总核数 = 物理CPU个数 X 每颗物理CPU的核数
总逻辑CPU数 = 物理CPU个数 X 每颗物理CPU的核数 X 超线程数
查看物理CPU个数
cat /proc/cpuinfo | grep "physical id"|sort|uniq|wc -l
查看每个物理CPU中core的个数(即核数)
cat /proc/cpuinfo | grep "cpu cores" | uniq
查看逻辑CPU的个数
cat /proc/cpuinfo | grep "processor" | wc -l
OCP
开闭原则
- 对扩展开发,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
- 对修改关闭,意味着类一旦设计完成,就可以独立的工作,而不要对其进行任何的修改。
DIP
依赖倒置原则面向抽象编程,面向接口编程,不要面向具体编程,让上层不再依赖下层,下面改动了,上面的代码不会受到牵连。这样可以大大降低程序的耦合度,耦合度低了,扩展力就强了,同时代码复用性也会增强。
SRP
单一职责原则一个类只应该负责一项职责
ISP
接口隔离原则不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口要好。做接口拆分时,也要尽量满足单一职责原则。将外部依赖减到最少,降低模块间的耦合.
LOD
迪米特原则也被称为最少知识原则,它提出一个模块对其他模块应该知之甚少,或者说模块之间应该彼此保持陌生,甚至意识不到对方的存在,以此最小化、简单化模块间的通信,并达到松耦合的目的。
CRP
合成复用原则优先使用合成/聚合,而不是类继承。
LSP
里式替换原则程序中的对象可以在不改变程序正确性的前提下被它的子类所替换,即子类可以替换任何基类能够出现的地方,并且经过替换后,代码还能正确工作。
SELECT *
FROM sys_user
WHERE id > #{offset} LIMIT 0,#{pageSize};
/**
* 优化Api
* @author dingwen
* @since 2022/8/5
*/
@Api(tags = "优化API")
@RestController
@Slf4j
@RequestMapping("optimize")
@Validated
public class OptimizeController {
@Resource
private SysUserService userService;
/**
* 适用于自增id
* 数据总条数:4301000 </br>
* <p>自带查询分页性能:</p>
* <ul>
* <li>第 1 页 10 条关闭count查询优化耗时:4秒</li>
* <li>第 100,00 页 100 条关闭count查询优化耗时:5秒</li>
* <li>第 100,000 页 100 条关闭count查询优化耗时:5秒</li>
*
* <li>第 1 页 10 条打开count查询优化耗时:4秒</li>
* <li>第 100,00 页 100 条打开count查询优化耗时:4秒</li>
* <li>第 100,000 页 100 条打开count查询优化耗时:4秒</li>
* </ul>
*
* <p>优化后性能:</p>
* <ul>
* <li>第 1 页 10 条耗时:30~80毫秒</li>
* <li>第 100,00 页 100 条耗时:30~80毫秒</li>
* <li>第 100,000 页 100 条耗时:30~80秒</li>
* </ul>
*
* 再进行统计总记录条数时会遇到瓶颈,正确的处理方式应该是杜绝深分页,不会有用户往下翻到地100页
* 一般选择50页即可
*
* @param pageDto 页面dto
*/
@PostMapping("/deep-page")
@ApiOperation("深分页查询优化")
public Result<PageData<SysUser>> deepPage(@RequestBody PageDto pageDto) {
Long current = pageDto.getCurrent();
Long pageSize = pageDto.getPageSize();
Page<SysUser> page = new Page<>(current, pageSize);
// 可以选择关闭count查询优化,解决一对多时分页查询总计记录条数不正确问题
page.setOptimizeCountSql(Boolean.TRUE);
//return ResultUtil.genResult(userService.page(page));
return ResultGenerator.genSuccessResult(userService.optimizeSelectPage(current, pageSize));
}
}
企业级的数据中台ETL处理服务,提供数据的定时抽取、转换、加载整体解决方案
官网:http://community.pentaho.com/projects/data-integration Carte接口文档:https://help.hitachivantara.com/Documentation
spoon
window客户端设计器。windows平台可以直接使用官方程序,linux or mac os 平台建议使用网页版本的spoon
pan:执行转换(命令行方式+操作系统定时任务)
kitchen: (命令行方式+操作系统定时任务)
carte: kettle服务,rest接口提供服务(支持主从)
spoon
配置连接后可自动创建表结构,共计46张表Java集成定时远程调用和远程服务方式调用以及使用
spoon
客户端进行远程调用需要启动carte
。 carte.sh指定配置文件启动,单机考虑一主一从。默认用户密码(cluster),可在启动文件中配置
远程调用执行
- id: master ip
- port: master port
- dataRepositoryName: 数据库资源库名称
- user: 集群名称(master中配置)
- password: 集群密码(master中配置)
- transName: 转换文件名称
- jobName: 作业文件名称
- path: 路径
# 转换执行
https://{ip}:{port}/kettle/executeTrans/?rep={dataRepositoryName}&user={userName}&pass={password}&trans={path/transName.ktr}
# 作业执行
https://{ip}:{port}/kettle/executeJob/?rep={dataRepositoryName}&user={userName}&pass={password}&job={path/jobName.kjb}
# 查看状态监控
https://{ip}:{port}/kettle/status
依赖于
carte
服务,可以采用spoon
客户端触发远程作业,进行定时调用。(无需代码,配置即可实现)
windows平台可以直接使用官方程序,linux or mac os 平台建议使用网页版本的spoon
docker search hiromuhota/webspoon
docker run -d -p 8080:8080 --name spoon hiromuhota/webspoon
# 手动添加如下依赖
mvn install:install-file -DgroupId=组织名称 -DartifactId=坐标 -Dversion=9.3.0.0-428 -Dpackaging=jar -Dfile= jar包名称
基于
quartz
的定时任务实现,API灵活控制,精确日志记录,分布式部署完整的解决方案。注意:当次解决方案在分布式应用场景中时,确保任务不重复执行依赖与quartz
持久化到数据库依赖数据库悲观锁实现。
- 分布式部署保障不重复执行不漏跑
- 模版方法:代码可重用性
- 实时接口调用控制任务执行、停止
- 接口调用修改任务信息
- 执行日志记录
QRTZ_BLOB_TRIGGERS
QRTZ_CALENDARS
QRTZ_CRON_TRIGGERS
QRTZ_FIRED_TRIGGERS
QRTZ_JOB_DETAILS
QRTZ_LOCKS
QRTZ_PAUSED_TRIGGER_GRPS
QRTZ_SCHEDULER_STATE
QRTZ_SIMPLE_TRIGGERS
QRTZ_SIMPROP_TRIGGERS
QRTZ_TRIGGERS
quartz_info
名称 | 类型 | 长度 | 备注 |
---|---|---|---|
id | bigint | 数据库自雪花id | |
code | varchar | 255 | 定时任务code标识 |
create_time | datetime | 创建时间 | |
cron_expression | varchar | 255 | cron表达式 |
fail | int | 失败次数 | |
full_class_name | varchar | 255 | 定时任务执行类 全类名,Job类 |
job_data_map | varchar | 255 | jobDataMap json格式 |
job_group_name | varchar | 255 | job组名称 |
job_name | varchar | 255 | job 名称 |
name | varchar | 255 | 定时任务名称 |
state | int | 是否启用 1-启用 0-禁用 | |
success | int | 成功执行次数 | |
trigger_group_name | varchar | 255 | 触发器组名称 |
trigger_name | varchar | 255 | 触发器名称 |
update_time | datetime | 更新时间 |
quartz_log
名称 | 类型 | 长度 | 备注 |
---|---|---|---|
id | bigint | 数据库自雪花id | |
quartz_id | bigint | 任务id关联 | |
activate_time | datetime | 激活时间 | |
consumer_time | int | 任务耗时 | |
execute_result | int | 执行结果: 1: 成功 0: 失败 |
|
remark | varchar | 255 | 备注 |
常用后台管理实现
公共模块
- base: 基础、通用
- config: 配置
- core:核心通用组件
- jpa:jpa场景
- mybatisplus mybatisplus场景
- web: web场景
- knife4j: API文档
- rabbitmq: RabbitMQ 应用场景
- redis: Redis 应用场景
基于
SpringBoot Admin
整合Spring Security
的监控实现,目前暴露所有端点,权限账户信息通过nacos
配置指定
spring:
security:
user:
name: actuator
password: actuator
TODO
- 自定义info、metrics、health、endpoint
- 邮件、钉钉预警
基于
xxl-job v2.4.0
封装的调度中心
xxl:
job:
accessToken: xxl-job-access-token
i18n: zh_CN
triggerpool:
fast:
max: 200
slow:
max: 100
logretentiondays: 30
treasure-poi-tl
poi-tl 官网: https://github.com/Sayi/poi-tl
word
模板渲染解决方案,拒绝手动维护xml
文件
treasure-dingtalk-ger
钉鸽官网: https://github.com/AnswerAIL/dingtalk-spring-boot-starter
DingerUtils
可在全局异常处理处调用
DingerUtils.send(e);
/**
* 钉鸽
*
* @author dingwen
* @since 2023/9/22 14:47
*/
public class DingerUtils {
public static void send(Exception e) {
DingerSender dingerSender = SpringUtil.getBean(DingerSender.class);
if (Objects.isNull(dingerSender)) {
return;
}
String msg = "用户:{},userId:{},请求地址:{},入参{},请求体参数:{},发生异常,消息:{},堆栈信息:{}";
String url = ServletUtils.getUrl();
String parameters = ServletUtils.getParameters();
String body = ServletUtil.getBody(ServletUtils.getRequest());
Long userId = SecurityUtils.getUserId();
String username = SecurityUtils.getUsername();
dingerSender.send(
MessageSubType.TEXT,
DingerRequest.request(StrUtil.format(msg, username,userId,url,parameters,body,e.getMessage(), printStackTraces(e)))
);
}
/**
* 堆栈信息
*
* @param e 异常
* @return 异常信息
*/
public static String printStackTraces(Exception e) {
StackTraceElement[] stackTraces = e.getStackTrace();
StringBuilder builder = new StringBuilder();
builder.append(e.getClass().getName())
.append(": ")
.append(e.getLocalizedMessage())
.append("\n");
for (StackTraceElement stackTrace : stackTraces) {
String lineMsg = " at ";
lineMsg = lineMsg + stackTrace.getClassName()
+ "(" + stackTrace.getFileName() + ":"
+ stackTrace.getLineNumber() + ")\n";
builder.append(lineMsg);
}
return builder.substring(0,300);
}
}
package com.dingwen.treasure.gtl.listener;
import com.dingwen.treasure.gtl.util.DingerUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Component;
/**
* 应用启动事件监听
*
* @author dingwen
* @since 2023/10/9 18:22
*/
@Component
@Slf4j
public class PreStopListener implements org.springframework.context.ApplicationListener<ContextClosedEvent> {
@Override
public void onApplicationEvent(ContextClosedEvent event) {
log.info("清廉系统后端服务已停止");
// 可依据环境判断是否执行
DingerUtils.send("清廉系统后端服务已停止");
}
}
package com.dingwen.treasure.gtl.listener;
import com.dingwen.treasure.gtl.util.DingerUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.stereotype.Component;
/**
* 应用启动事件监听
*
* @author dingwen
* @since 2023/10/9 18:22
*/
@Component
@Slf4j
public class StartListener implements org.springframework.context.ApplicationListener<ApplicationReadyEvent> {
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
// 可依据环境判断是否执行
DingerUtils.send("清廉系统后端服务已启动");
}
}
treasure-gof
优雅代码业务组件实现(MybatisPlus)
特色业务场景
- 线程池案例
- 批量save
- 自定义MVC反序列化方式
- 缓存业务场景自定义注解:@EasyCache
- 国际化
- 状态模式+简单工厂模式实现:多阶段灵活控制状态流转
设计模式六大原则:
状态模式: 对象不同的状态导致不同的行为
- MybatisPlus:乐观锁、逻辑删除、自动枚举转换、多数据源、Model、自动填充、分页等其他通用API
- 观察者模式:(事件对象、监听)下订单走库存以及日志,以事件驱动
- 支付案例:简单工厂 + 模版方法 + 策略模式 + 函数式接口
责任链模式:为请求创建了一个接收者对象的处理链,,对请求的发送者和接收者进行解耦
- 基于redis、redisson的分布式锁
- 防止重复提交自定义注解:@ReSubmit
- (解决数据库和缓存不一致的问题)系统配置:db、redis、自定义缓存(利用redis发布订阅实现三级缓存)
- 基于Mybatis的通用crudController封装已经整合API文档
- 全局统一返回
- 全局异常处理
- 优雅DTO、VO、BO、PO转换
- feign调用(整合hystrix实现服务降级)
- 系统配置实现(三级分布式缓存)redis 发布订阅实现跨内存刷新
- AOP 切面实现方法调用前后入参、返回值、耗时等调试日志(全局所有controller)、基于配置灵活开启关闭
- 消息队列业务模拟(保证顺序消费、保证不重复消费、消息可靠投递、消息可靠消费)
- 操作日志(AOP+Sprint EL 实现同步ElasticSearch)参考美团2021年技术年报实现、基于配置灵活开启关闭
TODO
- redisson 分布式锁
- 规范转换Bean mapstruct
- 基于JPA的通用crudController封装
- 通用crudController封装 mongoDB
网关,基于
nacos
的可配置白名单
TODO
- 统一请求日志
- 认证
- 服务间其他信息
基于
freemarker
模版实现的后端生成PDF或在线预览功能
后台管理模块 特色业务场景
- 消息队列业务模拟(顺序消费、不重复消费、可靠消费) 死信队列、延时队列
认证模块 TODO
- 权限
- RBAC
- 参考若依实现
- 菜单、路由
TODO
- 文件上传minio
API文档
test
123
监控面板
actuator
actuator
国际化
监控
定时任务
API文档
Aop
通用业务组件
Spring 高级
监控自定义 预警(钉钉、邮件)
Mybatis Plus
@MppMultiId
、IMppService
、MppBaseMapper
分布式锁
分布式事务
Sentinel
链路追踪
定时任务 Quartz
Rabbitmq
全局异常处理
权限
参考若依实现
SSO
系统配置
通用日志
参考美团技术年报、若依。初步实现思路:Aop及Spring EL 表达式实现日志数据组装,通过RabbitMq将数据同步到ElasticSearch
MongoDB
WebFlux
canal
依赖优化
规范转换Bean mapstruct
全局异常
权限(market、ruoyi)
系统配置
Redis 实现分布式锁
状态模式
策略模式
自定义MVC反序列化进行Java Bean 数据封装
文件存储
短信
spring 批处理 batch
网关统一日志
统一系统配置
redis & JVM 两级缓存,使用 redis 发布订阅实现,支持分布式
JDBC 批处理
DTO、VO、BO 转换
定时任务BUG
@EnableAspectJAutoProxy
在SpringBoot2已经无效,需要通过,
spring.aop.proxy-target-class=false
指定为JDK方式实现,默认值为true,即采用CGLIB实现
TODO
启动初始化
ApplicationRunner
CommandLineRunner
分布式文件存储 minio
调试日志(入参、返回值、耗时)es
后端渲染生产PDF (freemarker)
mybatis 场景整合 (动态标签等常用技巧备忘)
文件预览
excel 通用封装 (基于 hutool 、 poi) 参考若依
sql 优化
JVM
SQL 窗口函数
参数范围校验注解
字典
高德地图
日志配置
枚举
序列化、反序列化
excel
docker 部署
若依数据权限
存储过程没有返回值 procedure call 必须有返回值 function 直接调用
token 刷新
全局拦截器
nacos 刷新
慢sql监控
用户在线统计
站内信息
字典 aop
观察者模式
状态模式
享元模式
单例模式
构建者模式
原型模式
mongodb 索引优化
lambda return
常量定义
feign 调用 localDatetime反序列化问题
工厂方法模式 应用场景
抽象工厂模式 应用场景
JUC 中断三种方式
工作流
validator 分组校验
字典
依赖模块优化
消息可靠性
juc
spring cloud
kkfile
ffmepg
可靠消费 不丢失 重复 实战
缓存双写实战
代码生成
动态数据源 druid 监控
数据权限 租户 若依
xss
author2
mapstruct
HashMap
ConcurrentHashMap
druid 数据源
跨域(Cross Origin Resource Sharing)
UML:Unified Modeling Language
default-
private+
public#
protectednullSafeEquals
枚举优化
redis 队列、map
大文件上传、切片、多线程、断点续传
Spring cache
@Cacheable
InitializingBean
xss
@CacheEvict
@EventListener
微服务 过滤器认证 market
treasure 开放平台
交换平台
webservice
websocket
pig4
nacos内置
数据权限
代码生成
js 规则引擎
webflux
security 方式认证授权
开放平台 4种授权 三方登录
xss
@inner
webservice
websocket
@PositiveOrZero
base controller
redis 限流
通用返回优化
自增主键,分页优化方案
git config --local http.postBuffer 157286400
ER
详细设计
技术文档
@Configuration(proxyBeanMethods = false)
上下文待优化
登录待优化
TokenService待优化
StringJoiner
数据脱敏考虑隐私权限、用户权限
数据权限
开放平台
集成三方登录
短信、天气
本地缓存
如何停止一个线程
rpc
docker 部署
脚本 启动等
influxdb 时序数据库
navicat 模型
java -jar -Dfile.encoding=utf-8 -DTREASURE_NACOS_NAMESPACE=treasure treasure-business.jar
docker build -t treasure-business:v1.0 .
docker run -p 20903:20903 --name treasure-business -d treasure-business:v1.0
开放平台 oauth
第三放登录
高德地图导入行政区划
若依数据权限 【PLus】
验证码登录
枚举
若依 @Anonymous
异常国际化处理
git 指定某次提交合并到指定分支【先切换到目标分支 在执行git cherry-pick 8888189f】
XXXController
功能动词
obtainXXX
获得discardXXX
删除XXXManager
、IXXXManager
、IXXXManagerImpl
【Optional】
功能动词+For使用场景
obtainDeptTree
【获得部门树】XXXService
、IXXXService
、IXXXServiceImpl
【Optional】
功能动词+By条件+For使用场景
queryDeptById
【通过部门id查询部门信息】queryDeptsForMini
【小程序端查询部门列表】queryDeptsForWeb
【Web端查询部门列表】queryDeptPage
【部门列表分页查询】modifyDept
【修改部门信息】createDept
【创建一个部门】createDepts
【创建多个部门】removeDeptById
【通过部门id删除部门】removeDeptByIds
【通过部门ids删除部门】XXXMapper
insertDept
【插入一个部门】insertDepts
【批量插入部门】updateDeptById
【通过部门id修改部门信息】deleteDeptById
【通过部门id删除部门】deleteDeptByIds
【通过部门ids删除部门】selectDeptsByRLikeName
【通过部门名称右模糊查询部门列表】selectDeptPage
【分页查询部门列表】XXXProcessor
处理
XXXHolder
持有
XXXFactory
工厂
XXXProvider
提供者
XXXRegistor
注册
XXXEngine
核心处理逻辑
XXXTask
任务
XXXContext
上下文
XXXHandler
、XXXCallback
、XXXTrigger
、XXXListener
XXXAware
感知
XXXMetric
指标
XXXPool
池
XXXChain
链
XXXFilter
过滤
XXXInterceptor
拦截器
XXXEvaluator
判断条件是否成立
XXXStrategy
策略
XXXAdapter
适配器
XXXEvent
事件
XXXBuilder
构建
XXXTemplate
模版
XXXProxy
代理
XXXConverter
转换
XXXRessolver
解析
XXXParser
解析器
XXXUtils
工具类
XXXHelper
帮助类
XXXConstant
常量
XXXGenerator
生成
<!--maven 资源插件-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>${project.build.sourceEncoding}</encoding>
<delimiters>@</delimiters>
<useDefaultDelimiters>false</useDefaultDelimiters>
</configuration>
<version>${maven-resources-plugin.version}</version>
</plugin>
resource
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<!--注意会将匹配到文件排除在编译以后的结果之外,因此下面会再有一个
filtering 的模块,再通过include的形式,将xlsx文件再如引入进来-->
<excludes>
<exclude>**/*.xlsx</exclude>
<exclude>**/*.xml</exclude>
<exclude>**/*.docx</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<includes>
<include>**/*.xlsx</include>
<include>**/*.xml</include>
<include>**/*.docx</include>
</includes>
</resource>
</resources>
disruptor
的 log4j2
高性能异步日志整体性能有显著提升,适用C端的大量日志场景
<!--高性能内存队列-->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>${disruptor.version}</version>
</dependency>
<!--log4j-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
logback
冲突包 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
整体进行排除
<!--全局排除 logback 使用更高效的log4j实现-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
alibaba
微服务系Redis
实现2024-01-04 09:37:47
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。