AOP操作日志升级版

最近有小伙伴问我,我们管理端的后端日志是如果实现的,是每个接口都需要加上一段保存日志的代码吗?

这样当然是可以实现的了,但是相比稍微有点开发经验的同学们,肯定此刻脑子已浮现出"AOP"了,废话不多扯了,直接上代码了。

丐版

实现效果如下,保存操作时的接口入参,实现简单,但是日志可读性不高。

typescript 复制代码
public enum LogTypeEnum implements IBaseEnum<Integer> {
    ADD(1, "新增数据"),
    UPDATE (2, "修改数据"),
    DELETE (3, "删除数据"),
    UNKNOWN(-1, "未知"),
    ;
    @Getter
    private Integer value;

    @Getter
    private String label;

    @Getter
    private Integer type;

    LogTypeEnum(Integer value, String label) {
        this.value = value;
        this.label = label;
    }
}
ini 复制代码
@Aspect
@Component
@Slf4j
public class OperLogAspect {

    @Resource
    private SysLogService sysLogService;

    /**
     * 设置操作日志切入点 记录操作日志 在注解的位置切入代码
     */
    @Pointcut("@annotation(com.oneam.cnps.annotation.OperLog)")
    public void operLogPoinCut() {
    }

    /**
     * 正常返回通知,拦截用户操作日志,连接点正常执行完成后执行,
     * 如果连接点抛出异常,则不会执行
     */
    @AfterReturning(value = "operLogPoinCut()", returning = "keys")
    public void saveOperLog(JoinPoint joinPoint, Object keys) {
        // 获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 从获取RequestAttributes中获取HttpServletRequest的信息
        HttpServletRequest request = (HttpServletRequest) requestAttributes
                .resolveReference(RequestAttributes.REFERENCE_REQUEST);
        SysLog sysLog = new SysLog();
        try {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            OperLog operLog = method.getAnnotation(OperLog.class);
            String operDesc = "";
            LogTypeEnum logType = LogTypeEnum.UNKNOWN;
            if (sysLog != null) {
                logType = operLog.operType();
                operDesc = operLog.operDesc();
            }
            // 获取方法入参
            Object[] args = joinPoint.getArgs();
            // 将参数所在的数组转换成json
            String params = "";
            if (args.length > 0) {
                params = JSON.toJSONString(args[0]);
                operDesc = operDesc + ",参数:" + params;
            }
            sysLog.setOperDesc(operDesc);
            // 可以从请求数据里获取
            String username = "";
            sysLog.setOperUser(username);
            sysLogService.saveLog(request, username, logType, operDesc);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }
}
ini 复制代码
@OperLog(operType = LogTypeEnum.ADD, operDesc = "新增民俗文化数据")

升级版

日志的目的是为了方便查看数据修改记录,肯定是需要比较数据修改前后的数据项。

下面实现的前提是,工程已集成了mybaits-plus。

java 复制代码
public interface ContentParser {

    /**
     * 获取信息返回查询出的对象
     *
     * @param joinPoint       查询条件的参数
     * @param enableModifyLog 注解
     * @return 获得的结果
     */
    Object getOldResult(JoinPoint joinPoint, EnableModifyLog enableModifyLog);

    /**
     * 获取信息返回查询出的对象
     *
     * @param joinPoint       查询条件的参数
     * @param enableModifyLog 注解
     * @return 获得的结果
     */
    Object getNewResult(JoinPoint joinPoint, EnableModifyLog enableModifyLog);

}
less 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@Documented
public @interface DataName {

    /**
     * 字段对应的中文注释
     */
    String name() default ""; 
}
typescript 复制代码
@Component
public class DefaultContentParse implements ContentParser {

