【日志模块】Spring AOP + 自定义注解 + redis 构建系统关键日志记录

  • 场景

        最近做需求,需要在现有的系统上新增日志模块,用来记录一些系统的关键操作日志,比如用户的登录登出、关键页面的访问、关键数据增删改等……一说到日志,首先想起的肯定是AOP切面,通过AOP的各种通知,结合自定义注解的标记,就可以尽最大可能的与现有系统解耦,这是我开发构建这个日志模块时考虑最多的点。

      根据需求,日志模板分成多种,比如:

      常量模板日志:登录系统;退出系统;

      概要型日志:  新增接口:【接口名称】

      明细型日志:  修改了人员信息,修改【详细的修改内容】

拿到需求,经过查阅资料和思考,确定了基本的开发思路后,刚开始我的想法是采用根据动作增删改查 这四大类进行开发,但随着开发进行,对整体的日志模块和功能有了新的理解体会,发现这种方式不是很合理,换一种思路。可以根据数据的状态变更以及日志的记录时机分成 数据变更前、数据变更后、数据变更详情 三大类进行开发。

最终效果:

生成日志记录的同时,后台还会记录每一次的数据变更的前后内容,以供后续的数据追溯使用。

 

  • 准备

  • 创建日志模板

    新增接口:{0}

   修改了人员信息,修改{0}

日志模板中使用占位符,到AOP处理时使用 MessageFormat.format(模板, 内容); 替换占位符,生成最终日志内容。

  • 创建自定义注解

         因为要记录数据变更状态,肯定要获取model的属性中文名称,我这边采用的是在model的属性上添加自定义注解,通过自定义注解获取当前属性的中文名称和其他一些信息其他信息我暂时只用到数据字典的key。

  • 创建自定义获取属性名称注解 : @FieldName
