[新增功能](dev): 初始化提交

This commit is contained in:
song_jx 2023-04-18 15:12:12 +08:00
parent f358bf9e5b
commit 5e34c0c0b9
41 changed files with 2349 additions and 0 deletions

102
.gitignore vendored
View File

@ -1,3 +1,102 @@
# ---> Java
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
# ---> JetBrains
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
# ---> Maven
target/
pom.xml.tag
pom.xml.releaseBackup
@ -9,3 +108,6 @@ buildNumber.properties
.mvn/timing.properties
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
.mvn/wrapper/maven-wrapper.jar
/.idea/
/.fastRequest/

394
README.md Normal file
View File

@ -0,0 +1,394 @@
# dict-trans
## 前言
简单的字典翻译组件
## 功能
* [x] 字典翻译
* [x] 枚举翻译
* [x] 数据库翻译
* [x] 自定义翻译
* [x] 翻译结果脱敏
## 项目结构
```
EasyTranslate
└── src
├── main
│ └── java
│ └── com
│ └── aizuda
│ └── trans
│ ├── annotation 翻译注解
│ │ ├── Dictionary.java 字典注解,标识在字典数据类上(自动生成查询 SQL
│ │ ├── Translate.java 翻译字段注解,标识在需要翻译的字段上
│ │ └── Translator.java 翻译方法注解,对方法返回值进行翻译
│ ├── aspect
│ │ └── TranslateAspect.java 翻译切面
│ ├── config
│ │ └── TranslatorConfig.java 默认翻译方法注入配置
│ ├── constants 常量配置
│ │ └── DesensitizedTypeConstants.java
│ ├── desensitized 脱敏相关
│ │ ├── Desensitized.java
│ │ └── IDesensitized.java
│ ├── dict 数据字典相关
│ │ └── DictTranslate.java
│ ├── enums 枚举相关
│ │ ├── EnumPool.java
│ │ ├── FormatType.java
│ │ └── IEnum.java
│ ├── service
│ │ ├── DictTranslateService.java 字典翻译接口
│ │ ├── impl
│ │ │ ├── DefaultDictTranslateServiceImpl.java 默认数据字典翻译实现(实现字典翻译接口。仿照该方法,实现自己的业务)
│ │ │ ├── DictCacheTranslator.java 数据字典翻译实现(调用 字典翻译接口实现)
│ │ │ ├── DataBaseTranslator.java 数据库翻译服务
│ │ │ ├── DesensitizedTranslator.java 脱敏实现(没啥操作,就返回原值)
│ │ │ ├── EnumTranslator.java 枚举翻译实现
│ │ │ └── TranslatorHandle.java 翻译操作类
│ │ └── Translatable.java 翻译接口(字典、枚举、....接实现该接口)
│ ├── TranslatorBootApplication.java
│ └── util 一些工具类
│ ├── LambdaUtil.java
│ └── NameUtil.java
└── test demo演示
```
## 快速开始
### 引入jar包
```xml
<!-- 自行编译 -->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>dict-trans</artifactId>
<version>${latestVersion}</version>
</dependency>
<!-- hutool工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
<optional>true</optional>
</dependency>
<!-- 数据库 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<optional>true</optional>
</dependency>
```
### 配置
```yaml
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&allowMultiQueries=true&useSSL=false&rewriteBatchedStatements=true
username: root
password: 123456
```
### 注解说明
#### @Dictionary
> 需配合 `@Translate``@Translator` 食用
说明:标识在字典数据类上(自动生成查询 SQL**只能用在类上**
参数:
* table字典表名为空时取 `TableName` ,取不到则取 类名(驼峰转大写下划线)
* codeColumn字典编码的属性对应 查询条件的列字段(需要的是表字段)
* textColumn字典值的列名对应 要查询的字段(需要的是表字段)
* groupColumn字典组别属性对应 要查询的字段需要的是表字段某些字典可能会需要根据某个类别划分再进行翻译如静态字典中的DICT_ID
* translator自定义翻译方法遇到特殊的翻译场景可自定义翻译实现需要自行编写实现类实现Translatable接口并实现翻译方法程序将使用该方法进行翻译该注解中的所有配置信息将传递到实现方法中
示例:
```java
// =========================== 示例1 ===========================
/**
* 用户(数据库)
*
* @author nn200433
* @date 2022-12-16 016 14:07:27
*/
@Dictionary(codeColumn = "id", textColumn = {"user_name"})
@TableName("sys_user")
public class UserDB {
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
@TableField(value = "user_name")
private String name;
}
// =========================== 示例2 ===========================
/**
* 人
*
* @author nn200433
* @date 2022-12-16 016 11:40:30
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class People {
@Translate(dictClass = Dict.class, groupValue = "sex", translateField = "sexName")
private String sex;
private String sexName;
@Translate(dictClass = Desensitized.class, translateField = "phone", desensitizedModel = DesensitizedTypeConstants.MOBILE_PHONE)
private String phone;
@Translate(dictionary = @Dictionary(translator = CustomerTranslateServiceImpl.class), translateField = "name")
private String id;
private String name;
}
```
#### @Translate
> 需配合 `@Dictionary``@Translator` 食用
说明:标识在需要翻译的字段上,**只能用在字段上**
参数:
* dictClass字典配置类指定的 class 上必须是 `DictTranslate.class` 实现类 、 `IEnum` 接口的实现类 、 `Desensitized` 或者是 `@Dictionary` 注解;
* translateField翻译后的属性名注意使用驼峰命名默认为原属性名去除末尾的 "Id" 和 "Code" 再接上 "Name"
* groupValue组属性值在静态字典表这种拥有组属性的字典中需要手动传入一个定值字典分组的 code
* dictionary指定 `Dictionary` 并设置其属性,将覆盖 `dictClass` 上的 `Dictionary` 注解的配置,指定了该属性后也可不指定 `dictClass` ,一般情况下不会使用;
* conditionField指定判断条件字段仅自定义翻译实现时用来进行判断;
* desensitizedModel脱敏模型用来给数据脱敏打 `*` 使用。常见模型在 `DesensitizedTypeConstants` 常量中。也可自定义,格式:`{含开始位置,含结束位置}` ,举例:`{1,2}`
*注:字段自身脱敏时,需将 `dictClass` 设置为 `Desensitized.class`(此时字段仅返回原值后脱敏。可与翻译共用,那样先翻译后脱敏。)*
* valuedictClass的别名当只需要配置 `dictClass` 时,可以简写成 `@Translate(XX.class)`
示例:
```java
// =========================== 示例1 字典 ===========================
// 1.1 字典 code需要在实现字典翻译服务时自己实现
@Translate(dictClass = Dict.class, groupValue = "sex", translateField = "sexName")
private String sex;
private String sexName;
// 1.2 自定义完整字典(需要在实现字典翻译服务时自己实现)
@Translate(dictClass = Dict.class, groupValue = "{男:1;女:2;}", translateField = "sexName")
private String sex;
private String sexName;
// =========================== 示例2 数据库 ===========================
// 2.1. 字典定义
@Dictionary(codeColumn = "id", textColumn = {"user_name"})
@TableName("sys_user")
// 省略 @Data
// ....
public class UserDB {
}
// 2.2. 字段翻译
@Translate(dictClass = UserDB.class, translateField = "name")
private String id;
private String name;
// =========================== 示例3 枚举 ===========================
// 3.1. 枚举定义
public interface MyDict {
/**
* 示例1性别枚举
*/
enum SexDict implements IDict {
//
MALE("0", "男"),
FEMALE("1", "女");
SexDict(String code, String text) {
// 构造方法中只需要调用接口的init方法即可省略了属性的定义和赋值也不用定义累赘的get方法
init(code, text);
}
}
}
// 3.2. 枚举翻译
/**
* 性别
*/
@Translate(MyDict.SexDict.class)
private String sex;
private String sexName;
// =========================== 示例4 脱敏 ===========================
// 4.1 字段给自身脱敏
@ApiModelProperty("联系电话")
@Translate(dictClass = Desensitized.class, desensitizedModel = DesensitizedTypeConstants.MOBILE_PHONE, translateField = "phone")
private String phone;
// 4.2 字段翻译并脱敏
@ApiModelProperty("用户等级")
@Translate(dictClass = Dict.class, groupValue = DictConstants.APP_USER_LEVEL, translateField = "userLevelName", desensitizedModel = "{0,1}")
private String userLevel;
@ApiModelProperty("用户等级中文")
private String userLevelName;
// =========================== 示例5 自定义 ===========================
// 5.1 翻译字段
@Translate(dictionary = @Dictionary(translator = CustomerTranslateServiceImpl.class), translateField = "name")
private String id;
private String name;
// 5.2 自定义翻译方法
@Component
public class CustomerTranslateServiceImpl implements Translatable {
@Override
public List<String> translate(String groupValue, String conditionValue, String origin, Dictionary dictConfig,
Class dictClass) {
List<String> rList = new ArrayList<String>(1);
if (StrUtil.equals(origin, "1")) {
rList.add("结果1");
} else {
rList.add("结果2");
}
return rList;
}
}
// =========================== 示例6 数据库查询多字段进行字段映射 ===========================
// 6.1 数据库字典类
@Dictionary(codeColumn = "id", textColumn = {"user_name", "real_name"})
@TableName("sys_user")
// 省略 @Data
// ....
public class UserDB {
}
// 6.2 翻译字段
// 注意translateField 需要跟 textColumn 的下标一一对应!!!
@Translate(dictClass = UserDB.class, translateField = {"zh", "zsxm"})
private String id;
private String zh;
private String zsxm;
```
#### @Translator
> 需配合 `@Dictionary``@Translate` 食用
>
> 方法返回类型支持 `Map``Entity``List``IPage`
说明:对方法返回值进行翻译,**只能用在方法上**
参数:无
示例:
```java
@Component
public class DemoServiceImpl implements DemoService {
@Translator
@Override
public List<People> dictDemo() {
People man = People.builder().sex("1").id("1").phone("18612345678").build();
People woman = People.builder().sex("2").id("2").phone("18612345678").build();
return CollUtil.newArrayList(man, woman);
}
@Translator
@Override
public List<Device> enumDemo() {
Device man = Device.builder().status("1").build();
Device woman = Device.builder().status("2").build();
return CollUtil.newArrayList(man, woman);
}
@Translator
@Override
public List<People2> dbDemo() {
People2 man = People2.builder().id("1").build();
People2 woman = People2.builder().id("17ed02e3f05c629385371ce561f2dc50").build();
return CollUtil.newArrayList(man, woman);
}
}
```
## 演示
```java
@SpringBootTest
@RunWith(SpringRunner.class)
public class TranslatorTest {
@Autowired
private DemoService demoService;
@Test
public void demo1() {
List<People> peopleList = demoService.dictDemo();
Console.log("---> 翻译结果:{}", peopleList);
}
@Test
public void demo2() {
List<Device> deviceList = demoService.enumDemo();
Console.log("---> 翻译结果:{}", deviceList);
}
@Test
public void demo3() {
List<People2> peopleList = demoService.dbDemo();
Console.log("---> 翻译结果:{}", peopleList);
}
}
```
![运行示例](./doc/imgs/demo.png)
## 附注
该项目得益于 [Transformer v1 版本](https://github.com/luo-zhan/Transformer) 增加了 脱敏,完善了 数据库翻译 等功能。Transformer可能是最简单但最强大的字段转换插件一个注解搞定任意转换让开发变得更加丝滑基本上就是在此项目上增加功能。

BIN
doc/imgs/demo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

38
doc/t_test.sql Normal file
View File

@ -0,0 +1,38 @@
/*
Navicat Premium Data Transfer
Source Server : mysql-
Source Server Type : MySQL
Source Server Version : 50739
Source Host : 192.168.0.44:3306
Source Schema : t_test
Target Server Type : MySQL
Target Server Version : 50739
File Encoding : 65001
Date: 18/04/2023 11:36:32
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主键',
`user_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户名',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES ('1', '张三');
INSERT INTO `sys_user` VALUES ('2', '李四');
INSERT INTO `sys_user` VALUES ('3', '王五');
INSERT INTO `sys_user` VALUES ('4', '赵六');
SET FOREIGN_KEY_CHECKS = 1;

73
pom.xml Normal file
View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.13</version>
</parent>
<groupId>com.aizuda</groupId>
<artifactId>dict-trans</artifactId>
<packaging>jar</packaging>
<version>0.1</version>
<modelVersion>4.0.0</modelVersion>
<properties>
<!-- 数据库操作 -->
<mybatis-plus.version>3.5.2</mybatis-plus.version>
<!-- hutool -->
<hutool.version>5.8.8</hutool.version>
<!-- mysql -->
<mysql.version>5.1.49</mysql.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- AOP拦截 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
<optional>true</optional>
</dependency>
<!-- hutool工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,22 @@
package com.aizuda.trans;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;
/**
* 转换器引导应用程序
*
* @author nn200433
* @date 2022-12-16 11:30:52
*/
@SpringBootApplication
@Import(cn.hutool.extra.spring.SpringUtil.class)
public class TranslatorBootApplication {
public static void main(String[] args) {
SpringApplication.run(TranslatorBootApplication.class, args);
}
}

View File

@ -0,0 +1,60 @@
package com.aizuda.trans.annotation;
import com.aizuda.trans.service.Translatable;
import com.baomidou.mybatisplus.annotation.TableName;
import java.lang.annotation.*;
/**
* 字典翻译注解标识在字典数据类上
*
* @author luozhan
* @date 2019-03
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Dictionary {
/**
* 字典表名
* <p>
* 为空时取 {@link TableName#value()}取不到则取 类名驼峰转大写下划线
* </p>
*/
String table() default "";
/**
* 字典编码的属性
* <p>
* 查询条件的列字段需要的是表字段
* </p>
*/
String codeColumn() default "";
/**
* 字典值的列名
* <p>
* 要查询的字段需要的是表字段
* </p>
*/
String[] textColumn() default {};
/**
* 字典组别属性
* 某些字典可能会需要根据某个类别划分再进行翻译如静态字典中的DICT_ID
*/
String groupColumn() default "";
/**
* 自定义翻译方法
* <p>
* 1.不配置默认使用DataBaseTranslator翻译
* 2.遇到特殊的翻译场景可自定义翻译实现需要自行编写实现类实现Translatable接口并实现翻译方法
* 程序将使用该方法进行翻译该注解中的所有配置信息将传递到实现方法中
*
* @see Translatable
*/
Class<? extends Translatable> translator() default Translatable.class;
}

View File

@ -0,0 +1,67 @@
package com.aizuda.trans.annotation;
import com.aizuda.trans.constants.DesensitizedTypeConstants;
import java.lang.annotation.*;
/**
* 字典翻译注解标识在需要翻译的字段上
*
* @author luozhan
* @date 2019-03
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Translate {
/**
* 字典配置类 指定的class上必须有@Dictionary注解或者是IDictEnum接口的实现类 一般情况下本属性必须指定或者使用别名value属性
*
* <ul>
* <li>DictTranslate 实现类字典翻译</li>
* <li>IEnum 实现类枚举翻译</li>
* <li>Desensitized: 脱敏翻译</li>
* <li>其他: 数据库翻译</li>
* </ul>
*/
Class<?> dictClass() default void.class;
/**
* 翻译后的属性名注意使用驼峰命名 默认为原属性名去除末尾的"Id""Code"再接上"Name"
*/
String[] translateField() default {};
/**
* 组属性值 在静态字典表这种拥有组属性的字典中需要手动传入一个定值
* <p>
* 字典分组的 code
* </p>
*/
String groupValue() default "";
/**
* 指定Dictionary并设置其属性将覆盖dictClass上的Dictionary注解的配置 指定了该属性后也可不指定dictClass 一般情况下不会使用
*/
Dictionary dictionary() default @Dictionary;
/**
* dictClass的别名 当只需要配置dictClass时可以简写成@Translate(XX.class)
*/
Class<?> value() default void.class;
/**
* 判断条件字段仅自定义 dictionary 时有效
*/
String conditionField() default "";
/**
* 脱敏模型
* <ul>
* <li>常见格式使用常量类 DesensitizedTypeConstants </li>
* <li>自定义格式{含开始位置,含结束位置}举例{1,2}</li>
* </ul>
*/
String desensitizedModel() default DesensitizedTypeConstants.EMPTY;
}

