java利用切面(aspect)记录日志实现性能跟踪以及用户行为分析

记录日志的目的是多种多样的,这里主要以性能跟踪和用户行为分析为目的讨论如何记录日志。

1. 概述

一个有一定规模的应用系统,都会存在大量的功能,这些功能通过菜单,链接,按钮和页面进行展示,在系统建设初期,为了尽早将系统投入生产,对于系统性能优化方面可能考虑不够。当然,系统初期的用户量和数据量都相对较小,系统性能也不会是有明显的问题。但是,随着系统的持续运行,用户量和数据量的不断增加,性能性能的优化就变得越来越重要了。

一个请求的响应速度直接影响到用户的使用体验,只有用户系统的响应时间在正常的接受范围内时,用户才会感觉到系统是正常的,一旦超过这个时间,就会感觉系统很慢。但是系统慢,只是一个大概的描述,是个别的特定操作响应慢,还是所有的操作都慢,是个别用户觉得慢,还是所有用户都觉得慢,这就不容易描述清楚。所以,系统最好有自身处理时间的一个记录和统计,包括各种操作,各种条件下的操作,包括不同的用户,不同的时间,不同的输入条件等。

另一方面,系统在不断的增加功能和完善过程中,增加了各种各样的功能,这些功能有哪些使用得多,哪些使用得少,都是什么用户在使用,下一步的发展方向在什么地方。最好的方法就是系统能够提供针对性的统计,这是最直接的,也是最客观的,不受主观判断的影响。通过日志记录,可以分析出系统提供的功能中,哪些功能使用最频繁,哪些功能使用最少,这样有利于在后续的版本升级中,把重心放在用户关心的功能上。用户使用少的功能,可以进一步分析用户使用少的原因,比如是用户不了解,还是用户的确不需要等。

2. 主要代码

总体技术思路,采用切面方式拦截请求,记录每个请求的信息,包括请求用户,请求时间,请求参数以及执行结果等,有了这些请求数据,就可以进行统计分析,得出系统是否存在性能瓶颈。也可以统计出什么用户对什么功能的使用最频繁,什么功能几乎没有人使用。

2.1 注解代码

切面采用根据注解判断是否需要拦截方法的调用,所以首先需要创建一个自定义注解。

package com.ruoyi.common.annotation;


import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.enums.OperatorType;
import org.springframework.web.bind.annotation.ResponseBody;

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.PARAMETER, ElementType.METHOD})//作用在参数和方法上
@Retention(RetentionPolicy.RUNTIME)//运行时注解
@Documented//表明这个注解应该被 javadoc工具记录
// @ResponseBody//响应时转JSON格式
public @interface EventTrack {

    /**
     * 模块
     */
    public String title() default "";

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

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

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

    /**
     * 是否保存响应的参数
     */
    public boolean isSaveResponseData() default true;

    /**
     * 业务描述
     * @return
     */
    String description() default "";

    /**
     * 是否打日志 默认打
     */
    boolean isLog() default true;

}

在需要记录日志的方法上,添加该注解,切面会根据该注解进行拦截并日志记录。 在注解中,可以添加相关的参数,这些参数可以记录到日志中,用于区分不同的方法。

2.2 日志类代码

package com.ruoyi.system.domain;

import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;

/**
 * 事件跟踪对象 event_track_log
 * 
 * @author 
 * @date 2022-06-08
 */
public class EventTrackLog extends BaseEntity
{
    private static final long serialVersionUID = 1L;

    /** 跟踪编号 */
    private Long traceId;

    /** 应用名称 */
    @Excel(name = "应用名称")
    private String systemName;

    /** 模块名称 */
    @Excel(name = "模块名称")
    private String moduleTitle;

    /** 业务类型 */
    @Excel(name = "业务类型")
    private String businessType;

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

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

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

    /** 用户类型 */
    @Excel(name = "用户类型")
    private String userType;

    /** 商户编号 */
    @Excel(name = "商户编号")
    private String merchantNo;

