• 操作日志技术探究


    什么是操作日志

    任何一个项目都会有一个用户操作日志(也叫行为日志)的模块,它主要用来记录某个用户做了某个操作,当出现操作失败时,通过日志就可以快速的查找是哪个用户在哪个模块出现了错误,以便于开发人员快速定位问题所在。

    操作日志的功能是记录您在展台中所有的操作的一个记录。它像您的日记本,记录着每一步您操作的时间、内容等。如果您需要查看您的所有操作记录,那么,操作日志就会完全为您展现出来。

    基于AOP实现的操作日志

    使用Spring的AOP来实现记录用户操作,也是推荐的现如今都使用的一种做法。它的优势在于这种记录用户操作的代码独立于其他业务逻辑代码,不仅实现了解耦,而且避免了冗余代码。

    日志存储基本代码

    (1)定义用于存储日志的数据库表

    CREATE TABLE `sys_oper_log` (
      `oper_id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志主键',
      `title` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT '' COMMENT '模块标题',
      `business_type` int DEFAULT '0' COMMENT '业务类型(0其它 1新增 2修改 3删除)',
      `method` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT '' COMMENT '方法名称',
      `request_method` varchar(10) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT '' COMMENT '请求方式',
      `operator_type` int DEFAULT '0' COMMENT '操作类别(0其它 1后台用户 2手机端用户)',
      `oper_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT '' COMMENT '操作人员',
      `dept_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT '' COMMENT '部门名称',
      `oper_url` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT '' COMMENT '请求URL',
      `oper_ip` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT '' COMMENT '主机地址',
      `oper_location` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT '' COMMENT '操作地点',
      `oper_param` varchar(2000) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT '' COMMENT '请求参数',
      `json_result` varchar(2000) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT '' COMMENT '返回参数',
      `status` int DEFAULT '0' COMMENT '操作状态(0正常 1异常)',
      `error_msg` varchar(2000) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT '' COMMENT '错误消息',
      `oper_time` datetime DEFAULT NULL COMMENT '操作时间',
      PRIMARY KEY (`oper_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=21036 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='操作日志记录';

    (2)实体类定义

    package com.huike.clues.domain;

    import java.util.Date;
    import com.fasterxml.jackson.annotation.JsonFormat;
    import com.huike.common.annotation.Excel;
    import com.huike.common.annotation.Excel.ColumnType;
    import com.huike.common.core.domain.BaseEntity;

    /**
     * 操作日志记录表 oper_log
     * 
     * 
     */
    public class SysOperLog extends BaseEntity
    {
        private static final long serialVersionUID = 1L;

        /** 日志主键 */
        @Excel(name = "操作序号", cellType = ColumnType.NUMERIC)
        private Long operId;

        /** 操作模块 */
        @Excel(name = "操作模块")
        private String title;

        /** 业务类型(0其它 1新增 2修改 3删除) */
        @Excel(name = "业务类型", readConverterExp = "0=其它,1=新增,2=修改,3=删除,4=授权,5=导出,6=导入,7=强退,8=生成代码,9=清空数据")
        private Integer businessType;

        /** 业务类型数组 */
        private Integer[] businessTypes;

        /** 请求方法 */
        @Excel(name = "请求方法")
        private String method;

        /** 请求方式 */
        @Excel(name = "请求方式")
        private String requestMethod;

        /** 操作类别(0其它 1后台用户 2手机端用户) */
        @Excel(name = "操作类别", readConverterExp = "0=其它,1=后台用户,2=手机端用户")
        private Integer operatorType;

        /** 操作人员 */
        @Excel(name = "操作人员")
        private String operName;

        /** 部门名称 */
        @Excel(name = "部门名称")
        private String deptName;

        /** 请求url */
        @Excel(name = "请求地址")
        private String operUrl;

        /** 操作地址 */
        @Excel(name = "操作地址")
        private String operIp;

        /** 操作地点 */
        @Excel(name = "操作地点")
        private String operLocation;

        /** 请求参数 */
        @Excel(name = "请求参数")
        private String operParam;

        /** 返回参数 */
        @Excel(name = "返回参数")
        private String jsonResult;

        /** 操作状态(0正常 1异常) */
        @Excel(name = "状态", readConverterExp = "0=正常,1=异常")
        private Integer status;

        /** 错误消息 */
        @Excel(name = "错误消息")
        private String errorMsg;

        /** 操作时间 */
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        @Excel(name = "操作时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
        private Date operTime;
        
        //getter and setter....
    }

    (3)数据访问层

    mapper类

    package com.huike.clues.mapper;

    import java.util.List;
    import com.huike.clues.domain.SysOperLog;

    /**
     * 操作日志 数据层
     * 
     * 
     */
    public interface SysOperLogMapper
    {
        /**
         * 新增操作日志
         * 
         * @param operLog 操作日志对象
         */
        public void insertOperlog(SysOperLog operLog);

        /**
         * 查询系统操作日志集合
         * 
         * @param operLog 操作日志对象
         * @return 操作日志集合
         */
        public List selectOperLogList(SysOperLog operLog);

        /**
         * 批量删除系统操作日志
         * 
         * @param operIds 需要删除的操作日志ID
         * @return 结果
         */
        public int deleteOperLogByIds(Long[] operIds);

        /**
         * 查询操作日志详细
         * 
         * @param operId 操作ID
         * @return 操作日志对象
         */
        public SysOperLog selectOperLogById(Long operId);

        /**
         * 清空操作日志
         */
        public void cleanOperLog();
    }

    XML文件


    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

        
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
        

        
            select oper_id, title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_location, oper_param, json_result, status, error_msg, oper_time
            from sys_oper_log
       

        
        
            insert into sys_oper_log(title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_location, oper_param, json_result, status, error_msg, oper_time)
            values (#{title}, #{businessType}, #{method}, #{requestMethod}, #{operatorType}, #{operName}, #{deptName}, #{operUrl}, #{operIp}, #{operLocation}, #{operParam}, #{jsonResult}, #{status}, #{errorMsg}, sysdate())
        

        
        
        
        
             delete from sys_oper_log where oper_id in
             
                 #{operId}
           
     
         

         
         
        
        
            truncate table sys_oper_log
       

     

    (4)业务逻辑层 ISysOperLogService

    package com.huike.clues.service;

    import java.util.List;
    import com.huike.clues.domain.SysOperLog;

    /**
     * 操作日志 服务层
     * 
     * 
     */
    public interface ISysOperLogService
    {
        /**
         * 新增操作日志
         * 
         * @param operLog 操作日志对象
         */
        public void insertOperlog(SysOperLog operLog);

        /**
         * 查询系统操作日志集合
         * 
         * @param operLog 操作日志对象
         * @return 操作日志集合
         */
        public List selectOperLogList(SysOperLog operLog);

        /**
         * 批量删除系统操作日志
         * 
         * @param operIds 需要删除的操作日志ID
         * @return 结果
         */
        public int deleteOperLogByIds(Long[] operIds);

        /**
         * 查询操作日志详细
         * 
         * @param operId 操作ID
         * @return 操作日志对象
         */
        public SysOperLog selectOperLogById(Long operId);

        /**
         * 清空操作日志
         */
        public void cleanOperLog();
    }

    SysOperLogServiceImpl

    package com.huike.clues.service.impl;

    import java.util.List;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import com.huike.clues.domain.SysOperLog;
    import com.huike.clues.mapper.SysOperLogMapper;
    import com.huike.clues.service.ISysOperLogService;

    /**
     * 操作日志 服务层处理
     * 
     * 
     */
    @Service
    public class SysOperLogServiceImpl implements ISysOperLogService
    {
        @Autowired
        private SysOperLogMapper operLogMapper;

        /**
         * 新增操作日志
         * 
         * @param operLog 操作日志对象
         */
        @Override
        public void insertOperlog(SysOperLog operLog)
        {
            operLogMapper.insertOperlog(operLog);
        }

        /**
         * 查询系统操作日志集合
         * 
         * @param operLog 操作日志对象
         * @return 操作日志集合
         */
        @Override
        public List selectOperLogList(SysOperLog operLog)
        {
            return operLogMapper.selectOperLogList(operLog);
        }

        /**
         * 批量删除系统操作日志
         * 
         * @param operIds 需要删除的操作日志ID
         * @return 结果
         */
        @Override
        public int deleteOperLogByIds(Long[] operIds)
        {
            return operLogMapper.deleteOperLogByIds(operIds);
        }

        /**
         * 查询操作日志详细
         * 
         * @param operId 操作ID
         * @return 操作日志对象
         */
        @Override
        public SysOperLog selectOperLogById(Long operId)
        {
            return operLogMapper.selectOperLogById(operId);
        }

        /**
         * 清空操作日志
         */
        @Override
        public void cleanOperLog()
        {
            operLogMapper.cleanOperLog();
        }
    }

    自定义注解

    package com.huike.common.annotation;

    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;
    import com.huike.common.enums.BusinessType;
    import com.huike.common.enums.OperatorType;

    /**
     * 自定义操作日志记录注解
     * 
     * 
     *
     */
    @Target({ ElementType.PARAMETER, ElementType.METHOD })
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Log
    {
        /**
         * 模块 
         */
        public String title() default "";

        /**
         * 功能
         */
        public BusinessType businessType() default BusinessType.OTHER;

        /**
         * 操作人类别
         */
        public OperatorType operatorType() default OperatorType.MANAGE;

        /**
         * 是否保存请求的参数
         */
        public boolean isSaveRequestData() default true;
    }

    编写日志处理类

    package com.huike.framework.aspectj;

    import java.lang.reflect.Method;
    import java.util.Collection;
    import java.util.Iterator;
    import java.util.Map;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.Signature;
    import org.aspectj.lang.annotation.AfterReturning;
    import org.aspectj.lang.annotation.AfterThrowing;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;
    import org.springframework.web.multipart.MultipartFile;
    import org.springframework.web.servlet.HandlerMapping;
    import com.alibaba.fastjson.JSON;
    import com.huike.common.annotation.Log;
    import com.huike.common.core.domain.model.LoginUser;
    import com.huike.common.enums.BusinessStatus;
    import com.huike.common.enums.HttpMethod;
    import com.huike.common.utils.ServletUtils;
    import com.huike.common.utils.StringUtils;
    import com.huike.common.utils.ip.IpUtils;
    import com.huike.common.utils.spring.SpringUtils;
    import com.huike.framework.manager.AsyncManager;
    import com.huike.framework.manager.factory.AsyncFactory;
    import com.huike.framework.web.service.TokenService;
    import com.huike.clues.domain.SysOperLog;

    /**
     * 操作日志记录处理
     * 
     * 
     */
    @Aspect
    @Component
    public class LogAspect
    {
        private static final Logger log = LoggerFactory.getLogger(LogAspect.class);

        // 配置织入点
        @Pointcut("@annotation(com.huike.common.annotation.Log)")
        public void logPointCut()
        {
        }

        /**
         * 处理完请求后执行
         *
         * @param joinPoint 切点
         */
        @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
        public void doAfterReturning(JoinPoint joinPoint, Object jsonResult)
        {
            handleLog(joinPoint, null, jsonResult);
        }

        /**
         * 拦截异常操作
         * 
         * @param joinPoint 切点
         * @param e 异常
         */
        @AfterThrowing(value = "logPointCut()", throwing = "e")
        public void doAfterThrowing(JoinPoint joinPoint, Exception e)
        {
            handleLog(joinPoint, e, null);
        }

        protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult)
        {
            try
            {
                // 获得注解
                Log controllerLog = getAnnotationLog(joinPoint);
                if (controllerLog == null)
                {
                    return;
                }

                // 获取当前的用户
                LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest());

                // *========数据库日志=========*//
                SysOperLog operLog = new SysOperLog();
                operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
                // 请求的地址
                String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
                operLog.setOperIp(ip);
                // 返回参数
                operLog.setJsonResult(JSON.toJSONString(jsonResult));

                operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
                if (loginUser != null)
                {
                    operLog.setOperName(loginUser.getUsername());
                }

                if (e != null)
                {
                    operLog.setStatus(BusinessStatus.FAIL.ordinal());
                    operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
                }
                // 设置方法名称
                String className = joinPoint.getTarget().getClass().getName();
                String methodName = joinPoint.getSignature().getName();
                operLog.setMethod(className + "." + methodName + "()");
                // 设置请求方式
                operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
                // 处理设置注解上的参数
                getControllerMethodDescription(joinPoint, controllerLog, operLog);
                // 保存数据库
                AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
            }
            catch (Exception exp)
            {
                // 记录本地异常日志
                log.error("==前置通知异常==");
                log.error("异常信息:{}", exp.getMessage());
                exp.printStackTrace();
            }
        }

        /**
         * 获取注解中对方法的描述信息 用于Controller层注解
         * 
         * @param log 日志
         * @param operLog 操作日志
         * @throws Exception
         */
        public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog) throws Exception
        {
            // 设置action动作
            operLog.setBusinessType(log.businessType().ordinal());
            // 设置标题
            operLog.setTitle(log.title());
            // 设置操作人类别
            operLog.setOperatorType(log.operatorType().ordinal());
            // 是否需要保存request,参数和值
            if (log.isSaveRequestData())
            {
                // 获取参数的信息,传入到数据库中。
                setRequestValue(joinPoint, operLog);
            }
        }

        /**
         * 获取请求的参数,放到log中
         * 
         * @param operLog 操作日志
         * @throws Exception 异常
         */
        private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception
        {
            String requestMethod = operLog.getRequestMethod();
            if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod))
            {
                String params = argsArrayToString(joinPoint.getArgs());
                operLog.setOperParam(StringUtils.substring(params, 0, 2000));
            }
            else
            {
                Map paramsMap = (Map) ServletUtils.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
                operLog.setOperParam(StringUtils.substring(paramsMap.toString(), 0, 2000));
            }
        }

        /**
         * 是否存在注解,如果存在就获取
         */
        private Log getAnnotationLog(JoinPoint joinPoint) throws Exception
        {
            Signature signature = joinPoint.getSignature();
            MethodSignature methodSignature = (MethodSignature) signature;
            Method method = methodSignature.getMethod();

            if (method != null)
            {
                return method.getAnnotation(Log.class);
            }
            return null;
        }

        /**
         * 参数拼装
         */
        private String argsArrayToString(Object[] paramsArray)
        {
            String params = "";
            if (paramsArray != null && paramsArray.length > 0)
            {
                for (int i = 0; i < paramsArray.length; i++)
                {
                    if (!isFilterObject(paramsArray[i]))
                    {
                        Object jsonObj = JSON.toJSON(paramsArray[i]);
                        params += jsonObj.toString() + " ";
                    }
                }
            }
            return params.trim();
        }

        /**
         * 判断是否需要过滤的对象。
         * 
         * @param o 对象信息。
         * @return 如果是需要过滤的对象,则返回true;否则返回false。
         */
        @SuppressWarnings("rawtypes")
        public boolean isFilterObject(final Object o)
        {
            Class clazz = o.getClass();
            if (clazz.isArray())
            {
                return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
            }
            else if (Collection.class.isAssignableFrom(clazz))
            {
                Collection collection = (Collection) o;
                for (Iterator iter = collection.iterator(); iter.hasNext();)
                {
                    return iter.next() instanceof MultipartFile;
                }
            }
            else if (Map.class.isAssignableFrom(clazz))
            {
                Map map = (Map) o;
                for (Iterator iter = map.entrySet().iterator(); iter.hasNext();)
                {
                    Map.Entry entry = (Map.Entry) iter.next();
                    return entry.getValue() instanceof MultipartFile;
                }
            }
            return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse;
        }
    }

    异步方式存储日志

    为了不影响业务方法的执行,存储日志采用异步方式存储。

    (1)异步工厂类

    package com.huike.framework.manager.factory;

    import java.util.TimerTask;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import com.huike.common.constant.Constants;
    import com.huike.common.utils.LogUtils;
    import com.huike.common.utils.ServletUtils;
    import com.huike.common.utils.ip.AddressUtils;
    import com.huike.common.utils.ip.IpUtils;
    import com.huike.common.utils.spring.SpringUtils;
    import com.huike.clues.domain.SysLogininfor;
    import com.huike.clues.domain.SysOperLog;
    import com.huike.clues.service.ISysLogininforService;
    import com.huike.clues.service.ISysOperLogService;
    import eu.bitwalker.useragentutils.UserAgent;

    /**
     * 异步工厂(产生任务用)
     * 
     * 
     */
    public class AsyncFactory
    {

        /**
         * 操作日志记录
         * 
         * @param operLog 操作日志信息
         * @return 任务task
         */
        public static TimerTask recordOper(final SysOperLog operLog) {
            return new TimerTask()
            {
                @Override
                public void run()
                {
                    // 远程查询操作地点
                      operLog.setOperLocation(
                            AddressUtils.getRealAddressByIP(operLog.getOperIp()));
                    SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog);
                }
            };
        }
    }

    (2)异步任务管理器

    package com.huike.framework.manager;

    import java.util.TimerTask;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    import com.huike.common.utils.Threads;
    import com.huike.common.utils.spring.SpringUtils;

    /**
     * 异步任务管理器
     * 
     * 
     */
    public class AsyncManager
    {
        /**
         * 操作延迟10毫秒
         */
        private final int OPERATE_DELAY_TIME = 10;

        /**
         * 异步操作任务调度线程池
         */
        private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");

        /**
         * 单例模式
         */
        private AsyncManager(){}

        private static AsyncManager me = new AsyncManager();

        public static AsyncManager me()
        {
            return me;
        }

        /**
         * 执行任务
         * 
         * @param task 任务
         */
        public void execute(TimerTask task)
        {
            executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
        }

        /**
         * 停止任务线程池
         */
        public void shutdown()
        {
            Threads.shutdownAndAwaitTermination(executor);
        }
    }

    业务方法调用日志

    在业务方法上添加@Log注解即可

    @Log(title = "批量捞取", businessType = BusinessType.UPDATE)
    @PutMapping("/gain")
    public AjaxResult gain(@RequestBody AssignmentVo assignmentVo) {

        //......
    }

  • 相关阅读:
    【Redis】基础数据结构-skiplist跳跃表
    网页保存为pdf神器(可自定义编辑)—Print Edit WE
    [深入研究4G/5G/6G专题-54]: L3信令控制-3-软件功能与流程的切分-CU-UP网元的信令
    CodeArts Check代码检查服务用户声音反馈集锦(3)
    2001-2021年省、上市公司五年规划产业政策整理代码+匹配结果
    一文搞懂序列化
    【波形/信号发生器】基于 STC1524K32S4 for C on Keil
    如何用一个插件解决 Serverless 灰度发布难题?
    spark3.3.x处理excel数据
    vue3 + vite中按需使用ace-builds实现编辑器
  • 原文地址:https://blog.csdn.net/weixin_69413377/article/details/126583611