View File

@ -0,0 +1,21 @@
package com.aizuda.trans.annotation;
import java.lang.annotation.*;
/**
* 字典翻译注解指定在方法上对方法返回值进行翻译
*
* @author luozhan
* @date 2020-04
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Translator {
/**
* 指定翻译配置类可传多个一般为实体class
*/
Class<?>[] value() default void.class;
}

View File

@ -0,0 +1,40 @@
package com.aizuda.trans.aspect;
import com.aizuda.trans.annotation.Translator;
import com.aizuda.trans.service.impl.TranslatorHandle;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
/**
* 字典翻译
* 在方法上定义@Tanslator注解对方法返回值进行翻译
*
* @author luozhan
* @create 2020-04
* @see Translator
*/
@Slf4j
@Aspect
@Component
public class TranslateAspect {
@Pointcut("@annotation(com.aizuda.trans.annotation.Translator)")
public void pointCut() {
}
@AfterReturning(pointcut = "pointCut()", returning = "object")
public void doAfter(JoinPoint joinPoint, Object object) {
if (null == object) {
return;
}
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Translator config = signature.getMethod().getAnnotation(Translator.class);
TranslatorHandle.parse(object, config.value());
}
}

View File

@ -0,0 +1,24 @@
package com.aizuda.trans.config;
import com.aizuda.trans.service.DictTranslateService;
import com.aizuda.trans.service.impl.DefaultDictTranslateServiceImpl;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 翻译配置
*
* @author nn200433
* @date 2022-08-18 018 16:37:42
*/
@Configuration
public class TranslatorConfig {
@Bean
@ConditionalOnMissingBean
public DictTranslateService dictTranslateService() {
return new DefaultDictTranslateServiceImpl();
}
}

