Aop+自定义注解实现数据字典映射
数据字典
Web项目开发中,字典表的一般都会存在,主要用来给整个系统提供基础服务。
比如男女性别的类型可以使用0和1来进行表示,在存储数据和查询数据的时候,就可以使用字典表中的数据进行翻译处理。
再比如之前做的一个项目中宠物类型包含老虎-1、海豚-2、大象-3、长颈鹿-4等等;做答题处理时的答题类型,比如单选题、多选题、填空题等等;
比较常用的有用户类型,普通用户还是VIP用户等等;这些类型的数据也可以存储在数据字典中进行统一处理。
数据结构以及业务
举例:
在业务表中使用的是业务目录,而其中的“数据结构”,“数据目录类型”,“数据来源”使用的是字典表中的id,但我们在页面显示的时候,想用字典表中的value值,即我们在库里相应的业务表的外键存字典表的ID,查询的时候返回给前端在字典表中的value字段。
如以下表以及内容的展示:
目录表:
内容: 外键,字典的ID
如图所示:(字典表)需要我们在表中拿出外键所对应的Label
列举方法
那有什么办法可以让我们查询出key的同时,将value值也查询出来,当时处理问题的时候想了三种解决方法:
1.要么就写sql 匹配。(在SQL查询的时候做字段的匹配,但是增加了SQL的复杂性不易维护)
2.要么就业务处理。得写个工具类(针对当时的情况感觉可以实现,然后查找了一些资料比如,自定义注解啥的)
3.再就是前端调两次接口,查询一次数据,再查询一次字典表做转译。(所涉及到的字段有四五个甚至更多,这是我问的别的同学,他们公司采用的就是这种方法)
根据查找资料以及实际考虑,最终采用了使用自定义注解的方式去做字段映射。
实现步骤:
首先展示项目的目录结构
总共三个,自定义注解,切面,具体实现
一 ,创建自定义注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Dict {
/**
* 字典类型,对应数据库的类型,下方展示
*/
String dictType();
/**
* 实体类内对应的中文字段名,默认为“当前字段+Text”
* <p>
* 例如当前字段为“type”,则对应中文字段默认为“typeText”
* 就是你在实体中想要给某个字段赋值,这个字段就要写成那个字段
*/
String dictField() default "";
}
二,创建切面
@Slf4j
@Aspect
@Component
public class BydDaoDictAspect {
// * `@Pointcut` 注解用于定义一个切点,切点是决定哪些方法应该被通知(advice)的表达式。
// * `execution(* com.wiseom.asset.manage.service..*.*(..))` 是一个切点表达式,它表示匹配 `com.wiseom.asset.manage.service` 包及其子包下的所有类的所有方法。
// * `doPointcut()` 方法本身没有执行任何操作,它只是为切点提供了一个标识符。
@Pointcut("execution(* com.wiseom.asset.manage.service..*.*(..))")
public void doPointcut() {
}
// * `@AfterReturning` 注解表示在匹配的方法成功执行并返回结果后执行通知。
// * `pointcut = "doPointcut()"` 表示这个通知应用于之前定义的 `doPointcut` 切点。
// * `returning = "result"` 捕获返回的结果,并将其作为 `result` 参数传递给通知方法。
// * 在 `doAfterReturning` 方法中,`DictUtils.convertDict(result)` 是将返回的 `result` 对象转换为字典格式。如果在转换过程中发生异常,它会被捕获并记录到日志中。
@AfterReturning(pointcut = "doPointcut()", returning = "result")
public void doAfterReturning(JoinPoint pjp, Object result) {
try {
DictUtils.convertDict(result);
} catch (Exception e) {
log.error(String.valueOf(e));
}
}
}
备注:
1:切点在com.wiseom.asset.manage.service的原因,因为我们使用了mybitsplus ,在我们处理查询,或者执行SQL的时候大多都在service层就被处理了,所以切点设在了service层。
2:如果切点设置在Controller,则需要修改下方实现三中的 方法 convertDict,因为 Controller的返回值拿到的是 R 包裹的内容,返回值对象是R,而不是实体,具体问题查看下方的扩展四
三,创建实现
@Component
public class DictUtils {
private static final String DICT_FIELD_SUFFIX = "Text";
/**
* 统一获取当前实体类涉及到的字典表数据,避免多次查询数据库造成的性能消耗
* 这个工具类主要的改动就是这个,所以我就把这一段放在上面了
* @param dictNames 字典表type值
* @return 字典表数据
*/
@SneakyThrows
private static Map<String, Map<Long, String>> getDictMap(List<String> dictNames) {
//创建fegin的实例,调用系统的sys_dict_item表,需要新写一个接口
final RemoteDictService remoteDictService = SpringContextHolder.getBean(RemoteDictService.class);
//把注解里的所有字典的类型查出来,具体根据需求做查询
List<SysDictItem> dictList = remoteDictService.getDictItemByType(dictNames);
//根据类型做一个分组,这里对应的是 Map<DictType, Map<ID, Label>>
return dictList.stream().collect(Collectors.groupingBy(
SysDictItem::getDictType, Collectors.toMap(SysDictItem::getId, SysDictItem::getLabel, (v1, v2) -> v2)));
}
/**
* 这是主方法主要是判断AOP后返回的内容,并根据判断条件拿到里面的内容
* 然后先判断有哪些带有注解,根据注解拿到对应的dictType去库里查找对应的内容
* 根据拿到内容在对指定的字段dictField进行赋值
*/
public static void convertDict(Object target) {
//判断对象是不是分页的
if (target instanceof Page) {
//拿到分页中的records对象里边包含实体的内容
for(Object object : ((Page<?>)target).getRecords()){
//拿到实体
List<DictDefinition> dictDefinitions = getMetadata(object);
// 如果没有注解,则直接返回
if (CollectionUtils.isEmpty(dictDefinitions)) return;
// 从字典定义中提取所有的注解的 dictType(数据库字典的类型)
List<String> dictNames = dictDefinitions.stream().map(d -> d.getDict().dictType()).collect(Collectors.toList());
// 根据字典类型获取字典映射
Map<String, Map<Long, String>> dictMapMap = getDictMap(dictNames);
// 转换target对象的字典字段
doConvertDict(object, dictDefinitions, dictMapMap);
}
} else
if (target instanceof List) {
// 将target强制转换为List<?>类型
List<?> objectList = ((List<?>) target);
// 使用CollectionUtils.isNotEmpty来检查列表是否非空
if (CollectionUtils.isNotEmpty(objectList)) {
// 获取列表中第一个元素的字典定义
List<DictDefinition> dictDefinitions = getMetadata(objectList.get(0));
// 如果没有注解,则直接返回
if (CollectionUtils.isEmpty(dictDefinitions)) return;
// 从字典定义中提取所有的注解的 dictType(数据库字典的类型)
List<String> dictNames = dictDefinitions.stream().map(d -> d.getDict().dictType()).collect(Collectors.toList());
// 根据字典类型获取字典映射
Map<String, Map<Long, String>> dictMapMap = getDictMap(dictNames);
// 遍历列表中的每个元素,并转换其字典字段
objectList.forEach(t -> doConvertDict(t, dictDefinitions, dictMapMap));
}
} else {
// 如果target不是List,则直接获取其字典定义
List<DictDefinition> dictDefinitions = getMetadata(target);
// 如果没有注解,则直接返回
if (CollectionUtils.isEmpty(dictDefinitions)) return;
// 从字典定义中提取所有的注解的 dictType(数据库字典的类型)
List<String> dictNames = dictDefinitions.stream().map(d -> d.getDict().dictType()).collect(Collectors.toList());
// 根据字典类型获取字典映射
Map<String, Map<Long, String>> dictMapMap = getDictMap(dictNames);
// 转换target对象的字典字段
doConvertDict(target, dictDefinitions, dictMapMap);
}
}
/**
* 仅获取一次Dict元数据,降低多次反射造成的性能消耗
* @param target 目标实体类
* @return Dict元数据
*/
private static List<DictDefinition> getMetadata(Object target) {
//这一段是判断存在注解的字段
List<DictDefinition> dictDefinitions = new ArrayList<>();
if (ClassUtils.isPrimitiveOrWrapper(target.getClass())
|| target instanceof Map || target instanceof String) {
return dictDefinitions;
}
List<Field> fields = FieldUtils.getAllFieldsList(target.getClass());
for (Field field : fields) {
Dict dict = AnnotationUtils.getAnnotation(field, Dict.class);
if (dict != null) {
DictDefinition dictDefinition = new DictDefinition();
dictDefinition.setDict(dict);
dictDefinition.setField(field);
dictDefinitions.add(dictDefinition);
}
}
return dictDefinitions;
}
@SneakyThrows
private static void doConvertDict(Object target, List<DictDefinition> dictDefinitions,
Map<String, Map<Long, String>> dictMapMap) {
for (DictDefinition dictDefinition : dictDefinitions) {
//获取Dict注解和字段信息
Dict dict = dictDefinition.getDict();
Field field = dictDefinition.getField();
//获取字典映射
Map<Long, String> dictMap = dictMapMap.get(dict.dictType());
//读取实体中带有注解的字段原有的值
String dictCode = String.valueOf(FieldUtils.readField(target, field.getName(), true));
//类型转换,我在库中用的存储的字段是String,所以需要进行类型的转换,如果你的库设置的不是,或者其他的类型,需要注意到这个地方
Long longDictCode=Long.valueOf(dictCode);
//拿到字典注解的label
String dictField = StringUtils.isEmpty(dict.dictField()) ? field.getName() + DICT_FIELD_SUFFIX : dict.dictField();
//设置字段的字典文本值
FieldUtils.writeField(target, dictField, dictMap.get(longDictCode), true);
}
}
@Data
public static class DictDefinition {
private Dict dict;
private Field field;
}
}
系统表的查询接口:
//这个我是在SysDictController下添加的接口,目的是实现查找多个类型的内容,同时还需要写个fegin接口以供别的模块使用
/**
* 通过字典类型查找字典
* @param types 类型
* @return 同类型字典
*/
@PostMapping("/types")
public List<SysDictItem> getDictByTypes(@RequestBody List<String> types) {
return sysDictItemService.list(Wrappers.<SysDictItem>query().lambda().in(SysDictItem::getDictType, types));
}
使用方法
直接在实体字段上方添加注解
1.dictType:数据库中的字典的类型
2.dictField:实体中赋值的字段
如上方使用方法所示,如果你想将字典中的label值给实体中的 catalogStructure,则标注
如果前端需要,一个原来的ID,一个label,那就需要创建一个DTO,如图所示,将dictField 后边写上你想要的字段名称就行了
四,备注以及扩展
这个是公司用的pigx框架,没用到的同学,可以跳过了,仅供参考,依据具体项目实际来定
参数的入值:
@AfterReturning(pointcut = "doPointcut()", returning = "result")
public void doAfterReturning(JoinPoint pjp, Object result) {
try {
DictUtils.convertDict(result);
} catch (Exception e) {
log.error(String.valueOf(e));
}
}
这个代码中的result需要着重注意,他可能会存在判断的问题
举例:这个是正常的,只拿到这个返回的实体就行,在我们实现三中的方法convertDict所有if()的目的都是为了拿到返回值中的实体
比如底下这个图,分页返回的就需要做一个判断(实现三我已经加上了,这个做一个参考):
判断:
五:具体实现 优化1,多个字段的匹配,用“,”隔开代码优化
@Component
public class DictUtils {
private static final String DICT_FIELD_SUFFIX = "Text";
/**
* 执行字典转换的公共方法
* @param target 待转换的对象
*/
public static void convertDict(Object target) {
//判断是否是分页
if (target instanceof Page) {
for (Object object : ((Page<?>) target).getRecords()) {
convertDictInternal(object);
}
//是否是list
} else if (target instanceof List) {
if (CollectionUtils.isNotEmpty((List<?>) target)) {
for (Object object : (List<?>) target) {
convertDictInternal(object);
}
}
} else {
convertDictInternal(target);
}
}
/**
* 内部的字典转换实现方法
* @param target 待转换的对象
*/
@SneakyThrows
private static void convertDictInternal(Object target) {
// 获取目标对象的字典定义
List<DictDefinition> dictDefinitions = getMetadata(target);
// 如果没有字典定义则直接返回
if (CollectionUtils.isEmpty(dictDefinitions)) {
return;
}
// 提取字典类型并获取字典映射
String commaSeparatedString = dictDefinitions.stream()
.map(d -> d.getDict().dictType())
.collect(Collectors.joining(","));
List<String> dictNames = Arrays.asList(commaSeparatedString.split(","));
Map<String, Map<Long, String>> dictMapMap = getDictMap(dictNames);
// 执行具体的字典转换操作
doConvertDict(target, dictDefinitions, dictMapMap);
}
/**
* 获取目标对象的字典定义
* @param target 目标对象
* @return 字典定义列表
*/
private static List<DictDefinition> getMetadata(Object target) {
List<DictDefinition> dictDefinitions = new ArrayList<>();
// 排除基本类型、包装类型、Map 和 String 类型
if (ClassUtils.isPrimitiveOrWrapper(target.getClass())
|| target instanceof Map || target instanceof String) {
return dictDefinitions;
}
// 获取目标对象的所有字段
List<Field> fields = FieldUtils.getAllFieldsList(target.getClass());
for (Field field : fields) {
// 获取字段上的 Dict 注解
Dict dict = AnnotationUtils.getAnnotation(field, Dict.class);
if (dict!= null) {
// 创建并添加字典定义
DictDefinition dictDefinition = new DictDefinition();
dictDefinition.setDict(dict);
dictDefinition.setField(field);
dictDefinitions.add(dictDefinition);
}
}
return dictDefinitions;
}
/**
* 根据字典名称获取字典映射
* @param dictNames 字典名称列表
* @return 字典映射
*/
@SneakyThrows
private static Map<String, Map<Long, String>> getDictMap(List<String> dictNames) {
final RemoteDictService remoteDictService = SpringContextHolder.getBean(RemoteDictService.class);
List<SysDictItem> dictList = remoteDictService.getDictItemByType(dictNames);
return new HashMap<>(dictList.stream().collect(Collectors.groupingBy(
SysDictItem::getDictType, Collectors.toMap(SysDictItem::getId, SysDictItem::getLabel, (v1, v2) -> v2))));
}
/**
* 执行具体的字典字段转换
* @param target 目标对象
* @param dictDefinitions 字典定义列表
* @param dictMapMap 字典映射
*/
@SneakyThrows
private static void doConvertDict(Object target, List<DictDefinition> dictDefinitions,
Map<String, Map<Long, String>> dictMapMap) {
for (DictDefinition dictDefinition : dictDefinitions) {
Dict dict = dictDefinition.getDict();
Field field = dictDefinition.getField();
String[] dictTypes = dict.dictType().split(",");
for (String dictType : dictTypes) {
Map<Long, String> dictMap = dictMapMap.get(dictType.trim());
// 读取字段的字典编码
String dictCode = String.valueOf(FieldUtils.readField(target, field.getName(), true));
// 类型转换
Long longDictCode = Long.valueOf(dictCode);
// 确定字典字段名
String dictField = StringUtils.isEmpty(dict.dictField())? field.getName() + DICT_FIELD_SUFFIX : dict.dictField();
// 设置字段的字典文本值
if (dictMap!= null && dictMap.containsKey(longDictCode)) {
FieldUtils.writeField(target, dictField, dictMap.get(longDictCode), true);
}
}
}
}
@Data
public static class DictDefinition {
private Dict dict;
private Field field;
}
}
使用方式:
参考:
主要内容参考:
java自定义注解实现数据字典映射_字典映射 java-CSDN博客
分页扩展参考:
字典翻译@Dict - 莫大人 - 博客园