整合篇5:使用AOP增强记录日志和性能统计

142 阅读6分钟

作者:田力


1. 创建数据表

  1. 操作日志表

CREATE TABLE `aop_operate_log` (
  `operate_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '主键ID',
  `operate_module` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '功能模块',
  `operate_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作类型',
  `operate_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作描述',
  `operate_method_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '请求方法名',
  `operate_request_mode` varchar(255) DEFAULT NULL COMMENT '请求方式',
  `operate_request_param` json DEFAULT NULL COMMENT '请求参数',
  `operate_response_param` json DEFAULT NULL COMMENT '响应参数',
  `operate_perf_stats` int DEFAULT NULL COMMENT '性能统计',
  `operate_user_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作人ID',
  `operate_user_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作人名称',
  `operate_uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '请求URI',
  `operate_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '请求IP',
  `operate_system` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作系统名',
  `operate_time` datetime DEFAULT NULL COMMENT '操作时间',
  PRIMARY KEY (`operate_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AOP操作日志表';
  1. 异常日志表

CREATE TABLE `aop_except_log` (
  `except_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '主键ID',
  `operate_module` varchar(255) DEFAULT NULL COMMENT '功能模块',
  `operate_type` varchar(255) DEFAULT NULL COMMENT '操作类型',
  `operate_desc` varchar(255) DEFAULT NULL COMMENT '操作描述',
  `except_method_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '请求方法名',
  `except_request_mode` varchar(255) DEFAULT NULL COMMENT '请求方式',
  `except_request_param` json DEFAULT NULL COMMENT '请求参数',
  `except_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '异常名称',
  `except_message` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '异常信息',
  `operate_user_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作人ID',
  `operate_user_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作人名称',
  `operate_uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作URI',
  `operate_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '请求IP',
  `operate_system` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作系统',
  `except_time` datetime DEFAULT NULL COMMENT '报错时间',
  PRIMARY KEY (`except_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AOP异常日志表';

2. 引入pom依赖

       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
         <!-- Hutool:Java工具类库 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.15</version>
        </dependency>

3. 创建自定义注解类

package com.zhigong.heavenearth.aop;

import java.lang.annotation.*;

/**
 * @author 田治功
 * @create 2022-04-21 10:49
 * @Description 自定义注解
 */
@Target(ElementType.METHOD) //注解放置的目标位置,METHOD即方法级别
@Retention(RetentionPolicy.RUNTIME) //注解执行政策,运行时执行
@Documented //标记这些注解是否包含在用户文档中
public @interface LogAnnotation {

    /**
     * 操作模块注解
     *
     * @return
     */
    String OperateModule() default "";

    /**
     * 操作类型注解
     *
     * @return
     */
    String OperateType() default "";

    /**
     * 操作说明注解
     *
     * @return
     */
    String OperateDesc() default "";
}

4. 创建日志切面类

package com.zhigong.heavenearth.aop;


import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.zhigong.heavenearth.dto.user.LoginDetailsDTO;
import com.zhigong.heavenearth.dto.user.UserAccountDTO;
import com.zhigong.heavenearth.mapper.AopExceptLogMapper;
import com.zhigong.heavenearth.mapper.AopOperateLogMapper;
import com.zhigong.heavenearth.pojo.aop.AopExceptLog;
import com.zhigong.heavenearth.pojo.aop.AopOperateLog;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author 田治功
 * @create 2022-04-21 11:40
 * @Description 日志切面处理类
 */
@Aspect
@Component
public class LogAspect {

    //定义独立线程副本,为每一个线程提供独立的时间
    ThreadLocal<Long> startTime = new ThreadLocal<>();


    @Value("${spring.application.name}")
    private String operateSystem;


    @Autowired
    private AopOperateLogMapper aopOperateLogMapper;

    @Autowired
    private AopExceptLogMapper aopExceptLogMapper;


    @Autowired
    RedisTemplate redisTemplate;

    //请求头
    @Value("${token.tokenHeader}")
    private String tokenHeader;

    //请求令牌中特定的字符序列
    @Value("${token.tokenHead}")
    private String tokenHead;

    //redis路由键
    @Value("${token.redis_token_key}")
    private String redisTokenKey;


    /**
     * 设置操作日志切点,在自定义注解位置切入
     */
    @Pointcut("@annotation(com.zhigong.heavenearth.aop.LogAnnotation)")
    public void operationLogPointcut() {

    }

    /**
     * 设置异常日志切点,在扫描所有controller包下切入
     */
    @Pointcut("execution(* com.zhigong.heavenearth.controller..*.*(..))")
    public void exceptionLogPointcut() {

    }

    /**
     * 前置通知:此处切面在进入切点之前进行操作
     * <p>
     * 此前置为性能统计设置:在进入切点之时初始化当前线程的时间,从而在切面处理后进行性能运算统计入库
     * 使用前置通知计算性能则会将切面处理的时间加赋在Controller切点下的时间上
     */
    @Before(value = "execution(* com.zhigong.heavenearth.controller.*.*(..))")
    public void initializationStartTime() {
        startTime.set(System.currentTimeMillis());
    }

    /**
     * 后置通知:操作日志切面在切点正常处理后进行
     *
     * @param joinPoint 连接点静态信息反射代理
     * @param response  切面方法返回值
     */
    @AfterReturning(pointcut = "operationLogPointcut()", returning = "response")
    public void saveOperationLog(JoinPoint joinPoint, Object response) {

        //0.连接到切点的时间, 单位毫秒
        long endTime = System.currentTimeMillis();

        long durationTime = endTime - startTime.get();

        //1.获取当前请求线程的请求属性
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        //2.从请求属性获取request请求参数
        HttpServletRequest httpServletRequest = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
        //3.创建操作日志写入对象,逐步设置值
        AopOperateLog aopOperateLog = new AopOperateLog();

        try {
            aopOperateLog.setOperateId(IdUtil.simpleUUID());
            //3.1.从切面织入点处通过反射获取织入点的方法属性
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            //3.2.获取织入点所在的方法属性
            Method signatureMethod = signature.getMethod();
            //3.3.获取织入点方法上的自定义注解的内容
            LogAnnotation annotation = signatureMethod.getAnnotation(LogAnnotation.class);
            if (ObjectUtil.isNotEmpty(annotation)) {
                aopOperateLog.setOperateModule(annotation.OperateModule());
                aopOperateLog.setOperateType(annotation.OperateType());
                aopOperateLog.setOperateDesc(annotation.OperateDesc());
            }
            //3.4.拼接类名和方法名,组成存入库中的方法名,携带类名便于定位和同名方法干扰
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = signatureMethod.getName();
            aopOperateLog.setOperateMethodName(className + "." + methodName);

            //3.5.获取请求方式如GET或者POST
            String methodType = httpServletRequest.getMethod();
            aopOperateLog.setOperateRequestMode(httpServletRequest.getMethod());
            //3.6.获取请求参数,并进行格式转换,而后JSON序列化处理
            String requestParamJson;
            if (methodType.equals("GET")) {
                requestParamJson = JSONUtil.toJsonStr(convertMap(httpServletRequest.getParameterMap()));
            } else if (methodType.equals("POST")) {
                requestParamJson = StrUtil.sub(JSONUtil.toJsonStr(joinPoint.getArgs()), 1, -1);
            } else {
                requestParamJson = StrUtil.sub(JSONUtil.toJsonStr(joinPoint.getArgs()), 1, -1);
            }
            aopOperateLog.setOperateRequestParam(requestParamJson);
            //3.7.响应参数JSON序列化处理
            String responseParamJson = JSONUtil.toJsonStr(response);
            aopOperateLog.setOperateResponseParam(responseParamJson);

            aopOperateLog.setOperatePerfStats(new Long(durationTime).intValue());

            //3.8.获取操作人信息,设置账户和名称
            UserAccountDTO userAccountDTO = parsingToken(httpServletRequest);
            aopOperateLog.setOperateUserId(userAccountDTO.getAccount());
            aopOperateLog.setOperateUserName(userAccountDTO.getAccountNickname());
            aopOperateLog.setOperateUri(httpServletRequest.getRequestURI());
            aopOperateLog.setOperateIp(httpServletRequest.getRemoteAddr());
            aopOperateLog.setOperateSystem(operateSystem);
            aopOperateLog.setOperateTime(new Date());

            aopOperateLogMapper.insert(aopOperateLog);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 异常通知:异常日志切面在切点发生异常时进行
     *
     * @param joinPoint 连接点静态信息反射代理
     * @param throwable 异常
     */
    @AfterThrowing(pointcut = "exceptionLogPointcut()", throwing = "throwable")
    public void saveExceptionLog(JoinPoint joinPoint, Throwable throwable) {

        //1.获取当前请求线程的请求属性
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        //2.从请求属性获取request请求参数
        HttpServletRequest httpServletRequest = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
        //3.创建操作日志写入对象,逐步设置值
        AopExceptLog aopExceptLog = new AopExceptLog();

        try {
            aopExceptLog.setExceptId(IdUtil.simpleUUID());
            //3.1.从切面织入点处通过反射获取织入点的方法属性
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            //3.2.获取织入点所在的方法属性
            Method signatureMethod = signature.getMethod();
            //3.3.获取织入点方法上的自定义注解的内容
            LogAnnotation annotation = signatureMethod.getAnnotation(LogAnnotation.class);
            if (ObjectUtil.isNotEmpty(annotation)) {
                aopExceptLog.setOperateModule(annotation.OperateModule());
                aopExceptLog.setOperateType(annotation.OperateType());
                aopExceptLog.setOperateDesc(annotation.OperateDesc());
            }
            //3.4.拼接类名和方法名,组成存入库中的方法名,携带类名便于定位和同名方法干扰
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = signatureMethod.getName();
            aopExceptLog.setExceptMethodName(className + "." + methodName);

            //3.5.获取请求方式如GET或者POST
            String methodType = httpServletRequest.getMethod();
            aopExceptLog.setExceptRequestMode(methodType);

            //3.6.获取请求参数,并进行格式转换,而后JSON序列化处理
            String requestParamJson;
            if (methodType.equals("GET")) {
                requestParamJson = JSONUtil.toJsonStr(convertMap(httpServletRequest.getParameterMap()));
            } else if (methodType.equals("POST")) {
                requestParamJson = StrUtil.sub(JSONUtil.toJsonStr(joinPoint.getArgs()), 1, -1);
            } else {
                requestParamJson = StrUtil.sub(JSONUtil.toJsonStr(joinPoint.getArgs()), 1, -1);
            }
            aopExceptLog.setExceptRequestParam(requestParamJson);
            //3.7获取错误异常名称和信息
            aopExceptLog.setExceptName(throwable.getClass().getName());
            String messages = exceptionMessageToStr(throwable.getClass().getName(), throwable.getMessage(), throwable.getStackTrace());
            aopExceptLog.setExceptMessage(messages);

            //3.8获取操作人信息,设置账户和名称
            UserAccountDTO userAccountDTO = parsingToken(httpServletRequest);
            aopExceptLog.setOperateUserId(userAccountDTO.getAccount());
            aopExceptLog.setOperateUserName(userAccountDTO.getAccountNickname());
            aopExceptLog.setOperateUri(httpServletRequest.getRequestURI());
            aopExceptLog.setOperateIp(httpServletRequest.getRemoteAddr());
            aopExceptLog.setOperateSystem(operateSystem);
            aopExceptLog.setExceptTime(new Date());

            aopExceptLogMapper.insert(aopExceptLog);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 请求HTTP参数转换
     *
     * @param parameterMap
     * @return
     */
    private HashMap<String, String> convertMap(Map<String, String[]> parameterMap) {

        HashMap<String, String> returnParameterMap = new HashMap<>();

        for (String key : parameterMap.keySet()) {
            returnParameterMap.put(key, parameterMap.get(key)[0]);
        }
        return returnParameterMap;
    }


    /**
     * 根据HTTP令牌获取操作人信息
     *
     * @param request
     * @return
     */
    private UserAccountDTO parsingToken(HttpServletRequest request) {

        UserAccountDTO userAccountDTO = new UserAccountDTO();

        //1.从请求头部获取授权令牌
        final String authHead = request.getHeader(this.tokenHeader);
        //2.验证授权令牌是否为空,不为空再验证传入授权令牌字符串中手头存在指定的前缀(特定的字符序列)
        if (authHead != null && authHead.startsWith(tokenHead)) {
            //2.0 删除请求的授权令牌中的特定字符序列
            final String authToken = authHead.substring(tokenHead.length());
            //2.1 判断redis中是否存在相应的token信息
            if (redisTemplate.hasKey(redisTokenKey + ":" + authToken)) {
                //2.1.2 从redis中取出相应的用户信息详情
                ValueOperations<String, LoginDetailsDTO> valueOperations = redisTemplate.opsForValue();
                LoginDetailsDTO loginDetailsDTO = valueOperations.get(redisTokenKey + ":" + authToken);
                userAccountDTO = loginDetailsDTO.getUserAccountDTO();
            }
        }
        return userAccountDTO;
    }


    /**
     * 异常信息转字符串处理
     *
     * @param exceptionName      异常名称
     * @param exceptionMessage   异常信息
     * @param stackTraceElements 堆栈数据
     * @return
     */
    public String exceptionMessageToStr(String exceptionName, String exceptionMessage, StackTraceElement[] stackTraceElements) {
        StringBuffer stringBuffer = new StringBuffer();

        for (StackTraceElement stackTraceElement : stackTraceElements) {
            stringBuffer.append(stackTraceElement + "\n");
        }
        String messages = exceptionMessage + ":" + exceptionName + "\n" + stringBuffer;
        return messages;
    }
}

5. 扫描级别处增设自定义注解

    /**
     * 登录服务
     *
     * @param loginParamDTO
     * @return
     */
    @ApiOperation("登录")
    @PostMapping("login")
    @LogAnnotation(OperateModule = "登录", OperateType = "登录", OperateDesc = "系统登录")
    public Result login(@RequestBody LoginParamDTO loginParamDTO) {
        LoginDetailsDTO loginDetailsDTO = securityUserDetailService.loadUserByUsername(loginParamDTO.getUserName());
        return Result.success(loginDetailsDTO);
    }

--------------------------------------------------------------------------------------------------------------------
    
    @GetMapping("verification")
    @ApiOperation("验证码")
    @LogAnnotation(OperateModule = "登录", OperateType = "验证", OperateDesc = "验证码生成")
    public Result<Map<String, String>> getVerification() {
        return Result.success(null);
    }

6. 效果预览

  • 操作日志

  • 异常日志

注:日志入库之后可根据需求进行查询等业务管理操作,此处不在赘述