View File

@ -0,0 +1,34 @@
package com.aizuda.trans.constants;
/**
* 脱敏型常数
*
* @author nn200433
* @date 2022-12-08 008 10:08:26
*/
public class DesensitizedTypeConstants {
/** 不启用 */
public static final String EMPTY = "";
/** 用户id */
public static final String USER_ID = "USER_ID";
/** 中文名 */
public static final String CHINESE_NAME = "CHINESE_NAME";
/** 身份证号 */
public static final String ID_CARD = "ID_CARD";
/** 座机号 */
public static final String FIXED_PHONE = "FIXED_PHONE";
/** 手机号 */
public static final String MOBILE_PHONE = "MOBILE_PHONE";
/** 地址 */
public static final String ADDRESS = "ADDRESS";
/** 电子邮件 */
public static final String EMAIL = "EMAIL";
/** 密码 */
public static final String PASSWORD = "PASSWORD";
/** 中国大陆车牌,包含普通车辆、新能源车辆 */
public static final String CAR_LICENSE = "CAR_LICENSE";
/** 银行卡 */
public static final String BANK_CARD = "BANK_CARD";
}

View File

@ -0,0 +1,10 @@
package com.aizuda.trans.desensitized;
/**
* 脱敏
*
* @author nn200433
* @date 2022-12-08 008 10:50:37
*/
public class Desensitized implements IDesensitized {
}

View File

@ -0,0 +1,10 @@
package com.aizuda.trans.desensitized;
/**
* 脱敏接口
*
* @author nn200433
* @date 2022-12-08 008 10:47:22
*/
public interface IDesensitized {
}

View File

@ -0,0 +1,11 @@
package com.aizuda.trans.dict;
/**
* 字典翻译
*
* @author nn200433
* @date 2022-08-18 018 16:30:47
*/
public interface DictTranslate {
}

View File

@ -0,0 +1,44 @@
package com.aizuda.trans.enums;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 枚举字典池
*
* @author luozhan
* @create 2020-04
*/
class EnumPool {
private static final Map<IEnum, EnumBean> DICT_MAP = new ConcurrentHashMap<>();
static void putDict(IEnum dict, String code, String text) {
DICT_MAP.put(dict, new EnumBean(code, text));
}
static EnumBean getDict(IEnum dict) {
return DICT_MAP.get(dict);
}
static class EnumBean implements IEnum {
private String code;
private String text;
EnumBean(String code, String text) {
this.code = code;
this.text = text;
}
@Override
public String getCode() {
return code;
}
@Override
public String getText() {
return text;
}
}
}

View File

@ -0,0 +1,27 @@
package com.aizuda.trans.enums;
/**
* 格式类型
*
* @author luozhan
* @create 2020-04
* @date 2022-12-16 01:52:02
*/
public enum FormatType {
/**
* 大写下划线
*/
UPPERCASE_UNDERLINE,
/**
* 小写下划线
*/
LOWERCASE_UNDERLINE,
/**
* 驼峰
*/
CAMEL
}

View File

@ -0,0 +1,67 @@
package com.aizuda.trans.enums;
import java.util.stream.Stream;
/**
* 枚举接口
* <p>
* 自定义的枚举类实现本接口后在构造方法中只需调用init方法即可初始化
*
* @author luozhan
* @date 2019-03
*/
public interface IEnum {
/**
* 通过code获取value
*
* @param clazz 枚举class
* @param code code
* @return
*/
static String getTextByCode(Class<? extends IEnum> clazz, String code) {
return Stream.of(clazz.getEnumConstants())
.filter((IEnum e) -> e.getCode().equals(code))
.map(IEnum::getText)
.findAny().orElse(null);
}
/**
* 通过text获取code
*
* @param clazz 枚举class
* @param text text
* @return
*/
static String getCodeByText(Class<? extends IEnum> clazz, String text) {
return Stream.of(clazz.getEnumConstants())
.filter((IEnum e) -> e.getText().equals(text))
.map(IEnum::getCode)
.findAny().orElse(null);
}
/**
* 初始化
*
* @param code 字典编码
* @param text 字典文本
*/
default void init(String code, String text) {
EnumPool.putDict(this, code, text);
}
/**
* 获取code
*/
default String getCode() {
return EnumPool.getDict(this).getCode();
}
/**
* 获取text
*/
default String getText() {
return EnumPool.getDict(this).getText();
}
}