    @Override
    public Object getOldResult(JoinPoint joinPoint, EnableModifyLog enableModifyLog) {
        Object info = joinPoint.getArgs()[0];
        Long id = (Long) ReflectionUtils.getFieldValue(info, "id");
        Assert.notNull(id,"未解析到id值,请检查前台传递参数是否正确");
        Class idType=enableModifyLog.idType();
        if(idType.isInstance(id)){
            Class cls=enableModifyLog.serviceClass();
            IService service = (IService) SpringUtil.getBean(cls);
            Object result=service.getById(id);
            return  result;
        }else {
            throw new RuntimeException("请核实id type");
        }
    }

    @Override
    public Object getNewResult(JoinPoint joinPoint, EnableModifyLog enableModifyLog) {
        return getOldResult(joinPoint,enableModifyLog);
    }
}
less 复制代码
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface EnableModifyLog {
    /**
     * @return 操作的类型 可以直接调用ModifyName 不传时根据METHOD自动确定
     */
    LogTypeEnum modifyType() default LogTypeEnum.UNKNOWN;

    /**
     * @return 获取编辑信息的解析类,目前为使用id获取,复杂的解析需要自己实现,默认不填写
     *       则使用默认解析类
     */
    Class<? extends ContentParser> parseClass() default DefaultContentParse.class;

    /**
     * @return 查询数据库所调用的class文件
     */
    Class<? extends IService> serviceClass() default IService.class;

    /**
     * @return 是否需要默认的改动比较
     */
    boolean needDefaultCompare() default false;

    /**
     * @return id的类型
     */
    Class<?> idType() default Long.class;

    /**
     * @return 操作的描述
     */
    String operDesc() default "";
}
typescript 复制代码
/**
 * 反射工具类.
 * 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数.
 */
@SuppressWarnings("rawtypes")
public class ReflectionUtils {

    private static final String SETTER_PREFIX = "set";

    private static final String GETTER_PREFIX = "get";

    private static final String CGLIB_CLASS_SEPARATOR = "$$";

    private static Logger logger = LoggerFactory.getLogger(ReflectionUtils.class);


    /**
     * 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数.
     * @param obj 读取的对象
     * @param fieldName 读取的列
     * @return 属性值
     */
    public static Object getFieldValue(final Object obj, final String fieldName) {
        Field field = getAccessibleField(obj, fieldName);

        if (field == null) {
            throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + obj + "]");
        }

        Object result = null;
        try {
            result = field.get(obj);
        } catch (IllegalAccessException e) {
            logger.error("不可能抛出的异常{}", e.getMessage());
        }
        return result;
    }


    /**
     *  循环向上转型, 获取对象的DeclaredField, 并强制设置为可访问.
     *   如向上转型到Object仍无法找到, 返回null.
     * @param obj  查找的对象
     * @param fieldName  列名
     * @return 列
     */
    public static Field getAccessibleField(final Object obj, final String fieldName) {
        for (Class<?> superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) {
            try {
                Field field = superClass.getDeclaredField(fieldName);
                makeAccessible(field);
                return field;
            } catch (NoSuchFieldException e) {//NOSONAR
                // Field不在当前类定义,继续向上转型
                continue;// new add
            }
        }
        return null;
    }

    /**
     * 改变private/protected的成员变量为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。
     * @param field  列
     */

