需求来源
在开发过程中,必不可少会用到字典,例如,数据库字段性别字段可能是这样的:1:男;2:女,在数据存储的时候用1和2,但是在前端展示的时候需要使用男和女,而我们一般的开发方式有三种:
- 在sql查询的时候使用case函数判断,做字段值的转换
- 在查询出来结果后,遍历查询结果,根据数据库原值,新加上一个对应的字典值
- 返回原值,告诉调用方字典列表,让调用方自行转换
最终的结果大概是这样:
|
方式1:需要手动写SQL,很繁琐
方式2:查出来之后需要再次便利一次,也要写代码,容易重复造轮子
方式3:对调用方来说不是很方便,并且后续加入字典含义需要改变,又需要同步给调用方,让他们去更新上线(虽然一般不会出现这样的情况)
鉴于这样的情况是非常常见的,这里我想针对方式2做一个封装,避免重复的去遍历、设置,所有有了这个字典转换组件。
实现思路
可以使用aop的思想,使用环绕通知,获取到方法的结果后,在对结果进行遍历处理
- 定义两个注解
- DictCovert 用于定义切入点
- Dict 真正的字典注解
- DictAspect 用来实现具体的注解逻辑
- DictUtil 封装字典翻译的逻辑,可提供手动方式
代码实现
定义DictCovert注解:
import java.lang.annotation.*;
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DictCovert {
}
定义Dict注解:
import java.lang.annotation.*;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Dict {
/**
* 是否是本地字典
* 本地字典:使用dictVal值做解析
* 非本地字典:集合字典服务查询回来
**/
boolean isLocal() default true;
/**
* 字典存放后缀
* 默认 "Text"
* 例 原始字段名:type 翻译储存的字段名:typeText
**/
String suffix() default "Text";
/**
* 本地字典键值对,当isLocal=true时有效
* 例如:1:草稿;2:已提交;3:已删除
* 为了方便,使用的是中文的:和;
**/
String localDictVal() default "";
/**
* 在线字典编码,当isLocal=false时有效
* 例如:onlineDictCode=SEX,则会调用 sysDictService.getDictItemMap("SEX");去获取字典值
*/
String onlineDictCode() default "";
}
dictUtil工具类
注意:这里面有些类是需要替换成自己的,比如字典服务类,比如分页的对象等
import cn.hutool.core.util.ObjectUtil;
import com.xxx.*;
import com.google.common.collect.Lists;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author: v_luojinj
* @since: 2023/2/2
**/
@Component
public class DictUtil {
/**
* 使用个缓存,避免列表数据多次进行字典查询
*/
private static ThreadLocal<Map<String,Map<String,String>>> cache = ThreadLocal.withInitial(ConcurrentHashMap::new);
public static Object parseResult(Object result) throws Exception {
//判断空
if (Objects.isNull(result)) {
return null;
}
//判断结果类型
if (result instanceof List) {
//LIST类型
List<Object> list = Lists.newArrayList();
for (Object obj : (List<Object>) result) {
list.add(parseDict(obj));
}
return list;
} else if (result instanceof PageResult) {
//自定义的分页返回结果集类型 实际结果在 list字段中。 处理和LIST一致
PageResult pageResult = (PageResult) result;
List<Object> list = Lists.newArrayList();
for (Object obj : pageResult.getList()) {
list.add(parseDict(obj));
}
//分页数据中 重新放入结果
pageResult.setList(list);
return pageResult;
} else {
//单实例对象类型
return parseDict(result);
}
}
/**
* 字典转换
* @param obj
*/
private static Object parseDict(Object obj) throws NoSuchFieldException, IllegalAccessException {
Field[] fields = obj.getClass().getDeclaredFields();
//非空判断
if (ObjectUtil.isEmpty(fields)) {
return null;
}
for (Field field : fields) {
//判断每一个字典是否有Dict注解
if (Objects.nonNull(field.getAnnotation(Dict.class))) {
handleDict(obj, field);
}
}
return obj;
}
/**
* 处理字典注解的字段
* @param obj
* @param field
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static void handleDict(Object obj, Field field) throws NoSuchFieldException, IllegalAccessException {
Dict dict = field.getAnnotation(Dict.class);
boolean local = dict.isLocal();
if(local) {
handleLocalDict(obj, field, dict);
} else {
handleOnlineDict(obj, field, dict);
}
}
/**
* 处理本地字典转换的逻辑
* @param obj
* @param field
* @param dict
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static void handleLocalDict(Object obj, Field field, Dict dict) throws NoSuchFieldException, IllegalAccessException {
String suffix = dict.suffix();
String dictVal = dict.localDictVal();
field.setAccessible(true);
Object key = field.get(obj);
if(StringUtils.isEmpty(dictVal) || Objects.isNull(key)){
return;
}
Map<String, String> dictValMap = getLocalDictValMap(dictVal);
Field name = obj.getClass().getDeclaredField(field.getName() + suffix);
name.setAccessible(true);
name.set(obj, dictValMap.get(key.toString()));
name.setAccessible(false);
field.setAccessible(false);
}
/**
* 处理在线字典转换的逻辑
* @param obj
* @param field
* @param dict
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static void handleOnlineDict(Object obj, Field field, Dict dict) throws NoSuchFieldException, IllegalAccessException {
String suffix = dict.suffix();
String dictCode = dict.onlineDictCode();
field.setAccessible(true);
Object key = field.get(obj);
if(StringUtils.isEmpty(dictCode) || Objects.isNull(key)){
return;
}
Map<String, String> dictValMap = getOnlineDictValMap(dictCode);
Field name = obj.getClass().getDeclaredField(field.getName() + suffix);
name.setAccessible(true);
name.set(obj, dictValMap.get(key.toString()));
name.setAccessible(false);
field.setAccessible(false);
}
/**
* 获取本地字典值列表,格式要求:1:草稿,2:已提交,3:已删除
* @param dictVal 字典键值对
* @return 字典列表
*/
private static Map<String, String> getLocalDictValMap(String dictVal) {
if (StringUtils.isEmpty(dictVal)) {
return Collections.emptyMap();
}
Map<String, Map<String, String>> dictMap = cache.get();
if(dictMap.containsKey(dictVal)){
return dictMap.get(dictVal);
}
Map<String, String> valMap = new HashMap<>();
String[] split = dictVal.split(";");
for (String s : split) {
String[] val = s.split(":");
if (val.length != 2) {
continue;
}
valMap.put(val[0], val[1]);
}
dictMap.put(dictVal,valMap);
cache.set(dictMap);
return valMap;
}
/**
* 获取字典服务那边的字典列表
* @param dictCode 字典编码
* @return 字典列表
*/
private static Map<String, String> getOnlineDictValMap(String dictCode) {
if (StringUtils.isEmpty(dictCode)) {
return Collections.emptyMap();
}
Map<String, Map<String, String>> dictMap = cache.get();
if(dictMap.containsKey(dictCode)){
return dictMap.get(dictCode);
}
SysDictService sysDictService = SpringContextHolder.getBean(SysDictService.class);
Map<String, String> dictItemMap = sysDictService.getDictItemMap(dictCode);
if(CollectionUtils.isEmpty(dictItemMap)){
throw new ServiceException(ErrorCode.OBJECT_OR_PROPERTY_NOT_EXISTS,"未找到该字典编码:"+dictCode+",请检查");
}
dictMap.put(dictCode,dictItemMap);
cache.set(dictMap);
return dictItemMap;
}
/**
* 清除缓存
*/
public static void clearCache(){
cache.remove();
}
}
DictAsect切面:
import com.dg.ys.zsyz.annotation.dict.DictUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Configuration;
/**
* @author: v_luojinj
* @since: 2023/1/18
**/
@Aspect
@Configuration
public class DictAspect {
@Pointcut("@annotation(com.dg.ys.zsyz.annotation.dict.DictCovert)")
public void dictCovert() {
}
@Around("dictCovert()")
public Object process(ProceedingJoinPoint joinPoint) throws Throwable {
//1.执行原方法
Object proceed = joinPoint.proceed();
//2.拿到原方法的原返回值 调用parseResult进行字典转换
Object result = DictUtil.parseResult(proceed);
DictUtil.clearCache();
return result;
}
}
使用方式
Dict注解有两种使用方式:
1、isLocal = true,默认的方式,这种方式不用调用字典服务发起rpc请求,它通过解析localDictVal字段来做字典的翻译,格式见注释,一般情况下这种方式应该更常用;
2、isLocal = false,这种方式需要指定onlineDictCode,会调用字典服务发起rpc请求,获取字典服务那边的配置信息;
真正使用转换上也有两种使用方式:
1、使用DictCovert注解,在需要对结果对象做字典转换的接口或方法上添加该注解
2、使用DictUtil#parseResult 方法手动进行字典转换
详细步骤
通过注解作为aop切入点的方式:
1、在VO里需要做字典转换的字段添加@Dict,并新增用来存储的字段,如下面source和abc字段,source使用的是本地字典,abc使用的是在线字典
|
2、在control接口处添加@DictCovert
|
使用工具方法的方式:
直接调用DictUtil#parseResult 该方法即可
注意
目前只对返回结果是java.util.List、PageResult、普通的VO类这些类型才可以做字典转换,并且不会做深度的字段转换,例如一个对象A里面的属性又是一个对象B,B里面使用Dict注解是识别不到的,这种可以在获取对象B的时候去做这个转换动作,而不是在外层的对象A上去做。
如需新增其他类型的转换可以在DictUtil#parseResult 里面添加