View File

@ -0,0 +1,21 @@
package com.aizuda.trans.service;
/**
* 字典翻译服务
*
* @author nn200433
* @date 2022-08-18 018 16:31:43
*/
public interface DictTranslateService {
/**
* 获取字典标签
*
* @param dictCode 分组
* @param dictValue
* @return {@link String }
* @author nn200433
*/
public String findDictLabel(String dictCode, String dictValue);
}

View File

@ -0,0 +1,29 @@
package com.aizuda.trans.service;
import com.aizuda.trans.annotation.Dictionary;
import java.util.List;
/**
* 字典接口
* <p>
* 实现本接口后可在@Dictionary注解的method属性中指定实现类以实现特殊的翻译
*
* @author luozhan
* @create 2020-04
*/
public interface Translatable {
/**
* 自定义翻译方法需要自己实现
*
* @param groupValue 字典group值不用可忽略如静态字典需要先通过group值确定范围再根据code值得到对应的value
* @param conditionValue 条件字段值仅限用户自定义时生效
* @param origin 待翻译的原始值对应字典code属性
* @param dictConfig 字典注解可获取属性配置
* @param dictClass 字典class
* @return 字典value可以返回null值翻译时会处理如果为null则显示原始值
*/
public List<String> translate(String groupValue, String conditionValue, String origin, Dictionary dictConfig, Class dictClass);
}

View File

@ -0,0 +1,126 @@
package com.aizuda.trans.service.impl;
import cn.hutool.core.lang.Opt;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.db.Db;
import cn.hutool.db.Entity;
import cn.hutool.db.handler.EntityHandler;
import cn.hutool.db.handler.EntityListHandler;
import cn.hutool.extra.spring.SpringUtil;
import com.aizuda.trans.annotation.Dictionary;
import com.aizuda.trans.enums.FormatType;
import com.aizuda.trans.service.Translatable;
import com.aizuda.trans.util.NameUtil;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 翻译默认实现
* <p>
* 适用于字典为单表使用sql查询
*
* @author luozhan
* @create 2020-04
*/
@Slf4j
@Component
public class DataBaseTranslator implements Translatable {
/**
* 获取数据源
*/
private static DataSource dataSource = SpringUtil.getBean(DataSource.class);
@Override
public List<String> translate(String groupValue, String conditionValue, String origin, Dictionary dictConfig, Class dictClass) {
// 获取参数
String codeColumn = dictConfig.codeColumn();
String[] textColumnArray = dictConfig.textColumn();
String groupColumn = dictConfig.groupColumn();
String tableName = getTableName(dictConfig, dictClass);
Assert.isTrue(StrUtil.isNotEmpty(codeColumn), "@Dictionary注解codeColumn配置有误找不到指定的属性名class:" + dictClass.getSimpleName());
Assert.isTrue(ArrayUtil.isNotEmpty(textColumnArray), "@Dictionary注解textColumn配置有误找不到指定的属性名class:" + dictClass.getSimpleName());
List<String> rsList = new ArrayList<String>(textColumnArray.length);
try {
log.debug("---> 触发字典翻译:查询表 {} 中的字段 {} ,查询条件 {} = {}", tableName, textColumnArray, codeColumn, origin);
// 查询条件为空时直接返回相对应个数的空数组
if (ObjectUtil.isNull(origin) || StrUtil.isBlank(origin)) {
log.debug("---> 触发字典翻译:查询条件为空,直接返回对应个数的空数组");
return Opt.ofNullable(textColumnArray).stream().map(s -> StrUtil.EMPTY).collect(Collectors.toList());
}
Entity where = Entity.create(tableName);
if (StrUtil.isNotEmpty(groupColumn)) {
where.set(groupColumn, groupValue);
}
if (StrUtil.contains(origin, StrUtil.COMMA)) {
// 多条记录取单列查询结果为多条记录循环后转为单条
// 传入数据1,2,3
// 查询结果["张三","李四","王五"]
// 返回结果张三李四王五
final String field = textColumnArray[0];
where.set(codeColumn, StrUtil.split(origin, StrUtil.COMMA));
List<Entity> entityList = Db.use(dataSource).find(where, EntityListHandler.create(), textColumnArray);
rsList.add(entityList.stream().map(e -> e.getStr(field)).collect(Collectors.joining("")));
} else {
// 单条记录多列查询结果为单条多列循环后为多条根据传入的字段顺序返回
// 传入数据1
// 查询结果{ "列1": "aa", "列2": "bb"}
// 返回结果["aa","bb"]
where.set(codeColumn, origin);
Entity entity = Db.use(dataSource).find(where, EntityHandler.create(), textColumnArray);
for (String column : textColumnArray) {
rsList.add(entity.getStr(column));
}
}
} catch (NullPointerException e) {
// 空指针异常时可能查询条件为空返回空数组
log.error("---> DataBaseTranslator 字典翻译,空指针异常", e);
} catch (SQLException e) {
log.error("---> DataBaseTranslator 字典翻译SQL查询异常", e);
}
return rsList;
}
/**
* 获取表名
*
* @param dictConfig dict配置
* @param dictClass dict类
* @return {@link String }
* @author nn200433
*/
private String getTableName(Dictionary dictConfig, Class dictClass) {
if (StrUtil.isNotEmpty(dictConfig.table())) {
return dictConfig.table();
}
// 类名转表名
final String className = dictClass.getSimpleName();
String tName = className.substring(0, 1) + NameUtil.parseCamelTo(className.substring(1), FormatType.UPPERCASE_UNDERLINE);
// 获取mp注解上的表名
TableName tableName = (TableName) dictClass.getAnnotation(TableName.class);
if (null != tableName) {
tName = tableName.value();
}
return tName;
}
}

View File

@ -0,0 +1,18 @@
package com.aizuda.trans.service.impl;
import com.aizuda.trans.service.DictTranslateService;
/**
* 默认字典翻译服务实现
*
* @author nn200433
* @date 2022-08-18 018 16:32:24
*/
public class DefaultDictTranslateServiceImpl implements DictTranslateService {
@Override
public String findDictLabel(String dictCode, String dictValue) {
return dictValue;
}
}

View File

@ -0,0 +1,27 @@
package com.aizuda.trans.service.impl;
import com.aizuda.trans.annotation.Dictionary;
import com.aizuda.trans.service.Translatable;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
/**
* 脱敏翻译器
*
* @author nn200433
* @date 2022-12-08 10:38:56
*/
@Slf4j
@Component
public class DesensitizedTranslator implements Translatable {
@Override
public List<String> translate(String groupValue, String conditionValue, String origin, Dictionary dictConfig, Class dictClass) {
// 直接返回原值
return Collections.singletonList(origin);
}
}

View File

