<h1 align="center" style="margin: 10px 0px 0px 40px; font-weight: bold;">Treasure</h1>
<p align="center" style="margin: 30px 0 0 50px;">
<a href="https://img.shields.io"><img src="https://img.shields.io/badge/Spring%20Boot-2.7-blue.svg"></a>
<a href="https://img.shields.io"><img src="https://img.shields.io/badge/JDK-8-green.svg"></a>
</p>
<p align="center" style="margin: 44px 0 0 50px;position: relative;top: -46px">
<a href="https://gitee.com/dingwen-gitee/treasure/stargazers"><img src="https://gitee.com/dingwen-gitee/treasure/badge/star.svg?theme=dark"></a>
<a href="https://img.shields.io"><img src="https://img.shields.io/badge/Treasure-V1.0.0-blue"></a>
<a href="https://img.shields.io"><img src="https://img.shields.io/badge/license-MIT-orange"></a>
</p>
<p align="center" style="margin: 10px 0px 0px 24px; font-size: 10px;font-weight: bold; position: relative; top: -60px">2020-05-05~至今</p>
<div align="center" style="margin: -124px 0px -26px 30px; font-weight: bold;">
<img src="./img/dingwen_s_coding_treasure.png">
</div>
<table>
<tr>
<td><img src="img/treasure_概览1.png"></td>
<td><img src="img/treasure_概览2.png"></td>
<td><img src="img/treasure_概览3.png"></td>
<td><img src="img/treasure_概览4.png"></td>
</tr>
</table>
## 初衷
积累,成长
## 分支规划
> 目前分支规划存在一定的不合理性,将在后期进行完善优化.所有的架构都是基于业务进行演化的.
>
> 工具类 > 工具包 > 功能模块 > 场景启动器 > 功能服务 > 分布式
+ `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`分支
> 为了方便随取随用,没有做过多的聚合,各个模块相对独立
~~~
top.dingwen.io
├── scene[综合场景启动器使用示例]
│ └── pom.xml[ Maven依赖]
├── starter[启动器集]
│ └── base-spring-boot-starter[基础场景启动器][已上中央仓库,最新版本:1.2.4]
│ └── api-docs-spring-boot-starter[API文档再封装场景启动器]
│ └── screw-spring-boot-starter[数据库文档生成场景启动器][已上中央仓库,最新版本:1.0.2]
│ └── webplus-spring-boot-starter[Web再封装场景启动器][已上中央仓库,最新版本:1.2.6]
│ └── redis-spring-boot-starter[Redis场景启动器][已上中央仓库,最新版本:1.2.5]
│ └── caffeine-spring-boot-starter[本地缓存启动器][已上中央仓库,最新版本:1.0.5]
│ └── async-spring-boot-starter[异步场景启动器][已上中央仓库,最新版本:1.1.4]
│ └── enums-spring-boot-starter[枚举场景启动器][已上中央仓库,最新版本:1.2.6]
│ └── mybatisplus-spring-boot-starter[MybatisPlus场景启动器][已上中央仓库,最新版本:1.0.3]
│ └── log-spring-boot-starter[日志场景启动器][已上中央仓库,最新版本:1.0.3]
│ └── logv-spring-boot-starter[日志查看启动器][已上中央仓库,最新版本:1.0.2]
│ └── email-spring-boot-starter[电子邮件场景启动器][已上中央仓库,最新版本:1.0.3]
│ └── mongo-spring-boot-starter[MongoDB场景启动器]
│ └── xxl-job-spring-boot-starter[xxljob场景启动器]
│ └── translate-spring-boot-starter[翻译场景启动器][已上中央仓库,最新版本:1.0.0]
│ └── oss-spring-boot-starter[文件存储场景启动器][已上中央仓库,最新版本:1.0.0]
│ └── pipeline-spring-boot-starter[责任链Pipeline场景启动器][已上中央仓库,最新版本:1.0.2]
│ └── bar-spring-boot-starter[进度条场景启动器][已上中央仓库,最新版本:1.0.5]
│ └── dic-spring-boot-starter[字典场景启动器][已上中央仓库,最新版本:1.0.1]
│ └── 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场景启动器][已上中央仓库,最新版本:1.0.2]
│ └── security-plus-spring-boot-starter[Security场景启动器]
│ └── auth-spring-boot-starter[认证场景启动器]
│ └── dcache-spring-boot-starter[二级缓存启动器][已上中央仓库,最新版本:1.0.5]
│ └── kkFile-docker[文件预览场景启动器]
│ └── 规划中[监控场景启动器]
│ └── 规划中[审核场景启动器]
│ └── 规划中[Flowable启动器]
│ └── 规划中[Camunda启动器]
│ └── 规划中[微信公众号开发启动器]
│ └── 规划中[钉钉开发启动器]
│ └── 规划中[重要表单变更日志启动器]
│ └── 规划中[elasticsearch启动器]
│ └── 规划中[Dingger消息告警二次封装]
│ └── 规划中[文本预览,dox动态渲染poi-tl,动态模板管理]
├── 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架构实践
## 架构图
> 参考若依
> 规划中
## 概览
<table>
<tr>
<td><img src="./img/监控概览.png"></td>
<td><img src="./img/监控1.png"></td>
</tr>
<tr>
<td><img src="./img/慢SQL.png"></td>
<td><img src="./img/API文档.png"></td>
</tr>
</table>
### API文档
[在线文档](https://www.apifox.cn/apidoc/shared-2d9191fd-dfd4-45a3-bc29-57500b3a5f1b)
## 技术点
| | | | | | | | | | |
| ----------- | --------------------- | --------------- | ------------------------------ | -------------- | ------------------------ | ------- | ---- | ---- | ---------------- |
| `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 |
## 通用应用场景Starter封装
> 接口文档地址: https://apifox.com/apidoc/shared-8d3d332d-b936-4ec1-8d14-516d89a8f85d
### 基础场景启动器
> 基于`SpringBoot2.1.7`和`JDK1.8`封装的基础场景启动器
#### 核心功能
+ 基础异常
+ 通用工具类
+ `logback`配置优化,异步日志优化`disruptor`
+ 内置`logback`配置 `log4j2-spring.xml`
#### 使用
##### 引入依赖
```xml
<!--基础场景启动器-->
<dependency>
<groupId>top.dingwen.io</groupId>
<artifactId>base-spring-boot-starter</artifactId>
<version>1.2.4</version>
</dependency>
```
#### 核心工具类
+ `AspectUtils`: 切面工具类
+ `BeanCopyUtils`: bean深拷贝工具(基于 cglib 性能优异)
+ `DateUtils`: 日期工具类
+ `LaExUtils`: Lambdas受检异常封装处理
+ `AddressUtils`: 地址工具类
+ `RegionUtils`: IP离线定位
+ `ReflectUtils`: 反射工具
+ `SqlUtils`: sql操作工具类
+ `StringUtils`: 字符串操作工具
+ `StreamUtils`: stream工具类
+ `distinct()` List复杂类型去重+简单类型去重
+ `group2Map(Collection<E> collection, Integer groupSize)` List任务分组
+ `<T, R> List<R> map(Collection<T> collection, Function<T, R> mapper)`: 对象字段收集
+ `BigDecimal safeAdds(List<BigDecimal> data)`: 安全求和
+ ...
+ `IdUtils`: ID工具类
+ `Validatetils`: 编程式灵活校验工具
+ `SpringUtils`: Spring工具类(基于Hutool拓展,获取代理对象)+发布事件持久化拓展
+ `<T> T safeGetBean(String name)`: 安全获取对象
+ `<T> T safeGetBean(Class<T> clazz)`: 安全获取对象
+ ...
+ `OptimizeUtils` : 优化工具类
+ `PinYin4jUtils`: 文字转汉语拼音
+ `MapstructUtils`: 对象映射拷贝工具再封装
+ `AsyncTaskUtils`: 多线程任务编排
+ `EncryptUtils`: 加解密工具类
+ `JaDocUtils`: Java文档获取工具类(兼容JDK1.8)(1.9之后版本反射API能够获取)
+ `JsonSchemaUtils`: Java字节码转JsonSchema (包括类,属性及文档注释)
+ `SupWarsConstant`: 抑制警告常量类
#### 用户上下文支持
+ `ICurrentUserService`: 当前用户接口
+ `CurrentUserHelper`: 快捷访问工具
+ `ScopeContainer`: 范围控制拓展
#### 使用案例
##### `OptimizeUtils`
> 参考Spring StopWatch 的拓展优化,精确计算执行耗时,执行次数,方便进行优化
> `OptimizeUtilController`
```java
/**
* 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();
}
}
```
![优化工具类](./img/OptimizeUtil.png)
##### `SpringUtils`
> `publishEvent(event)`,发布订阅事件拓展,提供基础事件对象`BaseEvent`,同一发布事件入口,进行事件持久化方便进行监控和重发.当然监听实现方需要考虑幂等实现
> 将提供`event-spring-boot-starter`实现`IEvent`接口实现事件对象持久化,重发等功能
```java
/**
* 发布事件,将指定的应用事件发布到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`
> 提供顶层接口,供外部实现.从而实现与权限模块解耦. **注意: 次接口仅仅支持单实现,不支持策略**
<table>
<tr>
<td><img src="./img/ICurrentUserService.png"></td>
</tr>
</table>
### web场景启动器
> `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`
+ 自适应浏览器的国际化方案
+ `CheckerUtils`: Lambda形式对象校验包
+ `MessageUtils`: 国际化消息
+ 默认配置文件 `application-webplus.yml`
+ 全局可配置灵活控制的字符串空格替换处理方案 `globalTrimStringsEnabled` `@Trim`
+ `FileDpzUtils`: 文件下载、简单预览、压缩、压缩下载工具类
+ 热启动 `HeatStartApplication`
#### 使用
##### 引入依赖
```xml
<!--webplus场景-->
<dependency>
<groupId>top.dingwen.io</groupId>
<artifactId>webplus-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>
```
> 配置webplus
```yml
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/"
# 防止XSS攻击
# 过滤开关
xssEnabled: true
# 排除链接(多个用逗号分隔)
xssExcludes: /system/notice
# 匹配链接
xssUrlPatterns: /system/*,/monitor/*,/tool/*
# 全局异常消息发送
globalExMsgHandlerEnabled: true
# 是否开启默认API [默认关闭]
defaultApiEnabled: true
# 是否开启全局接口提交参数去空格,默认关闭
globalTrimStringsEnabled: false
# 热启动端口[默认值 6666]
heatStartBackupPort: 6666
# 优雅停机尝试次数[默认值20]
graceShutdownTryCount: 20
# 优雅停机尝试休眠毫秒数量[默认500]
graceShutdownTrySleep: 500
# 是否允许强制停机,默认关闭
shutdownKillFlag: false
```
#### 概览
<table>
<tr>
<td><img src="img/webplus_debug_api.png"></td>
<td><img src="img/webplus_debug_log.png"></td>
</tr>
<tr>
<td><img src="img/webplus-view-controller.png"></td>
<td><img src="img/webplus_crud_controller.png"></td>
</tr>
<tr>
<td><img src="img/webplus-ex.png"></td>
<td><img src="img/webplus-rege.png"></td>
</tr>
</table>
#### 链路追踪日志
> 基于过滤器+AOP+`InheritableThreadLocal`实现的可配置的链路追踪日志方案,后期可对接ELK加工处理
<table>
<tr>
<td><img src="img/traceId上下文.png"></td>
<td><img src="img/trace过滤器.png"></td>
</tr>
<tr>
<td><img src="img/traceId-aop.png"></td>
<td><img src="img/trace日志查看.png"></td>
</tr>
</table>
#### 全局异常优雅处理
> 区分不同的环境,通过全局异常捕获,统一返回国际化的友好的异常消息.
<table>
<tr>
<td><img src="img/webplus_全局优雅异常1.png"></td>
<td><img src="img/webplus_全局优雅异常2.png"></td>
<td><img src="img/webplus_全局优雅异常3.png"></td>
</tr>
</table>
#### 全局可支配过滤器
+ 全局日志请求耗时`GlobalLogFilter`
+ 构建可重复读取的请求对象`RepeatableFilter`
+ 发布式链路追踪日志`TraceIdFilter`
+ 防止`XSS`攻击`XssFilter`
#### 自适应浏览器的国际化方案
> 支持异常消息和`valid`校验. `I18nConfig`,`ValidateConfig`
```java
@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);
}
```
<table>
<tr>
<td><img src="img/valid_msg_us.png"></td>
<td><img src="img/valid_msg_cn.png"></td>
</tr>
</table>
### 高效的通用枚举处理启动器
> 从前端到服务乃至数据库到枚举解决方案,零代码,开箱即用
> 已加本地缓存,速度杆杆滴
#### 主要功能
+ 统一枚举实现
+ 统一枚举响应
+ 统一枚举MVC转换
+ 统一枚举序列化
+ 统一枚举反序列化
+ 统一枚举接口实现
+ 获取所有枚举时支持分页
+ 统一异常处理
+ 简单枚举`大道至简`
#### 内置接口
+ 获取服务端所有枚举 `/common/enums`
+ 分页查询枚举 `/common/enums/page`
+ 指定枚举类全路径查询执行枚举 `/common/enums/{enumClassName}`
#### 使用
> 枚举持久化依赖各自持久层实现。
> MybatisPlus 3.5.2后版本只需要在实体类中存储数据库值的字段标注
> `@EnumValue`即可
#### 引入依赖
```xml
<!--枚举场景-->
<dependency>
<groupId>top.dingwen.io</groupId>
<artifactId>enums-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>
```
#### 创建自己的枚举
```java
/**
* 逻辑删除
* @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;
}
```
#### 配置项
```yml
dingwen:
treasure:
# enums
enums:
# 是否开启默认API
defaultApiEnabled: true
# 枚举类扫描包
packagepath: "top.dingwen"
# 枚举类所在类路径
classpath: "/**/*.class"
```
#### 注意点
> IBaseEnum中已提供枚举比较方法以及转换方法可直接使用
#### 概览
<table>
<tr>
<td><img src="img/枚举-all.png"></td>
<td><img src="img/枚举-logicDelete.png"></td>
</tr>
</table>
#### 更高效的获取枚举
> 以空间换时间的方式实现.
>
> 枚举场景初始化时会将所有枚举缓存到`map`,使用时再通过`key`获取即可,省去了循环查找的过程.在枚举数量较多的场景下效率较高.
<table>
<tr>
<td><img src="img/枚举优化1.png"></td>
<td><img src="img/枚举优化2.png"></td>
</tr>
</table>
#### 大道至简的枚举
```java
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枚举转换获取描述信息
#### 全局异常处理拓展消息告警 `GlobalExceptionMsgSender`
> 使用方只需要实现`GlobalExceptionMsgSender`接口即可,当发生异常是会自动处理异常消息发送.业务系统可以自定义消息告警的方式.首推飞书的机器人告警.
> 成熟的开源解决方案: https://gitee.com/jaemon/dingtalk-spring-boot-starter.git
#### 自定义文件视图`WebPlusView`
> 继承自`org.springframework.web.servlet.view.AbstractView`实现自定义文件视图
#### `CheckerUtils`
```
Checker<SysUser> checker = Checkers.<SysUser>lambdaCheck()
.notNull(SysUser::getName)
.ne(SysUser::getAge, 0)
.custom(item -> item.getAge() > queryByDb(item.getId()), "年龄异常");
checker.check(sysUser);
```
#### 默认配置文件
> 需要时在使用时激活,具体配置参考`application-webplus.yml`
```yml
spring:
profiles:
active: test,webplus
```
#### 可配置的提交参数去除空格
> `globalTrimStringsEnabled` [是否开启全局接口提交参数去空格,默认关闭 ]
> 同时支持请求参数+路径参数+请求体参数
<table>
<tr>
<td><img src="img/webplus_去除请求参数空格.png"></td>
<td><img src="img/webplus_请求体去除空格.png"></td>
</tr>
</table>
### 异步通用场景启动器
> 实现SpringBoot中线程池动态化配置
#### 核心功能
+ 线程池参数详细动态化配置
+ 实时执行日志可配置打印
+ 通用异步任务组件 `AsyncTaskComponent`
+ 注解式+编程式灵活使用
+ 延迟定时任务线程池支持 `defaultScheduledExecutorService`
#### 使用
##### 引入依赖
```xml
<!--异步场景-->
<dependency>
<groupId>top.dingwen.io</groupId>
<artifactId>async-spring-boot-starter</artifactId>
<version>1.1.4</version>
</dependency>
```
> 配置线程池
```yml
dingwen:
treasure:
# async
async:
# 是否开启默认API
defaultApiEnabled: false
# 是否开启定时线程池
delayScheduledPollEnabled: false
# 定时线程池核心参数
delayScheduledPollCoreSize: 10
# 是否开启线程池实时日志打印
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);
```
### 电子邮件启动器
> 基于`sun.mail`实现的可配置的电子邮件使用场景
#### 使用
##### 引入依赖
```xml
<!--邮件场景启动器-->
<dependency>
<groupId>top.dingwen.io</groupId>
<artifactId>email-spring-boot-starter</artifactId>
<version>1.0.3</version>
</dependency>
```
#### 配置项
```yml
dingwen:
treasure:
# 电子邮件
email:
# 设置开启默认API
defaultApiEnabled: false
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
```
#### 使用
```java
/**
* 发送电子邮件
*/
@Test
public void sendEmail() {
String messageId = MailUtils.sendText("1981723769@qq.com", "test", "测试邮件内容");
System.out.println(messageId);
}
```
#### API概览
<table>
<tr>
<td><img src="img/电子邮件工具API.png"></td>
</tr>
</table>
### 本地缓存启动器
> 基于`caffeine`实现的配置化的本地缓存
#### 核心功能
+ 配置化实现,无需反锁的整合配置
+ 基于`Spring cache`实现
+ `@Cacheable`: 将方法的结果缓存起来,下一次方法执行参数相同时,将不执行方法,返回缓存中的结果
+ `@CacheEvict`: 移除指定缓存
+ `@CachePut`: 更新缓存
+ `@Caching`: 可以指定相同类型的多个缓存注解,例如根据不同的条件
+ `@CacheConfig`: 类级别注解,可以设置一些共通的配置,@CacheConfig(cacheNames=""), 代表该类下的方法均使用这个cacheNames
+ 提供丰富的API,可供前端页面展示
+ 获取所有的缓存
+ 获取所有的缓存项
+ 获取缓存详情
+ 清除缓存
+ 缓存数据下载
+ 缓存工具类`CacheHelper`
+ 自定义缓存`VisualCaffeineCache`
+ 本地缓存服务实现类 `CaffeineCacheServiceImpl`
#### 使用
##### 引入依赖
```xml
<!--caffeine场景-->
<dependency>
<groupId>top.dingwen.io</groupId>
<artifactId>caffeine-spring-boot-starter</artifactId>
<version>1.0.5</version>
</dependency>
```
##### 缓存配置项
```yml
dingwen:
treasure:
# caffeine
caffeine:
# 是否开启默认API
defaultApiEnabled: false
caches:
# 缓存Spring Bean名称
- name: testCache
# 有效时间,单位秒
invalidTime: 60
# 最大缓存数量
maximumSize: 100
- name: fileCache
invalidTime: 60
maximumSize: 1000
```
##### 使用
```java
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 = "defaultVisualCaffeineCache")
@Lazy
private CaffeineCache defaultVisualCaffeineCache;
/**
* 缓存get|set测试
*
* @return {@link ResultVO}<{@link String}>
*/
@Cacheable(cacheNames = "defaultVisualCaffeineCache")
@GetMapping("/test/gs")
public ResultVO<String> testCacheGs(@RequestParam String key,@RequestParam String value) {
defaultVisualCaffeineCache.putIfAbsent(key, value);
return success(defaultVisualCaffeineCache.get(key, String.class));
}
```
#### 通用缓存工具类`CacheHelper`
> 封装同一的现查询本地缓存,若存在则返回,不存在则查询数据库返回且同时加入本地缓存
```java
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);
}
}
```
#### 概览
<table>
<tr>
<td><img src="img/本地缓存API概览.png"></td>
<td><img src="img/本地缓存接口返回API.png"></td>
</tr>
</table>
#### 自定义缓存`VisualCaffeineCache`
> 显示更丰富的内容
<table>
<tr>
<td><img src="img/VisualCaffeineCache概览.png"></td>
</tr>
</table>
### Redis启动器
> 基于`RedisTemplate`封装的启动器
#### 核心功能
+ 常用API
+ 对象缓存
+ 过期时间
+ 缓存是否存在
+ 删除缓存
+ `List`,`Set`,`Map`缓存操作
+ 发布订阅
+ 自增自减
+ `void removeZset(final String key, Double scoreStart, Double scoreEnd)`
+ `<T> Set<T> getZsetCacheZet(final String key,Double scoreStart,Double scoreEnd)`
+ 常用功能
+ 通用逻辑轻松缓存 `@EasyCache`
+ 限流 `@RateLimiter`
+ 防重 `@ReSubmit`
+ 监控信息
+ `Redis`信息
+ 缓存信息
+ 缓存删除
+ 缓存预热统一封装 `RedisCache`
+ 分布式锁组件 `RedisShareLockComponent`
+ 基于`zset`实现延迟队列
+ `CAS`基于`lua`脚本实现
+ 延迟双删`DelayRemoves`
#### 使用
##### 引入依赖
```xml
<!--redis场景-->
<dependency>
<groupId>top.dingwen.io</groupId>
<artifactId>redis-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
```
##### 配置项
```yml
dingwen:
treasure:
# redis
redis:
# 缓存项配置
caches:
- keyPrefix: "re_submit:"
remark: "防止重复提交"
- keyPrefix: "common_lock:"
remark: "通用锁"
# 分布式锁超时时间,默认一分钟
shareLockTimeOut: 60000
# 是否开启延迟双删
delayRemoves: false
# 是否开启轻松缓存
easyCache: false
# rateLimiter
rateLimiter: false
# 是否开启防重复提交
reSubmit: false
# 是否开启默认API
defaultApiEnabled: false
```
##### 使用案例
```java
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`完成自身预热逻辑,由统一预热组件进行调用
<table>
<tr>
<td><img src="img/redis_缓存预热.png"></td>
<td><img src="img/Redis_缓存预热2.png"></td>
<td><img src="img/Redis_缓存预热组件3.png"></td>
</tr>
</table>
#### 分布式锁组件 `RedisShareLockComponent`
```java
/**
* 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进行排序获取,以及删除指定的值。从业务上,我们可以再新增任务的时候放入,再通过定时任务进行拉取,要注意的一点就是拉取的时候要有分布式锁,保证不进行重复拉取就可以了。
<table>
<tr>
<td><img src="img/Redis延迟任务组件1.png"></td>
<td><img src="img/Redis延迟任务2.png"></td>
</tr>
</table>
#### `CAS`基于`lua`脚本实现
<table>
<tr>
<td><img src="img/redis_lua脚本1.png"></td>
<td><img src="img/redis_lua2.png"></td>
</tr>
</table>
#### 延迟双删 `DelayRemoves`
> 使用延时双删,保证缓存数据库一致性问题
#### 概览
<table>
<tr>
<td><img src="img/Redis缓存API概览.png"></td>
<td><img src="img/Redis缓存监控.png"></td>
</tr>
<tr>
<td><img src="img/RedisAPI获取缓存项.png"></td>
<td><img src="img/RedisAPI获取缓存值.png"></td>
</tr>
</table>
### 二级缓存场景启动器
> 基于本地缓存场景启动器和Redis场景启动器,Caffeine + Redis + Spring Cache 实现的二级缓存
> 注意: **Redis缓存的时间会是本地缓存时间的两倍**
#### 使用
##### 引入依赖
```xml
<!--二级缓存场景启动器-->
<dependency>
<groupId>top.dingwen.io</groupId>
<artifactId>dcache-spring-boot-starter</artifactId>
<version>1.0.4</version>
</dependency>
```
##### 启动项配置
```yml
dingwen:
treasure:
dcache:
# 是否开启默认API
defaultApiEnabled: true
# 是否开启日志打印
logPrintEnabled: true
```
#### 核心功能
+ 支持分布式跨`JVM`进程刷新
+ `com.dingwen.treasure.dcache.core.DoubleCache` : 二级缓存
+ `com.dingwen.treasure.dcache.core.DoubleCacheManager`: 缓存管理器
+ `com.dingwen.treasure.dcache.core.RefreshCacheListener`: 字典缓存监听通过redis订阅发布实现缓存刷新
#### 使用案例
```shell
/**
* 测试二级缓存获取
*
* @param businessId 业务id
* @return {@link ResultVO }<{@link String }>
*/
@Cacheable(cacheNames = CaffeineConstant.C_L_P_C_IN_NAME,
key = "#businessId",
cacheManager = "doubleCacheManager"
)
@GetMapping("/test/gets")
public ResultVO<String> testDCacheGets(@RequestParam String businessId) {
log.info("{},{},业务方法执行,拟查询数据库", DCacheConstant.L_P_S,DCacheConstant.L_P_API);
return ResultVOGenerator.genSuccessResult(businessId);
}
```
<table>
<tr>
<td><img src="img/二级缓存日志.png"></td>
</tr>
</table>
### 进度条启动器
> 基于`CompletableFuture`实现的多线程任务调度的进度条任务场景
#### 核心功能
+ 提供模板类,封装通用逻辑
+ 提供丰富API接口
+ 任务提交
+ 查询进度
+ 多种缓存实现支持
+ `redisServiceImpl` : Redis缓存支持 [兼容分布式环境]
+ `caffeineCacheServiceImpl`: Caffeine缓存支持
+ `localCacheServiceImpl`: 本地缓存支持
+ 拓展接口`DefaultBarProcessSender`支持[可用于`websocket`]形式的实时进度推送
#### 使用
##### 引入依赖
```xml
<!--进度条场景-->
<dependency>
<groupId>top.dingwen.io</groupId>
<artifactId>bar-spring-boot-starter</artifactId>
<version>1.0.4</version>
</dependency>
```
##### 启动项配置
```yml
dingwen:
treasure:
bar:
# 是否开启默认API
defaultApiEnabled: true
```
#### 概览
<table>
<tr>
<td><img src="img/进度条API.png"></td>
<td><img src="img/任务进度信息redis.png"></td>
</tr>
<tr>
<td><img src="img/提交进度条任务.png"></td>
<td><img src="img/进度查询.png"></td>
</tr>
</table>
### 异常消息告警场景启动器
> TODO
### API文档场景启动器
> 基于`Knif4j`封装的API文档场景启动器
#### 使用
##### 引入依赖
```xml
<!--api文档生成启动器-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>api-docs-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
```
##### 启动项配置
> 在启动类上添加`@EnableApiDocs`注解,以开启接口文档功能
```
@EnableApiDocs
```
> 配置
```yml
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"
```
#### 概览
+ [协议://IP:端口/doc.html](协议://IP:端口/doc.html)
<table>
<tr>
<td><img src="img/logv.api.png"></td>
</tr>
</table>
### 日志通用场景启动器
> 实现方法级别的日志,请求级别的日志,业务级别的日志配置化,拓展化,可视化以及代码定位
#### 核心功能
##### 方法日志
> 方法日志细分为整体日志`MeLog`,参数日志`ParamMeLog`,返回结果日志`ResultMeLog`,方法异常日志`ThrowingMeLog`可灵活配置实现
> 可基于日志格式化接口定制实现,已提供默认实现
> 提供回调接口可灵活拓展处理日志
+ 方法日志
+ 参数日志
+ 返回结果日志
+ 异常日志
##### 请求日志
> 可整体配置所有请求的日志记录规则,提供回调接口可进行定制化
##### 操作日志
> 基于注解实现的操作日志,依附于Spring事件监听机制解耦,监听对应日志实现及可实现定制化
##### 登录日志
> Spring事件监听机制解耦,监听对应日志实现及可实现定制化
<table>
<tr>
<td><img src="img/login_log.png"></td>
</tr>
</table>
#### 使用
##### 引入依赖
```xml
<!--日志场景-->
<dependency>
<groupId>top.dingwen.io</groupId>
<artifactId>log-spring-boot-starter</artifactId>
<version>1.0.3</version>
</dependency>
```
> 日志配置
```yml
dingwen:
treasure:
# 请求日志
log:
# 是否开启默认API[默认关闭]
defaultApiEnabled: false
# 是否开启默认操作日志[默认关闭]
opLogEnabled: false
# 是否开启方法日志[默认关闭]
meLogEnabled: false
# 是否开启默认链路追踪[默认关闭]
tranceEnabled: false
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();
}
```
#### 概览
<table>
<tr>
<td><img src="img/log_console.png"></td>
<td><img src="img/log_mongo.png"></td>
</tr>
</table>
### 日志文件查看启动器
> 基于Websocket、SpringBoot2.x、layui实现的可配置的Web版日志查看器
#### 核心功能
+ 可选基础目录生成文件树
+ 日志文件编码配置化
+ 追加新日志自动滚动
+ 支持下载日志文件 [需要配合权限框架做好权限控制]
#### 优化点
+ 日志查看页增加文件下载
+ 文件树按照创建时间降序
+ 大量的日志输出导致内存飙升
+ 大量的客户端链接导致CPU飙高
+ UI美化
+ 异常提示完善
#### 使用
##### 引入依赖
```xml
<!--日志查看场景-->
<dependency>
<groupId>top.dingwen.io</groupId>
<artifactId>logv-spring-boot-starter</artifactId>
<version>1.0.2</version>
</dependency>
```
> 配置接口文档放行、线程池、监控项
```yml
dingwen:
treasure:
# webplus
webplus:
debug: true
handlers:
- handler: "static/**"
locations: "classpath:/static/"
- 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/logv)
+ [接口文档](/doc.htm)
#### 概览
<table>
<tr>
<td><img src="img/logv.start01.png"></td>
<td><img src="img/logv.start02.png"></td>
</tr>
<tr>
<td><img src="img/logv.index.png"></td>
<td><img src="img/logv.add.png"></td>
</tr>
<tr>
<td><img src="img/logv.文件树.png"></td>
<td><img src="img/logv列表.png"></td>
</tr>
<tr>
<td><img src="img/logv日志查看.png"></td>
<td><img src="img/logv.api.png"></td>
</tr>
</table>
### PipeLine启动器
> 工厂模式 + 策略模式 + 门面模式 + 模板方法 实现的定制化配置化规则引擎
#### 核心功能
+ 抽象上下文,实现通用逻辑`PipelineContext`
+ 异步执行管道支持
+ 外部配置化的管道流
+ 注解式的管道前后进行过滤操作
+ 详尽的日志追踪
+ 提供测试案例
#### 使用
##### 引入依赖
```xml
<!--pipeline场景-->
<dependency>
<groupId>top.dingwen.io</groupId>
<artifactId>pipeline-spring-boot-starter</artifactId>
<version>1.0.2</version>
</dependency>
```
#### 配置项
```yaml
dingwen:
treasure:
# pipeline
pipeline:
# 是否开启默认API
defaultApiEnabled: true
routes:
# 管道上下文名称 key
testContext:
# 第一个执行的管道
- onePipeLine
# 第二个执行的管道
- twoPipeLine
```
```java
// 配置管道过滤器
@PipeFilters(filters = {
@PipeFilter(beanName = "beforeOneFilter",exePoint = PipelineFilterExePoint.ALL),
@PipeFilter(beanName = "beforeTwoFilter",exePoint = PipelineFilterExePoint.BEFORE),
})
```
#### 核心类
+ `PipelineContext`管道上下文
+ `Pipeline`管道接口
+ `PipelineFilter`管道过滤器接口
+ `@PipeFilter`过滤器注解
+ `PipelineFactory`管道执行工厂
+ `PipelineExecutor`管道执行器
#### 概览
<table>
<tr>
<td><img src="img/pipeline包结构.png"></td>
<td><img src="img/pipeline初始化.png"></td>
</tr>
<tr>
<td><img src="img/pipeline使用.png"></td>
<td><img src="img/pipeline执行结果.png"></td>
</tr>
</table>
### 数据库文档生成启动器
> 基于Screw、Freemarker实现的支持word,markdown,html文件生成的数据库文档生器
#### 核心功能
+ 数据源支持
+ postgresql
+ mysql
+ oracle
+ 包含可配置的详细注释生成
+ 表名称,前缀,后缀等细粒度可配置化
+ 文档名称,标题可配置化
+ 同时支持work,markdown,html版本数据库设计文档
#### 使用
##### 引入依赖
```xml
<!--数据库文档生成场景启动器-->
<dependency>
<groupId>top.dingwen.io</groupId>
<artifactId>screw-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
```
> 配置文档参数
```yml
dingwen:
treasure:
# 数据库文档生成
screw:
# 数据库名称
dbName: test
# 数据源连接地址
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
```
#### 访问地址
+ [首页](http://127.0.0.1:8080/treasure_doc_1.0.0.html)
#### 概览
<table>
<tr>
<td><img src="img/screw-category.png"></td>
<td><img src="img/screw-table.png"></td>
</tr>
</table>
### MongoDB场景启动器
> 基于`MongoTemplate`分装的类似`MybatisPLus`的`lambda`形式的增删改查API
#### 核心功能
+ 复杂条件灵活构造查询
+ 分页查询
+ 模糊查询
+ 排序
+ 集合查询
+ 新增
+ 修改
+ 删除
#### 使用
##### 引入依赖
```xml
<!--mongo场景启动器-->
<dependency>
<groupId>top.dingwen.io</groupId>
<artifactId>mongo-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
```
##### 编写实体类
```java
/**
* 请求日志
*
* @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;
}
```
##### 编写接口
```java
/**
* 请求日志服务
*
* @author dingwen
* @since 2023/7/24 13:36
*/
public interface IRequestLogService extends MongoService<RequestLog> {
}
```
##### 编写实现类
```java
/**
* 请求日志服务
*
* @author dingwen
* @since 2023/7/24 13:38
*/
@Service
public class RequestLogServiceImpl extends MongoServiceImpl<RequestLog> implements IRequestLogService {
}
```
##### 调用
```java
/**
* 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`: 右模糊查询
#### 概览
<table>
<tr>
<td><img src="img/mongo-table.jpg"></td>
<td><img src="img/mongo-api.jpg"></td>
</tr>
</table>
### xxl-job定时人场景启动器
> 基于`xxl-job`v2.4.0版本的执行器封装,实现配置化、插件化使用
#### 核心功能
参考官网 [xxl-job](https://github.com/xuxueli/xxl-job)
#### 使用
##### 引入依赖
```xml
<!--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参数
```yml
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
```
#### 概览
<table>
<tr>
<td><img src="img/xxl-job-start.png"></td>
<td><img src="img/xxl-job-admin.png"></td>
</tr>
</table>
### OSS启动器
> 基于亚马逊`S3`封装的支持阿里云,`Minio`等多种存储方式的对象存储通用业务场景启动器
#### 使用
##### 引入依赖
```xml
<!--对象存储场景-->
<dependency>
<groupId>top.dingwen.io</groupId>
<artifactId>oss-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
```
#### 配置项
```yml
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
<table>
<tr>
<td><img src="img/OssTemplate文档概览.png"></td>
</tr>
</table>
### mybatis-plus启动器
> 基于`MybatisPlus`的二次封装
#### 核心功能
+ 统一实体
+ 统一查询对象
+ 场景异常封装处理
+ 逻辑删除
+ 枚举处理
+ 通用字段填充
+ 创建人
+ 创建时间
+ 修改人
+ 修改时间
+ 多租户
+ 数据权限
+ 通用选项查询组件
+ 完整`SQL`日志打印
+ `SQL`执行耗时
+ 通用字符串字段长度校验组件
+ 同一数据唯一性校验组件
+ 获取表信息工具
+ 字段比较工具
+ 通用查询组件 `QueryUtils` [支持数据权限]
+ `FULL`: 全模糊查询
+ `EQ`: 全等于查询
+ `LEFT`: 全以什么结尾模糊查询查询
+ `RIGHT`: 全以什么开头模糊查询查询
+ `IN`: 在集合查询
+ `NOT_IN`: 不在集合查询
+ `RANGE`: 范围内查询
+ `DESC`: 降序
+ `ASC`: 升序
+ 数据冗余解决方案`IRedundancyMaintainService`
#### 使用
##### 引入依赖
```xml
<!--mybatisplus场景启动器-->
<dependency>
<groupId>top.dingwen.io</groupId>
<artifactId>mybatisplus-spring-boot-starter</artifactId>
<version>1.0.3</version>
</dependency>
```
#### 选项查询通用组件
> 当你需要查询某个数据作为下拉选项时可使用此组件,支持四级选项、数据权限、中间表关联。已实现本地缓存优化。
##### 三级关联查询
```java
@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));
}
```
##### 两级父子查询
```java
@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));
}
```
##### 一级简单查询
```java
@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));
}
```
#### 基础Controller `CRUD`
```java
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));
}
}
```
#### 数据权限
> 足够灵活,足够高效,足够优雅,基于`mybatis-plus-ext-spring-boot-starter`拓展
#### 完整`SQL`日志及耗时日志
> 基于拦截器实现,核心类: `SQL`拼接拦截器`MybatisPlusSqlLogInterceptor`,`SQL`耗时拦截器`MybatisSqlStatementInterceptor`
> 添加以下配置启动:
```yaml
dingwen:
treasure:
# mybatisplus 场景启动器
mybatisplus:
# 是否开启SQL拦截器日志
sqlLog: true
# sql耗时统计(毫秒) 低档
consumeLow: 999
# sql耗时统计(毫秒) 中档
consumeMiddle: 5000
# sql耗时统计(毫秒) 高档
consumeHeight: 10000
```
<table>
<tr>
<td><img src="img/mybatisplus_Sql_log.png"></td>
</tr>
</table>
#### `druid`连接加密
> 使用`DruidEncryptUtils`生产密码,公钥,私钥,再增加对应配置即可
<table>
<tr>
<td><img src="img/druid_encry.png"></td>
</tr>
</table>
> 多数据源示例配置
```yaml
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 `已经很方便了,但是对于字符串类型长度的校验,若数据表发生了改变需要同步维护实体中的字段校验信息。通用字符串字段长度校验组件就是为了解决这一问题,通过查询数据表的方式来进行字符串的长度校验,提供丰富的日志,确保不会出现因为字段过长倒是的数据库操作错误
<table>
<tr>
<td><img src="img/mybatisPlus_表字段长度校验组件1.png"></td>
<td><img src="img/mybatisPlus_表字段长度校验组件2.png"></td>
<td><img src="img/mybatisPus_表字段长度校验组件3.png"></td>
</tr>
</table>
#### 统一数据唯一性校验组件
> 当你需要保证表中某一字段唯一而又不想依赖数据库的唯一性约束,在数据提交的时候就完成校验。直接过滤掉这一类错误数据的数据库访问时实用此组件
<table>
<tr>
<td><img src="img/mybatisPlus_数据唯一性校验1.png"></td>
<td><img src="img/mybatisPus_数据唯一性校验组件2.png"></td>
</tr>
</table>
> 若接口入口校验方法由父类继承而来会导致校验失效,可以重写父类方法再手动调用校验即可
<table>
<tr>
<td><img src="img/mybatisPlus数据唯一校验组件手动校验1.png"></td>
<td><img src="img/mybatisPlus_数据唯一性校验手动校验方式3.png"></td>
</tr>
</table>
#### 表信息获取工具`TableInfoUtils`
> 通过`SqlRunner`获取数据库表字段定义,注释等信息及表注释等信息。目前只支持`Mysql`。已做本地缓存优化
+ `getTableName(T ojb)`:获取表名称
+ `getTableComment(String tableName,String database)`:获取表注释信息
+ `getTableFieldInfos(T obj)`: 获取表字段信息
#### 实体字段比较工具`CompareUtils`
>为实现关键数据的变更历史提供支持。例如:名称:【】,描述:【】,发生了变更。由【】变更为【】。
#### 通用查询组件 `QueryUtil`
> 查询对象继承 `BaseQuery`,标注对应注解
```java
/**
* 字典查询对象
*
* @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;
}
```
> 支持的查询类型
```java
/**
* 查询方式
* @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;
}
```
> 使用
```java
/**
* 字典分页查询
*
* @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`的事件机制进行抽象复用.
> 当冗余字段发生变更的时候发布事件
```java
/**
* 数据冗余维护事件
*
* @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` : 可以通过常量访问属性
#### 其他配置项
```yml
# mybatisplus配置文件
dingwen:
treasure:
# mybatisplus 场景启动器
mybatisplus:
# 是否开启基于数据行方案的多租户支持
tenantForLine: false
# 是否开启自动分页插件
pagination: false
# 是否开启防止全表更新删除
blockAttack: false
# 是否开启乐观锁拦截器
optimisticLocker: false
# 是否开启基于动态多数据源方案的多租户支持
tenantForDataSource: false
# 需要进行多租户数据隔离的表名称[于数据行方案]
tables:
# 是否开启SQL拦截器日志
sqlLog: true
# sql耗时统计(毫秒) 低档
consumeLow: 999
# sql耗时统计(毫秒) 中档
consumeMiddle: 5000
# sql耗时统计(毫秒) 高档
consumeHeight: 10000
```
### 字典启动器
#### 核心功能
+ 通用字典,字典值实现
+ 提供丰富的字典API
+ 支持多层级的,分模块的字典值[树]
+ 分布式缓存支持
+ Redis缓存与本地缓存配合使用专注提升效率
+ Spring Retry整合翻译应用
+ 便捷查询字典工具
#### 使用
##### 引入依赖
```xml
<!--通用字典场景-->
<dependency>
<groupId>top.dingwen.io</groupId>
<artifactId>dic-spring-boot-starter</artifactId>
<version>1.0.1</version>
</dependency>
```
#### 默认API
<table>
<tr>
<td><img src="img/字典默认API接口.png"></td>
</tr>
</table>
#### 快速字典工具类`DictHelper`
> Redis + CaffeineCache 专注提升效率
```java
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));
}
}
```
#### 核心数据模型
<table>
<tr>
<td><img src="img/字典_字典数据模型v2023-07-09.png"></td>
</tr>
</table>
#### 分布式缓存实现逻辑图
<table>
<tr>
<td><img src="img/分布式缓存实现逻辑.jpg"></td>
</tr>
</table>
### 翻译启动器
> 基于Jackson反序列化进行的字典翻译组件
#### 核心功能
+ 基于注解的配置实现翻译
+ 通用表字段的翻译
+ 本地缓存与数据库查询并存提升性能
+ 提供顶层翻译接口方便拓展`ITranslateService`
+ 注解`TranslateResult`形式的方法返回值自动翻译
#### 使用
##### 引入依赖
```xml
<!--翻译场景启动器-->
<dependency>
<groupId>top.dingwen.io</groupId>
<artifactId>translate-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
```
##### 标注注解
```java
// 默认翻译实现,使用自定义名称新字段
@ApiModelProperty(value = "name")
@Translate(
service = "defaultTranslateServiceImpl", // 翻译实现接口
mapper = "id", // 翻译依据值
viewColumn = "file_name", // 翻译值对应表字段
tableName = "tre_c_file", // 翻译对应表名称
keyColumn = "file_id" // 翻译依据值对应表字段名称
)
private String name;
```
```java
// 自定义翻译实现,例: 字典翻译实现,使用已有字段名称
@Translate(
service = "dictTranslateServiceImpl",// 翻译实现接口
keyColumn = "d_field_type", // 翻译值对应表字段
mapper = "dictLabel" // 翻译依据值
)
@ApiModelProperty(value = "dictLabel")
private String dictLabel;
```
#### 翻译接口`ITranslateService`
```java
package top.dingwen.io.treasure.translate.core.service;
/**
* 翻译接口 </br>
* <p> 泛型说明</p>
* <ul>
* <li> T: 翻译的返回值 </li>
* <li> P1: 翻译展示的列名称</li>
* <li> P2: 表名称 </li>
* <li> P3: 数据库主键列名称 </li>
* <li> P4: 主键值 </li>
* <li></li>
* </ul>
*
* @author dingwen
* @since 2023/6/9 15:39
*/
public interface ITranslateService<T, P1, P2, P3, P4> {
/**
* 翻译
*
* @param viewColumn 翻译展示的列名称
* @param tableName 表名称
* @param keyColumn 数据库主键列名称
* @param keyValue 主键值
* @return t 翻译的结果
*/
T translate(P1 viewColumn, P2 tableName, P3 keyColumn, P4 keyValue);
}
```
#### 注解`Translate`
```java
package top.dingwen.io.treasure.translate.annotation;
import top.dingwen.io.treasure.translate.constant.TranslateConstant;
import top.dingwen.io.treasure.translate.core.handler.TranslationHandler;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.*;
/**
* 通用翻译注解
*
* @author dingwen
* @since 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 TranslateConstant.DE_TR_BE_NA;
/**
* 映射字段 </br>
* 默认取当前字段的值 如果设置了mapper 则取映射字段的值
*/
String mapper() default "";
/**
* 翻译展示的列名称
*/
String viewColumn() default "";
/**
* 表名称
*/
String tableName() default "";
/**
* 数据库主键列名称
*/
String keyColumn() default "";
/**
* 主键值
*/
String keyValue() default "";
}
```
#### 注解`TranslateResult`
> 在返回方式使用该注解,配合`Translate`可以实现自动翻译
```java
/**
* 系统配置管理组件
* @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定时任务启动器
> 基于数据库悲观锁实现分布式场景下的定时任务不漏跑,不重复执行问题
#### 核心功能
+ 支持分布式调度
+ 基于监听异步方式实现的日追踪
+ 二次封装丰富的API
+ 后续提供界面操作
+ 本地环境可灵活配置决定是否执行定时任务
#### 使用
##### 引入依赖
```xml
<!--Quartz定时任务场景-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>quartz-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
```
##### 启动项配置
> 在启动类上添加`@EnableQuartz`注解以开启`quartz`定时任务场景功能
```java
@EnableQuartz
```
##### 继承`AbstractJob`实现自定义任务
```java
/**
* 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 ...");
}
}
```
##### 配置定时任务
<table>
<tr>
<td><img src="img/quartz_task_add_api.png"></td>
</tr>
</table>
#### 默认API
+ 获取所有执行中的任务列表
+ 获取定时任务信息列表
+ 添加一个定时任务
+ 修改运行中的任务信息
+ 立即执行
+ 分页查询任务执行日志
+ 修改定时任务状态
+ 删除定时任务
#### 核心数据模型
<table>
<tr>
<td><img src="img/Quartz场景数据模型.jpg"></td>
</tr>
</table>
#### 待办
+ 加缓存减少数据库查询次数
### 系统配置启动器
#### 核心功能
+ 通用系统配置实现
+ 提供丰富的配置API
+ 分布式缓存支持
+ Redis缓存与本地缓存配合使用专注提升效率
+ 便捷查询配置工具
#### 使用
##### 引入依赖
```xml
<!--通用系统配置场景-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>config-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
```
##### 启动项配置
> 在启动类上添加`@EnableConfig`注解以开启系统通用配置场景功能
```java
@SpringBootApplication(scanBasePackages = "com.dingwen")
```
#### 默认API
<table>
<tr>
<td><img src="img/系统配置默认API接口.png"></td>
</tr>
</table>
#### 快速系统配置工具类`ConfigHelper`
> Redis + CaffeineCache 专注提升效率
```java
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));
}
}
```
#### 核心数据模型
<table>
<tr>
<td><img src="img/配置数据模型v2023-07-09.png"></td>
</tr>
</table>
#### 分布式缓存实现逻辑图
<table>
<tr>
<td><img src="img/分布式缓存实现逻辑.jpg"></td>
</tr>
</table>
### Excel启动器
> 基于阿里开源`EasyExcel`进行二次封装,开箱即用
#### 核心功能
+ 自定义转换器封装
+ 自定义数据校验封装
+ 导入、导出异常处理、事务处理
+ 多行表头导入
+ 大数据量分批次导入
+ 简单导出以及模型映射导出
+ 模板填充、组合模板填充
+ 文件导出下载、文件上传导入
+ 导出行高和列宽设置
+ 导出图片内容
+ Base64
+ 字节数组
+ 流
+ 导出动态表头
+ 合并单元格
+ 导出超链接、批注、公式
+ 表字段翻译
+ 字典翻译`translate-spring-boot-starter`
+ 枚举翻译
+ 自定义转换表达式翻译 `ExcelExpProperty`
+ 多线程导出导入优化
+ 平铺导出自动合并相同单元格 `ExcelAutoMergeHandler`
+ 导入校验封装+错误文件下载【OSS】
+ 自动计算宽度
+ 注解封装导出逻辑
### 文件启动器
> 一套包含前后端的一条龙的通用的文件场景,业务数据基于`MybatisPlus`存储,文件数据可存储与系统本地或任何一直OSS存储
#### 核心功能
+ 存储方式
+ 系统存储
+ MiniIO
+ 阿里OSS
+ 任何一种OSS
+ 业务数据API
+ 进度条可视化
+ 断点续传
+ 分片上传
+ 图片压缩
+ 图片水印、文件水印
+ 图片缩略图
+ 文件下载,文件压缩下载
+ 文件预览,图片预览
### 数据归档启动器
> **适用场景**: 一张表中有1000w的数据,但是可能其中有800w是历史数据(冷数据),我们可能在业务上已经不再使用这些数据,如果放在业务表中,可能会影响我们业务的效率,所以我们可以将其归档到另一张表中,将其变成冷数据
>
> **流程:**数据归档的流程大概是: 1)从原数据表获取需要归档的数据;2)将这部分数据插入归档的表中;3)将元数据表中这部分数据删除
>
> **注意点:**我们数据归档中,事务的提交应该采用手动事务提交,如果使用大事务的情况下,可能会导致事务超时等一系列的问题!还有,我们需要实现可控归档,需要达到我们可以手动控制是否归档、停止,并且还能动态配置归档范围
#### 概览
<table>
<tr>
<td><img src="img/数据归档概览.png"></td>
</tr>
</table>
#### 使用
##### 引入依赖
```xml
<!--数据归档场景-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>db-backup-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
```
##### 启动项配置
> 在启动类上添加`@EnableDbBackup`注解以开启场景功能
```java
@EnableDbBackup
```
##### 其他配置
```shell
dingwen:
treasure:
db:
backup:
maxLoopCount: TODO // 备份最大循环次数
backUpDataRules: TODO // 备份规则: 开始归档的id;结束归档的id;一次查询的条数
```
##### 实现`IBackupDataService`接口
```java
/**
* 测试用户表备份
* @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[用户数据删除]");
}
}
```
#### API
```java
BackupDataFactory.BACKUP_DATA_SERVICE.get(TODO).exeBackUpData();
```
### 数据变更记录启动器
> 针对字段更新的变更日志的通用实现
#### 核心功能
+ 实体映射到表字段变更比对
+ 实体外部关联表字段翻译
+ 实体枚举字段翻译
+ 自定义拓展字段
#### 概览
<table>
<tr>
<td><img src="img/数据变更场景_概览.png"></td>
</tr>
</table>
#### 数据模型
<table>
<tr>
<td><img src="img/变更记录_变更日志数据模型v20240102.png"></td>
</tr>
</table>
#### 使用
##### 引入依赖
```xml
<!--变更日志场景-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>change-log-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
```
##### 启动项配置
> 在启动类上添加`@EnableChangeLog `注解以开启场景功能
```java
@EnableChangeLog
```
##### 其他配置
> 配置数据表信息获取数据库名称以及忽略的字段
```shell
dingwen:
treasure:
change:
log:
dataBase: TODO //数据库
ignores: TODO // 忽略的字段
```
#### API
> 在需要使用的地方组装事件对象进行发布即可
```java
@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`发布事件就可以实现该功能
#### 类图概览
<table>
<tr>
<td><img src="img/event_class.png"></td>
</tr>
</table
#### 数据模型
<table>
<tr>
<td><img src="./img/event_数据模型.png"></td>
</tr>
</table>
#### 核心功能
> 采用持久化的方式保证重试可靠功能,当`Spring`环境中没有找到对应的实现时则会按照默认的方式进行.
>
> 初次之外也提供了基于`Redis`分布式锁方式的幂等不重复执行可靠性实现.
```java
/**
* 抽象事件监听器 </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());
}
});
}
}
```
> 并发安全的实现
```java
/**
* 安全的,能保证幂等的,不重复执行的,并发安全的监听器实现 </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);
}
}
}
```
#### 使用
##### 引入依赖
```xml
<!--事件场景启动器-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>event-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
```
##### 启动项配置
> 在启动类上添加`@EnableEvent `注解以开启场景功能
```java
@EnableEvent
```
##### 其他配置
> 若是并发安全的实现则需要配置锁相关参数
```shell
dingwen:
treasure:
# 事件
event:
# 时间
lock-time: 10
# 时间单位: seconds
lock-time-unit: seconds
```
#### API
+ 事件重发: `boolean retry(Long eventId)`
+ 事件删除
+ 事件列表 `Page<Event> queryEventPage(QueryWrapper<Event> queryWrapper)`
### Jwt场景启动器
> 通过灵活的配置,实现Jwt的生成,校验,刷新等.
#### 使用
##### 引入依赖
```xml
<!--jwt场景启动器-->
<dependency>
<groupId>top.dingwen.io</groupId>
<artifactId>jwt-spring-boot-starter</artifactId>
<version>1.0.2</version>
</dependency>
```
##### 配置
```shell
dingwen:
treasure:
# jwt
jwt:
# 是否开启默认API
default-api-enabled: true
# 头信息: 默认值: authorization
header: "Authorization"
# 令牌前缀: 默认值 Bearer
tokenPrefix: "Bearer "
# 令牌密钥: 最少长度 32
secretKey: "38329cc9d1b1496da21700d02ecd0690c348930073f"
# App端过期时间 (单位分钟)
appExpireTime: 5
# Web端过期时间 (单位分钟)
webExpireTime: 20
# App 刷新时间 秒
appRefreshTime: 10
# Web 刷新事件 秒
webRefreshTime: 120
# 令牌签发者
issuer: "treasure"
# 令牌签发主题
subject: "jwt"
```
#### API `Jwt`
<table>
<tr>
<td><img src="./img/jwt.png"></td>
</tr>
</table>
#### 测试API `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`的二次封装
#### 使用
##### 引入依赖
```xml
<!--安全场景启动器-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>security-plus-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
```
##### 启动项配置
> 在启动类上添加`@EnableSecurityPlus`注解以开启场景功能
```java
@EnableSecurityPlus
```
##### 其他配置
```shell
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
```
#### 资源权限数据模型
<table>
<tr>
<td><img src="./img/auth_res_data_model.png"></td>
</tr>
</table>
#### 资源权限内置API [规划中...]
+ 新增资源权限
+ 修改资源权限 [缓存管理...]
+ 删除资源权限[缓存管理...]
+ 资源权限列表
+ 开启/关闭资源权限管控[缓存管理...]
+ 新增资源
+ 修改资源[缓存管理...]
+ 删除资源[缓存管理...]
+ 开启/关闭资源管控[缓存管理...]
+ 权限下的资源列表
#### 核心特色功能
+ 统一配置
+ `Jwt`令牌实现
+ 动态接口权限
+ 权限校验规则
+ 排除所有 `ExcludesAuthorityStrategy`
+ 包含所有 `IncludesAuthorityStrategy`
+ 包含单个 `IncludeAuthorityStrategy`
+ 自定义规则
+ `Caffeine` + `Redis`二级缓存
+ 统一的令牌管理 `TokenService`
+ 刷新令牌
+ 生成令牌
+ 校验令牌等
+ 认证环境对象+工具类进一步抽象 `SecurityPlusGrantedAuthority`, `SecurityPlusUtils`
+ 认证失败,认证入口,异常统一进行国际化封装结果返回处理
#### 动态权限加载拓展接口`AbstractDynamicAttributeService`
```java
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;
}
}
```
#### 测试API `AuthController`
+ `POST` [刷新令牌]: `common/auths/refresh`
+ 在线用户统计 [规划中...]
+ 踢人 [规划中...]
### 认证场景启动器 `auth-spring-boot-starter`
> 基于安全场景启动器完成认证(但不局限于此方式),兼容`SaToken`等
#### 核心功能
+ 接口权限动态设定 [支持拓展规则]
+ 用户分组+层级
+ 角色分组+层级+互斥+继承
+ 菜单+按钮+权限灵活控制
+ 租户
+ 表字段方式
+ 动态数据源方式
#### 简化版权限模型
<table>
<tr>
<td><img src="img/权限体系初稿.png"></td>
</tr>
</table
#### 使用
##### 引入依赖
```xml
<!--认证场景启动器-->
<dependency>
<groupId>com.dingwen</groupId>
<artifactId>auth-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
```
##### 启动项配置
> 在启动类上添加`@EnableAuth`注解以开启场景功能
```java
@EnableAuth
```
##### 其他配置
```shell
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`
+ 短信验证码登录[规划中...]
+ 电子邮箱验证码登录[规划中...]
+ 用户名密码登录
+ 手机号密码登录[规划中...]
+ 三方登录[规划中...]
#### 测试API `AuthController`
+ `GET` [获取验证码]: `common/auths/captcha` [默认1分钟只能调用10次]
+ `POST` [Web端登录]: `common/auths/webs/logins`
## 核心服务
### treasure-business
> 业务场景模拟,最佳实践
+ RabbitMQ保证顺序、可靠投递、可靠消费`MessageController`
+ 执行次数、耗时优化工具`MarketingController`
+ JavaScript动态规则校验`JavaScriptController`
+ 基于Redis实现接口限流`RateLimiterController`
+ 多线程异步多任务进度条`ProgressBarTaskController`
+ 行政区划截取替代优化实现`AreaUtilController`
+ 自定义线程池、异步任务`AsyncController`
+ 批量插入方案优化对比`BatchSaveController`
+ `CompletableFuture`案例`CompletableFutureController`
+ 后台跨域处理方式一`CorsController`
+ Validate自定义注解进行个性化规则校验 `CustomValidateController`
+ 自定义注解实现数据脱敏`DesensitizationController`
+ jackson反序列化自定义对象`DeserializerController`
+ 自定义注解动态查询数据库进行字典翻译`DictionaryController`
+ 自定义注解实现通用缓存逻辑 `EasyCacheController`
+ 枚举类型序列化和反序列化 `EnumConvertController`
+ 全局异常处理 `ExceptionController`
+ 国际化 `LocaleController`
+ 设计模式-状态模式案例(活动营销状态流转)`MarketingController`
+ Mybatis-PLus 案例 `MybatisPlusController`
+ 自定义注解实现操作日志记录 `OperationLogRecordController`
+ 设计模式-观察者模式案例-创建订单`OrderController`
+ 异步短信,请求削峰 `OweFeeController`
+ 设计模式-简单工厂+模版方法+策略模式+函数式接口`PayController`
+ 自定义注解实现防止重复提交`ReSubmitController`
+ Redis`setnx`版分布式锁案例
+ `ResponseBodyAdvice`实现统一返回 `ResponseBodyAdviceController`
+ 开放接口对接-短信 `SmsController`
+ `feign`调用案例`TaskFeignController`
+ 设计模式-责任链-任务生成案例`TaskGenerateController`
+ 后端枚举类型统一翻译 `EnumController`
+ 自定义注解实现加解密、脱敏 `SensitiveController`
+ 微信公众号定制消息推送`WechatPubController`
+ 数据库缓存一致性解决方案
#### RabbitMQ全链路顺序、可靠消费,100%不丢失
> API入口:`MessageController`
+ 生产者进行可靠性消息投递
+ 消费者手动确认
+ 消息落库(状态管理、消费顺序控制)
+ 定时任务补偿
##### 流程图
![顺序可靠消息主流程](./img/顺序可靠消息主流程.png)
#### 优化工具类
> 参考Spring StopWatch 的拓展优化,精确计算执行耗时,执行次数,方便进行优化
> `OptimizeUtilController`
```java
/**
* 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();
}
}
```
![优化工具类](./img/OptimizeUtil.png)
#### 复杂规则校验
> Redis + JVM 双缓存
```js
// 测试js
function add(op1, op2) {
return op1 + op2
}
add(a, b)
```
##### 整体思路、功能点
![JavaScript规则业务思路](./img/JavaScript规则业务思路.png)
> 通过Java调用JavaScript进行规则校验,实现复杂且灵活可配置的规则校验功能`JavaScriptController`
+ 服务启动即进行规则缓存、脚本预编译
+ 多线程进行规则校验
+ 异步、实时返回结果
+ 灵活配置的特定业务特定规则
##### 表结构
###### 业务规则校验配置表(business_rule)
| | | | |
| ---------------- | -------- | ---- | --------------------------------------------------- |
| 名称 | 类型 | 长度 | 备注 |
| 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 | 备注(保留字段) |
#### Redis限流
> 基于`setnx`,通过自定义注解+脚本实现限流(参考若依实现)
+ 指定key + 凭借key
+ 基于方法
+ 基于IP
+ 指定时间、次数
```java
/**
* 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调用
```java
/**
* 进度条任务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));
}
}
```
##### 功能概览
<table>
<tr>
<td><img src="img/提交进度条任务.png"></td>
<td><img src="img/异步任务执行日志.png"></td>
</tr>
<tr>
<td><img src="img/任务进度信息redis.png"></td>
<td><img src="img/进度查询.png"></td>
</tr>
</table>
#### 自定义注解实现加解密、脱敏 `SensitiveController`
+ 枚举类`SensitiveEnum`
+ 实体 `SensitiveEntity`
+ 自定义注解
+ SensitiveDecode 解密
+ SensitiveEncode 加密
+ SensitiveField 字段标识
+ SensitiveInfoUtil 加解密、脱敏工具类
+ AesEncryptUtil AES工具类
##### 功能概览
<table>
<tr>
<td><img src="img/加密、脱敏.png"></td>
<td><img src="img/解密、脱敏.png"></td>
</tr>
</table>
#### 阿里云短信对接 `SmsController`
##### 功能概览
<table>
<tr>
<td><img src="img/模版、签名配置.png"></td>
<td><img src="img/短信内容.png"></td>
<td><img src="img/短信调用接口.png"></td>
<td><img src="img/aliyun调用返回.png"></td>
</tr>
</table>
#### 微信公众号定制消息推送`WechatPubController`
> 开放平台对接(基于Spring提供定时任务实现):
> + 天行数据
> + 百度地图
> + 微信公众号平台
<table>
<tr>
<td><img src="img/yue-morning.png"></td>
<td><img src="img/yue-message.png"></td>
</tr>
</table>
#### common-influxdb
> 时序数据库案例
> + [官网](https://docs.influxdata.com/)
> + 接口 `IotDataService`
<table>
<tr>
<td><img src="img/influxdb-data.png"></td>
<td><img src="img/influxdb-chart.png"></td>
</tr>
</table>
#### 数据库缓存一致性解决方案
##### 正常流程
<table>
<tr>
<td><img src="img/缓存一致性问题_常规流程.png"></td>
</tr>
</table>
> 以上的流程没有问题,当数据变更的时候,如何能保证将缓存同步到最新呢?
##### 先更新数据库,再更新缓存
> 假设数据库更新成功,缓存更新失败,在缓存过期失效之前,读取到的缓存数据都是旧的
##### 先更新缓存,再更新数据库
> 假设缓存更新成功,数据库更新失败,那读取到的数据都是错误的
##### 先删除缓存,再更新数据库
> 假设删除缓成功,此时A线程正在更新数据库,同时B线程也来了查询数据,发现缓存中没有,就查询数据库。此科查询到的数据任然是旧数据。
>
> 若此时做延迟删除缓存,根据业务时间灵活调整,确保修改数据线程已提交,延迟删除之后再查询就能保证数据是正确的了
>
> 若删除缓存失败,可加入消息队列,做删除重试
##### `cacal`方案
> 开发独立的服务,监控数据库的改变,同步对于的缓存
#### *线程数设置理论*
##### 宽泛不切实际的结论
> 1. CPU 密集型的程序 - 核心数 + 1
> 2. I/O 密集型的程序 - 核心数 * 2
##### 理论铺垫
> **CPU利用率**: 如果指令需要不断的执行,则CPU的利用率为100%,此时CPU将不能做除执行次指令之外的任何事情
> **线程上下文切换的代价**: 现代CPU基本都是多核心的,可以同时做核心数件事情互不打扰.如果要执行的线程大于核心数,那么就需要通过操作系统的调度了。操作系统给每个线程分配CPU时间片资源,然后不停的切换,从而实现“并行”执行的效果.**每次切换会伴随着寄存器数据更新,内存页表更新等操作**,就会导致CPU资源过多的浪费在上下文切换上,而不是在执行程序,得不偿失.
> **高效利用**:多程序在运行时都会有一些 I/O操作,可能是读写文件,网络收发报文等,这些 I/O 操作在进行时时需要等待反馈的。比如网络读写时,需要等待报文发送或者接收到,在这个等待过程中,线程是等待状态,CPU没有工作。此时操作系统就会调度CPU去执行其他线程的指令,这样就完美利用了CPU这段空闲期,提高了CPU的利用率。
##### 线程数和CPU利用率的小总结
> 1. 一个极端的线程(不停执行“计算”型操作时),就可以把单个核心的利用率跑满,多核心CPU最多只能同时执行等于核心数的“极端”线程数
> 2. 如果每个线程都这么“极端”,且同时执行的线程数超过核心数,会导致不必要的切换,造成负载过高,只会让执行更慢
> 3. I/O 等暂停类操作时,CPU处于空闲状态,操作系统调度CPU执行其他线程,可以提高CPU利用率,同时执行更多的线程
> 4. I/O 事件的频率频率越高,或者等待/暂停时间越长,CPU的空闲时间也就更长,利用率越低,操作系统可以调度CPU执行更多的线程
##### 线程数规划的公式
> 引用自《Java 并发编程实战》
<table>
<tr>
<td><img src="img/线程数规划公式1.png "></td>
</tr>
</table>
> 如果我期望目标利用率为90%(多核90),那么需要的线程数为:
>
> 核心数12 * 利用率0.9 * (1 + 50(sleep时间)/50(循环50_000_000耗时)) ≈ 22
> 通过线程数来计算CPU利用率
<table>
<tr>
<td><img src="img/线程数规划公式1.png "></td>
</tr>
</table>
> 线程数22 / (核心数12 * (1 + 50(sleep时间)/50(循环50_000_000耗时))) ≈ 0.9
>
> 虽然公式很好,但在真实的程序中,**一般很难获得准确的等待时间和计算时间,因为程序很复杂,不只是“计算”**。一段代码中会有很多的内存读写,计算,I/O 等复合操作,精确的获取这两个指标很难,所以光靠公式计算线程数过于理想化。
##### 真实程序中的线程数
> 没有固定答案,先设定预期,比如我期望的CPU利用率在多少,负载在多少,GC频率多少之类的指标后,再通过测试不断的调整到一个合理的线程数
>
> 比如一个普通的,SpringBoot 为基础的业务系统,默认Tomcat容器+HikariCP连接池+G1回收器,如果此时项目中也需要一个业务场景的多线程(或者线程池)来异步/并行执行业务流程。
>
> 此时我按照上面的公式来规划线程数的话,误差一定会很大。因为此时这台主机上,已经有很多运行中的线程了,Tomcat有自己的线程池,HikariCP也有自己的后台线程,JVM也有一些编译的线程,连G1都有自己的后台线程。这些线程也是运行在当前进程、当前主机上的,也会占用CPU的资源。
##### 一般的流程
> 1. 分析当前主机上,有没有其他进程干扰
> 2. 分析当前JVM进程上,有没有其他运行中或可能运行的线程
> 3. 设定目标
> 4. 目标CPU利用率 - 我最高能容忍我的CPU飙到多少?
> 5. 目标GC频率/暂停时间 - 多线程执行后,GC频率会增高,最大能容忍到什么频率,每次暂停时间多少?
> 6. 执行效率 - 比如批处理时,我单位时间内要开多少线程才能及时处理完毕
> 7. ……
> 8. 梳理链路关键点,是否有卡脖子的点,因为如果线程数过多,链路上某些节点资源有限可能会导致大量的线程在等待资源(比如三方接口限流,连接池数量有限,中间件压力过大无法支撑等)
> 9. 不断的增加/减少线程数来测试,按最高的要求去测试,最终获得一个“满足要求”的线程数
> **注意**:**不同场景下的线程数理念也有所不同**
>
> 1. Tomcat中的maxThreads,在Blocking I/O和No-Blocking I/O下就不一样
> 2. Dubbo 默认还是单连接呢,也有I/O线程(池)和业务线程(池)的区分,I/O线程一般不是瓶颈,所以不必太多,但业务线程很容易称为瓶颈
> 3. Redis 6.0以后也是多线程了,不过它只是I/O 多线程,“业务”处理还是单线程
##### 稳妥的方案
>
>
> 很多的内部业务系统,并不需要啥性能,稳定好用符合需求就可以了。推荐的线程数是:**CPU核心数**
##### Java 获取CPU核心数
> Runtime.getRuntime().availableProcessors()//获取逻辑核心数,如6核心12线程,那么返回的是12
##### Linux 获取CPU核心数
> 总核数 = 物理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`里式替换原则
> 程序中的对象可以在不改变程序正确性的前提下被它的子类所替换,即子类可以替换任何基类能够出现的地方,并且经过替换后,代码还能正确工作。
### treasure-slow-sql
#### 深分页查询优化
```sql
SELECT *
FROM sys_user
WHERE id > #{offset} LIMIT 0,#{pageSize};
```
```java
/**
* 优化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));
}
}
```
### treasure-kettle
> 企业级的数据中台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张表
![kettle数据库资源库](./img/kettle数据库资源库.png)
#### 方案思路
> Java集成定时远程调用和远程服务方式调用以及使用`spoon`客户端进行远程调用需要启动`carte`。
> carte.sh指定配置文件启动,单机考虑一主一从。默认用户密码(cluster),可在启动文件中配置
##### Java集成定时远程调用
> 远程调用执行
> + id: master ip
> + port: master port
> + dataRepositoryName: 数据库资源库名称
> + user: 集群名称(master中配置)
> + password: 集群密码(master中配置)
> + transName: 转换文件名称
> + jobName: 作业文件名称
> + path: 路径
```shell
# 转换执行
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`客户端触发远程作业,进行定时调用。(无需代码,配置即可实现)
##### spoon 客户端
> windows平台可以直接使用官方程序,linux or mac os 平台建议使用网页版本的spoon
```shell
docker search hiromuhota/webspoon
docker run -d -p 8080:8080 --name spoon hiromuhota/webspoon
```
<table>
<tr>
<td><img src="img/spoon-web.png"></td>
</tr>
</table>
#### 其他
##### Kettle自带监控页面
<table>
<tr>
<td><img src="img/Kettle监控页任务列表.png"></td>
<td><img src="img/Kettle监控页任务日志.png"></td>
</tr>
</table>
##### SpringBoot2.X和Kettle9整合
```shell
# 手动添加如下依赖
mvn install:install-file -DgroupId=组织名称 -DartifactId=坐标 -Dversion=9.3.0.0-428 -Dpackaging=jar -Dfile= jar包名称
```
### treasure-task-quartz
#### 整体介绍
> 基于`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 | | 执行结果:<br/>1: 成功<br/>0: 失败 |
| remark | varchar | 255 | 备注 |
### treasure-manage
> 常用后台管理实现
### treasure-common
> 公共模块
> + base: 基础、通用
> + config: 配置
> + core:核心通用组件
> + jpa:jpa场景
> + mybatisplus mybatisplus场景
> + web: web场景
> + knife4j: API文档
> + rabbitmq: RabbitMQ 应用场景
> + redis: Redis 应用场景
### treasure-admin
> 基于`SpringBoot Admin`整合`Spring Security`的监控实现,目前暴露所有端点,权限账户信息通过`nacos`配置指定
```yaml
spring:
security:
user:
name: actuator
password: actuator
```
> TODO
> + 自定义info、metrics、health、endpoint
> + 邮件、钉钉预警
### treasure-xxl-job-admin
> 基于`xxl-job v2.4.0`封装的调度中心
```yaml
xxl:
job:
accessToken: xxl-job-access-token
i18n: zh_CN
triggerpool:
fast:
max: 200
slow:
max: 100
logretentiondays: 30
```
<table>
<tr>
<td><img src="img/xxl-job-admin.png"></td>
</tr>
</table>
### `treasure-poi-tl`
> poi-tl 官网: https://github.com/Sayi/poi-tl
> `word`模板渲染解决方案,拒绝手动维护`xml`文件
<table>
<tr>
<td><img src="img/poi-tl-template-location.png"></td>
<td><img src="img/poi-tl-template.png"></td>
<td><img src="img/pol-tl-api.png"></td>
<td><img src="img/poi-tl-result.png"></td>
</tr>
</table>
### 钉钉企业微信预警`treasure-dingtalk-ger`
> 钉鸽官网: https://github.com/AnswerAIL/dingtalk-spring-boot-starter
#### 概览
<table>
<tr>
<td><img src="img/钉鸽1.png"></td>
<td><img src="img/钉鸽2.png"></td>
</tr>
</table>
#### 工具类 `DingerUtils`
> 可在全局异常处理处调用`DingerUtils.send(e);`
```java
/**
* 钉鸽
*
* @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);
}
}
```
#### 服务启停监听,dinger通知
```Java
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("清廉系统后端服务已停止");
}
}
```
```java
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`
### 其他临时文档
#### Maven私服配置
```xml
<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>
```
+ business
> 优雅代码业务组件实现(MybatisPlus)
> 特色业务场景
> + 线程池案例
> + 批量save
> + 自定义MVC反序列化方式
> + 缓存业务场景自定义注解:@EasyCache
> + 国际化
> + 状态模式+简单工厂模式实现:多阶段灵活控制状态流转
设计模式六大原则:
+ 单一职责
+ 接口隔离(Servlet filter)
+ 依赖倒置
+ 迪米特(最少知道原则)
+ 里氏替换 (父类抽象方法应保持行为一致,非抽象方法不应去重写)
+ 开闭原则
+ 合成复用
+
状态模式: 对象不同的状态导致不同的行为
+ 主要角色:
+ 抽象状态(接口或抽象类)
+ 具体状态(抽象状态的子类或实现类)
+ 环境类 (状态持有着,依据不同的状态出发不同的行为)
![状态模式案例UML类图](./img/状态模式案例UML类图.jpg)
> + MybatisPlus:乐观锁、逻辑删除、自动枚举转换、多数据源、Model、自动填充、分页等其他通用API
> + 观察者模式:(事件对象、监听)下订单走库存以及日志,以事件驱动
> + 支付案例:简单工厂 + 模版方法 + 策略模式 + 函数式接口
<table>
<tr>
<td><img src="img/支付案例UML.png"></td>
</tr>
</table>
+ 责任链模式:为请求创建了一个接收者对象的处理链,,对请求的发送者和接收者进行解耦
<table>
<tr>
<td><img src="img/责任链模式调用逻辑图.jpg"></td>
<td><img src="img/责任链模式UML类图.png"></td>
</tr>
</table>
> + 基于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
+ gateway
> 网关,基于`nacos`的可配置白名单
> TODO
> + 统一请求日志
> + 认证
> + 服务间其他信息
+ pdf
> 基于`freemarker`模版实现的后端生成PDF或在线预览功能
+ manage
> 后台管理模块
> 特色业务场景
> + 消息队列业务模拟(顺序消费、不重复消费、可靠消费) 死信队列、延时队列
+ auth
> 认证模块
> TODO
> + 权限
> + RBAC
> + 参考若依实现
> + 菜单、路由
> TODO
> + 文件上传minio
+ API文档
+ knife4j
+ business-service: 地址:http://127.0.0.1:20908/doc.html
+ task-service: 地址:http://127.0.0.1:20902/doc.html
+ 用户名:`test`
+ 密码:`123`
+ 监控面板
+ spring-boot-admin、spring-boot-starter-security
+ 地址:http://127.0.0.1:20906/login
+ 用户名:`actuator`
+ 密码:`actuator`
+ 国际化
+ 监控
+ 定时任务
+ API文档
+ Aop
+ ReSubmit(防止重复提交)
+ EasyCache (缓存)
+ 通用业务组件
+ 简单工厂 + 策略模式
+ 模版方法
+ Spring 高级
+ 观察者模式(监听、事件机制)
+ 自动注入Map
+ 监控自定义 预警(钉钉、邮件)
+ Mybatis Plus
+ 多数据源
+ 逻辑删除
+ 乐观锁
+ Model
+ lambda
+ 通用枚举
+ 联合主键`@MppMultiId`、`IMppService`、`MppBaseMapper`
+ 自动填充新增时间、修改时间
+ Mysql
+ 枚举类型
+ Json 类型
+ 分布式锁
+ 分布式事务
+ Sentinel
+ 链路追踪
+ 定时任务 Quartz
+ 设计模式:模版方法
+ Jpa 实现
+ 基于数据库悲观锁实现分布式锁,支持多节点部署
+ 保证不重复执行
+ 保证不漏执行
+ 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
+ 分布式定时任务框架 xxl_job
+ 分布式事务
+ 分布式锁
+ 并发编程
+ SQL优化
+ 脚本
+ 容器化
+ ELK
+ 日志配置
+ 启动初始化
+ `ApplicationRunner`
+ `CommandLineRunner`
+ http://127.0.0.1:20900 网关
+ 分布式文件存储 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)
+ 发生在前端
+ 浏览器的同源策略:协议、主机、端口
+ 三种后端解决方式
+ @CrossOrigin
+ Cross Filter
+ WebMvcConfigure
+ UML:Unified Modeling Language
+ 类图(两个矩形:顶类名称,上属性,下方法)
+ 属性: 权限 名称: 类型
+ 方法: 权限 方法名称(参数列表): 返回值类型
+ 权限:
+ ` ` default
+ `-` private
+ `+` public
+ `#` protected
+ 关系:
+ 关联关系:
+ 引用:实线实心三角形箭头指向被引用的一方
+ 双向关联:实线
+ 自关联: 实线实心三角形箭头指向自己
+ 聚合关系:整体和部分的关系,强烈的聚合关系,部分可以离开整体
+ 实线空心菱形指向整体
+ 组合关系: 整体和部分的关系,更强烈的聚合关系,部分不可以离开整体(头、嘴)
+ 实线实心菱形指向整体
+ 依赖关系: 耦合度最弱的一种关联关系(调用,引用)
+ 虚线虚线箭头指向被依赖的类
+ 继承关系(泛化关系)
+ 实线空心三角形指向父类
+ 实现关系
+ 虚线空心三角形箭头指向接口
+ nullSafeEquals
+ 枚举优化
+ 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)
+ true: 走代理,配置类中各个方法相互依赖
+ false: 不走代理,配置类中各个方法不依赖,可提高性能
+ pom优化
+ kettle
+ 缓存
+ 上下文待优化
+ 登录待优化
+ 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` 生成
## 场景启动器使用聚合scene
### Maven环境隔离
#### 引入打包插件
```xml
<!--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`
```xml
<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>
```
#### 配置使用
<table>
<tr>
<td><img src="img/maven环境隔离配置1.jpg"></td>
<td><img src="img/maven环境隔离配置2.jpg"></td>
</tr>
</table>
### `disruptor`的 ` log4j2`高性能异步日志
> 整体性能有显著提升,适用C端的大量日志场景
#### 添加依赖
```xml
<!--高性能内存队列-->
<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`冲突包
```xml
<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>
```
> 整体进行排除
```xml
<!--全局排除 logback 使用更高效的log4j实现-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
```
#### 配置 + 使用
<table>
<tr>
<td><img src="img/log4j异步日志配置.png"></td>
<td><img src="img/log3j异步日志测试API.png"></td>
<td><img src="img/log4j异步日志测试结果.png"></td>
</tr>
</table>
## 后续计划
+ 通用业务场景拆分封装
+ Cloud系、Dubbo系
+ 监控、告警
+ 前端部分
+ Vue3
+ Uniapp
+ Flutter
+ ELK
+ 容器化
## 待完成任务
+ es组件
+ 代码生成组件
+ 缓存预热进一步优化
+ httpclient5 优化
+ 京东async封装
+ fc-async
+ 微服务 cloud + eureka + ribbon...
+ 自定义ribbon负载均衡
+ `alibaba`微服务系
+ 设计模式系
+ file场景改造优化
+ 分片上传,断点续传,秒传
+ 基于nacaos动态配置策略
+ Guava 本地缓存
+ 数据权限通用实现
+ 其他优化
+ 多线程进度条任务脱离`Redis`实现
+ 迷你工作流
+ caffeine cache 本地缓存拓展 + 进一步优化
## 联系我
<img src="img/联系我.jpeg" style="width: 40%; height: 30%;text-align: center;">
## 文档版本
> 2024-01-04 09:37:47