最近项目中数据字典的配置使用越来越多,导致代码中有大量的数据字典翻译工作,使得代码的整洁性和维护性变差。排除掉对数据结果进行“清洗”的方案后,还是决定通过注解的方式来解决这个问题。参考了一些文章后完成了这个功能(具体实在记不清了,雷同处,海涵!)。最近有些时间顺便整理一下思路。
目标功能是这样,在返回数据的实体上通过注解标记当前字段(dict_field)对应的code值,然后通过反射读取这个字段的code和字段对应的值,然后从字典表中基于code和当前的值(key)获取对应的value,然后将value赋值在另一个字段(dict_value_field,当然也可以根据需要自行处理),返回处理后的内容。
1 字典表字段
这里主要是code,dict_key,dict_value这三个字段
实体对象
/*
*/
package org.springblade.modules.system.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 实体类
*
* @author Chill
*/
@Data
@TableName("blade_dict")
@ApiModel(value = "Dict对象", description = "Dict对象")
public class Dict implements Serializable {
public final static Long TOPDICT_PARENTID=0L;//父类的parentid
public final static String KUAIDI_COM="kuaidi_com";//快递公司
public final static String CANCEL_REASON="cancelreason";//取消订单的原因
public final static Integer IS_SEALED_YES = 1;//封存
public final static Integer IS_SEALED_NO = 0;//没有封存
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@JsonSerialize(using = ToStringSerializer.class)
@ApiModelProperty(value = "主键")
@TableId(value = "id", type = IdType.ID_WORKER)
private Long id;
/**
* 父主键
*/
@JsonSerialize(using = ToStringSerializer.class)
@ApiModelProperty(value = "父主键")
private Long parentId;
/**
* 字典码
*/
@ApiModelProperty(value = "字典码")
private String code;
/**
* 字典值
*/
@ApiModelProperty(value = "字典值")
private String dictKey;
/**
* 字典名称
*/
@ApiModelProperty(value = "字典名称")
private String dictValue;
/**
* 排序
*/
@ApiModelProperty(value = "排序")
private Integer sort;
/**
* 字典备注
*/
@ApiModelProperty(value = "字典备注")
private String remark;
/**
* 是否已封存
*/
@ApiModelProperty(value = "是否已封存")
private Integer isSealed;
/**
* 是否已删除
*/
@TableLogic
@ApiModelProperty(value = "是否已删除")
private Integer isDeleted;
}
2 基本思路,步骤
2.1 新建注解DataDict,主要是用户标记实体字段对应的code。
package org.springblade.modules.focus.aspect;
import io.swagger.annotations.ApiModelProperty;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface DataDict {
@ApiModelProperty(value = "可以用于设置默认的key值")
String dictKey() default "";
@ApiModelProperty(value = "需要翻译的code")
String code() default "";
}
2.2 新建注解DataDictClass,这个注解主要给AOP使用,放在需要翻译的方法上,然后AOP基于这个注解做一个切面,进行数据处理。
package org.springblade.modules.focus.aspect;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DataDictClass {
}
2.3 切面逻辑
2.3.1 切入点-基于注解dataDictClass
@Pointcut("@annotation(dataDictClass)")
public void doDataDictClass(DataDictClass dataDictClass) {
}
2.3.2 数据处理,先说一下当前项目的返回数据。结果全部封装在R对象中。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springblade.core.tool.api;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
import java.util.Optional;
import org.springblade.core.tool.utils.ObjectUtil;
import org.springframework.lang.Nullable;
@ApiModel(
description = "返回信息"
)
public class R<T> implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(
value = "状态码",
required = true
)
private int code;
@ApiModelProperty(
value = "是否成功",
required = true
)
private boolean success;
@ApiModelProperty("承载数据")
private T data;
@ApiModelProperty(
value = "返回消息",
required = true
)
private String msg;
private R(IResultCode resultCode) {
this(resultCode, (Object)null, resultCode.getMessage());
}
private R(IResultCode resultCode, String msg) {
this(resultCode, (Object)null, msg);
}
private R(IResultCode resultCode, T data) {
this(resultCode, data, resultCode.getMessage());
}
private R(IResultCode resultCode, T data, String msg) {
this(resultCode.getCode(), data, msg);
}
private R(int code, T data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
this.success = ResultCode.SUCCESS.code == code;
}
public R() {
}
}
这里需要处理的数据类型
2.3.2.1: 分页数据R<IPage<T>>
2.3.2.2: 普通列表R<List<T>>
2.3.2.3 :普通数据R<T>
所以这里主要针对这几种数据来处理
@Around("@annotation(dataDictClass)")
public Object translation(final ProceedingJoinPoint pjp, DataDictClass dataDictClass) throws Throwable {
// 拿到要返回的数据--这里可以打印下看看内容
Object resultR = pjp.proceed();
if (ObjectUtil.isNull(resultR)) {
return resultR;
}
Object obj;
Object resultPage = ((R) resultR).getData();
if (resultPage instanceof IPage) {
// 分页的情况
return translationPage(pjp);
}
Object result = ((R) resultR).getData();
result = translate(result);
((R) resultR).setData(result);
// 返回处理后的数据
return resultR;
}
完整代码
package org.springblade.modules.focus.aspect;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
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.springblade.core.tool.api.R;
import org.springblade.modules.focus.dto.DataDictDTO;
import org.springblade.modules.system.entity.Dict;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 字典注解内容翻译
*/
@Aspect
@Component
@SuppressWarnings({"unused"})
public class DatDictAspect {
// @Autowired
// private DictUtils dictUtils;
private Map<String, String> dictInfoMap = new ConcurrentHashMap<>();
@Pointcut("@annotation(dataDictClass)")
public void doDataDictClass(DataDictClass dataDictClass) {
}
@Around("@annotation(dataDictClass)")
public Object translation(final ProceedingJoinPoint pjp, DataDictClass dataDictClass) throws Throwable {
Object resultR = pjp.proceed();
if (ObjectUtil.isNull(resultR)) {
return resultR;
}
Object obj;
Object resultPage = ((R) resultR).getData();
if (resultPage instanceof IPage) {
// 分页的情况
return translationPage(pjp);
}
Object result = ((R) resultR).getData();
result = translate(result);
((R) resultR).setData(result);
return resultR;
}
public Object translationPage(final ProceedingJoinPoint pjp) throws Throwable {
Object resultR = pjp.proceed();
if (resultR == null) {
return resultR;
}
Object resultPage = ((R) resultR).getData();
IPage page = (IPage) resultPage;
Object result = ((IPage) resultPage).getRecords();
result = translate(result);
page.setRecords((List) result);
((R) resultR).setData(page);
return resultR;
}
private Object translate(Object result) {
Object obj;
if (result instanceof List || result instanceof ArrayList) {
List olist = ((List) result);
if (olist.size() == 0) {
return result;
}
obj = olist.get(0);
} else {
obj = result;
}
// 获取所有配置注解的code 和实体字段
List<DataDictDTO> dictParams = getDictMapping(obj.getClass());
if (dictParams.size() == 0) {
return result;
}
//根据code 获取对应的字典内容
// List<Dict> dictInfos = dictUtils.getDicts(getCodes(dictParams));
List<Dict> dictInfos = getDictTest();
if (dictInfos == null && dictInfos.size() == 0) {
return result;
}
//先把字典值转成map
for (Dict dict : dictInfos) {
dictInfoMap.put(dict.getCode() + "_" + dict.getDictKey(), dict.getDictValue());
}
if (result instanceof List || result instanceof ArrayList) {
for (Object entity : (List) result) {
assign(entity, dictParams, dictInfoMap);
}
} else {
assign(result, dictParams, dictInfoMap);
}
return result;
}
/**
* 测试用---实际中最好把所有的数据封装在redis中
* @return
*/
private List<Dict> getDictTest(){
List<Dict> dicts = new ArrayList<>();
Dict dict1 = new Dict();
dict1.setCode("channelCode");
dict1.setDictKey("20");
dict1.setDictValue("版本20");
dicts.add(dict1);
return dicts;
}
/**
* 单个设置值
* 这里的逻辑是获取配置注解的字段名称---比如 dict
* 然后将从字典拿到的值赋值给另一个字段---dictValue
* 当然可以根据具体情况去处理
*
* @param entity
* @param dictParams
* @param dictInfoMap
*/
public void assign(Object entity, List<DataDictDTO> dictParams, Map<String, String> dictInfoMap) {
for (DataDictDTO dictParam : dictParams) {
String code = dictParam.getCode();
String dictName = dictParam.getFieldName();
try {
Class c = entity.getClass();
if (c != null) {
Field f = c.getDeclaredField(dictName);
f.setAccessible(true);
Object preValue = f.get(entity);
if (ObjectUtil.isNotNull(preValue)) {
// 需要赋值的字段
Field fvalue = c.getDeclaredField(dictName + "Value");
fvalue.setAccessible(true);
fvalue.set(entity, dictInfoMap.getOrDefault(code + "_" + preValue.toString(), preValue.toString()));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 获取实体中所有需要翻译的code
* @param dataDictDTOS
* @return
*/
private List<String> getCodes(List<DataDictDTO> dataDictDTOS) {
List<String> codes = new ArrayList<>();
if (CollectionUtil.isEmpty(dataDictDTOS)) {
return codes;
}
for (DataDictDTO dataDictDTO : dataDictDTOS) {
codes.add(dataDictDTO.getCode());
}
return codes;
}
/**
* 获取实体中配置的code 和对应的字段内容
* @param cla
* @return
*/
private List<DataDictDTO> getDictMapping(Class cla) {
Field[] fields = cla.getDeclaredFields();
List<DataDictDTO> list = new ArrayList<>();
DataDictDTO dataDictDTO;
DataDict dataDict;
for (Field field : fields) {
if (field.isAnnotationPresent(DataDict.class)) {
dataDict = field.getAnnotation(DataDict.class);
dataDictDTO = new DataDictDTO(dataDict.code(), field.getName());
list.add(dataDictDTO);
}
}
return list;
}
}
3 测试
3.1 实体添加注解
package org.springblade.modules.thirdparty.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springblade.modules.focus.aspect.DataDict;
import javax.validation.constraints.NotNull;
@Data
@ApiModel("测试对象")
public class SharpernerVO {
@NotNull
@ApiModelProperty("id")
private String serId;
@NotNull
@ApiModelProperty("需要翻译的字段")
@DataDict(code = "channelCode")
private String dictKey;
@NotNull
@ApiModelProperty("翻译后的值放在这个字段")
private String dictKeyValue;
public SharpernerVO() {
}
public SharpernerVO(@NotNull String dictKey, @NotNull String dictKeyValue) {
this.dictKey = "20";
this.dictKeyValue = dictKeyValue;
}
}
3.2 方法添加注解
@GetMapping(value = "/dict")
@ApiOperationSupport(order = 2)
@ApiOperation(value = "话费提交接口", notes = "话费提交接口")
@DataDictClass
public R dict() {
return R.data(sharpenerdaservice.getDicts());
}
3.3 测试结果
{
"code": 200,
"success": true,
"data": [
{
"serId": "",
"dictKey": "20",
"dictKeyValue": "版本20"
}
],
"msg": "操作成功"
}
已经将字典配置的value返回过来。