@ -0,0 +1,32 @@
package com.aizuda.trans.service.impl;
import com.aizuda.trans.annotation.Dictionary;
import com.aizuda.trans.service.DictTranslateService;
import com.aizuda.trans.service.Translatable;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
/**
* 数据字典翻译
*
* @author luozhan
* @create 2020-04
*/
@Slf4j
@Component
public class DictCacheTranslator implements Translatable {
@Autowired
private DictTranslateService dictTranslateService;
@Override
@SuppressWarnings("unchecked")
public List<String> translate(String groupValue, String conditionValue, String origin, Dictionary dictConfig, Class dictClass) {
return Collections.singletonList(dictTranslateService.findDictLabel(groupValue, origin));
}
}

View File

@ -0,0 +1,33 @@
package com.aizuda.trans.service.impl;
import com.aizuda.trans.annotation.Dictionary;
import com.aizuda.trans.enums.IEnum;
import com.aizuda.trans.service.Translatable;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
/**
* 枚举翻译
*
* @author luozhan
* @create 2020-04
*/
@Component
public class EnumTranslator implements Translatable {
@Override
@SuppressWarnings("unchecked")
public List<String> translate(String groupValue, String conditionValue, String origin, Dictionary dictConfig, Class dictClass) {
Assert.isTrue(IEnum.class.isAssignableFrom(dictClass), dictClass.getSimpleName() + "不是IDictEnum的实现类无法使用EnumTranslator进行翻译");
final String s = Stream.of(((Class<IEnum>) dictClass).getEnumConstants())
.filter((IEnum e) -> e.getCode().equals(origin))
.map(IEnum::getText)
.findAny().orElse(null);
return Collections.singletonList(s);
}
}

View File