    public static void makeAccessible(Field field) {
        if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers()) || Modifier
                .isFinal(field.getModifiers())) && !field.isAccessible()) {
            field.setAccessible(true);
        }
    }

    /**
     * 获取两个对象同名属性内容不相同的列表
     * @param class1 old对象
     * @param class2 new对象
     * @return  区别列表
     * @throws ClassNotFoundException 异常
     * @throws IllegalAccessException 异常
     */
    public static List<Map<String ,Object>> compareTwoClass(Object class1, Object class2) throws ClassNotFoundException, IllegalAccessException {
        List<Map<String,Object>> list=new ArrayList<Map<String, Object>>();
        //获取对象的class
        Class<?> clazz1 = class1.getClass();
        Class<?> clazz2 = class2.getClass();
        //获取对象的属性列表
        Field[] field1 = clazz1.getDeclaredFields();
        Field[] field2 = clazz2.getDeclaredFields();
        StringBuilder sb=new StringBuilder();
        //遍历属性列表field1
        for(int i=0;i<field1.length;i++) {
            //遍历属性列表field2
            for (int j = 0; j < field2.length; j++) {
                //如果field1[i]属性名与field2[j]属性名内容相同
                if (field1[i].getName().equals(field2[j].getName())) {
                    if (field1[i].getName().equals(field2[j].getName())) {
                        field1[i].setAccessible(true);
                        field2[j].setAccessible(true);
                        //如果field1[i]属性值与field2[j]属性值内容不相同
                        if (!compareTwo(field1[i].get(class1), field2[j].get(class2))) {
                            Map<String, Object> map2 = new HashMap<String, Object>();
                            DataName name=field1[i].getAnnotation(DataName.class);
                            String fieldName="";
                            if(name!=null){
                                fieldName=name.name();
                            }else {
                                fieldName=field1[i].getName();
                            }
                            map2.put("name", fieldName);
                            map2.put("old", field1[i].get(class1));
                            map2.put("new", field2[j].get(class2));
                            list.add(map2);
                        }
                        break;
                    }
                }
            }
        }
        return list;

    }
    /**
     * 对比两个数据是否内容相同
     *
     * @param  object1  比较对象1
     * @param  object2  比较对象2
     * @return boolean类型
     */
    public static boolean compareTwo(Object object1,Object object2){

        if(object1==null&&object2==null){
            return true;
        }
        if(object1==null&&object2!=null){
            return false;
        }
        if(object1.equals(object2)){
            return true;
        }
        return false;
    }
}
scss 复制代码
@Slf4j
public class ObjectCompareUtil {

    public static String defaultDealUpdate(Object newObject, Map<String, Object> oldMap) {
        try {
            AtomicReference<Boolean> isUpdate = new AtomicReference<>(false);
            Map<String, Object> newMap = (Map<String, Object>) objectToMap(newObject);
            Long id = (Long) newMap.get("id");
            StringBuilder str = new StringBuilder();
            str.append("修改了ID为【").append(id).append("】的数据;\n");
            Object finalNewObject = newObject;
            oldMap.forEach((k, v) -> {
                Object newResult = newMap.get(k);
                if ((v==null && newResult !=null) || (v != null && !v.equals(newResult))) {
                    Field field = ReflectionUtils.getAccessibleField(finalNewObject, k);
                    if(!field.getName().equals("updateTime")){
                        isUpdate.set(true);
                        DataName dataName = field.getAnnotation(DataName.class);
                        if (dataName != null) {
                            str.append("【").append(dataName.name()).append("】从【")
                                    .append(v).append("】改为了【").append(newResult).append("】;\n");
                        } else {
                            str.append("【").append(field.getName()).append("】从【")
                                    .append(v).append("】改为了【").append(newResult).append("】;\n");
                        }
                    }
                }
            });
            if (!isUpdate.get()) {
                return null;
            }
            return str.toString();
        } catch (Exception e) {
            log.error("比较异常", e);
            throw new RuntimeException("比较异常", e);
        }
    }

    public static String defaultDealUpdate(Object newObject, Object oldObject) {
        try {
            AtomicReference<Boolean> isUpdate = new AtomicReference<>(false);
            Map<String, Object> newMap = (Map<String, Object>) objectToMap(newObject);
            Map<String, Object> oldMap = (Map<String, Object>) objectToMap(oldObject);
            Long id = (Long) newMap.get("id");
            StringBuilder str = new StringBuilder();
            str.append("修改了ID为【").append(id).append("】的数据;\n");
            Object finalNewObject = newObject;
            oldMap.forEach((k, v) -> {
                Object newResult = newMap.get(k);
                if (v != null && !v.equals(newResult)) {
                    Field field = ReflectionUtils.getAccessibleField(finalNewObject, k);
                    if(!field.getName().equals("updateTime")){
                        isUpdate.set(true);
                        DataName dataName = field.getAnnotation(DataName.class);
                        if (dataName != null) {
                            str.append("【").append(dataName.name()).append("】从【")
                                    .append(v).append("】改为了【").append(newResult).append("】;\n");
                        } else {
                            str.append("【").append(field.getName()).append("】从【")
                                    .append(v).append("】改为了【").append(newResult).append("】;\n");
                        }
                    }
                }
            });
            if (!isUpdate.get()) {
                return null;
            }
            return str.toString();
        } catch (Exception e) {
            log.error("比较异常", e);
            throw new RuntimeException("比较异常", e);
        }
    }

