• 自定义注解+AOP实现字典值的翻译


    前言

    字典部分这一块无论是前端和后端都可以做,在目前所接触的项目环境中是前端来做会比较方便的,但是有遇到需要不断远程调用查询的场景,类似的动作不断重复的代码,因此就自己写了个注解实现字典翻译,故此来记录一下过程。

    环境:SpringBoot+Mybatis-plus

    分析过程

    很多场景下,我们存储的数据是需要经过翻译的,就比如:

    • 1=男;0=女
    • 1=已启用;2=已停用

    这些数据存在系统的字典中,表数据存储的时候为了性能考虑不直接存 男 、 女,只是存1 或 0 ,前台展示的时候就需要把该值进行翻译。

    每个业务都写的话会比较多类似的操作,可以使用AOP的形式简化工作。

    问题一

    一般我们存在男女结构的字段可能是这样的:

    	/**
    	 * 性别
    	 */
    	private Integer sex;
    
    • 1
    • 2
    • 3
    • 4

    是一个Int类型的数据,而翻译过后的数据是String类型的, 我们可以手动添加String类型字段:

    	/**
    	 * 性别
    	 */
    	private Integer sex;
    
    	/**
    	 * 性别
    	 */
    	private String sexText;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    那有大量的操作我们都需要手动添加字段,貌似也有点麻烦。

    解决办法:

    注解应该放在controller上,在controller执行完后才对数据增强做翻译工作,因前后端分离返回给前端的是JSON,可以用JSONObject对象

    那么JSONObject是怎么做的呢?

    //使用put方法增加一个key、value
    jsonObject.put(fieldName + SUFFIX, value);
    
    • 1
    • 2

    这里的fieldName可以理解为sex,SUFFIX是后缀,比如统一为TEXT

    那么前端展示数据的时候就可以直接使用sexText字段即可。

    问题二

    切点应该放在controller吗?注解怎么设计?

    这里有两个注解,一个注解放在entity的成员变量上,另一个放在controller方法上,切点只要捕捉注解即可。设计的时候还是尽量偏向于要的时候再做,减少对整个系统的影响。

    在entity上标注@Dict注解,注明需要翻译的字段为sex,这里的key是只字典参数的key

    	/**
    	 * 性别
    	 */
    	@Dict(fieldName = "sex", key="SYSTEM_SEX")
    	private Integer sex;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在controller上标注@DoDict作为aop切面的切点

    	@DoDict
    	@GetMapping("/page")
    	public R page() {
            //...执行操作
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    其中@Dict注解:

    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Dict {
    
    	/**
    	 * 需要翻译的字段名称
    	 */
    	String fieldName();
    
    	/**
    	 * 系统参数的key值
    	 * @return paramKey
    	 */
    	String key();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    其中fieldName表名是哪个字段需要翻译,key作为字典的参数key,用于查询字典时用的。

    这里可能每个项目设计的有不同,因此可以灵活设计。

    其中@DoDict注解:

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface DoDict {
        
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    该注解作用在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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    问题三

    使用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;
    	}
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    通过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;
    	}
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    这里我们可以看到,我们具体执行逻辑都在环绕方法中,获取程序执行完的数据后对数据增强,具体的实现逻辑在

    	Object proceed = point.proceed();
    	//翻译数据
    	doDict(proceed);
    
    • 1
    • 2
    • 3

    核心代码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));
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
  • 相关阅读:
    nginx-location和proxy_pass的url拼接
    spring面试题
    一位大咖写给软件编程新手的建议 - 经验谈
    SpringBoot自动装配原理(简单易懂)
    el-tree结合el-switch实现状态切换
    Python实现文字识别OCR
    如何摆脱打工人任人宰割的命运
    【JVM】JVM实战笔记-随笔
    深入浅出PyTorch函数torch.rand与torch.randn
    Codeforces Round #684 (Div. 1)
  • 原文地址:https://blog.csdn.net/wq2323/article/details/125410048