@ -0,0 +1,394 @@
package com.aizuda.trans.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.*;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONUtil;
import com.aizuda.trans.annotation.Dictionary;
import com.aizuda.trans.annotation.Translate;
import com.aizuda.trans.desensitized.IDesensitized;
import com.aizuda.trans.dict.DictTranslate;
import com.aizuda.trans.enums.FormatType;
import com.aizuda.trans.enums.IEnum;
import com.aizuda.trans.service.Translatable;
import com.aizuda.trans.util.LambdaUtil;
import com.aizuda.trans.util.NameUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import sun.reflect.annotation.AnnotationType;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 翻译工具 结合注解使用
*
* @author luozhan
* @see Translate
* @see com.aizuda.trans.annotation.Dictionary
*/
@Slf4j
@Component
public class TranslatorHandle {
/**
* 翻译Map或Entity或Page
*
* @param origin 需要翻译的数据
* @param clazz 指定翻译的模板class可传多个
* @param <T> Map或Entity
* @return origin
*/
public static <T> T parse(T origin, Class<?>... clazz) {
if (origin == null) {
return null;
}
if (origin instanceof List) {
return (T) parse((List) origin, clazz);
} else if (origin instanceof Map) {
Map<String, Object> m = (Map<String, Object>) origin;
Collection<Object> values = m.values();
// 查询不为基本类型的数量
long count = values.stream().filter(v -> !ClassUtil.isSimpleTypeOrArray(ClassUtil.getClass(v))).count();
if (values.size() == count) {
for (Map.Entry<String, Object> entry : m.entrySet()) {
Object value = entry.getValue();
// 跳过空对象
if (ObjectUtil.isNull(value)) {
continue;
}
Class<Object> classType = ClassUtil.getClass(value);
value = parse(Collections.singletonList(value), classType).get(0);
}
return (T) m;
}
} else if (origin instanceof IPage) {
Page p = (Page) origin;
p.setRecords(parse(p.getRecords(), clazz));
return (T) p;
}
return parse(Collections.singletonList(origin), clazz).get(0);
}
/**
* 翻译集合 集合元素为Map或Entity
* <p>
* 注意 如果不指定class默认使用List中元素的class上配置的翻译规则进行翻译 当List集合元素不为Entity类型时class参数至少需要指定1个
*
* @param origins 待翻译数据集合
* @param classes 配置了翻译规则的Entity类型可传多个
* @param <T> 支持Entity或者Map
* @return List
*/
public static <T> List<T> parse(List<T> origins, Class<?>... classes) {
if (CollUtil.isEmpty(origins)) {
return origins;
}
classes = (classes.length != 0 && classes[0] != void.class) ? classes : new Class[]{origins.get(0).getClass()};
// 获取bo中需要翻译的属性
List<Field> translateFieldList = Arrays.stream(classes)
.map(Class::getDeclaredFields)
.flatMap(Stream::of)
.filter(field -> field.isAnnotationPresent(Translate.class))
.collect(Collectors.toList());
// 源数据中属性的格式大写下划线小写下划线驼峰
FormatType fieldFormatType = getFieldType(origins);
for (Field field : translateFieldList) {
// 1.获取要翻译的属性名
String fieldName = NameUtil.parseCamelTo(field.getName(), fieldFormatType);
// 2.获取每个待翻译属性的配置
Translate translateConfig = field.getAnnotation(Translate.class);
// 配置的字典class
Class<?> dictClass = getDictClass(translateConfig);
// 获取翻译值写入的字段名
List<String> translateFields = NameUtil.parseCamelTo(
getTranslateFieldName(translateConfig, field.getName()), fieldFormatType);
// 字典组字段值
String groupValue = translateConfig.groupValue();
// 判断条件字段
String conditionField = translateConfig.conditionField();
// 获取敏感词模型
String desensitizedModel = translateConfig.desensitizedModel();
// 字典注解配置
com.aizuda.trans.annotation.Dictionary dictionaryConfig = handle(translateConfig);
for (T origin : origins) {
String originValue = Convert.toStr(getProperty(origin, fieldName));
if (originValue == null) {
continue;
}
String conditionFieldValue = null;
if (StrUtil.isNotBlank(conditionField)) {
conditionFieldValue = Convert.toStr(getProperty(origin, conditionField));
}
// 翻译
List<String> translateValues = parse(originValue, dictionaryConfig, dictClass, groupValue, conditionFieldValue);
// 如果敏感词模型不为空则将结果进行敏感词处理
if (StrUtil.isNotBlank(desensitizedModel)) {
final DesensitizedUtil.DesensitizedType desensitizedType = EnumUtil.fromString(
DesensitizedUtil.DesensitizedType.class, desensitizedModel, null);
if (null == desensitizedType && StrUtil.isWrap(desensitizedModel, StrUtil.DELIM_START, StrUtil.DELIM_END)) {
// 找不到匹配脱敏模型时用hide解析
final String model = StrUtil.unWrap(desensitizedModel, StrUtil.DELIM_START, StrUtil.DELIM_END);
final int[] indexArray = StrUtil.splitToInt(model, StrUtil.COMMA);
Assert.isTrue(indexArray.length == 2, "自定义敏感词模型格式:{含开始位置,含结束位置}。举例:{1,2}");
translateValues = translateValues.stream()
.map(s -> StrUtil.hide(s, indexArray[0], indexArray[1]))
.collect(Collectors.toList());
} else {
translateValues = translateValues.stream()
.map(s -> StrUtil.desensitized(s, desensitizedType))
.collect(Collectors.toList());
}
}
// 填值
setProperty(origin, translateFields, translateValues);
}
}
return origins;
}
/**
* 判断源数据中的属性格式类型
*
* @param origins 对象数组
* @return {@link FormatType }
* @author nn200433
*/
private static <T> FormatType getFieldType(List<T> origins) {
T element = origins.get(0);
if (Map.class.isAssignableFrom(element.getClass())) {
Set<String> keySet = ((Map) element).keySet();
for (String key : keySet) {
if (key.toUpperCase().equals(key)) {
return FormatType.UPPERCASE_UNDERLINE;
} else if (key.contains("_")) {
return FormatType.LOWERCASE_UNDERLINE;
}
}
}
return FormatType.CAMEL;
}
/**
* 获取字典类
*
* @param translateConfig 转换配置
* @return {@link Class }<{@link ? }>
* @author nn200433
*/
private static Class<?> getDictClass(Translate translateConfig) {
final Class<?> classType = translateConfig.dictClass();
return classType == void.class ? translateConfig.value() : classType;
}
/**
* 将dictClass类上的Dictionary的配置填充到Translate注解中的dictionary属性中 此步骤将合并两个注解中的配置且Translate注解中的配置优先级更高
*
* @param translateConfig 转换配置
* @return {@link com.aizuda.trans.annotation.Dictionary }
* @author nn200433
*/
private static com.aizuda.trans.annotation.Dictionary handle(Translate translateConfig) {
Class<?> dictClass = getDictClass(translateConfig);
com.aizuda.trans.annotation.Dictionary dictionaryConfigOnDictClass = dictClass.getAnnotation(com.aizuda.trans.annotation.Dictionary.class);
com.aizuda.trans.annotation.Dictionary dictionaryConfigInTranslateConfig = translateConfig.dictionary();
return (com.aizuda.trans.annotation.Dictionary) joinAnnotationValue(dictionaryConfigOnDictClass, dictionaryConfigInTranslateConfig);
}
/**
* 将注解属性填充到另一个相同类型的注解中目标注解中已经存在属性值的不会被覆盖
*
* @param annotationFrom
* @param annotationTo
* @return 返回annotationTo如果annotationTo为空返回annotationFrom
*/
private static Annotation joinAnnotationValue(Annotation annotationFrom, Annotation annotationTo) {
if (annotationTo == null) {
return annotationFrom;
}
if (annotationFrom == null) {
return annotationTo;
}
Object handlerFrom = Proxy.getInvocationHandler(annotationFrom);
Object handlerTo = Proxy.getInvocationHandler(annotationTo);
Field fieldFrom = LambdaUtil.uncheck(() -> handlerFrom.getClass().getDeclaredField("memberValues"));
Field fieldTo = LambdaUtil.uncheck(() -> handlerTo.getClass().getDeclaredField("memberValues"));
fieldFrom.setAccessible(true);
fieldTo.setAccessible(true);
Map<String, Object> memberValuesFrom = LambdaUtil.uncheck(() -> (Map) fieldFrom.get(handlerFrom));
Map<String, Object> memberValuesTo = LambdaUtil.uncheck(() -> (Map) fieldTo.get(handlerTo));
// 注解默认值注意不会包含没有默认值的属性
Map<String, Object> defaultValueMap = AnnotationType.getInstance(annotationTo.annotationType())
.memberDefaults();
// 若目标注解中全都是默认值代表没有设置则直接返回原注解
// 否则属性填充后会直接改变目标注解的默认值影响其他引用的地方
if (defaultValueMap.equals(memberValuesTo)) {
return annotationFrom;
}
// 如果目标注解属性未设置则往目标里填充值
memberValuesTo.forEach((field, value) -> {
if (value.equals(defaultValueMap.get(field))) {
memberValuesTo.put(field, memberValuesFrom.get(field));
}
});
return annotationTo;
}
/**
* 翻译单值
*
* @param originValue 原始值
* @param dictConfig 字典配置
* @param dictClass 字典class包含组别属性字典code属性字典值属性三个信息
* @param groupValue 组别的值由使用者指定
* @param conditionValue 条件字段值
* @return 翻译后的值如果字典中找不到翻译值返回原始值
* @author nn200433
*/
public static List<String> parse(String originValue, Dictionary dictConfig, Class<?> dictClass, String groupValue,
String conditionValue) {
if (originValue == null) {
return null;
}
Class<? extends Translatable> translatorClass = dictConfig.translator();
if (translatorClass == Translatable.class) {
if (DictTranslate.class.isAssignableFrom(dictClass)) {
// 1.dictClass是字典类采用字典翻译
translatorClass = DictCacheTranslator.class;
} else if (IEnum.class.isAssignableFrom(dictClass)) {
// 2.dictClass是枚举类采用枚举翻译
translatorClass = EnumTranslator.class;
} else if (IDesensitized.class.isAssignableFrom(dictClass)) {
// 3.dictClass是脱敏类采用脱敏翻译
translatorClass = DesensitizedTranslator.class;
} else {
// 4.否则使用数据库翻译
translatorClass = DataBaseTranslator.class;
}
}
// 调用翻译方法
Translatable translator;
if (translatorClass.isAnnotationPresent(Component.class)) {
// 实现类上配置了@Component则使用Spring容器获取
translator = SpringUtil.getBean(translatorClass);
} else {
translator = LambdaUtil.uncheck(translatorClass::newInstance);
}
List<String> translateResult = translator.translate(groupValue, conditionValue, originValue, dictConfig,
dictClass);
return CollUtil.defaultIfEmpty(translateResult, Collections.emptyList());
}
/**
* 若注解中未配置translateField则默认将原属性名的Id或Code字样替换成Name
* <p>
* resTypeId -> resTypeCode staff -> staffName
*
* @param translateConfig 转换配置
* @param originFieldName 原始字段名称
* @return {@link List }<{@link String }>
* @author nn200433
*/
private static List<String> getTranslateFieldName(Translate translateConfig, String originFieldName) {
String[] translateFieldArray = translateConfig.translateField();
String newName = originFieldName.replaceFirst("(Id|Code)$|$", "Name");
for (int i = 0; i < translateFieldArray.length; i++) {
final String translateField = translateFieldArray[i];
if (StrUtil.isBlank(translateField)) {
translateFieldArray[i] = newName + i;
}
}
return CollUtil.newArrayList(translateFieldArray);
}
/**
* 根据对象获取属性
*
* @param o 对象
* @param fieldName 字段名称
* @return {@link Object }
* @author nn200433
*/
private static Object getProperty(Object o, String fieldName) {
if (o instanceof Map) {
return ((Map) o).get(fieldName);
} else {
Method getMethod = getMethod(o.getClass(), fieldName, "get");
// 此处不会抛异常
return LambdaUtil.uncheck(() -> getMethod.invoke(o));
}
}
/**
* 设置属性
*
* @param o 对象
* @param fieldNameList 字段名称列表
* @param valueList 值列表
* @author nn200433
*/
private static void setProperty(Object o, List<String> fieldNameList, List<String> valueList) {
if (fieldNameList.size() > valueList.size()) {
log.error(
"字典翻译查询结果不够翻译,导致翻译异常。\n ---> 当前翻译数据:{} \n ---> 翻译结果需设置 {} 个字段({}\n ---> 实际翻译出 {} 个字段({}",
JSONUtil.toJsonStr(o), fieldNameList.size(), fieldNameList, valueList.size(), valueList);
return;
}
for (int i = 0; i < fieldNameList.size(); i++) {
final String fieldName = fieldNameList.get(i);
final String value = valueList.get(i);
if (o instanceof Map) {
((Map) o).put(fieldName, value);
} else {
Method setMethod = getMethod(o.getClass(), fieldName, "set");
// 此处不会抛异常
LambdaUtil.uncheck(() -> setMethod.invoke(o, value));
}
}
}
/**
* 通过方法名找到get或set方法 只是简单根据名称匹配要求clazz是个常规的bean
*
* @param clazz 对象类型
* @param fieldName 字段名称
* @param prefix 前缀
* @return {@link Method }
* @author nn200433
*/
private static Method getMethod(Class<?> clazz, String fieldName, String prefix) {
String methodName = prefix + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
return Stream.of(clazz.getDeclaredMethods())
.filter(method -> method.getName().equals(methodName))
.findAny()
.orElseThrow(() -> new IllegalArgumentException(
clazz.getSimpleName() + ".class中未添加翻译属性" + fieldName + "或其对应get/set方法"));
}
}