    public static Map<?, ?> objectToMap(Object obj) {
        if (obj == null) {
            return null;
        }
        ObjectMapper mapper = new ObjectMapper();
        mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        //mapper.addMixIn(Object.class, IgnoreHibernatePropertiesInJackson.class);
        Map<?, ?> mappedObject = mapper.convertValue(obj, Map.class);
        return mappedObject;
    }
}
ini 复制代码
@Aspect
@Component
public class ModifyAspect {

    private final static Logger logger = LoggerFactory.getLogger(ModifyAspect.class);

    @Resource
    private SysLogService sysLogService;

    @Around("@annotation(enableModifyLog)")
    public Object around(ProceedingJoinPoint joinPoint, EnableModifyLog enableModifyLog) throws Throwable {
        Map<String, Object> oldMap = new HashMap<>();
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        if (LogTypeEnum.UPDATE.equals(enableModifyLog.modifyType())) {
            try {
                ContentParser contentParser = (ContentParser) SpringUtil.getBean(enableModifyLog.parseClass());
                Object oldObject = contentParser.getOldResult(joinPoint, enableModifyLog);
                if (enableModifyLog.needDefaultCompare()) {
                    oldMap = (Map<String, Object>) ObjectCompareUtil.objectToMap(oldObject);
                }
            } catch (Exception e) {
                logger.error("service加载失败:", e);
            }
        }
        Object object = joinPoint.proceed();
        Object newObject = null;
        if (LogTypeEnum.UPDATE.equals(enableModifyLog.modifyType())) {
            ContentParser contentParser;
            try {
                contentParser = (ContentParser) SpringUtil.getBean(enableModifyLog.parseClass());
                newObject = contentParser.getNewResult(joinPoint, enableModifyLog);
            } catch (Exception e) {
                logger.error("service加载失败:", e);
            }
            String operDesc = enableModifyLog.operDesc();
            String updateContent = ObjectCompareUtil.defaultDealUpdate(newObject, oldMap);
            if(StringUtils.isNotBlank(updateContent)){
                logger.info("修改内容为:{}", updateContent);
                sysLogService.saveLog(request, AuthContextUtil.getUsername(), LogTypeEnum.UPDATE, operDesc + ":" +updateContent);
            }
        }
        return object;
    }
}
相关推荐
Victor3566 分钟前
MongoDB(87)如何使用GridFS?
后端
Victor3569 分钟前
MongoDB(88)如何进行数据迁移?
后端
小红的布丁26 分钟前
单线程 Redis 的高性能之道
redis·后端
GetcharZp31 分钟前
Go 语言只能写后端?这款 2D 游戏引擎刷新你的认知!
后端
宁瑶琴2 小时前
COBOL语言的云计算
开发语言·后端·golang
普通网友2 小时前
阿里云国际版服务器,真的是学生党的性价比之选吗?
后端·python·阿里云·flask·云计算
IT_陈寒3 小时前
Vue的这个响应式问题,坑了我整整两小时
前端·人工智能·后端
Soofjan4 小时前
Go 内存回收-GC 源码1-触发与阶段
后端
shining4 小时前
[Golang]Eino探索之旅-初窥门径
后端
掘金者阿豪4 小时前
Mac 程序员效率神器:6 个我每天都在用的 Mac 工具推荐(Alfred / Paste / PixPin / HexHub / iTerm2 /)
后端