Bootstrap

SpringBoot AOP切面编程 记录操作日志、异常日志

  • 概述

Spring AOP(面向切面编程)提供了一种优雅的解决方案,用于处理这种跨多个方法或类的常见任务,如日志记录、性能统计、安全控制和事务管理等。通过AOP,我们可以将这些横切关注点(Cross-cutting Concerns)从业务逻辑中分离出来,使业务代码更加清晰和专注。

在Spring Boot应用中,我们可以利用AOP来简化日志记录和异常处理的过程。通过定义切面(Aspect)和切入点(Pointcut),我们可以指定哪些方法或类的操作应该被拦截,并自动执行日志记录或异常处理逻辑。这样,我们不仅可以减少代码冗余,提高可维护性,还可以实时地将日志信息存储到数据库中,便于后续的分析和监控。

简而言之,我们可以利用Spring Boot的AOP功能,将日志记录和异常处理逻辑从业务代码中分离出来,实现更加高效、灵活和可维护的系统。

  • 表结构

数据库:MySql

1.操作记录表

create table log_info
(
id varchar(64) not null comment '主键id'
primary key,
module varchar(255) null comment '功能模块',
type varchar(255) null comment '操作类型',
message varchar(255) null comment '操作描述',
req_param text null comment '请求参数',
res_param text null comment '响应参数',
take_up_time mediumtext null comment '耗时',
method varchar(255) null comment '操作方法',
url varchar(255) null comment '请求url',
ip varchar(255) null comment '请求ip',
version varchar(64) null comment '版本号',
create_time datetime null comment '创建时间'
)
comment '操作记录表';

2.异常记录表

create table log_error_info
(
id varchar(64) not null comment '主键id'
primary key,
req_param text null comment '请求参数',
name varchar(255) null comment '异常名称',
message text null comment '异常信息',
method varchar(255) null comment '请求方法',
url varchar(255) null comment '请求url',
ip varchar(255) null comment '请求ip',
version varchar(255) null comment '版本号',
create_time datetime null comment '创建时间'
)
comment '操作日志异常信息';

 

  •  实现

 1.依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.操作日志注解类:ThomasLog .java

import java.lang.annotation.*;
/**
 * 操作日志注解类
 * @author Thomas 
 * @date 11:37 2024/2/24  
 **/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ThomasLog {

    /**
     * 操作模块
     * @return 操作模块
     */
    String modul() default "";

    /**
     * 操作类型
     * @return 操作类型
     */
    String type() default "";

    /**
     * 操作说明
     * @return 操作说明
     */
    String desc() default "";

}

3.切面类记录操作日志:ThomasLogAspect.java

import com.alibaba.fastjson.JSON;
import com.dyl.online.annotation.ThomasLog;
import com.dyl.online.mapper.LogErrorInfoMapper;
import com.dyl.online.mapper.LogInfoMapper;
import com.dyl.online.pojo.entity.LogErrorInfo;
import com.dyl.online.pojo.entity.LogInfo;
import com.dyl.online.utils.IPUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;

@Aspect
@Component
public class ThomasLogAspect {

    /**
     * 操作版本号
     * 项目启动时从命令行传入,例如:java -jar xxx.war --version=201902
     */
    @Value("1.0.0")
    private String version;

    /**
     * 统计请求的处理时间
     */
    ThreadLocal<Long> startTime = new ThreadLocal<>();

    @Resource
    LogInfoMapper logInfoMapper;
    @Resource
    LogErrorInfoMapper logErrorInfoMapper;

    @Pointcut("@annotation(com.dyl.online.annotation.ThomasLog)")
    public void logPointCut() {
    }


    @Pointcut("execution(* com.dyl.online.controller..*.*(..))")
    public void exceptionLogPointCut() {
    }

    @Before("logPointCut()")
    public void doBefore() {
        // 接收到请求,记录请求开始时间
        startTime.set(System.currentTimeMillis());
    }


    @AfterReturning(value = "logPointCut()", returning = "keys")
    public void doAfterReturning(JoinPoint joinPoint, Object keys) {
        // 获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        // 从获取RequestAttributes中获取HttpServletRequest的信息
        assert requestAttributes != null;
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);