View File

@ -0,0 +1,190 @@
package com.aizuda.trans.util;
import java.util.function.*;
/**
* Lambda异常处理工具类
*
* @author luozhan
* @date 2019-05
* @link <a>https://github.com/Robot-L/LambdaExceptionUtil</a>觉得不错的话请给个Starthx! :D
*/
public final class LambdaUtil {
/**
* 包装普通函数Function
* <p>
* 方法签名中的" throws E "编译器会提示多余但其实是为了将具体的异常向外传递如果不抛出的话
* 1.外层代码中编译器将无法提示有异常需要处理
* 2.也无法主动在外层捕获具体的异常如果尝试try一个具体的异常编译器将提示在try语句体中永远不会抛出相应异常Exception 'XXX' is never thrown in the corresponding try block
*/
public static <T, R, E extends Exception> Function<T, R> wrapFunction(FunctionWithExceptions<T, R, E> function) throws E {
return t -> {
try {
return function.apply(t);
} catch (Exception exception) {
throwAsUnchecked(exception);
return null;
}
};
}
/**
* 包装双入参普通函数BiFunction
*/
public static <T, U, R, E extends Exception> BiFunction<T, U, R> wrapBiFunction(BiFunctionWithExceptions<T, U, R, E> biFunction) throws E {
return (t, u) -> {
try {
return biFunction.apply(t, u);
} catch (Exception exception) {
throwAsUnchecked(exception);
return null;
}
};
}
/**
* 包装消费函数Consumer
*/
public static <T, E extends Exception> Consumer<T> wrapConsumer(ConsumerWithExceptions<T, E> consumer) throws E {
return t -> {
try {
consumer.accept(t);
} catch (Exception exception) {
throwAsUnchecked(exception);
}
};
}
/**
* 包装双重消费函数BiConsumer
*/
public static <T, U, E extends Exception> BiConsumer<T, U> wrapBiConsumer(BiConsumerWithExceptions<T, U, E> biConsumer) throws E {
return (t, u) -> {
try {
biConsumer.accept(t, u);
} catch (Exception exception) {
throwAsUnchecked(exception);
}
};
}
/**
* 包装生产函数Supplier
*/
public static <T, E extends Exception> Supplier<T> wrapSupplier(SupplierWithExceptions<T, E> function) throws E {
return () -> {
try {
return function.get();
} catch (Exception exception) {
throwAsUnchecked(exception);
return null;
}
};
}
/**
* 包装条件函数Predicate
*/
public static <T, E extends Exception> Predicate<T> wrapPredicate(PredicateWithExceptions<T, E> predicate) throws E {
return t -> {
try {
return predicate.test(t);
} catch (Exception exception) {
throwAsUnchecked(exception);
return false;
}
};
}
/**
* 包装双入参条件函数BiPredicate
*/
public static <T, U, E extends Exception> BiPredicate<T, U> wrapBiPredicate(BiPredicateWithExceptions<T, U, E> predicate) throws E {
return (t, u) -> {
try {
return predicate.test(t, u);
} catch (Exception exception) {
throwAsUnchecked(exception);
return false;
}
};
}
/**
* 包装纯执行函数Runnable
*/
public static <E extends Exception> void wrapRunnable(RunnableWithExceptions<E> runnable) throws E {
try {
runnable.run();
} catch (Exception exception) {
throwAsUnchecked(exception);
}
}
/**
* 如果一个方法绝对不会抛出所申明的异常可以使用该方法进行包装
* new String(byteArr, "UTF-8")申明了UnsupportedEncodingException
* 但编码"UTF-8"是必定不会抛异常的所以可以使用uncheck()进行包装
* String text = uncheck(() -> new String(byteArr, "UTF-8"))
*/
public static <R, E extends Exception> R uncheck(SupplierWithExceptions<R, E> supplier) {
try {
return supplier.get();
} catch (Exception exception) {
throwAsUnchecked(exception);
return null;
}
}
@SuppressWarnings("unchecked")
public static <E extends Throwable> void throwAsUnchecked(Exception exception) throws E {
throw (E) exception;
}
@FunctionalInterface
public interface ConsumerWithExceptions<T, E extends Exception> {
void accept(T t) throws E;
}
@FunctionalInterface
public interface BiConsumerWithExceptions<T, U, E extends Exception> {
void accept(T t, U u) throws E;
}
@FunctionalInterface
public interface FunctionWithExceptions<T, R, E extends Exception> {
static <T> FunctionWithExceptions<T, T, Exception> identity() {
return t -> t;
}
R apply(T t) throws E;
}
@FunctionalInterface
public interface BiFunctionWithExceptions<T, U, R, E extends Exception> {
R apply(T t, U u) throws E;
}
@FunctionalInterface
public interface SupplierWithExceptions<T, E extends Exception> {
T get() throws E;
}
@FunctionalInterface
public interface RunnableWithExceptions<E extends Exception> {
void run() throws E;
}
@FunctionalInterface
public interface PredicateWithExceptions<T, E extends Exception> {
boolean test(T t) throws E;
}
@FunctionalInterface
public interface BiPredicateWithExceptions<T, U, E extends Exception> {
boolean test(T t, U u) throws E;
}
}

View File

@ -0,0 +1,57 @@
package com.aizuda.trans.util;
import com.aizuda.trans.enums.FormatType;
import java.util.List;
/**
* 名字工具类
*
* @author luozhan
* @create 2020-04
* @date 2023-04-17 04:30:20
*/
public class NameUtil {
/**
* 解析驼峰
*
* @param param 参数
* @param type 类型
* @return {@link String }
* @author nn200433
*/
public static String parseCamelTo(String param, FormatType type) {
if (type == FormatType.CAMEL) {
return param;
}
int len = param.length();
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
char c = param.charAt(i);
if (Character.isUpperCase(c)) {
sb.append("_");
}
char result = type == FormatType.UPPERCASE_UNDERLINE ?
Character.toUpperCase(c) : Character.toLowerCase(c);
sb.append(result);
}
return sb.toString();
}
/**
* 解析驼峰
*
* @param params 参数个数
* @param type 类型
* @return {@link List }<{@link String }>
* @author nn200433
*/
public static List<String> parseCamelTo(List<String> params, FormatType type) {
for (String param : params) {
param = parseCamelTo(param, type);
}
return params;
}
}

View File

@ -0,0 +1 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.aizuda.trans.config.TranslatorConfig

View File