    /** 操作状态 */
    @Excel(name = "操作状态")
    private Long 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 startTime;

    /** 结束时间 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @Excel(name = "结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
    private Date endTime;

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

    /** 描述信息 */
    @Excel(name = "描述信息")
    private String description;

    /** 客户端ip地址 */
    @Excel(name = "客户端ip地址")
    private String ipAddress;

    /** 户端主机名 */
    @Excel(name = "户端主机名")
    private String hostName;

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

    /** 访问url */
    @Excel(name = "访问url")
    private String url;

    /** 异常信息 */
    @Excel(name = "异常信息")
    private String exceptMsg;

    /** 返回结果 */
    @Excel(name = "返回结果")
    private String result;

    /** 时间长度 */
    @Excel(name = "时间长度")
    private Long timeSpend;

    /** 调用次数 */
    @Excel(name = "调用次数")
    private Long number;

    /** 访问渠道 */
    @Excel(name = "访问渠道")
    private String channel;

    public void setTraceId(Long traceId) 
    {
        this.traceId = traceId;
    }

    public Long getTraceId() 
    {
        return traceId;
    }
    public void setSystemName(String systemName) 
    {
        this.systemName = systemName;
    }

    public String getSystemName() 
    {
        return systemName;
    }
    public void setModuleTitle(String moduleTitle) 
    {
        this.moduleTitle = moduleTitle;
    }

    public String getModuleTitle() 
    {
        return moduleTitle;
    }
    public void setBusinessType(String businessType) 
    {
        this.businessType = businessType;
    }

    public String getBusinessType() 
    {
        return businessType;
    }
    public void setRequestMethod(String requestMethod) 
    {
        this.requestMethod = requestMethod;
    }

    public String getRequestMethod() 
    {
        return requestMethod;
    }
    public void setUserName(String userName) 
    {
        this.userName = userName;
    }

    public String getUserName() 
    {
        return userName;
    }
    public void setDeptName(String deptName) 
    {
        this.deptName = deptName;
    }

    public String getDeptName() 
    {
        return deptName;
    }
    public void setUserType(String userType) 
    {
        this.userType = userType;
    }

    public String getUserType() 
    {
        return userType;
    }
    public void setMerchantNo(String merchantNo) 
    {
        this.merchantNo = merchantNo;
    }

    public String getMerchantNo() 
    {
        return merchantNo;
    }
    public void setStatus(Long status) 
    {
        this.status = status;
    }

    public Long getStatus() 
    {
        return status;
    }
    public void setErrorMsg(String errorMsg) 
    {
        this.errorMsg = errorMsg;
    }

    public String getErrorMsg() 
    {
        return errorMsg;
    }
    public void setStartTime(Date startTime) 
    {
        this.startTime = startTime;
    }

    public Date getStartTime() 
    {
        return startTime;
    }
    public void setEndTime(Date endTime) 
    {
        this.endTime = endTime;
    }

    public Date getEndTime() 
    {
        return endTime;
    }
    public void setMethod(String method) 
    {
        this.method = method;
    }

    public String getMethod() 
    {
        return method;
    }
    public void setDescription(String description) 
    {
        this.description = description;
    }

    public String getDescription() 
    {
        return description;
    }
    public void setIpAddress(String ipAddress) 
    {
        this.ipAddress = ipAddress;
    }

    public String getIpAddress() 
    {
        return ipAddress;
    }
    public void setHostName(String hostName) 
    {
        this.hostName = hostName;
    }

    public String getHostName() 
    {
        return hostName;
    }

    public String getReqParams() {
        return reqParams;
    }

    public void setReqParams(String reqParams) {
        this.reqParams = reqParams;
    }

    public void setUrl(String url)
    {
        this.url = url;
    }

    public String getUrl() 
    {
        return url;
    }
    public void setExceptMsg(String exceptMsg) 
    {
        this.exceptMsg = exceptMsg;
    }