        LogInfo logInfo = LogInfo.builder().build();
        try {
            // 从切面织入点处通过反射机制获取织入点处的方法
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();

            // 获取切入点所在的方法
            Method method = signature.getMethod();

            // 获取请求的类名
            String className = joinPoint.getTarget().getClass().getName();

            // 获取操作
            ThomasLog log = method.getAnnotation(ThomasLog.class);
            if (Objects.nonNull(log)) {
                logInfo.setModule(log.modul());
                logInfo.setType(log.type());
                logInfo.setMessage(log.desc());
            }

            logInfo.setId(UUID.randomUUID().toString());
            logInfo.setMethod(className + "." + method.getName()); // 请求的方法名
            assert request != null;
            logInfo.setReqParam(JSON.toJSONString(converMap(request.getParameterMap()))); // 请求参数
            logInfo.setResParam(JSON.toJSONString(keys)); // 返回结果
            logInfo.setIp(IPUtil.getIpAddr(request)); // 请求IP
            logInfo.setUrl(request.getRequestURI()); // 请求URI
            logInfo.setCreateTime(LocalDateTime.now()); // 创建时间
            logInfo.setVersion(version); // 操作版本
            logInfo.setTakeUpTime(System.currentTimeMillis() - startTime.get()); // 耗时
            logInfoMapper.insert(logInfo);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    @AfterThrowing(pointcut = "exceptionLogPointCut()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
        // 获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        // 从获取RequestAttributes中获取HttpServletRequest的信息
        assert requestAttributes != null;
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);

        try {
            // 从切面织入点处通过反射机制获取织入点处的方法
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();

            // 获取切入点所在的方法
            Method method = signature.getMethod();

            // 获取请求的类名
            String className = joinPoint.getTarget().getClass().getName();
            assert request != null;
            LogErrorInfo logErrorInfo = LogErrorInfo.builder()
                            .id(UUID.randomUUID().toString())
                            .reqParam(JSON.toJSONString(converMap(request.getParameterMap()))) // 请求参数
                            .method(className + "." + method.getName()) // 请求方法名
                            .name(e.getClass().getName()) // 异常名称
                            .message(stackTraceToString(e.getClass().getName(), e.getMessage(), e.getStackTrace())) // 异常信息
                            .url(request.getRequestURI()) // 操作URI
                            .ip(IPUtil.getIpAddr(request)) // 操作员IP
                            .version(version) // 版本号
                            .createTime(LocalDateTime.now()) // 发生异常时间
                            .build();
                    logErrorInfoMapper.insert(logErrorInfo);

        } catch (Exception e2) {
            e2.printStackTrace();
        }
    }


    public Map<String, String> converMap(Map<String, String[]> paramMap) {
        Map<String, String> rtnMap = new HashMap<>();
        for (String key : paramMap.keySet()) {
            rtnMap.put(key, paramMap.get(key)[0]);
        }
        return rtnMap;
    }


    public String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {
        StringBuilder struct = new StringBuilder();
        for (StackTraceElement stet : elements) {
            struct.append(stet).append("<br/>");
        }
        return exceptionName + ":" + exceptionMessage + "<br/>" + struct;
    }
}
  • 实体类

 1.操作记录日志实体类:LogInfo.java


import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "log_info")
public class LogInfo {

    // 主键id
    @TableId(type = IdType.ASSIGN_UUID)
    private String id;

    // 功能模块
    private String module;

    // 操作类型
    private String type;

    // 操作描述
    private String message;

    // 请求参数
    private String reqParam;

    // 响应参数
    private String resParam;

    // 耗时
    private Long takeUpTime;


    // 操作方法
    private String method;

    // 请求url
    private String url;

    // 请求IP
    private String ip;

    // 版本号
    private String version;

    // 创建时间
    private LocalDateTime createTime;

}

2.异常记录日志实体类:LogErrorInfo.java


import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;


@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "log_error_info")
public class LogErrorInfo {

    // 主键id
    @TableId(type = IdType.ASSIGN_UUID)
    private String id;

    // 请求参数
    private String reqParam;

    // 异常名称
    private String name;

    // 异常信息
    private String message;



    // 请求方法
    private String method;

    // 请求url
    private String url;

    // 请求IP
    private String ip;

    // 版本号
    private String version;

    // 创建时间
    private LocalDateTime createTime;

}
  • 在Controller层添加注解

@PostMapping("/addManager")
    @Operation(summary = "新增管理员")
    @ThomasLog(modul = ThomasLogConstant.MODULE_MANAGER, type = ThomasLogConstant.ADD, desc = "新增管理员")
    public Result<Object> add(@RequestBody ManagerAddDTO managerAddDTO) {
        log.info("新增管理员:{}", managerAddDTO);
        return managerService.add(managerAddDTO);
    }
  •  枚举类

 枚举类仅为了方便,对功能实现没有影响,可根据自己需要定义

public class ThomasLogConstant {
    public static final String SELECT = "查询";
    public static final String ADD = "增加";
    public static final String UPDATE = "修改";
    public static final String DELETE = "删除";
    public static final String LOGIN = "登录";
    public static final String MODULE_MANAGER = "管理者模块";
    public static final String MODULE_DRIVER = "司机模块";
    public static final String MODULE_PASSENGER = "乘客模块";
    public static final String MODULE_ORDER = "订单模块";
}

本文仅为学习和研究目的而提供,不涉及任何商业应用。我们坚决反对任何形式的侵权行为,并尊重他人的知识产权。若您发现本文内容存在任何侵犯您权益的情况,请通过电子邮件联系我们:[email protected]。我们将尽快处理并采取措施,以确保知识产权得到妥善保护。感谢您的理解和支持。

;