@Documented
@Target(ElementType.FIELD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldName {

    String name() default "";      //中文名称

    String dictKey() default "";    //数据字典的key,用于区分当前属性是否为数据字典,不填表示不是数据字典

}
  • 创建使用固定日志模板注解: @LogOfCustomTemplate 

import java.lang.annotation.*;

/**
 * 自定义模板日志注解(1)
 *
 * 使用条件:
 * 1.调用方传入完整的日志内容
 * 2.当前操作不需要记录数据变更历史
 *
 */
@Documented
@Target(ElementType.METHOD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOfCustomTemplate {

    String content();        //日志内容

}
  • 创建记录数据变更之前日志模板注解: @LogForBefore
import java.lang.annotation.*;

/**
 * 数据变更之前记录日志模板注解
 *
 * 支持多条数据同时操作,比如删除,一次性删除多条数据,删除后会生成多条日志记录
 * 多个id用,区格
 */
@Documented
@Target(ElementType.METHOD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface LogForBefore {

    String content();        //日志模板

    String paramKeyName() default "ids";    //入参主键名称

    String dbIdName() default "id";    //查询历史数据的参数KEY

    String nameSpace();      //查询历史数据的mapper命名空间

    String showFields() default "";     //需要展示的字段名称,多个用,区分,不填表示全部
}
  • 创建记录数据变更之后日志模板注解: @LogForAfter

import java.lang.annotation.*;

/**
 * 操作数据之后记录日志模板注解
 *
 */
@Documented
@Target(ElementType.METHOD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface LogForAfter {

    String content() ;    //日志模板

    String dbIdName() default "id";    //查询新数据的参数KEY

    String paramKeyName() default "ids";    //入参主键名称

    String nameSpace() ;      //查询新数据的mapper命名空间

    String showFields() default "";     //需要展示的字段属性,多个用,区分,不填表示全部
}
  • 创建记录数据变更详情注解: @LogForAfter
import java.lang.annotation.*;

/**
 * 记录数据变更详情
 *
 */
@Documented
@Target(ElementType.METHOD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOfUpdateDetail {

    String content();    //日志模板

    String dbIdName() default "id";    //数据查询主表的主键在bean中的字段名称

    String nameSpace();      //查询当前更新数据的mapper命名空间

    String paramKeyName() default "id";     //入参主键名称

    String busCode();        //业务编码

    String showFields() default "";     //需要展示的字段属性,多个用,区分,不填表示全部

    /**
     * 是否展示属性变更详情
     *
     * false: 根据showFields顺序替换模板中的占位符,用于展示概况,例如:【新增接口】:雷管上传接口
     * true: 根据showFields,展示值变更记录,例如:【修改人员信息】:身份证号码为*****
     *
     * 默认为true
     */
    boolean isShowFieldDataChange() default true;
}

 以上为记录日志需要的几个自定义注解,其中几个注解的属性大同小异,类似于重复创建的目的是为了便于再后面AOP中精确设置切点路径。如果还需要有其他的属性,可以直接在里面添加,然后对应设值。

  • 缓存@FieldName值至redis中

项目启动的时候通过扫描获取这些被 @FieldName 标记过的属性,取出当前的注解中的值,存入redis中备用。

import com.alibaba.fastjson.JSONObject;
import com.sharp.vocb.cache.JedisUtils;
import com.sharp.wmc.common.WMCConstant;
import com.sharp.wmc.common.annotation.FieldName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;
import org.springframework.web.context.ServletContextAware;

import javax.servlet.ServletContext;
import java.lang.reflect.Field;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * 项目启动加载@FieldName 注解值
 */
@Service
public class WebContextListener implements InitializingBean, ServletContextAware {

    private Logger logger = LoggerFactory.getLogger(WebContextListener.class);


    @Override
    public void afterPropertiesSet() throws Exception {
    }

    @Override
    public void setServletContext(ServletContext servletContext) {

        //扫描加载model自定义属性中文名称注解数据
        loadModelFieldDataToRedis();
    }

    /**
     * 加载model自定义属性中文名称注解数据
     *
     * redis中的key:
     *      WMCConstant.MODEL_FIELD_FIXED_CACHE_KEY_PART1 + 属性;
     * 过期时间:永久有效
     */
    private void loadModelFieldDataToRedis(){
        logger.info(">>>>>开始扫描加载model自定义属性中文名称注解数据");
        long beginDate = System.currentTimeMillis();
        //存入redis中的key的第一部分
        String redisKeyPart1 = WMCConstant.MODEL_FIELD_FIXED_CACHE_KEY_PART1;
        //清除历史缓存数据
        JedisUtils.batchDel(redisKeyPart1);

        try {
            //获取当前文件所在路径,并组装出model jar包所在的路径
            String url = this.getClass().getResource("/").getPath();
            url = url.substring(0, url.length()-8);
            url = url + "lib/hhh-model-1.0-SNAPSHOT.jar";    //jar包路径

            //解析jar包,获取所有的类信息
            JarFile jar = new JarFile(url);
            Enumeration<JarEntry> enumFiles = jar.entries();
            JarEntry entry;
            while(enumFiles.hasMoreElements()) {
                entry = (JarEntry) enumFiles.nextElement();
                if (entry.getName().indexOf("META-INF") < 0) {
                    String classFullName = entry.getName();
                    if(!classFullName.endsWith(".class")){
                        classFullName = classFullName.substring(0,classFullName.length()-1);
                    } else{
                        //去掉后缀.class 的全类名
                        String className = classFullName.substring(0,classFullName.length()-6).replace("/", ".");
                        //根据全类名,反射获取类对象
                        Class<?> clazz = Class.forName(className);
                        //获取类对象中的所有属性
                        Field[] fields = clazz.getDeclaredFields();
                        for (Field field: fields){
                            //判断当前属性是否有注解
                            boolean fieldHasAnno = field.isAnnotationPresent(FieldName.class);
                            if (fieldHasAnno){
                                FieldName annotation = field.getAnnotation(FieldName.class);
                                String name = annotation.name();
                                String dictKey = annotation.dictKey();
                                String redisKey = redisKeyPart1 + field.getName();
                                JSONObject object = new JSONObject();
                                object.put("fieldNameCn", name);
                                object.put("dictKey", dictKey);
                                //将属性和自定义属性注解所对应的值存入redis中,并设为永久不过期
                                JedisUtils.setObject(redisKey, object, 0);
                            }
                        }
                    }
                }
            }
        } catch (Exception e){
            e.printStackTrace();
        }
        logger.info(">>>>>model自定义属性中文名称注解数据扫描加载完成,耗时: " + (double) Math.round((System.currentTimeMillis() - beginDate))/1000 + " 秒\n");
    }
}

上述代码中, JedisUtils.batchDel(redisKeyPart1);  是一个根据key前缀批量删除key的方法,详见 【Java】根据指定key前缀,批量删除redis缓存  , JedisUtils.setObject(redisKey, object, 0); 是把值存入redis中的方法,0表示永久不过期,这个可以在网上找对应的方法。

  • 日志逻辑代码实现

  • AOP拦截处理
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.bean.sys.SysBusCode;
import com.constants.MBCont;
import com.constants.SystemKeyWord;
import com.framework.utils.StringUtils;
import com.bean.sys.SysLog;
import com.common.annotation.LogForBefore;
import com.common.annotation.LogForAfter;
import com.common.annotation.LogOfUpdateDetail;
import com.service.sys.SysLogDetailService;
import com.service.sys.SysLogService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.mybatis.spring.SqlSessionTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.*;

/**
 * 日志切面实现
 *
 * @autho He
 * @date 2018-11-23 9:30
 */
@Aspect
@Component
public class RequestAspectForLog {

    private Logger log = LoggerFactory.getLogger(RequestAspectForLog.class);


    @Autowired
    private HttpServletRequest request;


    /**
     * 所有的自定义日志内容注解
     */
    @Pointcut("@annotation(com.common.annotation.LogOfCustomTemplate)")
    public void logOfCustomTemplate() {
    }

    /**
     * 所有的更新详情日志注解
     */
    @Pointcut("@annotation(com.common.annotation.LogOfUpdateDetail)")
    public void logOfUpdateDetail() {
    }

    /**
     * 所有的成功操作之后日志注解
     */
    @Pointcut("@annotation(com.common.annotation.LogForAfter)")
    public void logForAfter() {

    }

    /**
     * 所有的成功操作之前日志注解
     */
    @Pointcut("@annotation(com.common.annotation.LogForBefore)")
    public void logForBefore() {

    }

    /**
     * 退出操作
     */
    @Pointcut("execution(* com.controller.sys.LoginController.loginout(..))" +
            "|| execution(* com.controller.sys.LoginController.mobileLoginout(..))")
    public void logOut() {
    }


    /**
     * 退出操作日志
     *
     * @param joinPoint
     */
    @Before(value = "logOut() && logOfCustomTemplate()")
    public void logOut(JoinPoint joinPoint) {
        try {
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            Method targetMethod = methodSignature.getMethod();
            LogUtil.insertCustomTemplateLog(targetMethod, request, sysLogService);
        } catch (Exception e) {
            log.error(">>>>退出操作日志异常!异常信息: " + e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 记录操作之前且返回结果成功的日志
     *
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around(value = "logForBefore()")
    public Object logForBeforeAdvice(ProceedingJoinPoint pjp) throws Throwable {
        String ids = "", logRemark = "";
        //旧数据集合
        List<JSONObject> oldJsonDataList = new ArrayList<>();
        Object object = new Object();
        try {
            MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
            Method targetMethod = methodSignature.getMethod();
            LogForBefore logForBefore = targetMethod.getAnnotation(LogForBefore.class);

            //日志内容模板
            String content = logForBefore.content();
            //查询历史数据的命名空间
            String nameSpace = logForBefore.nameSpace();
            //查询历史数据的参数KEY
            String dbIdName = logForBefore.dbIdName();
            //业务编码
            String busCode = logForBefore.busCode();
            //入参中的参数主键
            String paramKeyName = logForBefore.paramKeyName();
            //要展示的字段
            String showFields = logForBefore.showFields();

            String ip = request.getHeader(MBCont.GET_IP_KEY);
            String userId = String.valueOf(request.getSession().getAttribute(SystemKeyWord.USER_KEY_NAME));

            /**
             * 获取入参主键
             * 在入参列表中匹配通过注解传入的主表id查询字段属性名
             * 匹配不到,默认取当前登录用户的ID
             */
            JSONObject paramObj = LogUtil.getParamsMap(pjp.getArgs());
            if (paramObj.containsKey(paramKeyName)) {
                ids = paramObj.getString(paramKeyName);
            } else {
                ids = userId;
                logRemark = "数据查询key主键(dbIdName)属性名: " + paramKeyName + ",在入参列表中没有找到,查询ID值取当前登录用户";
            }

            String[] idsArr = ids.split(",");
			//查询旧数据
            for (String str : idsArr) {
                JSONObject oldObj = LogUtil.queryData(nameSpace, str, dbIdName, sqlSession);
                oldObj.put("delPrimaryKey", str);   //记录当前的主键ID值
                oldJsonDataList.add(oldObj);
            }

            /****************** 楚河 ******************/

            object = pjp.proceed();

            /****************** 汉界 ******************/

            //获取方法返回数据
            JSONObject retJson = JSON.parseObject(object.toString());
            String returnCode = retJson.getString(SystemKeyWord.RETURN_CODE);
            if (StringUtils.isEmpty(returnCode)) {
                returnCode = retJson.getString(SystemKeyWord.APP_RETURN_CODE);
            }

            //判断返回值,只有返回成功才进行记录
            if (SystemKeyWord.SUCCESS_CODE.equals(returnCode)
                    || SystemKeyWord.APP_SUCCESS_CODE.equals(returnCode)) {
                JSONObject newJson = new JSONObject();
                for (JSONObject oldDataJson : oldJsonDataList) {
                    //缓存更改的数据
                    Map<String, Map<String, String>> diffDataMap = new HashMap<>();

                    //获取数据变更记录
                    LogUtil.getDataDifference(oldDataJson, newJson, diffDataMap, "", sqlSession);

                    //需要展示的字段属性数组
                    String[] showFieldArr = showFields.split(",");
                    //获取展示值
                    List<String> showValueList = LogUtil.getShowValue(oldDataJson, showFieldArr, sqlSession);
                    Object[] showValueArr = showValueList.toArray();
                    //替换占位符,生成最终日志信息
                    content = MessageFormat.format(content, showValueArr);

                    //根据业务编码查询日志类型
                    SysBusCode sysBusCode = sysBuscodeService.queryByBusCode(busCode);

                     /**
					   * 日志内容已经生成,
					   * 接下来就是自行根据实际情况实现新增日志信息及保存变更数据
                       * 
                       */

                }
            }

        } catch (Exception e) {
            log.error(">>>>退出操作日志异常!异常信息: " + e.getMessage());
            e.printStackTrace();
        }

        return object;
    }

    /**
     * 记录操作之后且返回结果成功的日志
     *
     * @return
     */
    @AfterReturning(returning = "retObj", value = "logForAfter()")
    public void aroundOfLogForAfter(JoinPoint joinPoint, Object retObj) {

        JSONObject oldObj = new JSONObject();   //旧数据

        //获取方法对象
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method targetMethod = signature.getMethod();
        LogForAfter logForAfter = targetMethod.getAnnotation(LogForAfter.class);
        try {
            if (targetMethod.isAnnotationPresent(LogForAfter.class)) {
                String nameSpace = logForAfter.nameSpace();
                String content = logForAfter.content();
                String busCode = logForAfter.busCode();
                String objIdName = logForAfter.dbIdName();
                String ip = request.getHeader(MBCont.GET_IP_KEY);
                String showFields = logForAfter.showFields();
                String userId = String.valueOf(request.getSession().getAttribute(SystemKeyWord.USER_KEY_NAME));

                boolean isSuccess = false;

                //根据参数返回类型进行不同的处理
                if (retObj instanceof Integer) {
                    int result = Integer.parseInt(String.valueOf(retObj));
                    isSuccess = result > 0;
                } else if (retObj instanceof String) {
                    JSONObject retJson = JSON.parseObject(retObj.toString());
                    //获取返回值
                    String returnCode = retJson.getString(SystemKeyWord.RETURN_CODE);
                    if (StringUtils.isEmpty(returnCode)) {
                        returnCode = retJson.getString(SystemKeyWord.APP_RETURN_CODE);
                    }

                    //判断返回值,只有返回成功才进行记录
                    isSuccess = SystemKeyWord.SUCCESS_CODE.equals(returnCode) || SystemKeyWord.APP_SUCCESS_CODE.equals(returnCode);
                }

                if (isSuccess) {
                    //获取入参中的所有参数
                    JSONObject paramObj = LogUtil.getParamsMap(joinPoint.getArgs());
                    String objIdVal = paramObj.getString(objIdName);
                    //缓存更改的数据
                    Map<String, Map<String, String>> diffDataMap = new HashMap<>();

                    //查询新数据
                    JSONObject newObj = LogUtil.queryData(nameSpace, objIdVal, objIdName, sqlSession);

                    //获取数据差异
                    LogUtil.getDataDifference(oldObj, newObj, diffDataMap, "", sqlSession);

                    //需要展示的字段属性数组
                    String[] showFieldArr = showFields.split(",");
                    //获取展示值
                    List<String> showValueList = LogUtil.getShowValue(newObj, showFieldArr, sqlSession);
                    Object[] showValueArr = showValueList.toArray();
                    //替换占位符,生成最终的日志模板信息
                    content = MessageFormat.format(content, showValueArr);

                    /**
					  * 日志内容已经生成,
					  * 接下来就是自行根据实际情况实现新增日志信息及保存变更数据
                      * 
                      */
                }
            }
        } catch (Exception e) {
            log.error(">>>>> 记录新增操作日志时出现异常,异常信息:" + e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 记录更新详情日志
     *
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around(value = "logOfUpdateDetail()")
    public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {

        JSONObject oldObj = new JSONObject();      //历史数据
        String content = "", dbIdName = "", paramKeyName = "", nameSpace = "";
        String busCode = "", logRemark = "", objIdVal = "", showFields = "";
        boolean isShowFieldDataChange = false;

        String userId = String.valueOf(request.getSession().getAttribute(SystemKeyWord.USER_KEY_NAME));

        //获取方法对象
        Signature signature = pjp.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method targetMethod = methodSignature.getMethod();

        try {
            LogOfUpdateDetail logOfUpdateDetail = targetMethod.getAnnotation(LogOfUpdateDetail.class);
            content = logOfUpdateDetail.content();
            dbIdName = logOfUpdateDetail.dbIdName();
            busCode = logOfUpdateDetail.busCode();
            nameSpace = logOfUpdateDetail.nameSpace();
            showFields = logOfUpdateDetail.showFields();
            paramKeyName = logOfUpdateDetail.paramKeyName();
            isShowFieldDataChange = logOfUpdateDetail.isShowFieldDataChange();

            /**
             * 获取入参主键
             * 在入参列表中匹配通过注解传入的主表id查询字段属性名
             * 匹配不到,默认取当前登录用户的ID
             */
            JSONObject paramObj = LogUtil.getParamsMap(pjp.getArgs());
            if (paramObj.containsKey(paramKeyName)) {
                objIdVal = paramObj.getString(paramKeyName);
            } else {
                objIdVal = userId;
                logRemark = "注解传入的主键ID属性名: " + dbIdName + ",在入参列表中没有找到,主键ID值取当前登录用户";
            }
            //查询旧数据
            oldObj = LogUtil.queryData(nameSpace, objIdVal, dbIdName, sqlSession);


        } catch (Exception e) {
            log.error(">>>> 日志记录环绕通知方法前置出现异常!异常信息: " + e.getMessage());
            e.printStackTrace();
        }

        /****************** 楚河 ******************/

        Object obj = pjp.proceed();

        /****************** 汉界 ******************/

        try {
            JSONObject retJson = JSON.parseObject(obj.toString());
            //获取返回值
            String returnCode = retJson.getString(SystemKeyWord.RETURN_CODE);
            if (StringUtils.isEmpty(returnCode)) {
                returnCode = retJson.getString(SystemKeyWord.APP_RETURN_CODE);
            }

            //判断返回值,只有返回成功才进行记录
            if (SystemKeyWord.SUCCESS_CODE.equals(returnCode) || SystemKeyWord.APP_SUCCESS_CODE.equals(returnCode)) {
                //缓存更改的数据
                Map<String, Map<String, String>> diffDataMap = new HashMap<>();
                //查询新数据
                JSONObject newObj = LogUtil.queryData(nameSpace, objIdVal, dbIdName, sqlSession);

                //获取修改记录并生成修改日志
                //生成详细数据变更记录
                String diffStr = LogUtil.getDataDifference(oldObj, newObj, diffDataMap, showFields, sqlSession);
                if (isShowFieldDataChange){
                    //当前操作未做修改,不生成日志记录。
                    if (StringUtils.isEmpty(diffStr)){
                        return obj;
                    }
                    content = MessageFormat.format(content, diffStr);   //替换占位符
                } else {
                    //生成数据变更概况
                    //需要展示的字段属性数组
                    String[] showFieldArr = showFields.split(",");
                    //获取展示值
                    List<String> showValueList = LogUtil.getShowValue(newObj, showFieldArr, sqlSession);
                    Object[] showValueArr = showValueList.toArray();
                    //替换占位符,生成最终的日志信息
                    content = MessageFormat.format(content, showValueArr);
                }

                /**
				  * 日志内容已经生成,
				  * 接下来就是自行根据实际情况实现新增日志信息及保存变更数据
                  * 
                  */

            }
        } catch (Exception e) {
            log.error(">>>> 日志记录环绕通知方法后置出现异常!异常信息: " + e.getMessage());
            e.printStackTrace();
        }
        return obj;
    }


    /**
     * 新增自定义内容日志,且非退出操作日志
     *
     * @param joinPoint
     * @param retObj
     * @return
     */
    @AfterReturning(returning = "retObj", pointcut = "logOfCustomTemplate() && !logOut()")
    public Object afterReturningAdvice(JoinPoint joinPoint, Object retObj) {
        try {
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            Method targetMethod = methodSignature.getMethod();
            JSONObject retJson = JSON.parseObject(retObj.toString());

            //获取返回值
            String returnCode = retJson.getString(SystemKeyWord.RETURN_CODE);
            if (StringUtils.isEmpty(returnCode)) {
                returnCode = retJson.getString(SystemKeyWord.APP_RETURN_CODE);
            }
            //判断返回值,只有返回成功才进行记录
            if (!StringUtils.isEmpty(returnCode)
                    && (SystemKeyWord.SUCCESS_CODE.equals(returnCode) || SystemKeyWord.APP_SUCCESS_CODE.equals(returnCode))) {
                
				String logContent = "";     //日志内容
				String ip = request.getHeader(IP_KEY);             //访问IP
				LogOfCustomTemplate logOfCustomTemplate = targetMethod.getAnnotation(LogOfCustomTemplate.class);
				if (null != logOfCustomTemplate) {
					logContent = logOfCustomTemplate.content();
					logType = logOfCustomTemplate.logType();
				}
				/**
				  *
				  * 接下来就是自行根据实际情况实现新增日志信息及保存变更数据
                  * 
                  */
            }
        } catch (Exception e) {
            log.error(">>>>记录自定义日志出现异常,异常信息: " + e.getMessage());
            e.printStackTrace();
        }
        return retObj;
    }

}

这边代码还没有整理过,主要就是实现通过AOP的各种通知,对我们用自定义日志注解标记进行拦截处理,生成最终的操作日志信息和数据变更记录,并保存到数据库中。

这边有用到几个方法,我封装在另一个工具类 LogUtil 中,主要实现 比对获取数据变更记录,各种取值


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.bean.sys.SysDict;
import com.cache.JedisUtils;
import com.constants.SystemKeyWord;
import com.framework.utils.StringUtils;
import com.bean.sys.SysLog;
import com.bean.sys.SysLogDetail;
import com.common.WMCConstant;
import com.common.annotation.LogOfCustomTemplate;
import com.service.sys.SysLogDetailService;
import com.service.sys.SysLogService;
import org.apache.ibatis.session.SqlSession;
import org.mybatis.spring.SqlSessionTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @autho He
 * @date 2018-11-27 11:29
 */
@Component
public class LogUtil {

    private static Logger log = LoggerFactory.getLogger(LogUtil.class);

    /**数据字典命名空间**/
    public static String SYS_DICT_NAME_SPACE = "SysDictMapper.querySysDictListByInfo";

    private static String OLD_VALUE = "oldVal";
    private static String NEW_VALUE = "newVal";
    private static String FILED_NAME_CN = "fieldNameCN";


    /**
     * 新旧数据比对,获取差异数据
     *
     * @param oldObj
     * @param newDataObj
     * @param diffDataMap
     * @param showFields  需要展示的字段,多个用,区分,空 表示全部
     * @return
     */
    public static String getDataDifference(JSONObject oldObj, JSONObject newDataObj, Map<String, Map<String, String>> diffDataMap, String showFields, SqlSessionTemplate sqlSession) {
        StringBuffer sb = new StringBuffer();
        try {

            JSONObject newObj = (JSONObject) newDataObj.clone(); //防止后续数据出错,拷贝一份,解析副本

            List<String> showFieldList = new ArrayList<>();     //需要展示的字段属性集合
            if (!StringUtils.isEmpty(showFields)) {
                String[] strArr = showFields.split(",");
                showFieldList = Arrays.asList(strArr);
            }

            Set<String> oldKeys = oldObj.keySet();
            for (String key : oldKeys) {
                Map<String, String> tempMap = new HashMap<>();
                String fieldNameCn = "", dictKey = "";
                String oldVal = oldObj.getString(key);
                String newVal = newObj.getString(key);
                String redisKey = WMCConstant.MODEL_FIELD_FIXED_CACHE_KEY_PART1;
                Object object = JedisUtils.getObject(redisKey + key);

                if (null != object){
                    JSONObject paramObj = JSONObject.parseObject(object.toString());
                    fieldNameCn = paramObj.getString("fieldNameCn");
                    dictKey = paramObj.getString("dictKey");

                }

                //记录所有字段变更值
                if (!oldVal.equals(newVal)){
                    tempMap.put(OLD_VALUE, oldVal);
                    tempMap.put(NEW_VALUE, newVal);
                    tempMap.put(FILED_NAME_CN, fieldNameCn);   //属性中文名
                    diffDataMap.put(key, tempMap);
                }

                //从新数据中移除已经记录过的数据
                newObj.remove(key);

                /**
                 * 跳过条件:
                 *
                 * 1.当前属性没有被自定义注解标记过
                 * 2.调用者有传入展示字段,但当前字段不在展示字段范围内
                 */
                if (StringUtils.isEmpty(fieldNameCn)
                        || (showFieldList.size() > 0 && !showFieldList.contains(key))) {
                    continue;
                }
                /**
                 * 数据字典不为空,进行数据转换,取出中文名称
                 */
                if (!StringUtils.isEmpty(dictKey)){
                    Map<String, Object> map = new HashMap<>();
                    map.put("key", dictKey);
                    map.put("code",newVal);
                    SysDict sysDict = sqlSession.selectOne(SYS_DICT_NAME_SPACE, map);
                    if (null != sysDict ){
                        newVal = sysDict.getName();
                    }
                }

                if (StringUtils.isEmpty(newVal)) {
                    sb.append(fieldNameCn + "为空");
                    sb.append(",");
                } else if (!oldVal.equals(newVal)) {
                    sb.append(fieldNameCn + "为" + newVal);
                    sb.append(",");
                }
            }
            if (newObj.size() > 0) {
                for (String key : newObj.keySet()) {
                    Map<String, String> tempMap = new HashMap<>();
                    String fieldNameCn = "", dictKey = "";
                    String redisKey = WMCConstant.MODEL_FIELD_FIXED_CACHE_KEY_PART1;
                    String newVal = newObj.getString(key);
                    Object object = JedisUtils.getObject(redisKey + key);
                    if (null != object){
                        JSONObject paramObj = JSONObject.parseObject(object.toString());
                        fieldNameCn = paramObj.getString("fieldNameCn");
                        dictKey = paramObj.getString("dictKey");
                    }

                    if (!tempMap.containsKey(key)){
                        //记录属性变更值,新增
                        tempMap.put(OLD_VALUE, "");
                        tempMap.put(NEW_VALUE, newVal);
                        tempMap.put(FILED_NAME_CN, fieldNameCn);   //属性中文名
                        diffDataMap.put(key, tempMap);
                    }

                    /**
                     * 跳过条件:
                     *
                     * 1.当前属性没有被自定义注解标记过
                     * 2.调用者有传入展示字段,但当前子弹不在展示字段范围内
                     */
                    if (StringUtils.isEmpty(fieldNameCn)
                            || (showFieldList.size() > 0 && !showFieldList.contains(key))) {
                        continue;
                    }

                    /**
                     * 数据字典不为空,进行数据转换,取出中文名称
                     */
                    if (!StringUtils.isEmpty(dictKey)){
                        Map<String, Object> map = new HashMap<>();
                        map.put("key", dictKey);
                        map.put("code",newVal);
                        SysDict sysDict = sqlSession.selectOne(SYS_DICT_NAME_SPACE, map);
                        if (null != sysDict ){
                            newVal = sysDict.getName();
                        }
                    }

                    sb.append(fieldNameCn + "为" + newVal);
                    sb.append(",");
                }
            }
        } catch (Exception e) {
            log.error(">>>>比对修改数据时出现异常!错误信息: " + e.getMessage());
            e.printStackTrace();
        }

        String retStr = sb.toString();
        if (StringUtils.isEmpty(retStr)) {
            retStr = "";
        } else {
            retStr = retStr.endsWith(",") ? retStr.substring(0, retStr.lastIndexOf(",")) : retStr;
        }
        return retStr;
    }

    /**
     * 获取需要展示的字段值,按照 showFields 的顺序
     * @param dataObj
     * @param showFieldArr
     * @return
     */
    public static List<String> getShowValue(JSONObject dataObj, String[] showFieldArr, SqlSession sqlSession){
        List<String> retList = new ArrayList<>();
        try {

            for (String key : showFieldArr){
                String value = dataObj.getString(key);

                String redisKey = WMCConstant.MODEL_FIELD_FIXED_CACHE_KEY_PART1;
                Object object = JedisUtils.getObject(redisKey + key);

                if (null != object){
                    JSONObject paramObj = JSONObject.parseObject(object.toString());
                    String dictKey = paramObj.getString("dictKey");
                    /**
                     * 数据字典不为空,进行数据转换,取出中文名称
                     */
                    if (!StringUtils.isEmpty(dictKey)){
                        Map<String, Object> map = new HashMap<>();
                        map.put("key", dictKey);
                        map.put("code",value);
                        SysDict sysDict = sqlSession.selectOne(SYS_DICT_NAME_SPACE, map);
                        if (null != sysDict ){
                            value = sysDict.getName();
                        }
                    }
                }
                retList.add(value);
            }

        } catch (Exception e) {
            log.error(">>>>获取展示内容时出现异常!错误信息: " + e.getMessage());
            e.printStackTrace();
        }

        return retList;
    }

    /**
     * 根据命名空间和查询主键值查询数据
     *
     * @param nameSpace
     * @param objIdVal
     * @param sqlSession
     * @return
     */
    public static JSONObject queryData(String nameSpace, String objIdVal, String objIdName, SqlSessionTemplate sqlSession) {
        JSONObject retObj = new JSONObject();
        Map<String, Object> param = new HashMap<>();
        param.put(objIdName , objIdVal);
        try {
            List<Object> oldList = sqlSession.selectList(nameSpace, param);
            if (null != oldList && oldList.size() > 0) {
                Object oldObj = oldList.get(0);
                retObj = JSON.parseObject(JSON.toJSONString(oldObj));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return retObj;
    }


    /**
     * 获取header参数内容
     *
     * @return
     */
    public static Map<String, String> getHeader(HttpServletRequest request) {
        Map<String, String> map = new HashMap<>(0);
        Enumeration headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String key = (String) headerNames.nextElement();
            String value = request.getHeader(key);
            map.put(key, value);
        }
        return map;
    }


    /**
     * 获取利用反射获取类里面的值和名称
     *
     * @param obj
     * @return
     * @throws IllegalAccessException
     */
    public static Map<String, Object> objectToMap(Object obj) throws IllegalAccessException {
        Map<String, Object> map = new HashMap<>();
        Class<?> clazz = obj.getClass();
        System.out.println(clazz);
        for (Field field : clazz.getDeclaredFields()) {
            field.setAccessible(true);
            String fieldName = field.getName();
            Object value = field.get(obj);
            map.put(fieldName, value);
        }
        return map;
    }

    /**
     * 获取入参中的所有参数,返回JSONObject,参数为key,值为value
     * @param args
     * @return
     * @throws IllegalAccessException
     */
    public static JSONObject getParamsMap(Object[] args) throws IllegalAccessException {
        JSONObject retObj = new JSONObject();
        for (Object obj: args){
            Map<String, Object> map = new HashMap<>();
            try{
                //json字符串
                JSONObject paramObj = JSONObject.parseObject(obj.toString());
                map = JSONObject.toJavaObject(paramObj, Map.class);
            } catch (Exception e){
                //对象字符串
                map =objectToMap(obj);
            }

            for (String key : map.keySet()){
                retObj.put(key, map.get(key));
            }
        }
        return retObj;
    }
}
  • 注解使用

        至此,所有的准备和实现逻辑都已完成,接下来就是几种简单的使用场景:

  • 1. 使用固定日志模板注解 @LogOfCustomTemplate,直接外部传入完整的模板信息

    @LogOfCustomTemplate(content = LogTemplate.SYS.LOG_OUT)
    public String loginout(HttpServletRequest request, @RequestBody String requestBody) {

    }
  • 2. 使用记录数据变更之前日志模板注解  @LogForBefore,通过自定义注解传入各种参数 ,@LogOfUpdateDetail、@LogForAfter 和 @LogForBefore 注解的使用方法一样,这里不再进一步举例。
@LogForBefore(content = LogTemplate.DEL_TEMPLATE.DELETE_OUTSIDE_ACCESS, dbIdName = "id",
            nameSpace = "com.dao.intManager.OutsideAccessInfoMapper.queryById",
             showFields = "appid")
    public String delete(HttpServletRequest request, @RequestBody String requestBody) throws ServiceException{
		}

 

注意事项:

1. @LogForAfter 如果用于新增操作,则当前注解必须设在service接口实现类或者mapper实现类,因为新增操作需要获取ID,mybatis 的insert 方法执行完后,会返回当前ID,并注入到参数中,这样就可以在AOP中获取,如果不需要记录ID则无需考虑这个;

2. nameSpace 命名空间所标注的方法,必须自己实现,如果查询返回多条,默认取最新的一条。

3.把日志所有的属性值都放在自定义注解中,配置使用时,会显得有些麻烦,可以选择放在数据库配置表中

 

上面的很多方法或逻辑的实现不是很友好,欢迎大家提出更好的解决方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值