    public String getExceptMsg() 
    {
        return exceptMsg;
    }
    public void setResult(String result) 
    {
        this.result = result;
    }

    public String getResult() 
    {
        return result;
    }
    public void setTimeSpend(Long timeSpend) 
    {
        this.timeSpend = timeSpend;
    }

    public Long getTimeSpend() 
    {
        return timeSpend;
    }
    public void setNumber(Long number) 
    {
        this.number = number;
    }

    public Long getNumber() 
    {
        return number;
    }
    public void setChannel(String channel) 
    {
        this.channel = channel;
    }

    public String getChannel() 
    {
        return channel;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
            .append("traceId", getTraceId())
            .append("systemName", getSystemName())
            .append("moduleTitle", getModuleTitle())
            .append("businessType", getBusinessType())
            .append("requestMethod", getRequestMethod())
            .append("userName", getUserName())
            .append("deptName", getDeptName())
            .append("userType", getUserType())
            .append("merchantNo", getMerchantNo())
            .append("status", getStatus())
            .append("errorMsg", getErrorMsg())
            .append("startTime", getStartTime())
            .append("endTime", getEndTime())
            .append("method", getMethod())
            .append("description", getDescription())
            .append("ipAddress", getIpAddress())
            .append("hostName", getHostName())
            .append("reqParams", getReqParams())
            .append("url", getUrl())
            .append("exceptMsg", getExceptMsg())
            .append("result", getResult())
            .append("timeSpend", getTimeSpend())
            .append("number", getNumber())
            .append("channel", getChannel())
            .toString();
    }
}

事件日志信息记录类,与对应的数据库表脚本一一对应。事件日志记录的信息,可以根据业务需要进行扩展,比如有的业务场景中需要用户编号,客户编号或者用户手机号等进行关联标识。

