# treasure **Repository Path**: packagejava/treasure ## Basic Information - **Project Name**: treasure - **Description**: Treasure是一个Java技术生态项目,涵盖了单体、微服务、DDD等架构实践,以兴趣、学习目的、技术积累为理念,逐步完善迭代。主要包含学习成长过程中一些技术点、工作中积累的一些心得,面试中一些业务场景模拟及解决方案一些常见、通用业务的解决方案、合理应用设计模式进行一些业务代码的重构优化、常用小轮子的积累、一些更优雅的编码实现、通用场景封装等内容。 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 65 - **Created**: 2025-07-25 - **Last Updated**: 2025-07-25 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README
2020-05-05~至今
P: 持久化对象
implements BaseCrudController
, BaseViewController { /** * 服务 */ @Autowired private IService
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)); } /** * 分页查询 * 分页参数由分页工具自动从Servlet上下文中获取 * * @param p 查询实体 * @return {@link ResultVO}<{@link PageVO} */ @ApiOperation("分页查询,返回PageVO") @ApiOperationSupport(author = "dingwen") @GetMapping("/default/page") @Override public ResultVO> 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> find(@PathVariable List ids) { return success(iService.listByIds(ids)); } /** * 获取数据表总记录条数 * * @return {@link ResultVO}<{@link Long}> */ @ApiOperation("获取数据表总记录条数,返回统计数量") @ApiOperationSupport(author = "dingwen") @GetMapping("/default/count") @Override public ResultVO count() { return success(iService.count()); } /** * 通过唯一键查询是否存在 * * true: 存在 * false: 不存在 * * * @param id 唯一键 * @return {@link ResultVO} */ @ApiOperation("通过唯一键查询是否存在,返回布尔值") @ApiOperationSupport(author = "dingwen") @GetMapping("/default/exists/{id}") @Override public ResultVO 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 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 create(@RequestBody List ps) { return genResult(iService.saveBatch(ps)); } /** * 删除单条记录 * * @param id 唯一键 * @return {@link ResultVO} */ @ApiOperation("删除单条记录,返回布尔值") @ApiOperationSupport(author = "dingwen") @DeleteMapping("/default/{id}") @Override public ResultVO 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 remove(@PathVariable List 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 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 ``` #### `druid`连接加密 > 使用`DruidEncryptUtils`生产密码,公钥,私钥,再增加对应配置即可 > 多数据源示例配置 ```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 `已经很方便了,但是对于字符串类型长度的校验,若数据表发生了改变需要同步维护实体中的字段校验信息。通用字符串字段长度校验组件就是为了解决这一问题,通过查询数据表的方式来进行字符串的长度校验,提供丰富的日志,确保不会出现因为字段过长倒是的数据库操作错误 #### 统一数据唯一性校验组件 > 当你需要保证表中某一字段唯一而又不想依赖数据库的唯一性约束,在数据提交的时候就完成校验。直接过滤掉这一类错误数据的数据库访问时实用此组件 > 若接口入口校验方法由父类继承而来会导致校验失效,可以重写父类方法再手动调用校验即可 #### 表信息获取工具`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 { /** * 全模糊 */ 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> dictPage(@ModelAttribute DictQuery dictQuery) { QueryWrapper queryWrapper = QueryUtils.buildQueryWrapper(dictQuery); Page 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 top.dingwen.io dic-spring-boot-starter 1.0.1 ``` #### 默认API #### 快速字典工具类`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 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 getDictValue(String dictType, String dictLabel) { DictDataServiceImpl dictDataService = SpringUtil.getBean(DictDataServiceImpl.class); if (Objects.isNull(dictDataService)) { return Optional.empty(); } return Optional.ofNullable(dictDataService.revertTranslate(dictType, dictLabel)); } } ``` #### 核心数据模型 #### 分布式缓存实现逻辑图 ### 翻译启动器 > 基于Jackson反序列化进行的字典翻译组件 #### 核心功能 + 基于注解的配置实现翻译 + 通用表字段的翻译 + 本地缓存与数据库查询并存提升性能 + 提供顶层翻译接口方便拓展`ITranslateService` + 注解`TranslateResult`形式的方法返回值自动翻译 #### 使用 ##### 引入依赖 ```xml top.dingwen.io translate-spring-boot-starter 1.0.0 ``` ##### 标注注解 ```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; /** * 翻译接口 * 泛型说明 * * T: 翻译的返回值 * P1: 翻译展示的列名称 * P2: 表名称 * P3: 数据库主键列名称 * P4: 主键值 * * * * @author dingwen * @since 2023/6/9 15:39 */ public interface ITranslateService { /** * 翻译 * * @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; /** * 映射字段 * 默认取当前字段的值 如果设置了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 getAllConfigs() { return converter.convert(configService.list(),ConfigVO.class); } } ``` ### Quartz定时任务启动器 > 基于数据库悲观锁实现分布式场景下的定时任务不漏跑,不重复执行问题 #### 核心功能 + 支持分布式调度 + 基于监听异步方式实现的日追踪 + 二次封装丰富的API + 后续提供界面操作 + 本地环境可灵活配置决定是否执行定时任务 #### 使用 ##### 引入依赖 ```xml com.dingwen quartz-spring-boot-starter 1.0.0-SNAPSHOT ``` ##### 启动项配置 > 在启动类上添加`@EnableQuartz`注解以开启`quartz`定时任务场景功能 ```java @EnableQuartz ``` ##### 继承`AbstractJob`实现自定义任务 ```java /** * TestJob * Quartz禁止并发执行 DisallowConcurrentExecution * @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 ..."); } } ``` ##### 配置定时任务 #### 默认API + 获取所有执行中的任务列表 + 获取定时任务信息列表 + 添加一个定时任务 + 修改运行中的任务信息 + 立即执行 + 分页查询任务执行日志 + 修改定时任务状态 + 删除定时任务 #### 核心数据模型 #### 待办 + 加缓存减少数据库查询次数 ### 系统配置启动器 #### 核心功能 + 通用系统配置实现 + 提供丰富的配置API + 分布式缓存支持 + Redis缓存与本地缓存配合使用专注提升效率 + 便捷查询配置工具 #### 使用 ##### 引入依赖 ```xml com.dingwen config-spring-boot-starter 1.0.0-SNAPSHOT ``` ##### 启动项配置 > 在启动类上添加`@EnableConfig`注解以开启系统通用配置场景功能 ```java @SpringBootApplication(scanBasePackages = "com.dingwen") ``` #### 默认API #### 快速系统配置工具类`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 getVal(String configKey) { Optional configOp = getConfig(configKey); return configOp.map(Config::getConfigVal); } /** * 获取配置 * * @param configKey 配置关键 * @return {@link Config} */ public static Optional getConfig(String configKey) { ConfigServiceImpl configService = SpringUtil.getBean(ConfigServiceImpl.class); if (Objects.isNull(configService)) { return Optional.empty(); } return Optional.ofNullable(configService.getOneByConfigKey(configKey)); } } ``` #### 核心数据模型 #### 分布式缓存实现逻辑图 ### Excel启动器 > 基于阿里开源`EasyExcel`进行二次封装,开箱即用 #### 核心功能 + 自定义转换器封装 + 自定义数据校验封装 + 导入、导出异常处理、事务处理 + 多行表头导入 + 大数据量分批次导入 + 简单导出以及模型映射导出 + 模板填充、组合模板填充 + 文件导出下载、文件上传导入 + 导出行高和列宽设置 + 导出图片内容 + Base64 + 字节数组 + 流 + 导出动态表头 + 合并单元格 + 导出超链接、批注、公式 + 表字段翻译 + 字典翻译`translate-spring-boot-starter` + 枚举翻译 + 自定义转换表达式翻译 `ExcelExpProperty` + 多线程导出导入优化 + 平铺导出自动合并相同单元格 `ExcelAutoMergeHandler` + 导入校验封装+错误文件下载【OSS】 + 自动计算宽度 + 注解封装导出逻辑 ### 文件启动器 > 一套包含前后端的一条龙的通用的文件场景,业务数据基于`MybatisPlus`存储,文件数据可存储与系统本地或任何一直OSS存储 #### 核心功能 + 存储方式 + 系统存储 + MiniIO + 阿里OSS + 任何一种OSS + 业务数据API + 进度条可视化 + 断点续传 + 分片上传 + 图片压缩 + 图片水印、文件水印 + 图片缩略图 + 文件下载,文件压缩下载 + 文件预览,图片预览 ### 数据归档启动器 > **适用场景**: 一张表中有1000w的数据,但是可能其中有800w是历史数据(冷数据),我们可能在业务上已经不再使用这些数据,如果放在业务表中,可能会影响我们业务的效率,所以我们可以将其归档到另一张表中,将其变成冷数据 > > **流程:**数据归档的流程大概是: 1)从原数据表获取需要归档的数据;2)将这部分数据插入归档的表中;3)将元数据表中这部分数据删除 > > **注意点:**我们数据归档中,事务的提交应该采用手动事务提交,如果使用大事务的情况下,可能会导致事务超时等一系列的问题!还有,我们需要实现可控归档,需要达到我们可以手动控制是否归档、停止,并且还能动态配置归档范围 #### 概览 #### 使用 ##### 引入依赖 ```xml com.dingwen db-backup-spring-boot-starter 1.0.0-SNAPSHOT ``` ##### 启动项配置 > 在启动类上添加`@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 { @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 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 queryData(BackUpDataRule backUpDataRule) { log.info("[数据归档模块]\t[用户数据查询]"); return Collections.emptyList(); } @Override public void insertData(List datas) { log.info("[数据归档模块]\t[用户数据插入]"); } @Override public void deleteData(List datas) { log.info("[数据归档模块]\t[用户数据删除]"); } } ``` #### API ```java BackupDataFactory.BACKUP_DATA_SERVICE.get(TODO).exeBackUpData(); ``` ### 数据变更记录启动器 > 针对字段更新的变更日志的通用实现 #### 核心功能 + 实体映射到表字段变更比对 + 实体外部关联表字段翻译 + 实体枚举字段翻译 + 自定义拓展字段 #### 概览 #### 数据模型 #### 使用 ##### 引入依赖 ```xml com.dingwen change-log-spring-boot-starter 1.0.0-SNAPSHOT ``` ##### 启动项配置 > 在启动类上添加`@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> fieldConverts = new HashMap<>(2); fieldConverts.put("dictType", dictService::convert); fieldConverts.put("status", o -> IBaseEnum.fastDescFrom(DicStatus.class,((IBaseEnum)o).getCode())); event.setFieldConverts(fieldConverts); List extFields = new ArrayList<>(2); extFields.add(TableFieldInfo.builder().columnName("dictModule").columnComment("字典所属模块").build()); event.setExtFields(extFields); SpringUtil.publishEvent(event); } ``` ### 事件场景启动器 > 常规的`Spring`事件订阅机制实现存在不能重发,可靠性,幂等性等问题.故对事件机制进行拓展解决上述问题. > 业务方只需要继承`BaseEvent`事件及`BaseListener`再通过`SpringUtils`发布事件就可以实现该功能 #### 类图概览 #### 核心功能 > 采用持久化的方式保证重试可靠功能,当`Spring`环境中没有找到对应的实现时则会按照默认的方式进行. > > 初次之外也提供了基于`Redis`分布式锁方式的幂等不重复执行可靠性实现. ```java /** * 抽象事件监听器 * * TODO 事务控制 * TODO 分布式锁实现 * * @author dingwen * @since 2024/3/27 11:12 */ @Slf4j public abstract class BaseListener implements ApplicationListener { /** * 处理业务方法 * * @param baseEvent base event */ abstract protected void handler(T baseEvent); /** * 当应用程序发生事件时调用此方法 * * 此方法并不能保证幂等,也不能保证多线程条件下不重复执行. * 若有此需求请考虑使用分布式锁控制的实现 * * * @param event event */ @Override public void onApplicationEvent(T event) { log.info("[base]抽象事件监听器,开始执行,事件对象:{}", JSONUtil.toJsonStr(event)); Map 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 /** * 安全的,能保证幂等的,不重复执行的,并发安全的监听器实现 * * 后期可采用动态代理优化 * * * @author dingwen * @since 2024/3/27 15:10 */ @Slf4j public abstract class AbstractSafeBaseListener extends AbstractBaseListener { @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 com.dingwen event-spring-boot-starter 1.0.0-SNAPSHOT ``` ##### 启动项配置 > 在启动类上添加`@EnableEvent `注解以开启场景功能 ```java @EnableEvent ``` ##### 其他配置 > 若是并发安全的实现则需要配置锁相关参数 ```shell dingwen: treasure: # 事件 event: # 时间 lock-time: 10 # 时间单位: seconds lock-time-unit: seconds ``` #### API + 事件重发: `boolean retry(Long eventId)` + 事件删除 + 事件列表 `Page queryEventPage(QueryWrapper queryWrapper)` ### Jwt场景启动器 > 通过灵活的配置,实现Jwt的生成,校验,刷新等. #### 使用 ##### 引入依赖 ```xml top.dingwen.io jwt-spring-boot-starter 1.0.2 ``` ##### 配置 ```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` #### 测试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 com.dingwen security-plus-spring-boot-starter 1.0.0-SNAPSHOT ``` ##### 启动项配置 > 在启动类上添加`@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 ``` #### 资源权限数据模型 #### 资源权限内置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> loadSourceAttributes() { List 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> buildAttributes(List authResList) { Map> result = new HashMap<>(authResList.size()); for (AuthResBO ar : authResList) { List 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`等 #### 核心功能 + 接口权限动态设定 [支持拓展规则] + 用户分组+层级 + 角色分组+层级+互斥+继承 + 菜单+按钮+权限灵活控制 + 租户 + 表字段方式 + 动态数据源方式 #### 简化版权限模型 com.dingwen auth-spring-boot-starter 1.0.0-SNAPSHOT ``` ##### 启动项配置 > 在启动类上添加`@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` ### WebSocket场景启动器 > https://gitee.com/dingwen-gitee/websocket-spring-boot-starter ## 核心服务 ### 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` + 生产者进行可靠性消息投递 + 消费者手动确认 + 消息落库(状态管理、消费顺序控制) + 定时任务补偿 ##### 流程图  #### 优化工具类 > 参考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(); } } ```  #### 复杂规则校验 > Redis + JVM 双缓存 ```js // 测试js function add(op1, op2) { return op1 + op2 } add(a, b) ``` ##### 整体思路、功能点  > 通过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 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 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 queryProgress(@PathVariable("taskId") String taskId, @RequestParam("taskType") String taskType) { TaskType taskTypeByCode = EnumUtil.getEnumByCode(TaskType.class, taskType); return ResultGenerator.genSuccessResult(testProgressBarTask.queryProcess(taskId, taskTypeByCode)); } } ``` ##### 功能概览 #### 自定义注解实现加解密、脱敏 `SensitiveController` + 枚举类`SensitiveEnum` + 实体 `SensitiveEntity` + 自定义注解 + SensitiveDecode 解密 + SensitiveEncode 加密 + SensitiveField 字段标识 + SensitiveInfoUtil 加解密、脱敏工具类 + AesEncryptUtil AES工具类 ##### 功能概览 #### 阿里云短信对接 `SmsController` ##### 功能概览 #### 微信公众号定制消息推送`WechatPubController` > 开放平台对接(基于Spring提供定时任务实现): > + 天行数据 > + 百度地图 > + 微信公众号平台 #### common-influxdb > 时序数据库案例 > + [官网](https://docs.influxdata.com/) > + 接口 `IotDataService` #### 数据库缓存一致性解决方案 ##### 正常流程 > 以上的流程没有问题,当数据变更的时候,如何能保证将缓存同步到最新呢? ##### 先更新数据库,再更新缓存 > 假设数据库更新成功,缓存更新失败,在缓存过期失效之前,读取到的缓存数据都是旧的 ##### 先更新缓存,再更新数据库 > 假设缓存更新成功,数据库更新失败,那读取到的数据都是错误的 ##### 先删除缓存,再更新数据库 > 假设删除缓成功,此时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 并发编程实战》 > 如果我期望目标利用率为90%(多核90),那么需要的线程数为: > > 核心数12 * 利用率0.9 * (1 + 50(sleep时间)/50(循环50_000_000耗时)) ≈ 22 > 通过线程数来计算CPU利用率 > 线程数22 / (核心数12 * (1 + 50(sleep时间)/50(循环50_000_000耗时))) ≈ 0.9 > > 虽然公式很好,但在真实的程序中,**一般很难获得准确的等待时间和计算时间,因为程序很复杂,不只是“计算”**。一段代码中会有很多的内存读写,计算,I/O 等复合操作,精确的获取这两个指标很难,所以光靠公式计算线程数过于理想化。 ##### 真实程序中的线程数 > 没有固定答案,先设定预期,比如我期望的CPU利用率在多少,负载在多少,GC频率多少之类的指标后,再通过测试不断的调整到一个合理的线程数 > > 比如一个普通的,SpringBoot 为基础的业务系统,默认Tomcat容器+HikariCP连接池+G1回收器,如果此时项目中也需要一个业务场景的多线程(或者线程池)来异步/并行执行业务流程。 > > 此时我按照上面的公式来规划线程数的话,误差一定会很大。因为此时这台主机上,已经有很多运行中的线程了,Tomcat有自己的线程池,HikariCP也有自己的后台线程,JVM也有一些编译的线程,连G1都有自己的后台线程。这些线程也是运行在当前进程、当前主机上的,也会占用CPU的资源。 ##### 一般的流程 > 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 * 自带查询分页性能: * * 第 1 页 10 条关闭count查询优化耗时:4秒 * 第 100,00 页 100 条关闭count查询优化耗时:5秒 * 第 100,000 页 100 条关闭count查询优化耗时:5秒 * * 第 1 页 10 条打开count查询优化耗时:4秒 * 第 100,00 页 100 条打开count查询优化耗时:4秒 * 第 100,000 页 100 条打开count查询优化耗时:4秒 * * * 优化后性能: * * 第 1 页 10 条耗时:30~80毫秒 * 第 100,00 页 100 条耗时:30~80毫秒 * 第 100,000 页 100 条耗时:30~80秒 * * * 再进行统计总记录条数时会遇到瓶颈,正确的处理方式应该是杜绝深分页,不会有用户往下翻到地100页 * 一般选择50页即可 * * @param pageDto 页面dto */ @PostMapping("/deep-page") @ApiOperation("深分页查询优化") public Result> deepPage(@RequestBody PageDto pageDto) { Long current = pageDto.getCurrent(); Long pageSize = pageDto.getPageSize(); Page 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张表  #### 方案思路 > 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 ``` #### 其他 ##### Kettle自带监控页面 ##### 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 | | 执行结果:1: 成功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 ``` ### `treasure-poi-tl` > poi-tl 官网: https://github.com/Sayi/poi-tl > `word`模板渲染解决方案,拒绝手动维护`xml`文件 ### 钉钉企业微信预警`treasure-dingtalk-ger` > 钉鸽官网: https://github.com/AnswerAIL/dingtalk-spring-boot-starter #### 概览 #### 工具类 `DingerUtils` > 可在全局异常处理处调用`DingerUtils.send(e);` ```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 { @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 { @Override public void onApplicationEvent(ApplicationReadyEvent event) { // 可依据环境判断是否执行 DingerUtils.send("清廉系统后端服务已启动"); } } ``` ### 设计模式`treasure-gof` ### 其他临时文档 #### Maven私服配置 ```xml rdc-releases 62c3e2f6a908b6a4db54fa26 5V8N7KJ]rvtt rdc-snapshots 62c3e2f6a908b6a4db54fa26 5V8N7KJ]rvtt mirror central,jcenter,!rdc-releases,!rdc-snapshots mirror https://maven.aliyun.com/nexus/content/groups/public rdc rdc-releases::default::https://packages.aliyun.com/maven/repository/2380560-release-WXL1gl/ rdc-snapshots::default::https://packages.aliyun.com/maven/repository/2380560-snapshot-vKCASA/ rdc ``` + business > 优雅代码业务组件实现(MybatisPlus) > 特色业务场景 > + 线程池案例 > + 批量save > + 自定义MVC反序列化方式 > + 缓存业务场景自定义注解:@EasyCache > + 国际化 > + 状态模式+简单工厂模式实现:多阶段灵活控制状态流转 设计模式六大原则: + 单一职责 + 接口隔离(Servlet filter) + 依赖倒置 + 迪米特(最少知道原则) + 里氏替换 (父类抽象方法应保持行为一致,非抽象方法不应去重写) + 开闭原则 + 合成复用 + 状态模式: 对象不同的状态导致不同的行为 + 主要角色: + 抽象状态(接口或抽象类) + 具体状态(抽象状态的子类或实现类) + 环境类 (状态持有着,依据不同的状态出发不同的行为)  > + MybatisPlus:乐观锁、逻辑删除、自动枚举转换、多数据源、Model、自动填充、分页等其他通用API > + 观察者模式:(事件对象、监听)下订单走库存以及日志,以事件驱动 > + 支付案例:简单工厂 + 模版方法 + 策略模式 + 函数式接口 + 责任链模式:为请求创建了一个接收者对象的处理链,,对请求的发送者和接收者进行解耦 > + 基于redis、redisson的分布式锁 > + 防止重复提交自定义注解:@ReSubmit > + (解决数据库和缓存不一致的问题)系统配置:db、redis、自定义缓存(利用redis发布订阅实现三级缓存) > + 基于Mybatis的通用crudController封装已经整合API文档 > + 全局统一返回 > + 全局异常处理 > + 优雅DTO、VO、BO、PO转换 > + feign调用(整合hystrix实现服务降级) > + 系统配置实现(三级分布式缓存)redis 发布订阅实现跨内存刷新 > + AOP 切面实现方法调用前后入参、返回值、耗时等调试日志(全局所有controller)、基于配置灵活开启关闭 > + 消息队列业务模拟(保证顺序消费、保证不重复消费、消息可靠投递、消息可靠消费) > + 操作日志(AOP+Sprint EL 实现同步ElasticSearch)参考美团2021年技术年报实现、基于配置灵活开启关闭 > TODO > + redisson 分布式锁 > + 规范转换Bean mapstruct > + 基于JPA的通用crudController封装 > + 通用crudController封装 mongoDB + 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 org.apache.maven.plugins maven-resources-plugin ${project.build.sourceEncoding} @ false ${maven-resources-plugin.version} ``` #### `resource` ```xml src/main/resources true **/*.xlsx **/*.xml **/*.docx src/main/resources false **/*.xlsx **/*.xml **/*.docx ``` #### 配置使用 ### `disruptor`的 ` log4j2`高性能异步日志 > 整体性能有显著提升,适用C端的大量日志场景 #### 添加依赖 ```xml com.lmax disruptor ${disruptor.version} org.springframework.boot spring-boot-starter-log4j2 ``` #### 排除相关`logback`冲突包 ```xml org.springframework.boot spring-boot-starter-web spring-boot-starter-logging org.springframework.boot ``` > 整体进行排除 ```xml org.springframework.boot spring-boot-starter-logging * * ``` #### 配置 + 使用 ## 后续计划 + 通用业务场景拆分封装 + Cloud系、Dubbo系 + 监控、告警 + 前端部分 + Vue3 + Uniapp + Flutter + ELK + 容器化 ## 待完成任务 + es组件 + 代码生成组件 + 缓存预热进一步优化 + httpclient5 优化 + 京东async封装 + fc-async + 微服务 cloud + eureka + ribbon... + 自定义ribbon负载均衡 + `alibaba`微服务系 + 设计模式系 + file场景改造优化 + 分片上传,断点续传,秒传 + 基于nacaos动态配置策略 + Guava 本地缓存 + 数据权限通用实现 + 其他优化 + 多线程进度条任务脱离`Redis`实现 + 迷你工作流 + caffeine cache 本地缓存拓展 + 进一步优化 ## 联系我 ## 文档版本 > 2024-01-04 09:37:47
ps) { return genResult(iService.saveBatch(ps)); } /** * 删除单条记录 * * @param id 唯一键 * @return {@link ResultVO} */ @ApiOperation("删除单条记录,返回布尔值") @ApiOperationSupport(author = "dingwen") @DeleteMapping("/default/{id}") @Override public ResultVO 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 remove(@PathVariable List 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 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 ``` #### `druid`连接加密 > 使用`DruidEncryptUtils`生产密码,公钥,私钥,再增加对应配置即可 > 多数据源示例配置 ```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 `已经很方便了,但是对于字符串类型长度的校验,若数据表发生了改变需要同步维护实体中的字段校验信息。通用字符串字段长度校验组件就是为了解决这一问题,通过查询数据表的方式来进行字符串的长度校验,提供丰富的日志,确保不会出现因为字段过长倒是的数据库操作错误 #### 统一数据唯一性校验组件 > 当你需要保证表中某一字段唯一而又不想依赖数据库的唯一性约束,在数据提交的时候就完成校验。直接过滤掉这一类错误数据的数据库访问时实用此组件 > 若接口入口校验方法由父类继承而来会导致校验失效,可以重写父类方法再手动调用校验即可 #### 表信息获取工具`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 { /** * 全模糊 */ 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> dictPage(@ModelAttribute DictQuery dictQuery) { QueryWrapper queryWrapper = QueryUtils.buildQueryWrapper(dictQuery); Page 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 top.dingwen.io dic-spring-boot-starter 1.0.1 ``` #### 默认API #### 快速字典工具类`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 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 getDictValue(String dictType, String dictLabel) { DictDataServiceImpl dictDataService = SpringUtil.getBean(DictDataServiceImpl.class); if (Objects.isNull(dictDataService)) { return Optional.empty(); } return Optional.ofNullable(dictDataService.revertTranslate(dictType, dictLabel)); } } ``` #### 核心数据模型 #### 分布式缓存实现逻辑图 ### 翻译启动器 > 基于Jackson反序列化进行的字典翻译组件 #### 核心功能 + 基于注解的配置实现翻译 + 通用表字段的翻译 + 本地缓存与数据库查询并存提升性能 + 提供顶层翻译接口方便拓展`ITranslateService` + 注解`TranslateResult`形式的方法返回值自动翻译 #### 使用 ##### 引入依赖 ```xml top.dingwen.io translate-spring-boot-starter 1.0.0 ``` ##### 标注注解 ```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; /** * 翻译接口 * 泛型说明 * * T: 翻译的返回值 * P1: 翻译展示的列名称 * P2: 表名称 * P3: 数据库主键列名称 * P4: 主键值 * * * * @author dingwen * @since 2023/6/9 15:39 */ public interface ITranslateService { /** * 翻译 * * @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; /** * 映射字段 * 默认取当前字段的值 如果设置了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 getAllConfigs() { return converter.convert(configService.list(),ConfigVO.class); } } ``` ### Quartz定时任务启动器 > 基于数据库悲观锁实现分布式场景下的定时任务不漏跑,不重复执行问题 #### 核心功能 + 支持分布式调度 + 基于监听异步方式实现的日追踪 + 二次封装丰富的API + 后续提供界面操作 + 本地环境可灵活配置决定是否执行定时任务 #### 使用 ##### 引入依赖 ```xml com.dingwen quartz-spring-boot-starter 1.0.0-SNAPSHOT ``` ##### 启动项配置 > 在启动类上添加`@EnableQuartz`注解以开启`quartz`定时任务场景功能 ```java @EnableQuartz ``` ##### 继承`AbstractJob`实现自定义任务 ```java /** * TestJob * Quartz禁止并发执行 DisallowConcurrentExecution * @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 ..."); } } ``` ##### 配置定时任务 #### 默认API + 获取所有执行中的任务列表 + 获取定时任务信息列表 + 添加一个定时任务 + 修改运行中的任务信息 + 立即执行 + 分页查询任务执行日志 + 修改定时任务状态 + 删除定时任务 #### 核心数据模型 #### 待办 + 加缓存减少数据库查询次数 ### 系统配置启动器 #### 核心功能 + 通用系统配置实现 + 提供丰富的配置API + 分布式缓存支持 + Redis缓存与本地缓存配合使用专注提升效率 + 便捷查询配置工具 #### 使用 ##### 引入依赖 ```xml com.dingwen config-spring-boot-starter 1.0.0-SNAPSHOT ``` ##### 启动项配置 > 在启动类上添加`@EnableConfig`注解以开启系统通用配置场景功能 ```java @SpringBootApplication(scanBasePackages = "com.dingwen") ``` #### 默认API #### 快速系统配置工具类`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 getVal(String configKey) { Optional configOp = getConfig(configKey); return configOp.map(Config::getConfigVal); } /** * 获取配置 * * @param configKey 配置关键 * @return {@link Config} */ public static Optional getConfig(String configKey) { ConfigServiceImpl configService = SpringUtil.getBean(ConfigServiceImpl.class); if (Objects.isNull(configService)) { return Optional.empty(); } return Optional.ofNullable(configService.getOneByConfigKey(configKey)); } } ``` #### 核心数据模型 #### 分布式缓存实现逻辑图 ### Excel启动器 > 基于阿里开源`EasyExcel`进行二次封装,开箱即用 #### 核心功能 + 自定义转换器封装 + 自定义数据校验封装 + 导入、导出异常处理、事务处理 + 多行表头导入 + 大数据量分批次导入 + 简单导出以及模型映射导出 + 模板填充、组合模板填充 + 文件导出下载、文件上传导入 + 导出行高和列宽设置 + 导出图片内容 + Base64 + 字节数组 + 流 + 导出动态表头 + 合并单元格 + 导出超链接、批注、公式 + 表字段翻译 + 字典翻译`translate-spring-boot-starter` + 枚举翻译 + 自定义转换表达式翻译 `ExcelExpProperty` + 多线程导出导入优化 + 平铺导出自动合并相同单元格 `ExcelAutoMergeHandler` + 导入校验封装+错误文件下载【OSS】 + 自动计算宽度 + 注解封装导出逻辑 ### 文件启动器 > 一套包含前后端的一条龙的通用的文件场景,业务数据基于`MybatisPlus`存储,文件数据可存储与系统本地或任何一直OSS存储 #### 核心功能 + 存储方式 + 系统存储 + MiniIO + 阿里OSS + 任何一种OSS + 业务数据API + 进度条可视化 + 断点续传 + 分片上传 + 图片压缩 + 图片水印、文件水印 + 图片缩略图 + 文件下载,文件压缩下载 + 文件预览,图片预览 ### 数据归档启动器 > **适用场景**: 一张表中有1000w的数据,但是可能其中有800w是历史数据(冷数据),我们可能在业务上已经不再使用这些数据,如果放在业务表中,可能会影响我们业务的效率,所以我们可以将其归档到另一张表中,将其变成冷数据 > > **流程:**数据归档的流程大概是: 1)从原数据表获取需要归档的数据;2)将这部分数据插入归档的表中;3)将元数据表中这部分数据删除 > > **注意点:**我们数据归档中,事务的提交应该采用手动事务提交,如果使用大事务的情况下,可能会导致事务超时等一系列的问题!还有,我们需要实现可控归档,需要达到我们可以手动控制是否归档、停止,并且还能动态配置归档范围 #### 概览 #### 使用 ##### 引入依赖 ```xml com.dingwen db-backup-spring-boot-starter 1.0.0-SNAPSHOT ``` ##### 启动项配置 > 在启动类上添加`@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 { @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 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 queryData(BackUpDataRule backUpDataRule) { log.info("[数据归档模块]\t[用户数据查询]"); return Collections.emptyList(); } @Override public void insertData(List datas) { log.info("[数据归档模块]\t[用户数据插入]"); } @Override public void deleteData(List datas) { log.info("[数据归档模块]\t[用户数据删除]"); } } ``` #### API ```java BackupDataFactory.BACKUP_DATA_SERVICE.get(TODO).exeBackUpData(); ``` ### 数据变更记录启动器 > 针对字段更新的变更日志的通用实现 #### 核心功能 + 实体映射到表字段变更比对 + 实体外部关联表字段翻译 + 实体枚举字段翻译 + 自定义拓展字段 #### 概览 #### 数据模型 #### 使用 ##### 引入依赖 ```xml com.dingwen change-log-spring-boot-starter 1.0.0-SNAPSHOT ``` ##### 启动项配置 > 在启动类上添加`@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> fieldConverts = new HashMap<>(2); fieldConverts.put("dictType", dictService::convert); fieldConverts.put("status", o -> IBaseEnum.fastDescFrom(DicStatus.class,((IBaseEnum)o).getCode())); event.setFieldConverts(fieldConverts); List extFields = new ArrayList<>(2); extFields.add(TableFieldInfo.builder().columnName("dictModule").columnComment("字典所属模块").build()); event.setExtFields(extFields); SpringUtil.publishEvent(event); } ``` ### 事件场景启动器 > 常规的`Spring`事件订阅机制实现存在不能重发,可靠性,幂等性等问题.故对事件机制进行拓展解决上述问题. > 业务方只需要继承`BaseEvent`事件及`BaseListener`再通过`SpringUtils`发布事件就可以实现该功能 #### 类图概览 #### 核心功能 > 采用持久化的方式保证重试可靠功能,当`Spring`环境中没有找到对应的实现时则会按照默认的方式进行. > > 初次之外也提供了基于`Redis`分布式锁方式的幂等不重复执行可靠性实现. ```java /** * 抽象事件监听器 * * TODO 事务控制 * TODO 分布式锁实现 * * @author dingwen * @since 2024/3/27 11:12 */ @Slf4j public abstract class BaseListener implements ApplicationListener { /** * 处理业务方法 * * @param baseEvent base event */ abstract protected void handler(T baseEvent); /** * 当应用程序发生事件时调用此方法 * * 此方法并不能保证幂等,也不能保证多线程条件下不重复执行. * 若有此需求请考虑使用分布式锁控制的实现 * * * @param event event */ @Override public void onApplicationEvent(T event) { log.info("[base]抽象事件监听器,开始执行,事件对象:{}", JSONUtil.toJsonStr(event)); Map 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 /** * 安全的,能保证幂等的,不重复执行的,并发安全的监听器实现 * * 后期可采用动态代理优化 * * * @author dingwen * @since 2024/3/27 15:10 */ @Slf4j public abstract class AbstractSafeBaseListener extends AbstractBaseListener { @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 com.dingwen event-spring-boot-starter 1.0.0-SNAPSHOT ``` ##### 启动项配置 > 在启动类上添加`@EnableEvent `注解以开启场景功能 ```java @EnableEvent ``` ##### 其他配置 > 若是并发安全的实现则需要配置锁相关参数 ```shell dingwen: treasure: # 事件 event: # 时间 lock-time: 10 # 时间单位: seconds lock-time-unit: seconds ``` #### API + 事件重发: `boolean retry(Long eventId)` + 事件删除 + 事件列表 `Page queryEventPage(QueryWrapper queryWrapper)` ### Jwt场景启动器 > 通过灵活的配置,实现Jwt的生成,校验,刷新等. #### 使用 ##### 引入依赖 ```xml top.dingwen.io jwt-spring-boot-starter 1.0.2 ``` ##### 配置 ```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` #### 测试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 com.dingwen security-plus-spring-boot-starter 1.0.0-SNAPSHOT ``` ##### 启动项配置 > 在启动类上添加`@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 ``` #### 资源权限数据模型 #### 资源权限内置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> loadSourceAttributes() { List 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> buildAttributes(List authResList) { Map> result = new HashMap<>(authResList.size()); for (AuthResBO ar : authResList) { List 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`等 #### 核心功能 + 接口权限动态设定 [支持拓展规则] + 用户分组+层级 + 角色分组+层级+互斥+继承 + 菜单+按钮+权限灵活控制 + 租户 + 表字段方式 + 动态数据源方式 #### 简化版权限模型 com.dingwen auth-spring-boot-starter 1.0.0-SNAPSHOT ``` ##### 启动项配置 > 在启动类上添加`@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` ### WebSocket场景启动器 > https://gitee.com/dingwen-gitee/websocket-spring-boot-starter ## 核心服务 ### 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` + 生产者进行可靠性消息投递 + 消费者手动确认 + 消息落库(状态管理、消费顺序控制) + 定时任务补偿 ##### 流程图  #### 优化工具类 > 参考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(); } } ```  #### 复杂规则校验 > Redis + JVM 双缓存 ```js // 测试js function add(op1, op2) { return op1 + op2 } add(a, b) ``` ##### 整体思路、功能点  > 通过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 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 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 queryProgress(@PathVariable("taskId") String taskId, @RequestParam("taskType") String taskType) { TaskType taskTypeByCode = EnumUtil.getEnumByCode(TaskType.class, taskType); return ResultGenerator.genSuccessResult(testProgressBarTask.queryProcess(taskId, taskTypeByCode)); } } ``` ##### 功能概览 #### 自定义注解实现加解密、脱敏 `SensitiveController` + 枚举类`SensitiveEnum` + 实体 `SensitiveEntity` + 自定义注解 + SensitiveDecode 解密 + SensitiveEncode 加密 + SensitiveField 字段标识 + SensitiveInfoUtil 加解密、脱敏工具类 + AesEncryptUtil AES工具类 ##### 功能概览 #### 阿里云短信对接 `SmsController` ##### 功能概览 #### 微信公众号定制消息推送`WechatPubController` > 开放平台对接(基于Spring提供定时任务实现): > + 天行数据 > + 百度地图 > + 微信公众号平台 #### common-influxdb > 时序数据库案例 > + [官网](https://docs.influxdata.com/) > + 接口 `IotDataService` #### 数据库缓存一致性解决方案 ##### 正常流程 > 以上的流程没有问题,当数据变更的时候,如何能保证将缓存同步到最新呢? ##### 先更新数据库,再更新缓存 > 假设数据库更新成功,缓存更新失败,在缓存过期失效之前,读取到的缓存数据都是旧的 ##### 先更新缓存,再更新数据库 > 假设缓存更新成功,数据库更新失败,那读取到的数据都是错误的 ##### 先删除缓存,再更新数据库 > 假设删除缓成功,此时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 并发编程实战》 > 如果我期望目标利用率为90%(多核90),那么需要的线程数为: > > 核心数12 * 利用率0.9 * (1 + 50(sleep时间)/50(循环50_000_000耗时)) ≈ 22 > 通过线程数来计算CPU利用率 > 线程数22 / (核心数12 * (1 + 50(sleep时间)/50(循环50_000_000耗时))) ≈ 0.9 > > 虽然公式很好,但在真实的程序中,**一般很难获得准确的等待时间和计算时间,因为程序很复杂,不只是“计算”**。一段代码中会有很多的内存读写,计算,I/O 等复合操作,精确的获取这两个指标很难,所以光靠公式计算线程数过于理想化。 ##### 真实程序中的线程数 > 没有固定答案,先设定预期,比如我期望的CPU利用率在多少,负载在多少,GC频率多少之类的指标后,再通过测试不断的调整到一个合理的线程数 > > 比如一个普通的,SpringBoot 为基础的业务系统,默认Tomcat容器+HikariCP连接池+G1回收器,如果此时项目中也需要一个业务场景的多线程(或者线程池)来异步/并行执行业务流程。 > > 此时我按照上面的公式来规划线程数的话,误差一定会很大。因为此时这台主机上,已经有很多运行中的线程了,Tomcat有自己的线程池,HikariCP也有自己的后台线程,JVM也有一些编译的线程,连G1都有自己的后台线程。这些线程也是运行在当前进程、当前主机上的,也会占用CPU的资源。 ##### 一般的流程 > 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 * 自带查询分页性能: * * 第 1 页 10 条关闭count查询优化耗时:4秒 * 第 100,00 页 100 条关闭count查询优化耗时:5秒 * 第 100,000 页 100 条关闭count查询优化耗时:5秒 * * 第 1 页 10 条打开count查询优化耗时:4秒 * 第 100,00 页 100 条打开count查询优化耗时:4秒 * 第 100,000 页 100 条打开count查询优化耗时:4秒 * * * 优化后性能: * * 第 1 页 10 条耗时:30~80毫秒 * 第 100,00 页 100 条耗时:30~80毫秒 * 第 100,000 页 100 条耗时:30~80秒 * * * 再进行统计总记录条数时会遇到瓶颈,正确的处理方式应该是杜绝深分页,不会有用户往下翻到地100页 * 一般选择50页即可 * * @param pageDto 页面dto */ @PostMapping("/deep-page") @ApiOperation("深分页查询优化") public Result> deepPage(@RequestBody PageDto pageDto) { Long current = pageDto.getCurrent(); Long pageSize = pageDto.getPageSize(); Page 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张表  #### 方案思路 > 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 ``` #### 其他 ##### Kettle自带监控页面 ##### 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 | | 执行结果:1: 成功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 ``` ### `treasure-poi-tl` > poi-tl 官网: https://github.com/Sayi/poi-tl > `word`模板渲染解决方案,拒绝手动维护`xml`文件 ### 钉钉企业微信预警`treasure-dingtalk-ger` > 钉鸽官网: https://github.com/AnswerAIL/dingtalk-spring-boot-starter #### 概览 #### 工具类 `DingerUtils` > 可在全局异常处理处调用`DingerUtils.send(e);` ```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 { @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 { @Override public void onApplicationEvent(ApplicationReadyEvent event) { // 可依据环境判断是否执行 DingerUtils.send("清廉系统后端服务已启动"); } } ``` ### 设计模式`treasure-gof` ### 其他临时文档 #### Maven私服配置 ```xml rdc-releases 62c3e2f6a908b6a4db54fa26 5V8N7KJ]rvtt rdc-snapshots 62c3e2f6a908b6a4db54fa26 5V8N7KJ]rvtt mirror central,jcenter,!rdc-releases,!rdc-snapshots mirror https://maven.aliyun.com/nexus/content/groups/public rdc rdc-releases::default::https://packages.aliyun.com/maven/repository/2380560-release-WXL1gl/ rdc-snapshots::default::https://packages.aliyun.com/maven/repository/2380560-snapshot-vKCASA/ rdc ``` + business > 优雅代码业务组件实现(MybatisPlus) > 特色业务场景 > + 线程池案例 > + 批量save > + 自定义MVC反序列化方式 > + 缓存业务场景自定义注解:@EasyCache > + 国际化 > + 状态模式+简单工厂模式实现:多阶段灵活控制状态流转 设计模式六大原则: + 单一职责 + 接口隔离(Servlet filter) + 依赖倒置 + 迪米特(最少知道原则) + 里氏替换 (父类抽象方法应保持行为一致,非抽象方法不应去重写) + 开闭原则 + 合成复用 + 状态模式: 对象不同的状态导致不同的行为 + 主要角色: + 抽象状态(接口或抽象类) + 具体状态(抽象状态的子类或实现类) + 环境类 (状态持有着,依据不同的状态出发不同的行为)  > + MybatisPlus:乐观锁、逻辑删除、自动枚举转换、多数据源、Model、自动填充、分页等其他通用API > + 观察者模式:(事件对象、监听)下订单走库存以及日志,以事件驱动 > + 支付案例:简单工厂 + 模版方法 + 策略模式 + 函数式接口 + 责任链模式:为请求创建了一个接收者对象的处理链,,对请求的发送者和接收者进行解耦 > + 基于redis、redisson的分布式锁 > + 防止重复提交自定义注解:@ReSubmit > + (解决数据库和缓存不一致的问题)系统配置:db、redis、自定义缓存(利用redis发布订阅实现三级缓存) > + 基于Mybatis的通用crudController封装已经整合API文档 > + 全局统一返回 > + 全局异常处理 > + 优雅DTO、VO、BO、PO转换 > + feign调用(整合hystrix实现服务降级) > + 系统配置实现(三级分布式缓存)redis 发布订阅实现跨内存刷新 > + AOP 切面实现方法调用前后入参、返回值、耗时等调试日志(全局所有controller)、基于配置灵活开启关闭 > + 消息队列业务模拟(保证顺序消费、保证不重复消费、消息可靠投递、消息可靠消费) > + 操作日志(AOP+Sprint EL 实现同步ElasticSearch)参考美团2021年技术年报实现、基于配置灵活开启关闭 > TODO > + redisson 分布式锁 > + 规范转换Bean mapstruct > + 基于JPA的通用crudController封装 > + 通用crudController封装 mongoDB + 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 org.apache.maven.plugins maven-resources-plugin ${project.build.sourceEncoding} @ false ${maven-resources-plugin.version} ``` #### `resource` ```xml src/main/resources true **/*.xlsx **/*.xml **/*.docx src/main/resources false **/*.xlsx **/*.xml **/*.docx ``` #### 配置使用 ### `disruptor`的 ` log4j2`高性能异步日志 > 整体性能有显著提升,适用C端的大量日志场景 #### 添加依赖 ```xml com.lmax disruptor ${disruptor.version} org.springframework.boot spring-boot-starter-log4j2 ``` #### 排除相关`logback`冲突包 ```xml org.springframework.boot spring-boot-starter-web spring-boot-starter-logging org.springframework.boot ``` > 整体进行排除 ```xml org.springframework.boot spring-boot-starter-logging * * ``` #### 配置 + 使用 ## 后续计划 + 通用业务场景拆分封装 + Cloud系、Dubbo系 + 监控、告警 + 前端部分 + Vue3 + Uniapp + Flutter + ELK + 容器化 ## 待完成任务 + es组件 + 代码生成组件 + 缓存预热进一步优化 + httpclient5 优化 + 京东async封装 + fc-async + 微服务 cloud + eureka + ribbon... + 自定义ribbon负载均衡 + `alibaba`微服务系 + 设计模式系 + file场景改造优化 + 分片上传,断点续传,秒传 + 基于nacaos动态配置策略 + Guava 本地缓存 + 数据权限通用实现 + 其他优化 + 多线程进度条任务脱离`Redis`实现 + 迷你工作流 + caffeine cache 本地缓存拓展 + 进一步优化 ## 联系我 ## 文档版本 > 2024-01-04 09:37:47
泛型说明
Quartz禁止并发执行 DisallowConcurrentExecution
* TODO 事务控制 * TODO 分布式锁实现 *
* 此方法并不能保证幂等,也不能保证多线程条件下不重复执行. * 若有此需求请考虑使用分布式锁控制的实现 *
* 后期可采用动态代理优化 *
自带查询分页性能:
优化后性能: