通用的日志记录功能

本文介绍了一种通过切面编程和自定义注解来实现日志记录的方法,旨在提供一种灵活、低侵入性的日志管理系统。文章详细阐述了如何定义注解、切面、工具类以及相关实现,包括参数处理和对象差异比较。通过这种方式,可以自定义记录日志的字段,支持多种日志类型,并能适应不同服务的扩展需求。

目标:

最近公司有需求需要记录公司活动的修改日志,我就在想能不能做一个通用的日志记录

本篇代码较多, 主要是觉得光说不练假把式, 且为了同学可以拿走自己跑起来

  • 目标

    • 需要有日志记录操作的点支持自定义
    • 需要记录的内容(字段)支持自定义
  • 计划

    • 使用切面完成修改记录
    • 切点由自定义的注解定位,切点的属性
      • 参数 id params
      • 需要记录的字段 needSaveField
      • 日志类型
      • 是否全部字段需要记录
      • 不需要记录的字段 unSaveField1
      • 获取修改数据的服务 serviceName2

类图(聚合关系)

在这里插入图片描述

从图中可以看出,aspect 调用了两个工具类和baseQueryData的实现类 以及一个枚举(枚举的作用是用来表示日志的类型) 其中baseQueryData定义了两个查询和一个保存日志的方法(规则),并提供了公共的方法,这样不同的日志就可以通过实现query接口提供统一标准的服务

在这里插入图片描述

在这里插入图片描述

使用到的工具类

可获取容器的实例的工具类


import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * @author yang
 * @program 
 * @description 容器工具
 * @create 2021/06/08 14:01
 */
@Component
public class SpringUtil  implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if(SpringUtil.applicationContext == null) {
            SpringUtil.applicationContext = applicationContext;
        }
    }

    //获取applicationContext
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    //通过name获取 Bean.
    public static Object getBean(String name){
        return getApplicationContext().getBean(name);
    }

    //通过class获取Bean.
    public static <T> T getBean(Class<T> clazz){
        return getApplicationContext().getBean(clazz);
    }

    //通过name,以及Clazz返回指定的Bean
    public static <T> T getBean(String name,Class<T> clazz){
        return getApplicationContext().getBean(name, clazz);
    }

}

参数处理工具类

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.CodeSignature;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author yang
 * @program 
 * @description 通用工具方法
 * @create 2021/06/09 07:28
 */
@Slf4j
public class PointUtil {



    /**
     * 通过参数和标签获取storeIds
     * @param point
     * @param param
     * @return
     */
    public static List<Long> getIds(ProceedingJoinPoint point, String param){
        Map<String, Object> map = getNameAndValue(point);
        String[] parmas = param.split("\\.");
        //参数本身为id
        if(parmas.length==1){
            Object o = map.get(parmas[0]);
            if(o instanceof Long){
                return Arrays.asList((Long)o);
            }else if(o instanceof List){
                return (List<Long>) o;
            }else{
                return new ArrayList<>();
            }
            //参数某个字段为id
        }else{
            Object o = map.get(parmas[0]);
            String parmaName=parmas[1];
            if(o instanceof List){
                List<Object> list = (List) o;
                return list.stream().map(l -> {
                    Long id = getIdByObject(l, parmaName);
                    return id;
                }).collect(Collectors.toList());
            }else{
                return Arrays.asList(getIdByObject(o,parmaName));
            }
        }

    }

    /**
     * 通过反射获取id
     * @param obj
     * @param paramsName;
     * @return
     */
    public static  Long getIdByObject(Object obj,String paramsName) {
        try {
            Class<?> aClass = obj.getClass();
            String methodName = "get"+paramsName.substring(0, 1).toUpperCase() + paramsName.substring(1);
            Method method = aClass.getMethod(methodName);
            return (Long) method.invoke(obj);
        }catch (Exception e){
            log.error("无法通过反射获得参数",e);
            // 这个异常就是自己定义的运行时异常, 可以自定义
            throw new RrException("无法通过反射获得参数:"+paramsName);
        }
    }

    /**
     * 获取参数Map集合
     * @param joinPoint
     * @return
     */
    public static Map<String, Object> getNameAndValue(ProceedingJoinPoint joinPoint) {
        Map<String, Object> param = new HashMap<>();

        Object[] paramValues = joinPoint.getArgs();
        String[] paramNames = ((CodeSignature)joinPoint.getSignature()).getParameterNames();

        for (int i = 0; i < paramNames.length; i++) {
            param.put(paramNames[i], paramValues[i]);
        }

        return param;
    }

}

获取两个对象不同的属性


import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.sql.Timestamp;
import java.util.*;

/**
 * @author dongwang
 * @version V1.0
 * @since 2019-08-24 10:11
 */
public class ClassCompareUtils {
    /**
     * 比较两个实体属性值,返回一个boolean,true则表时两个对象中的属性值无差异
     * @param oldObject 进行属性比较的对象1
     * @param newObject 进行属性比较的对象2
     * @return 属性差异比较结果boolean
     */
    public static boolean compareObject(Object oldObject, Object newObject) {
        Map<String, Map<String,Object>> resultMap=compareFields(oldObject,newObject);

        if(resultMap.size()>0) {
            return true;
        }else {
            return false;
        }
    }


    /**
     * 比较两个实体属性值,返回一个map以有差异的属性名为key,value为一个Map分别存oldObject,newObject此属性名的值
     * @param oldObject 进行属性比较的对象1
     * @param newObject 进行属性比较的对象2
     * @return 属性差异比较结果map
     */
    @SuppressWarnings("rawtypes")
    public static Map<String, Map<String,Object>> compareFields(Object oldObject, Object newObject) {
        Map<String, Map<String, Object>> map = null;

        try{
            /**
             * 只有两个对象都是同一类型的才有可比性
             */
            if (oldObject.getClass() == newObject.getClass()) {
                map = new HashMap<String, Map<String,Object>>();

                Class clazz = oldObject.getClass();
                //获取object的所有属性
                PropertyDescriptor[] pds = Introspector.getBeanInfo(clazz,Object.class).getPropertyDescriptors();

                for (PropertyDescriptor pd : pds) {
                    //遍历获取属性名
                    String name = pd.getName();

                    //获取属性的get方法
                    Method readMethod = pd.getReadMethod();

                    // 在oldObject上调用get方法等同于获得oldObject的属性值
                    Object oldValue = readMethod.invoke(oldObject);
                    // 在newObject上调用get方法等同于获得newObject的属性值
                    Object newValue = readMethod.invoke(newObject);
                    if(oldValue instanceof List && newValue instanceof List){
                        Object[] oldArray = null;
                        Object[] newArrays = null;
                        if (oldValue != null && ((List) oldValue).size() > 0) {
                            if (((List) oldValue).get(0) instanceof Long) {
                                oldArray = ((List) oldValue).toArray(new Long[((List) oldValue).size()]);
                            }
                            if (((List) oldValue).get(0) instanceof String) {
                                oldArray = ((List) oldValue).toArray(new String[((List) oldValue).size()]);
                            }
                        }
                        if (newValue != null && ((List) newValue).size() > 0) {
                            if (((List) newValue).get(0) instanceof Long) {
                                newArrays = ((List) newValue).toArray(new Long[((List) newValue).size()]);
                            }
                            if (((List) newValue).get(0) instanceof String) {
                                newArrays = ((List) newValue).toArray(new String[((List) newValue).size()]);
                            }
                        }
                        if (!(oldArray == null && newArrays == null) && !Arrays.deepEquals(oldArray, newArrays)) {
                            Map<String,Object> valueMap = new HashMap<String,Object>();
                            valueMap.put("oldValue",oldValue);
                            valueMap.put("newValue",newValue);
                            map.put(name, valueMap);
                        }
                    }


                    if(oldValue instanceof Timestamp){
                        oldValue = new Date(((Timestamp) oldValue).getTime());
                    }

                    if(newValue instanceof Timestamp){
                        newValue = new Date(((Timestamp) newValue).getTime());
                    }

                    if(oldValue == null && newValue == null){
                        continue;
                    }else if(oldValue == null && newValue != null){
                        Map<String,Object> valueMap = new HashMap<String,Object>();
                        valueMap.put("oldValue",oldValue);
                        valueMap.put("newValue",newValue);

                        map.put(name, valueMap);

                        continue;
                    }

                    //比较这两个值是否相等,不等就可以放入map了
                    if (!oldValue.equals(newValue)) {
                        Map<String,Object> valueMap = new HashMap<String,Object>();
                        valueMap.put("oldValue",oldValue);
                        valueMap.put("newValue",newValue);

                        map.put(name, valueMap);
                    }
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }

        return map;
    }

}

实现代码:

注解


import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UpdateActivityTag {


    /**
     * 主键id所对应的参数名称
     * 如果参数本身为 id 或者 id List 则直接传参数名
     * 如果参数某个字段为 id 则传 参数名.字段名
     * @return
     */
    String idsParmas() default "";
    
    /**
     * 日志类型,可根据业务需求自定义
     * @return
     */
    int logType() default 0;

    /**
     * 服务名
     *
     * @return
     */
    String serviceName() default "";

    /**
     * 需要校验保存的服务名 如果和unSaveField 同时存在 以需要的为准
     *
     * @return
     */
    String[] needSaveField() default {};

    /**
     * 不需要校验保存的服务名
     *
     * @return
     */
    String[] unSaveField() default {};


}

注解中的属性都是要在切面中使用到的

顶层抽象


import com.shuguolili.web.common.aspect.UpdateActivityTag;
import com.shuguolili.web.utils.ClassCompareUtils;
import org.springframework.scheduling.annotation.Async;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * @author yang
 * @program 
 * @description 查询数据的顶层接口
 * @create 2021/06/08 14:20
 */

public abstract class BaseQueryData {

    public abstract Object QueryData(Long id);
    public abstract Object QueryData(List<Long> ids);

    @Async
    public abstract void saveCompareLog(Object oldData, Object newData, Long recordId, String operName, Long operId, UpdateActivityTag tag);

    public Map<String,Map<String, Object>> getUpdateLogDTO(Object oldData, Object newData, UpdateActivityTag tag) {

        Map<String,Map<String, Object>> result = new HashMap<>();
        Map<String, Map<String, Object>> maps = ClassCompareUtils.compareFields(oldData, newData);
        if (maps.size() > 0) {
            Iterator<String> keys = maps.keySet().iterator();
            Map<String, Object> beforeMap = new HashMap<>();
            Map<String, Object> afterMap = new HashMap<>();
            while (keys.hasNext()) {
                String key = keys.next();
                if (checkNeedSave(key,tag)) {
                    Map<String, Object> values = maps.get(key);
                    String oldValue = getValue(values.get("oldValue"));
                    String newValue = getValue(values.get("newValue"));
                    beforeMap.put(key,oldValue);
                    afterMap.put(key,newValue);
                }
            }
            if(afterMap.size() > 0) {
                result.put("after",afterMap);
            }
            if(beforeMap.size() > 0 ) {
                result.put("before",beforeMap);
            }
        }
        return result;
    }

    private boolean checkNeedSave(String key,UpdateActivityTag tag) {
        //如果是所有的字段都有保存,直接返回true
        if(tag.allSave()){
            return true;
        }
        if(tag.needSaveField() != null && tag.needSaveField().length > 0){
            List<String> fields = Arrays.asList(tag.needSaveField());
            return  fields.contains(key);

        }
        if(tag.unSaveField() != null && tag.unSaveField().length > 0){
            List<String> fields = Arrays.asList(tag.unSaveField());
            return  !fields.contains(key);
        }

        return false;
    }

    private String getValue(Object object) {
        String value = "";
        if (object != null) {
            if (object instanceof Boolean) {
                Boolean flag = (Boolean) object;
                value = flag ? "是" : "否";
            } else {
                value = object.toString();
            }
        }
        return value;

    }

}

具体实现(其一)

只用一个来举例,如果有多个日志需要保存, 新增实现即可

import com.shuguolili.api.model.dto.UpdateLogDTO;
import com.shuguolili.api.service.read.IActivityInfoReadService;
import com.shuguolili.api.service.write.IUpdateLogWriteService;
import com.shuguolili.utils.JsonUtils;
import com.shuguolili.web.common.aspect.UpdateActivityTag;
import com.shuguolili.web.service.update.BaseQueryData;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Service;


import java.util.List;
import java.util.Map;

/**
 * @author yang
 * @program 
 * @description 活动的查询接口
 * @create 2021/06/08 14:21
 */
@Service("activityInfoDataImpl")
public class ActivityInfoDataImpl extends BaseQueryData {

    //实现类就是业务(保存日志)代码
    @Reference(version = "1.0.0", group = "${dubbo.service.group}", check = false)
    private IActivityInfoReadService activityInfoReadService;
    @Reference(version = "1.0.0", group = "${dubbo.service.group}", check = false)
    private IUpdateLogWriteService updateLogWriteService;


    @Override
    public Object QueryData(Long id) {

        return activityInfoReadService.findDetailById(id);
    }

    @Override
    public Object QueryData(List<Long> ids) {
        return activityInfoReadService.listByIds(ids.toArray(new Long[0]));
    }


    @Override
    public void saveCompareLog(Object oldData, Object newData, Long recordId, String operName, Long operId, UpdateActivityTag tag) {

        if(oldData == null || newData == null){
            return;
        }
        //根据数据中的自动的注解判断注解是否需要记录修改日志
        Class<?> aClass = oldData.getClass();
        if(!aClass.equals(newData.getClass())){
            return;
        }

        //old 和 new 同种对象
        UpdateLogDTO dto = new UpdateLogDTO();
        //被修改的id
        dto.setRecordId(recordId);
        //日志类型,通过注解的tag.logType 传递
        dto.setDataType(tag.logType());
        //操作人id
        dto.setOperatorId(operId);
        //操作人姓名
        dto.setOperatorName(operName);
        //通过getUpdateLogDTO 获得被修改字段的修后值
        Map<String, Map<String, Object>> updateLogDTO = getUpdateLogDTO(oldData, newData, tag);
        Map<String, Object> before = updateLogDTO.get("before");
        Map<String, Object> after = updateLogDTO.get("after");
        // 将数据转换为json 这个工具比较常见就没有贴代码
        if(before!= null && before.size()>0){
            dto.setBeforData(JsonUtils.toJsonString(before));
        }
        if(after!= null && after.size()>0){
            dto.setAfterData(JsonUtils.toJsonString(after));
        }
        //保存
        if(dto.getAfterData() != null || dto.getBeforData() != null){
            updateLogWriteService.save(dto);
        }

    }
}

切面


//定义日志类型的枚举
import com.shuguolili.api.model.enums.UpdateLogTypeEnum;
//这里有引用两个工具类
import com.shuguolili.web.common.util.PointUtil;
import com.shuguolili.web.common.util.SpringUtil;
import lombok.extern.slf4j.Slf4j;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.List;

/**
 * @author yang
 * @program 
 * @description 修改日志
 * @create 2021/06/08 11:11
 */
@Aspect
@Component
@Slf4j
public class ActivityInfoUpdateLongAspect {


    @Pointcut("@annotation(com.shuguolili.web.common.aspect.UpdateActivityTag)")
    public void logPointCut() {

    }


    @Around("logPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Parameter[] parameters = method.getParameters();
        UpdateActivityTag tag = method.getAnnotation(UpdateActivityTag.class);

        //通过注解 定位 参数  和 具体处理的实现类别名
        //根据别名找到spring注册过得的服务bean实例,bean实例通过参数查询数据
        List<Long> ids = PointUtil.getIds(point,tag.idsParmas());
        BaseQueryData query = (BaseQueryData)SpringUtil.getBean(tag.serviceName());
        //变更前
        Object oldData = query.QueryData(ids.get(0));
        //执行方法
        Object result = point.proceed();
        //变更后
        Object newData = query.QueryData(ids.get(0));
        //异步保存,通过自己的实现的服务是将修改前和修改后的数据保存到数据库就OK了
        query.saveCompareLog(oldData,newData,ids.get(0), CurrentAdminUtil.getCurrentName(),CurrentAdminUtil.getCurrentId(),tag);


        return result;
    }

}
  • 根据注解中提供的要素:
    • 参数id/ids
    • 需要/不需要校验保存的字段,
    • spring注册的实例名,在切面中通过工具类获取服务,根据id分别查询修改后和修改前的数据, 调服务进行校验并保存日志记录
    • 日志类型
    • 是否所有字段都需要校验保存

总结

  1. 通过切面编程将需要有日志记录的地方统一加上日志记录(侵入性低)
  2. 通过注解达到不同的数据校验保存不同的字段变化(灵活性高)
  3. 通过抽象规范顶层规则,便于扩展(扩展性高)

如果对你有帮助希望点个赞, 鼓励下
另:如果考虑用到自己项目,有需要可以评论, 思想1+1 > 2 .


2.0版

  1. 在注解中添加新的属性serverClass,支持通过类名来指定服务的类名,方便功能扩展,降低了同事的学习成本
  2. 将通过@Async实现的异步替换为CompletableFuture.runAsync的方式实现,为抽取为starter 做铺垫
  3. 提供默认的class实现,并对异常信息处理,达到无侵入性,用户没有指定,不会影响原功能
	/**
     * 服务类
     *
     * @return
     */
    Class serviceClass() default DefaultUpdateImpl.class;

切面around

    @Around("logPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        UpdateLogTag tag = method.getAnnotation(UpdateLogTag.class);
        List<Long> ids = PointUtil.getIds(point,tag.idsParmas());
        BaseQueryData query = null;
        try {
            if (StringUtils.isNotBlank(tag.serviceName())) {
                query = (BaseQueryData) SpringUtil.getBean(tag.serviceName());
            } else {
                query = (BaseQueryData) SpringUtil.getBean(tag.serviceClass());
            }
        } catch (Exception e) {
            log.error("获取修改日志处理类失败,{}", ExceptionUtils.getStackTrace(e));
        }

        //变更前
        Object oldData =null;
        if(query != null) {
            oldData = query.QueryData(ids.get(0));
        }
        //执行方法
        Object result = point.proceed();
        //变更后
        if(query != null) {
            Object newData = query.QueryData(ids.get(0));
            //异步保存
            Object finalOldData = oldData;
            String currentName = CurrentAdminUtil.getCurrentName();
            Long currentId = CurrentAdminUtil.getCurrentId();
            BaseQueryData finalQuery = query;
            CompletableFuture.runAsync(() -> finalQuery.saveCompareLog(finalOldData,newData,ids.get(0), currentName,currentId,tag));
        }

        return result;
    }

后续有时间和机会,会将改功能抽取为stater ,提供通用的日志插件


  1. 需要记录的字段和不需要记录的字段互斥,当两个都存在时以需要记录的字段为准 ↩︎

  2. serviceName 是通过id params查询到别修改的数据的服务,且服务都是继承了顶层接口的实现类, 这样统一服务规则 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值