字典部分这一块无论是前端和后端都可以做,在目前所接触的项目环境中是前端来做会比较方便的,但是有遇到需要不断远程调用查询的场景,类似的动作不断重复的代码,因此就自己写了个注解实现字典翻译,故此来记录一下过程。
环境:SpringBoot+Mybatis-plus
很多场景下,我们存储的数据是需要经过翻译的,就比如:
这些数据存在系统的字典中,表数据存储的时候为了性能考虑不直接存 男 、 女,只是存1 或 0 ,前台展示的时候就需要把该值进行翻译。
每个业务都写的话会比较多类似的操作,可以使用AOP的形式简化工作。
一般我们存在男女结构的字段可能是这样的:
/**
* 性别
*/
private Integer sex;
是一个Int类型的数据,而翻译过后的数据是String类型的, 我们可以手动添加String类型字段:
/**
* 性别
*/
private Integer sex;
/**
* 性别
*/
private String sexText;
那有大量的操作我们都需要手动添加字段,貌似也有点麻烦。
解决办法:
注解应该放在controller上,在controller执行完后才对数据增强做翻译工作,因前后端分离返回给前端的是JSON,可以用JSONObject
对象
那么JSONObject是怎么做的呢?
//使用put方法增加一个key、value
jsonObject.put(fieldName + SUFFIX, value);
这里的fieldName可以理解为sex,SUFFIX是后缀,比如统一为TEXT
那么前端展示数据的时候就可以直接使用sexText字段即可。
切点应该放在controller吗?注解怎么设计?
这里有两个注解,一个注解放在entity的成员变量上,另一个放在controller方法上,切点只要捕捉注解
即可。设计的时候还是尽量偏向于要的时候再做,减少对整个系统的影响。
在entity上标注@Dict注解,注明需要翻译的字段为sex,这里的key是只字典参数的key
/**
* 性别
*/
@Dict(fieldName = "sex", key="SYSTEM_SEX")
private Integer sex;
在controller上标注@DoDict作为aop切面的切点
@DoDict
@GetMapping("/page")
public R page() {
//...执行操作
}
其中@Dict注解:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Dict {
/**
* 需要翻译的字段名称
*/
String fieldName();
/**
* 系统参数的key值
* @return paramKey
*/
String key();
}
其中fieldName表名是哪个字段需要翻译,key作为字典的参数key,用于查询字典时用的。
这里可能每个项目设计的有不同,因此可以灵活设计。
其中@DoDict注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DoDict {
}
该注解作用在controller方法上,仅仅标识需要开启自动翻译。有的教程切点是作为controller的,但并不是所有场景都需要,因此增加了该注解。
其中统一返回的结果集R:
@Data
public class R<T> implements Serializable {
private static final long serialVersionUID = 1L;
private int code;
private boolean success;
private T data;
private String msg;
}
使用JSONObject可以添加字段,但是对象里的sex值应该怎么获取到?
主要是通过反射的形式,java的反射能够知道这个类的所有属性和方法,甚至是改变它的属性值,因此从反射中拿即可。
public class BeanUtil{
/**
* 通过反射获取包括父类的所有类属性
*
* @param object obj
* @return 获取类的所有属性
*/
public static Field[] getAllFields(Object object) {
Class<?> clazz = object.getClass();
List<Field> fieldList = new ArrayList<>();
while (clazz != null) {
fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())));
clazz = clazz.getSuperclass();
}
Field[] fields = new Field[fieldList.size()];
fieldList.toArray(fields);
return fields;
}
}
通过Field对象循环遍历即可。
AOP的代码(核心):
/**
* 自定义切面,翻译字典字段
*
*/
@Aspect
@Component
@Slf4j
public class DoDictAspect {
private final String SUFFIX = "Text";
/**
* 当DoDict标注时执行
*/
@Pointcut("@annotation(org.springblade.system.business.smsreminder.anno.DoDict)")
public void execute() {
}
@Around("execute()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long start = System.currentTimeMillis();
//获取执行完的数据
Object proceed = point.proceed();
//翻译数据
doDict(proceed);
log.info("解析数据执行时间:{}", (System.currentTimeMillis() - start));
return proceed;
}
}
这里我们可以看到,我们具体执行逻辑都在环绕方法中,获取程序执行完的数据后对数据增强,具体的实现逻辑在
Object proceed = point.proceed();
//翻译数据
doDict(proceed);
核心代码doDict:
/**
* 翻译字典值、调用远程方法获取号码归属地
*
* @param proceed controller执行完的数据
*/
private void doDict(Object proceed) {
//如果返回结果不是R的话就返回
if (!(proceed instanceof R)) {
return;
}
R<Object> r = (R) proceed;
//获取相关数据
Object data = r.getData();
if (data == null) {
return;
}
JSONObject pageJsonObject = null;
JSONObject jsonObject = JSON.parseObject(JSON.toJSONString(data));
if (data instanceof List) {
//当返回结果为集合时执行
pageJsonObject = convertList(data, jsonObject);
} else {
//当返回结果为Mybatis-plus分页结果时
pageJsonObject = convertPage(data, jsonObject);
}
//重新set数据
r.setData(pageJsonObject);
}
/**
* 获取Mybatis-plus分页records,并将数据翻译
*
* @param data IPage>
* @param jsonObject IPage> -> jsonObject
* @return 翻译好的数据
*/
private JSONObject convertPage(Object data, JSONObject jsonObject) {
//校验
if (!(data instanceof IPage)) {
return jsonObject;
}
IPage page = (IPage) data;
if (CollectionUtil.isEmpty(page.getRecords())) {
return jsonObject;
}
//开始执行翻译字典或归属地
List<JSONObject> result = doConvertDict(page.getRecords());
page.setRecords(result);
//返回整个IPage
return JSON.parseObject(JSON.toJSONString(page));
}
/**
* 利用反射执行翻译操作
*
* @param records
* @return
*/
private List<JSONObject> doConvertDict(List<?> records) {
List<JSONObject> result = new ArrayList<>();
for (Object record : records) {
//利用反射获取该对象的属性
for (Field field : BeanUtil.getAllFields(record)) {
if (field.getAnnotation(Dict.class) == null) {
continue;
}
JSONObject jsonObject = JSON.parseObject(JSON.toJSONString(record));
//获取需要翻译的字段名称,比如 sex
String fieldName = field.getAnnotation(Dict.class).fieldName();
//获取当前需要翻译的字段的值 比如 1
String currentFieldName = String.valueOf(jsonObject.get(fieldName));
//执行翻译字典的相关操作 该内容为具体的业务实现,项目不同过程不同。
String systemParamKey = field.getAnnotation(Dict.class).key();
String value = ParamCache.getValue(systemParamKey);
//获取字段参数值
String paramValues = CommonUtil.getParamValues(systemParamKey, currentFieldName);
//put数据,并且加上后缀
jsonObject.put(fieldName + SUFFIX, paramValues);
result.add(jsonObject);
}
}
return result;
}
private JSONObject convertList(Object data, JSONObject jsonObject) {
List<?> list = (List<?>) data;
if (CollectionUtil.isEmpty(list)) {
return jsonObject;
}
//执行翻译字典或归属地
List<JSONObject> result = doConvertDict(list);
//返回整个IPage
return JSON.parseObject(JSON.toJSONString(result));
}