@ -0,0 +1,47 @@
package com.aizuda.trans;
import cn.hutool.core.lang.Console;
import com.aizuda.trans.demo.DemoService;
import com.aizuda.trans.entity.Device;
import com.aizuda.trans.entity.People;
import com.aizuda.trans.entity.People2;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
/**
* 转换测试
*
* @author nn200433
* @date 2020年05月18日 0018 15:05:05
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class TranslatorTest {
@Autowired
private DemoService demoService;
@Test
public void demo1() {
List<People> peopleList = demoService.dictDemo();
Console.log("---> 翻译结果:{}", peopleList);
}
@Test
public void demo2() {
List<Device> deviceList = demoService.enumDemo();
Console.log("---> 翻译结果:{}", deviceList);
}
@Test
public void demo3() {
List<People2> peopleList = demoService.dbDemo();
Console.log("---> 翻译结果:{}", peopleList);
}
}

View File

@ -0,0 +1,41 @@
package com.aizuda.trans.demo;
import com.aizuda.trans.entity.Device;
import com.aizuda.trans.entity.People;
import com.aizuda.trans.entity.People2;
import java.util.List;
/**
* 演示服务
*
* @author nn200433
* @date 2022-12-16 016 11:45:46
*/
public interface DemoService {
/**
* 字典演示
*
* @return {@link List }<{@link People }>
* @author nn200433
*/
public List<People> dictDemo();
/**
* 枚举演示
*
* @return {@link List }<{@link Device }>
* @author nn200433
*/
public List<Device> enumDemo();
/**
* 数据库演示
*
* @return {@link List }<{@link People2 }>
* @author nn200433
*/
public List<People2> dbDemo();
}

View File

@ -0,0 +1,32 @@
package com.aizuda.trans.demo.impl;
import cn.hutool.core.util.StrUtil;
import com.aizuda.trans.annotation.Dictionary;
import com.aizuda.trans.service.Translatable;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义翻译
*
* @author nn200433
* @date 2022-12-16 016 11:46:57
*/
@Component
public class CustomerTranslateServiceImpl implements Translatable {
@Override
public List<String> translate(String groupValue, String conditionValue, String origin, Dictionary dictConfig,
Class dictClass) {
List<String> rList = new ArrayList<String>(1);
if (StrUtil.equals(origin, "1")) {
rList.add("结果1");
} else {
rList.add("结果2");
}
return rList;
}
}

View File

@ -0,0 +1,46 @@
package com.aizuda.trans.demo.impl;
import cn.hutool.core.collection.CollUtil;
import com.aizuda.trans.annotation.Translator;
import com.aizuda.trans.demo.DemoService;
import com.aizuda.trans.entity.Device;
import com.aizuda.trans.entity.People;
import com.aizuda.trans.entity.People2;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 演示服务实现
*
* @author nn200433
* @date 2022-12-16 016 11:46:57
*/
@Component
public class DemoServiceImpl implements DemoService {
@Translator
@Override
public List<People> dictDemo() {
People man = People.builder().sex("1").id("1").phone("18612345678").build();
People woman = People.builder().sex("2").id("2").phone("18612345678").build();
return CollUtil.newArrayList(man, woman);
}
@Translator
@Override
public List<Device> enumDemo() {
Device d1 = Device.builder().status("1").build();
Device d2 = Device.builder().status("2").build();
return CollUtil.newArrayList(d1, d2);
}
@Translator
@Override
public List<People2> dbDemo() {
People2 man = People2.builder().id("1").build();
People2 woman = People2.builder().id("2").build();
return CollUtil.newArrayList(man, woman);
}
}

View File

@ -0,0 +1,37 @@
package com.aizuda.trans.dict;
import com.aizuda.trans.service.DictTranslateService;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* @author nn200433
* @date 2022-12-16 016 11:35:30
*/
@Component
public class CustomerDictImpl implements DictTranslateService {
private static Map<String, Map<String, String>> DICT_DEMO = new HashMap<String, Map<String, String>>(2);
static {
Map<String, String> sexMap = new HashMap<String, String>(2) {{
put("1", "");
put("2", "");
}};
Map<String, String> stateMap = new HashMap<String, String>(2) {{
put("1", "发布");
put("2", "草稿");
put("3", "下架");
}};
DICT_DEMO.put("sex", sexMap);
DICT_DEMO.put("state", stateMap);
}
@Override
public String findDictLabel(String dictCode, String dictValue) {
return DICT_DEMO.get(dictCode).get(dictValue);
}
}

View File

@ -0,0 +1,28 @@
package com.aizuda.trans.entity;
import com.aizuda.trans.annotation.Translate;
import com.aizuda.trans.enums.DeviceStatus;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 设备
*
* @author nn200433
* @date 2022-12-16 016 11:40:30
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Device {
/** 枚举翻译 */
@Translate(dictClass = DeviceStatus.class, translateField = "statusName")
private String status;
private String statusName;
}

View File

@ -0,0 +1,20 @@
package com.aizuda.trans.entity;
import com.aizuda.trans.dict.DictTranslate;
/**
* 字典类
*
* <pre>
* 这里直接弄了个空类
*
* 业务系统的话字典一般就一个类
*
* 主要加 implements DictTranslate 这句话来判断走哪个方法
* </pre>
*
* @author nn200433
* @date 2022-12-16 016 13:57:20
*/
public class Dict implements DictTranslate {
}

View File

@ -0,0 +1,40 @@
package com.aizuda.trans.entity;
import com.aizuda.trans.annotation.Dictionary;
import com.aizuda.trans.annotation.Translate;
import com.aizuda.trans.constants.DesensitizedTypeConstants;
import com.aizuda.trans.demo.impl.CustomerTranslateServiceImpl;
import com.aizuda.trans.desensitized.Desensitized;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
*
*
* @author nn200433
* @date 2022-12-16 016 11:40:30
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class People {
@Translate(dictClass = Dict.class, groupValue = "sex", translateField = "sexName")
private String sex;
private String sexName;
/** 手机号脱敏 */
@Translate(dictClass = Desensitized.class, translateField = "phone", desensitizedModel = DesensitizedTypeConstants.MOBILE_PHONE)
private String phone;
/** 自定义翻译 */
@Translate(dictionary = @Dictionary(translator = CustomerTranslateServiceImpl.class), translateField = "name")
private String id;
private String name;
}

View File

@ -0,0 +1,27 @@
package com.aizuda.trans.entity;
import com.aizuda.trans.annotation.Translate;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
*
*
* @author nn200433
* @date 2022-12-16 016 11:40:30
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class People2 {
/** 数据库翻译 */
@Translate(dictClass = UserDB.class, translateField = "name")
private String id;
private String name;
}

View File

@ -0,0 +1,25 @@
package com.aizuda.trans.entity;
import com.aizuda.trans.annotation.Dictionary;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
/**
* 用户(数据库)
*
* @author nn200433
* @date 2022-12-16 016 14:07:27
*/
@Dictionary(codeColumn = "id", textColumn = {"user_name"})
@TableName("sys_user")
public class UserDB {
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
@TableField(value = "user_name")
private String name;
}

View File

@ -0,0 +1,28 @@
package com.aizuda.trans.enums;
/**
* 设备状态
*
* @author nn200433
* @date 2022-08-18 018 20:07:47
*/
public enum DeviceStatus implements IEnum {
NORMAL("0", "正常使用"),
UN_USE("1", "未使用"),
TRY_USE("2", "试运行"),
FAULT("3", "故障"),
REPAIR("4", "维修"),
REJECT("5", "报废");
private DeviceStatus(String code, String text) {
init(code, text);
}
}

View File

@ -0,0 +1,6 @@
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.0.44:3306/t_test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&allowMultiQueries=true&useSSL=false&rewriteBatchedStatements=true
username: root
password: 123456