自定义注解 通过AOP切面的方式实现所有业务实力类的变更记录

    技术2022-07-10  154

    自定义注解 通过AOP切面的方式实现所有业务实力类的变更记录

    需求:重点难点整体思路:app_changelog 存放变更记录的表自定义注解changeLog自定义注解FieldDescpojo类切面方法切面关键在于通过反射获取对应的类、方法和属性、属性值

    需求:

    实力类的属性值在修改时变化了 ,需要将具体对象,什么属性 ,变化前后的值记录下来 ,形成变更记录;

    例如 AppParty 党员实力类 的属性民族 从汉族变更成傣族

    具体展现形式如下:

    重点难点

    在于不干扰业务的情况下 ,不传参数,仅仅通过切面 获取到具体对象,什么属性 ,变化前后的值;灵活利用反射获取实例; 主要通过切面获取方法和参数;应用反射获取相应的属性和属性值 细节和问题很多,经过多次完善和更新,已经能够应用到所有的情况的变更记录 具体的操作都在ChangeLogAspect ;重点都在代码注释中。

    整体思路:

    创建自定义注解 FieldDesc 对实力类做注释,最基本的是获取属性的名称 ,如nation --民族 ;其次通过type,来区分 属于一下那种类型 ,并做相应的处理,例如修改时 前端参数和数据库对应的值是字典值 比如1-汉族,12-傣族,要将1和12都转换成对应的汉字;又例如变更的是地区id–42,要将42转换成对应的湖北省; 创建自定义注解changeLog,加到具体的更新方法,当更新方法被调用,切面方法ChangeLogAspect会对获取变更的对象和变更前后的属性值进行遍历对比,将变化的值转换后存入数据库

    app_changelog 存放变更记录的表

    CREATE TABLE `app_changelog` ( `id` varchar(32) NOT NULL COMMENT '主键', `object_id` varchar(32) DEFAULT NULL COMMENT '对象ID', `unit_info` varchar(100) DEFAULT NULL COMMENT '单位信息', `type` varchar(10) DEFAULT NULL COMMENT '类型,0更新,1删除,2新增', `user_id` varchar(32) DEFAULT NULL COMMENT '用户ID', `user_name` varchar(50) DEFAULT NULL COMMENT '用户名称', `field_name` varchar(30) DEFAULT NULL COMMENT '字段名', `before_change_value` varchar(255) DEFAULT NULL COMMENT '变更前值', `after_change_value` varchar(255) DEFAULT NULL COMMENT '变更后值', `create_time` datetime DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='变更记录';

    自定义注解changeLog

    添加到server或者controller的update方法上面 作为切点

    package com.sinoecare.vc2.app.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface ChangeLog { String value() default ""; }

    自定义注解FieldDesc

    添加到POJO类的属性上 针对不同的情况做对应的处理

    package com.sinoecare.vc2.common.core.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.FIELD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface FieldDesc { /** * 变更记录的字段名称 * @return */ String name() default ""; /** * 变更记录的字段类型 * “dict” :字典 * “area”:地区id,需要转换全名的 * “pass” :不插入变更记录的字段 * “checkbox” :带字典的多选框 * “date” :日期类型 * “photo” :不展示细节,只提示修改 * @return */ String type() default ""; /** * 变更记录类型为字典 “type” 时,才需要此字段 * @return */ String dictValue() default ""; /** * 变更记录类型为Date 时 确定精度 year day second * @return */ String patten() default "day"; /** * 变更记录类型为photo 时 “对活动封面图进行了修改” * @return */ String photo() default "图片已修改"; }

    pojo类

    例如 AppParty 党员实力类 的属性民族

    /** * 民族 */ @FieldDesc(name = "民族", type = "dict", dictValue = "nation") private String nation;

    切面方法

    兼容Controller 如果注解加在Controller上 则转换到对应的serverImpl实例

    package com.sinoecare.vc2.app.aop; import cn.hutool.core.annotation.AnnotationUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.annotation.TableField; import com.sinoecare.vc2.app.annotations.ChangeLog; import com.sinoecare.vc2.app.mapper.AppChangeLogMapper; import com.sinoecare.vc2.app.remoteService.RemoteSysService; import com.sinoecare.vc2.common.core.annotations.FieldDesc; import com.sinoecare.vc2.common.core.bean.*; import com.sinoecare.vc2.common.core.util.R; import com.sinoecare.vc2.common.core.util.ReflectBeanUtil; import com.sinoecare.vc2.common.core.vo.DictDetailVO; import com.sinoecare.vc2.common.security.service.VillcloudUser; import com.sinoecare.vc2.common.security.util.SecurityUtils; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.Serializable; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.text.SimpleDateFormat; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAccessor; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @Component @Aspect @Slf4j public class ChangeLogAspect { @Autowired private AppChangeLogMapper appChangeLogMapper; @Autowired private RemoteSysService remoteSysService; @Pointcut(value = "@annotation(com.sinoecare.vc2.app.annotations.ChangeLog)") public void aspect() { } @Before("@annotation(changeLog)") public void Around(JoinPoint joinPoint, ChangeLog changeLog) { try { ChangeLog aspect = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(ChangeLog.class); VillcloudUser securityUser = SecurityUtils.getUser(); AppChangeLog alog = new AppChangeLog(); //得到当前业务层实现类类型 Class<?> implClass = joinPoint.getThis().getClass(); Object target = joinPoint.getTarget(); String objClassName = implClass.getName(); Object serviceImpl = target; //兼容Controller 如果注解加在Controller上 则转换到对应的serverImpl实例 if (objClassName.contains("Controller")) { objClassName = objClassName.substring(objClassName.lastIndexOf(".") + 1, objClassName.indexOf("$")); objClassName = objClassName.replace("Controller", "Service"); objClassName = (new StringBuilder()).append(Character.toLowerCase(objClassName.charAt(0))).append(objClassName.substring(1)).toString(); serviceImpl = ReflectUtil.getFieldValue(target, objClassName); } //得到执行方法名 String MethodName = joinPoint.getSignature().getName(); //得到传入参数 ******更新和新增传参为对象,删除传参为主键id Object[] args = joinPoint.getArgs(); Object afterObject = args[0]; //得到传入参数的类类型 Class<?> PareamClass = afterObject.getClass(); //参数的id String id = null; String objectId = null; List<String> ids = null; if (afterObject instanceof String) { id = (String) afterObject; } else if (afterObject instanceof List) { ids = (List<String>) afterObject; } else { id = ReflectUtil.invoke(afterObject, "getId"); //特殊业务处理 if (afterObject instanceof AppFarmerToilet) { objectId = ReflectUtil.invoke(afterObject, "getFarmerId"); } else if (afterObject instanceof AppFarmerOthers) { objectId = ReflectUtil.invoke(afterObject, "getFarmerId"); } else if (afterObject instanceof AppPartyWorkInfo) { objectId = ReflectUtil.invoke(afterObject, "getPartyId"); } else if (afterObject instanceof AppPartyFiveStar) { objectId = ReflectUtil.invoke(afterObject, "getPartyId"); } else if (afterObject instanceof AppPartyDisciplinaryInfo) { objectId = ReflectUtil.invoke(afterObject, "getPartyId"); } } //将通用信息先赋给alog对象 alog.setId(IdUtil.simpleUUID()); alog.setObjectId(id); alog.setUserId(securityUser.getId()); alog.setUserName(securityUser.getRealName()); alog.setCreateTime(LocalDateTime.now()); //将单位id替换到单位名 R<SysUnit> response = remoteSysService.getUnitById(securityUser.getUnitId()); if (response.getData() != null) { alog.setUnitInfo(response.getData().getName()); } //判断调用的方法为update/add/delete 处理相应的逻辑 if (MethodName.startsWith("update") || MethodName.startsWith("modify") || MethodName.startsWith("change") || MethodName.startsWith("add") || MethodName.startsWith("save") || MethodName.startsWith("insert") || MethodName.startsWith("create")) { // 传了id 说明是更新 if (StrUtil.isNotEmpty(id)) { //将类型转化成对应的释义 0 更新,1 删除,2 新增 3.图片 alog.setType("0"); //根据afterObject的id,调用主键查询方法,查询修改前的对象 Method selectByPrimaryKey = serviceImpl.getClass().getMethod("getById", Serializable.class); Object beforeObject = selectByPrimaryKey.invoke(serviceImpl, id); // Object beforeObject = ReflectUtil.invoke(implClass, "getById", (Serializable) id); //得到参数的所有getter方法,遍历,不为null且beforeName 不等于 afterName 的属性就是被修改的值,则插入操作日志 Class<?> OriginalClass = beforeObject.getClass(); Field[] fields = ReflectUtil.getFields(OriginalClass); //要处理的行政区划的相关字段 K-字段 V-行政区划 lanlan 2019-4-24 Map<String, String[]> areaCodes = new HashMap<>(); fieldRow: for (Field field : fields) { boolean isDate = false; String fieldName = field.getName(); //主键的getter方法跳过 if ("id".equals(field.getName()) || "serialVersionUID".equals(field.getName())) { continue; } String getterMethodName = ReflectBeanUtil.getGetterMethodName(fieldName, PareamClass); Object objectBefore = ReflectBeanUtil.invokeMethodByName(beforeObject, getterMethodName); Object objectAfter = ReflectBeanUtil.invokeMethodByName(afterObject, getterMethodName); String beforeName = null; String afterName = null; //属性的值为Data,则转换成string SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"); if (objectBefore instanceof String) { beforeName = (String) objectBefore; } else if (objectBefore instanceof Date) { beforeName = sdf.format(objectBefore); } else if (objectBefore instanceof LocalDateTime) { beforeName = df.format((TemporalAccessor) objectBefore); } else if (objectBefore instanceof Integer) { beforeName = objectBefore.toString(); }else if (objectBefore instanceof Double) { beforeName = objectBefore.toString(); } if (objectAfter instanceof String) { afterName = (String) objectAfter; } else if (objectAfter instanceof Date) { afterName = sdf.format(objectAfter); isDate = true; } else if (objectAfter instanceof LocalDateTime) { afterName = df.format((TemporalAccessor) objectAfter); isDate = true; } else if (objectAfter instanceof Integer) { afterName = objectAfter.toString(); }else if (objectAfter instanceof Double) { afterName = objectAfter.toString(); } //未修改的字段直接跳过,不插入日志 if (afterName == null || (StrUtil.isBlank(afterName) && beforeName == null) || beforeName == afterName || afterName.equals(beforeName)) { continue; } //换算金额的变更记录 if ("cost".equals(fieldName)) { if ("0".equals(beforeName)) { beforeName = "000"; } if ("0".equals(afterName)) { afterName = "000"; } String template = "{}.{}元"; String before1 = beforeName.substring(0, (beforeName.length() - 2)); String before2 = beforeName.substring(beforeName.length() - 2); beforeName = StrUtil.format(template, before1, before2); String after1 = afterName.substring(0, (afterName.length() - 2)); String after2 = afterName.substring(afterName.length() - 2); afterName = StrUtil.format(template, after1, after2); } /* //取得字段的ApiModelProperty注解 if (field.isAnnotationPresent(ApiModelProperty.class)) {//是否使用ApiModelProperty注解 Map<String, Object> annotationValueMap = AnnotationUtil.getAnnotationValueMap(field, ApiModelProperty.class); if (annotationValueMap != null) { fieldName = annotationValueMap.get("value") != null ? annotationValueMap.get("value").toString() : ""; } }*/ //取得字段的TableField注解 如果属性exist 为false的话 表示该字段不是数据库字段则需要跳过 if (field.isAnnotationPresent(TableField.class)) { Map<String, Object> annotationValueMap = AnnotationUtil.getAnnotationValueMap(field, TableField.class); if (annotationValueMap != null && annotationValueMap.get("exist") != null) { boolean flag = Boolean.valueOf(annotationValueMap.get("exist").toString()); if (!flag) { continue fieldRow; } } } //取得字段的FieldDesc注解 if (field.isAnnotationPresent(FieldDesc.class)) {//是否使用FieldDesc注解 Map<String, Object> annotationValueMap = AnnotationUtil.getAnnotationValueMap(field, FieldDesc.class); fieldName = annotationValueMap.get("name") != null ? annotationValueMap.get("name").toString() : ""; String fieldType = annotationValueMap.get("type") != null ? annotationValueMap.get("type").toString() : ""; String DictfieldName = annotationValueMap.get("dictValue") != null ? annotationValueMap.get("dictValue").toString() : ""; String patten = annotationValueMap.get("patten") != null ? annotationValueMap.get("patten").toString() : ""; String photo = annotationValueMap.get("photo") != null ? annotationValueMap.get("photo").toString() : ""; //根据注解的内容做相应处理 switch (fieldType) { case "dict": DictDetailVO dictDetailVO = remoteSysService.queryDictDetailByCode(DictfieldName).getData(); //是有字典的值,将属性的字母转换成对应的释义 如 “sfz” 转换成 “身份证” // fieldName = dictDetailVO.getName(); //查询字典,将单选多选中的值转换成对应的释义 如性别 0 , 1 转换成 女 , 男 List<SysDictionaryDetail> sysDictionaryDetails = dictDetailVO.getDictionaryDetails(); if (sysDictionaryDetails != null && sysDictionaryDetails.size() > 0) { before: for (SysDictionaryDetail sysDictionaryDetail : sysDictionaryDetails) { if (beforeName != null && beforeName.equals(sysDictionaryDetail.getDictValue())) { beforeName = sysDictionaryDetail.getDictName(); break before; } } after: for (SysDictionaryDetail sysDictionaryDetail : sysDictionaryDetails) { if (afterName != null && afterName.equals(sysDictionaryDetail.getDictValue())) { afterName = sysDictionaryDetail.getDictName(); break after; } } } break; //注解为地区id类型 需要将地区编码转换成地区全名 case "area": beforeName = StrUtil.isBlank(beforeName) ? "" : remoteSysService.queryAreaFullName(beforeName).getData(); afterName = StrUtil.isBlank(afterName) ? "" : remoteSysService.queryAreaFullName(afterName).getData(); if (beforeName.equals(afterName)) { continue fieldRow; } break; //注解为带字典的多选框 case "checkbox": String[] befores = JSONUtil.parseArray(beforeName).toArray(new String[0]); beforeName = ""; String[] afters = JSONUtil.parseArray(afterName).toArray(new String[0]); afterName = ""; DictDetailVO checkDetailVO = remoteSysService.queryDictDetailByCode(DictfieldName).getData(); //是有字典的值,将属性的字母转换成对应的释义 如 “sfz” 转换成 “身份证” fieldName = checkDetailVO.getName(); //查询字典,将单选多选中的值转换成对应的释义 如性别 0 , 1 转换成 女 , 男 List<SysDictionaryDetail> checkDictionaryDetails = checkDetailVO.getDictionaryDetails(); if (checkDictionaryDetails != null && checkDictionaryDetails.size() > 0) { for (String beforePart : befores) { for (SysDictionaryDetail sysDictionaryDetail : checkDictionaryDetails) { if (beforePart != null && beforePart.equals(sysDictionaryDetail.getDictValue())) { beforePart = sysDictionaryDetail.getDictName(); beforeName = beforeName + beforePart + ","; break; } } } if (beforeName.endsWith(",")) { beforeName = beforeName.substring(0, beforeName.length() - 1); } for (String afterPart : afters) { for (SysDictionaryDetail sysDictionaryDetail : checkDictionaryDetails) { if (afterPart != null && afterPart.equals(sysDictionaryDetail.getDictValue())) { afterPart = sysDictionaryDetail.getDictName(); afterName = afterName + afterPart + ","; break; } } } if (afterName.endsWith(",")) { afterName = afterName.substring(0, afterName.length() - 1); } } break; case "date": switch (patten) { case "year": if (StrUtil.isBlank(beforeName)) { beforeName = ""; } else { beforeName = beforeName.substring(0, 4); } afterName = afterName.substring(0, 4); break; case "day": if (StrUtil.isBlank(beforeName)) { beforeName = ""; } else { beforeName = beforeName.substring(0, 10); } afterName = afterName.substring(0, 10); break; case "second": default: break; } break; //注解为跳过 该字段 不插入变更记录 case "photo": alog.setType("3"); beforeName = ""; afterName = photo; break; //不展示细节,只提示修改 case "pass": continue fieldRow; //进入default说明注解什么都没写或者注解不正确 跳过不插入变更记录 default: break ; } } else { //无FieldDesc时 处理正常的Date类型的变更记录 if (isDate) { if (StrUtil.isNotEmpty(beforeName)) { beforeName = beforeName.substring(0, 10); } if (StrUtil.isNotEmpty(afterName)) { afterName = afterName.substring(0, 10); } } } //子表变更记录关联到主表上 if (StrUtil.isNotEmpty(objectId)) { alog.setObjectId(objectId); } //插入更新日志 alog.setId(IdUtil.simpleUUID()); alog.setFieldName(fieldName); alog.setBeforeChangeValue(beforeName); alog.setAfterChangeValue(afterName); appChangeLogMapper.insert(alog); alog.setType("0"); } } else { //如果没穿id 则说明是新增 List<String> fields = ReflectBeanUtil.getFields(afterObject.getClass()); for (String field : fields) { if ("xm".equals(field) || "name".equals(field)) { String getterMethodName = ReflectBeanUtil.getGetterMethodName(field, afterObject.getClass()); String fieldValue = (String) ReflectBeanUtil.invokeMethodByName(afterObject, getterMethodName); //新增的对象名 alog.setFieldName(fieldValue); } if ("level".equals(field)) { String getterMethodName = ReflectBeanUtil.getGetterMethodName(field, afterObject.getClass()); Integer fieldValue = (Integer) ReflectBeanUtil.invokeMethodByName(afterObject, getterMethodName); //新增的对象名 alog.setAfterChangeValue(fieldValue + "星党员"); } if ("company".equals(field)) { String getterMethodName = ReflectBeanUtil.getGetterMethodName(field, afterObject.getClass()); String fieldValue = (String) ReflectBeanUtil.invokeMethodByName(afterObject, getterMethodName); //新增的对象名 alog.setAfterChangeValue(fieldValue); } if ("disciplinaryInfo".equals(field)) { String getterMethodName = ReflectBeanUtil.getGetterMethodName(field, afterObject.getClass()); String fieldValue = (String) ReflectBeanUtil.invokeMethodByName(afterObject, getterMethodName); //新增的对象名 alog.setAfterChangeValue(fieldValue); } } if (afterObject instanceof AppPartyFiveStar) { objectId = ReflectUtil.invoke(afterObject, "getPartyId"); alog.setFieldName("五星党员信息"); } else if (afterObject instanceof AppPartyWorkInfo) { objectId = ReflectUtil.invoke(afterObject, "getPartyId"); alog.setFieldName("党员工作信息"); } else if (afterObject instanceof AppPartyDisciplinaryInfo) { objectId = ReflectUtil.invoke(afterObject, "getPartyId"); alog.setFieldName("党员违纪信息"); } alog.setObjectId(objectId); //将类型转化成对应的释义 0 更新,1 删除,2 新增 alog.setType("2"); //插入新增日志 appChangeLogMapper.insert(alog); } } else if (MethodName.startsWith("delete") || MethodName.startsWith("batchDelete") || MethodName.startsWith("remove") || MethodName.startsWith("drop")) { //批量删除遍历主键 for (String currentId : ids) { //根据id,调用主键查询方法,查询删除前的对象 Method selectByPrimaryKey = serviceImpl.getClass().getMethod("getById", Serializable.class); Object beforeObject = selectByPrimaryKey.invoke(serviceImpl, currentId); List<String> fields = ReflectBeanUtil.getFields(beforeObject.getClass()); for (String field : fields) { if ("xm".equals(field) || "name".equals(field)) { String getterMethodName = ReflectBeanUtil.getGetterMethodName(field, beforeObject.getClass()); String fieldValue = (String) ReflectBeanUtil.invokeMethodByName(beforeObject, getterMethodName); //删除的对象名 alog.setFieldName(fieldValue); } } alog.setId(IdUtil.simpleUUID()); alog.setObjectId(currentId); //将类型转化成对应的释义 0 更新,1 删除,2 新增 alog.setType("1"); //插入删除日志 appChangeLogMapper.insert(alog); } } // Object proceed = joinPoint.proceed(); } catch (Exception e) { log.info(e.getMessage()); throw new RuntimeException(e); } catch (Throwable throwable) { throwable.printStackTrace(); } } }

    切面关键在于通过反射获取对应的类、方法和属性、属性值

    反射工具类/也可以用hutool的反射工具类

    package com.sinoecare.vc2.common.core.util; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * JavaBeanUtil<br> * Description: 反射相关方法 * * @author lanlan * @version 1.4 * @create 2019/1/22 */ public class ReflectBeanUtil { /** * 根据类名反射创建对象 * @param name 类名 * @return 对象 * @throws Exception */ public static Object getInstance(String name) throws Exception { Class<?> cls = Class.forName(name); return cls.newInstance(); } /** * 获取包含所有父类字段在内的所有字段 * @param clazz 要获取字段的类 * @return 字段名集合 */ public static List<String> getFields(Class clazz){ List<Field> fields = new ArrayList<>(); //为null时到达了父类的最高级Object while (clazz.getSuperclass() != null){ fields.addAll(Arrays.asList(clazz.getDeclaredFields())); //得到父类并赋值给自己 clazz = clazz.getSuperclass(); } List<String> fieldNames = new ArrayList<>(); for (Field field : fields) { fieldNames.add(field.getName()); } return fieldNames; } /** * 根据属性名称和java类型,获取对应的getter方法名 * @param property * @param * @return */ public static String getGetterMethodName(String property, Class clazz) { StringBuilder sb = new StringBuilder(); sb.append(property); if (Character.isLowerCase(sb.charAt(0))) { if (sb.length() == 1 || !Character.isUpperCase(sb.charAt(1))) { sb.setCharAt(0, Character.toUpperCase(sb.charAt(0))); } } if ("boolean".equals(clazz.getName())) { sb.insert(0, "is"); } else { sb.insert(0, "get"); } return sb.toString(); } /** * 根据属性名称获取对应的setter方法名称 * @param property * @return */ public static String getSetterMethodName(String property) { StringBuilder sb = new StringBuilder(); sb.append(property); if (Character.isLowerCase(sb.charAt(0))) { if (sb.length() == 1 || !Character.isUpperCase(sb.charAt(1))) { sb.setCharAt(0, Character.toUpperCase(sb.charAt(0))); } } sb.insert(0, "set"); return sb.toString(); } /** * 根据方法名调用无参方法 * @param object 要执行该方法的对象 * @param methodName 方法名 * @return */ public static Object invokeMethodByName(Object object, String methodName) throws Exception { Method method = object.getClass().getMethod(methodName); method.setAccessible(true); Object o = method.invoke(object); return o; } }
    Processed: 0.015, SQL: 9