CREATE TABLE `event_track_log` (
  `trace_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '跟踪编号',
  `system_name` varchar(32) DEFAULT NULL COMMENT '应用名称',
  `module_title` varchar(32) DEFAULT NULL COMMENT '模块名称',
  `business_type` varchar(20) DEFAULT NULL COMMENT '业务类型',
  `request_method` varchar(10) DEFAULT NULL COMMENT '请求方式',
  `user_name` varchar(64) DEFAULT NULL COMMENT '操作人员',
  `dept_name` varchar(64) DEFAULT NULL COMMENT '部门名称',
  `user_type` varchar(20) DEFAULT NULL COMMENT '用户类型',
  `merchant_no` varchar(255) DEFAULT NULL COMMENT '商户编号',
  `status` int(11) DEFAULT NULL COMMENT '操作状态',
  `error_msg` varchar(255) DEFAULT NULL COMMENT '错误消息',
  `start_time` datetime DEFAULT NULL COMMENT '开始时间',
  `end_time` datetime DEFAULT NULL COMMENT '结束时间',
  `method` varchar(64) DEFAULT NULL COMMENT '方法名',
  `description` varchar(255) DEFAULT NULL COMMENT '描述信息',
  `ip_address` varchar(32)  DEFAULT NULL COMMENT '客户端ip地址',
  `host_name` varchar(32)  DEFAULT NULL COMMENT '户端主机名',
  `req_params` varchar(255)  DEFAULT NULL COMMENT '请求参数',
  `url` varchar(255) DEFAULT NULL COMMENT '访问url',
  `except_msg` varchar(255) DEFAULT NULL COMMENT '异常信息',
  `result` varchar(255) DEFAULT NULL COMMENT '返回结果',
  `time_spend` bigint(20) DEFAULT NULL COMMENT '时间长度',
  `number` int(11) DEFAULT NULL COMMENT '调用次数',
  `channel` varchar(20) DEFAULT NULL COMMENT '访问渠道',
  `create_by` varchar(32) DEFAULT NULL COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`trace_id`)
) ENGINE=InnoDB AUTO_INCREMENT=481 DEFAULT CHARSET=utf8 COMMENT='事件跟踪';

2.3 切面类代码

package com.ruoyi.framework.aspectj;

import com.alibaba.fastjson.JSON;
import com.ruoyi.common.annotation.EventTrack;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.ShiroUtils;
import com.ruoyi.system.domain.EventTrackLog;
import com.ruoyi.system.service.IEventTrackLogService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

/**
 * author:
 * 埋点切面
 */
@Aspect
@Component
public class EventTrackAspect {

    //日志工厂获取日志对象
    static Logger logger = LoggerFactory.getLogger(EventTrackAspect.class);

    /** 排除敏感属性字段 */
    public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };

    @Autowired
    private IEventTrackLogService eventTrackLogService;

    //startTime存放开始时间
    private ThreadLocal<Map<String, Long >> startTime = new ThreadLocal<>();

    //eventTrackLog日志访问对象
    private ThreadLocal<EventTrackLog> eventTrackLog = new ThreadLocal<>();

    //Controller层切点
    @Pointcut("@annotation(com.ruoyi.common.annotation.EventTrack)")
    public void controllerAspectse() {
    }

    //前置通知  用于拦截Controller层记录用户的操作
    @Before("controllerAspectse()")
    public void before(JoinPoint pjp) {
        //方法调用之前初始化
        EventTrackLog eventTrackLog = this.eventTrackLog.get();
        eventTrackLog = new EventTrackLog();

        Map<String, Long> map = new HashMap<>();
        map.put("startTime",System.currentTimeMillis());
        this.startTime.set(map);

        logger.info("==============前置通知开始:记录用户的操作==============");
        String currentTime = DateUtils.getTime();

        logger.info("请求开始时间:" + currentTime);
        eventTrackLog.setStartTime(new Date());
        String resultString = "";

        // 是否打日志 默认打
        boolean isLog = true;
        try {
            MethodSignature signature = (MethodSignature) pjp.getSignature();
            EventTrack eventTrack = signature.getMethod().getAnnotation(EventTrack.class);
            //是否开启日志打印
            isLog = eventTrack.isLog();
            if(isLog){
                //开始打印日志
                HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
                HttpSession session = request.getSession();
                String api = pjp.getTarget().getClass().getName() + "." + pjp.getSignature().getName();
                logger.info("请求API:" + api);
                eventTrackLog.setMethod(api);

                String methodDescription = getControllerMethodDescription(pjp);
                logger.info("方法描述:" + methodDescription);
                eventTrackLog.setDescription(methodDescription);

                String ipAddress = InetAddress.getLocalHost().toString().substring(InetAddress.getLocalHost().toString().lastIndexOf("/") + 1);
                logger.info("请求ip:"+ ipAddress);
                eventTrackLog.setIpAddress(ipAddress);

                String hostName = InetAddress.getLocalHost().getHostName();
                logger.info("机器名:" + hostName);
                eventTrackLog.setHostName(hostName);

                Enumeration<?> enu = request.getParameterNames();
                String params = "{";
                while (enu.hasMoreElements()) {
                    String paraName = (String) enu.nextElement();
                    List<String> list = Arrays.asList(EXCLUDE_PROPERTIES);
                    if(list.contains(paraName)) {
                        continue;
                    }

                    params += "\"" + paraName + "\":\"" + request.getParameter(paraName) + "\",";
                }
                String methodParams = params + "}";
                String substring = methodParams.substring(0, methodParams.length() - 2);
                substring = substring + "}";
                logger.info("方法参数:" + substring);
                eventTrackLog.setReqParams(substring);

                StringBuffer url = request.getRequestURL();
                logger.info("URL:" + url);
                eventTrackLog.setUrl(String.valueOf(url));
            }
        } catch (Exception e) {
            StackTraceElement stackTraceElement2 = e.getStackTrace()[2];
            String reason = "异常:【"+
                    "类名:"+stackTraceElement2.getClassName()+";"+
                    "文件:"+stackTraceElement2.getFileName()+";"+"行:"+
                    stackTraceElement2.getLineNumber()+";"+"方法:"
                    +stackTraceElement2.getMethodName() + "】";
            //记录本地异常日志
            logger.error("==============前置通知异常:记录访问异常信息==============");
            String message = e.getMessage() + "|" + reason;
            logger.error("异常信息:",message);
            eventTrackLog.setErrorMsg(message);
            eventTrackLog.setResult("请求发生异常,异常信息:" + message);
        }finally {
            this.eventTrackLog.set(eventTrackLog);
        }
    }

    @Around("controllerAspectse()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {

        // 获取方法签名
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        EventTrack eventTrack = signature.getMethod().getAnnotation(EventTrack.class);
        //是否开启日志打印
        Boolean isLog = eventTrack.isLog();

        EventTrackLog eventTrackLog = new EventTrackLog();

        Long startTime = System.currentTimeMillis();

        eventTrackLog.setStartTime(new Date());

        try {

            //开始打印日志
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            HttpSession session = request.getSession();
            String api = pjp.getTarget().getClass().getName() + "." + pjp.getSignature().getName();
            logger.info("请求API:" + api);
            eventTrackLog.setMethod(api);

            String methodDescription = getControllerMethodDescription(pjp);
            logger.info("方法描述:" + methodDescription);
            eventTrackLog.setDescription(methodDescription);

            String ipAddress = InetAddress.getLocalHost().toString().substring(InetAddress.getLocalHost().toString().lastIndexOf("/") + 1);
            logger.info("请求ip:"+ ipAddress);
            eventTrackLog.setIpAddress(ipAddress);

            String hostName = InetAddress.getLocalHost().getHostName();
            logger.info("机器名:" + hostName);
            eventTrackLog.setHostName(hostName);

            Enumeration<?> enu = request.getParameterNames();
            String params = "{";
            while (enu.hasMoreElements()) {
                String paraName = (String) enu.nextElement();

                List<String> list = Arrays.asList(EXCLUDE_PROPERTIES);
                if(list.contains(paraName)) {
                    continue;
                }

                params += "\"" + paraName + "\":\"" + request.getParameter(paraName) + "\",";
            }
            String methodParams = params + "}";
            String substring = methodParams.substring(0, methodParams.length() - 2);
            substring = substring + "}";
            logger.info("方法参数:" + substring);
            eventTrackLog.setReqParams(substring);

            StringBuffer url = request.getRequestURL();
            logger.info("URL:" + url);
            eventTrackLog.setUrl(String.valueOf(url));

        }catch (Exception e) {

        }

        Object proceed = pjp.proceed();
        String result = JSON.toJSONString(proceed);
        logger.info("==============环切方法执行完成==============");
        logger.info("请求结果:" + result);

        try {

            Long endTime = System.currentTimeMillis();

            Long timeSpan = endTime - startTime;
            logger.info("timeSpan = " + timeSpan);

            eventTrackLog.setTimeSpend(timeSpan);

            eventTrackLog.setEndTime(new Date());

            // 获取当前的用户
            SysUser currentUser = ShiroUtils.getSysUser();

            if(currentUser != null)
            {
                eventTrackLog.setUserName(currentUser.getLoginName());
                eventTrackLog.setDeptName(currentUser.getDept().getDeptName());
            }

            // eventTrackLog.setSystemName("平台管理系统");
            eventTrackLog.setSystemName(RuoYiConfig.getName());
            // eventTrackLog.setModuleTitle("后台管理");



            eventTrackLog.setStatus(0l);
            eventTrackLog.setNumber(1L);

            eventTrackLog.setModuleTitle(eventTrack.title());
            eventTrackLog.setBusinessType(eventTrack.businessType().toString());

            if(ServletUtils.checkAgentIsMobile(ServletUtils.getRequest().getHeader("User-Agent")))
            {
                eventTrackLog.setChannel("Mobile");
            }
            else {
                eventTrackLog.setChannel("PC");
            }

            eventTrackLog.setCreateBy("around");
            eventTrackLog.setCreateTime(new Date());


        } catch (Exception e) {


        } finally {

            // 添加日志信息入库
            eventTrackLogService.insertEventTrackLog(eventTrackLog);
        }



        return proceed;
    }

    /**
     * 拦截异常操作
     *
     * @param joinPoint 切点
     * @param ex 异常
     */
    @AfterThrowing(value = "@annotation(eventTrack)", throwing = "ex")
    public void doAfterThrowing(JoinPoint joinPoint, EventTrack eventTrack, Exception ex)
    {
        logger.info("==============异常方法执行==============");
        EventTrackLog eventTrackLog = this.eventTrackLog.get();

        try {
            //获取方法名
            String methodName = joinPoint.getSignature().getName();

            Long end = System.currentTimeMillis();
            Long total =  end - startTime.get().get("startTime");
            logger.info("执行总耗时为:" +total);

            eventTrackLog = this.eventTrackLog.get();
            eventTrackLog.setTimeSpend(total);
            String endTime = DateUtils.getTime();
            logger.info("请求结束时间:" + endTime);
            eventTrackLog.setEndTime(new Date());

            // 获取当前的用户
            SysUser currentUser = ShiroUtils.getSysUser();

            if(currentUser != null)
            {
                eventTrackLog.setUserName(currentUser.getLoginName());
                eventTrackLog.setDeptName(currentUser.getDept().getDeptName());
            }

            // eventTrackLog.setSystemName("平台管理系统");
            eventTrackLog.setSystemName(RuoYiConfig.getName());

            // eventTrackLog.setModuleTitle("后台管理");

            eventTrackLog.setStatus(2l);
            eventTrackLog.setNumber(1L);

            eventTrackLog.setExceptMsg(ex.getMessage());

            eventTrackLog.setModuleTitle(eventTrack.title());
            eventTrackLog.setBusinessType(eventTrack.businessType().toString());

            if(ServletUtils.checkAgentIsMobile(ServletUtils.getRequest().getHeader("User-Agent")))
            {
                eventTrackLog.setChannel("Mobile");
            }
            else {
                eventTrackLog.setChannel("PC");
            }

            eventTrackLog.setCreateBy("except");
            eventTrackLog.setCreateTime(new Date());

        } catch (Exception e) {
            StackTraceElement stackTraceElement2 = e.getStackTrace()[2];
            String reason = "异常:【"+
                    "类名:"+stackTraceElement2.getClassName()+";"+
                    "文件:"+stackTraceElement2.getFileName()+";"+"行:"+
                    stackTraceElement2.getLineNumber()+";"+"方法:"
                    +stackTraceElement2.getMethodName() + "】";
            //记录本地异常日志
            logger.error("==============通知异常:记录访问异常信息==============");
            String message = e.getMessage() + "|" + reason;
            logger.error("异常信息:",message);
            eventTrackLog.setExceptMsg(message);
            eventTrackLog.setResult("请求发生异常!!!");
        } finally {
            // 添加日志信息入库
            eventTrackLogService.insertEventTrackLog(eventTrackLog);

            // 处理使用结束,清理变量
            this.eventTrackLog.remove();
        }

    }


    /**
     *
     * @param jp
     */

    @AfterReturning(pointcut = "@annotation(eventTrack)", returning = "jsonResult")
    public void afterMethod(JoinPoint jp, EventTrack eventTrack, Object jsonResult) {
        logger.info("==============方法执行完成==============");
        EventTrackLog eventTrackLog = this.eventTrackLog.get();
        try {
            //获取方法名
            String methodName = jp.getSignature().getName();

            Long end = System.currentTimeMillis();
            Long total =  end - startTime.get().get("startTime");
            logger.info("执行总耗时为:" +total);

            eventTrackLog = this.eventTrackLog.get();
            eventTrackLog.setTimeSpend(total);
            String endTime = DateUtils.getTime();
            logger.info("请求结束时间:" + endTime);
            eventTrackLog.setEndTime(new Date());

            // 获取当前的用户
            SysUser currentUser = ShiroUtils.getSysUser();

            if(currentUser != null)
            {
                eventTrackLog.setUserName(currentUser.getLoginName());
                eventTrackLog.setDeptName(currentUser.getDept().getDeptName());
            }

            // eventTrackLog.setSystemName("平台管理系统");
            eventTrackLog.setSystemName(RuoYiConfig.getName());
            // eventTrackLog.setModuleTitle("后台管理");

            eventTrackLog.setStatus(0l);
            eventTrackLog.setNumber(1L);

            eventTrackLog.setModuleTitle(eventTrack.title());
            eventTrackLog.setBusinessType(eventTrack.businessType().toString());

            if(ServletUtils.checkAgentIsMobile(ServletUtils.getRequest().getHeader("User-Agent")))
            {
                eventTrackLog.setChannel("Mobile");
            }
            else {
                eventTrackLog.setChannel("PC");
            }

            eventTrackLog.setCreateBy("return");
            eventTrackLog.setCreateTime(new Date());

        } catch (Exception e) {
            StackTraceElement stackTraceElement2 = e.getStackTrace()[2];
            String reason = "异常:【"+
                    "类名:"+stackTraceElement2.getClassName()+";"+
                    "文件:"+stackTraceElement2.getFileName()+";"+"行:"+
                    stackTraceElement2.getLineNumber()+";"+"方法:"
                    +stackTraceElement2.getMethodName() + "】";
            //记录本地异常日志
            logger.error("==============通知异常:记录访问异常信息==============");
            String message = e.getMessage() + "|" + reason;
            logger.error("异常信息:",message);
            eventTrackLog.setExceptMsg(message);
            eventTrackLog.setResult("请求发生异常!!!");

        } finally {
            // this.eventTrackLog.set(eventTrackLog);
            // 添加日志信息入库
            eventTrackLogService.insertEventTrackLog(eventTrackLog);

            // 处理使用结束,清理变量
            this.eventTrackLog.remove();
        }
    }

    /**
     * 获取注解中对方法的描述信息 用于Controller层注解
     */
    public static String getControllerMethodDescription(JoinPoint joinPoint) throws Exception {
        String targetName = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();//目标方法名
        Object[] arguments = joinPoint.getArgs();
        Class targetClass = Class.forName(targetName);
        Method[] methods = targetClass.getMethods();
        String description = "";
        for (Method method:methods) {
            if (method.getName().equals(methodName)){
                Class[] clazzs = method.getParameterTypes();
                if (clazzs.length==arguments.length){
                    description = method.getAnnotation(EventTrack.class).description();
                    break;
                }
            }
        }
        return description;
    }

}

在切面类中,定义了前置通知和后置通知,前置通知记录方法调用的开始时间,后置通知包括正常返回和抛出异常两种情况,后置通知记录方法的结束时间,通过两个时间,就可以计算出方法的执行时间。另外一种方法是采用环切通知,执行前记录一次时间,执行后记录一次时间,因为在同一个方法内,不需要定义线程本地变量了。 

根据需要,两种方式选择一种就可以了。

2.4 添加切面注解

@RequiresPermissions("system:user:list")
@PostMapping("/list")
@EventTrack(title = "用户管理", businessType = BusinessType.QUERY, description="查询用户")
@ResponseBody
public TableDataInfo list(SysUser user)
{
    startPage();
    List<SysUser> list = userService.selectUserList(user);
    return getDataTable(list);
}

在方法上添加注释:

 @EventTrack(title = "用户管理", businessType = BusinessType.QUERY, description="查询用户")

相关参数根据场景进行设置。

2.5 日志信息

 通过日志信息,可以看出每个操作所花费的时间,单位是毫秒。通过日志时间,也可以看出是什么时间进行的操作,通过操作人员,可以知道是谁操作的。还可以关联其它的业务信息,比如用户类型,用户所属机构等。