SpringBoot框架使用AOP + 自定义注解实现请求日志记录

一、SpringBoot记录日志

文章目录

1.1、环境搭建

  • 搭建SpringBoot工程。
  • 引入【spring-boot-starter-parent】依赖。
  • 引入【spring-boot-starter-web】依赖。
  • 引入【spring-boot-starter-aop】依赖。
  • 引入【fastjson】依赖。
xml 复制代码
<!-- 引入 SpringBoot 父工程依赖 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.0.RELEASE</version>
</parent>
 
<!-- 引入 web 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <!-- 排除 jackson 依赖 -->
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-json</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!-- 引入 aop 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 引入 fastjson 依赖 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.77</version>
</dependency>

1.2、配置FastJson

java 复制代码
package com.spring.boot.demo.config;
 
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
 
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
 
/**
 * @author Js
 * @version 1.0.0
 * @Date: 2023/11/02 12:47
 * @Description FastJson 配置类
 */
@Configuration
public class CustomFastJsonConfig {
 
    @Bean
    public HttpMessageConverters fastjsonHttpMessageConverters() {
        // 创建 FastJsonHttpMessageConverter 消息转换器对象
        FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
 
        // 创建 FastJsonConfig 配置类对象
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        // 设置编码字符集
        fastJsonConfig.setCharset(StandardCharsets.UTF_8);
        // 设置日期格式
        fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
 
        // 设置序列化特征: SerializerFeature 是一个枚举,可以选择不同的序列化特征
        SerializerFeature[] serializerFeatures = new SerializerFeature[] {
                // WriteNullStringAsEmpty: 如果字符串等于 null,那么会被序列化成空字符串 ""
                SerializerFeature.WriteNullStringAsEmpty,
                // WriteNullNumberAsZero: 如果数字等于 null,那么会被序列化成 0
                SerializerFeature.WriteNullNumberAsZero,
                // WriteNullBooleanAsFalse: 如果布尔类型等于 null,那么会被序列化成 false
                SerializerFeature.WriteNullBooleanAsFalse,
                // PrettyFormat: 美化JSON
                SerializerFeature.PrettyFormat
        };
        fastJsonConfig.setSerializerFeatures(serializerFeatures);
 
        // 配置添加到消息转换器里面
        fastJsonHttpMessageConverter.setDefaultCharset(StandardCharsets.UTF_8);
        fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
 
        // 设置响应JSON格式数据
        List<MediaType> mediaTypeList = new ArrayList<>();
        mediaTypeList.add(MediaType.APPLICATION_JSON); // JSON 格式数据
        // 设置消息转换器支持的格式
        fastJsonHttpMessageConverter.setSupportedMediaTypes(mediaTypeList);
 
        // 返回消息转换器
        return new HttpMessageConverters(fastJsonHttpMessageConverter);
    }
 
}

1.3、自定义LogRecord注解

  • 这里我们自定义一个@LogRecord注解,该注解使用在方法上面,用于标记AOP切面会拦截这个方法,并且记录请求日志信息。
java 复制代码
package com.spring.boot.demo.anno;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
/**
 * @author Js
 * @version 1.0.0
 * @Date: 2023/11/02 12:47
 * @Description 自定义日志注解
 */
// 注解可以保留到运行期间
@Retention(RetentionPolicy.RUNTIME)
// 注解使用在方法上面
@Target(ElementType.METHOD)
public @interface LogRecord {
 
    /**
     * 操作名称
     */
    String opName();
    
    /**
     * 描述信息
     */
    String desc() default "";
}

1.4、定义日志实体类

为了能够收集请求日志的信息,这里定义一个日志实体类来保存每一次请求的日志信息。

java 复制代码
package com.spring.boot.demo.pojo;
 
import java.io.Serializable;
 
/**
 * @author Js
 * @version 1.0.0
 * @Date: 2023/11/2 22:45
 * @Description 日志实体类
 */
public class LogRecordEntity implements Serializable {
    /** 日志唯一标识 */
    private String id;
    /** 操作名称 */
    private String opName;
    /** 请求路径 */
    private String path;
    /** 请求方式 */
    private String method;
    /** 请求IP地址 */
    private String requestIp;
    /** 全限定类名称 */
    private String qualifiedName;
    /** 请求入参 */
    private String inputParam;
    /** 请求出参 */
    private String outputParam;
    /** 异常信息 */
    private String errorMsg;
    /** 请求开始时间 */
    private String requestTime;
    /** 请求响应时间 */
    private String responseTime;
    /** 接口耗时,单位:ms */
    private String costTime;
    /** 请求是否成功 */
    private String status;
 
    public String getId() {
        return id;
    }
 
    public void setId(String id) {
        this.id = id;
    }
 
    public String getOpName() {
        return opName;
    }
 
    public void setOpName(String opName) {
        this.opName = opName;
    }
 
    public String getPath() {
        return path;
    }
 
    public void setPath(String path) {
        this.path = path;
    }
 
    public String getMethod() {
        return method;
    }
 
    public void setMethod(String method) {
        this.method = method;
    }
 
    public String getRequestIp() {
        return requestIp;
    }
 
    public void setRequestIp(String requestIp) {
        this.requestIp = requestIp;
    }
 
    public String getQualifiedName() {
        return qualifiedName;
    }
 
    public void setQualifiedName(String qualifiedName) {
        this.qualifiedName = qualifiedName;
    }
 
    public String getInputParam() {
        return inputParam;
    }
 
    public void setInputParam(String inputParam) {
        this.inputParam = inputParam;
    }
 
    public String getOutputParam() {
        return outputParam;
    }
 
    public void setOutputParam(String outputParam) {
        this.outputParam = outputParam;
    }
 
    public String getErrorMsg() {
        return errorMsg;
    }
 
    public void setErrorMsg(String errorMsg) {
        this.errorMsg = errorMsg;
    }
 
    public String getRequestTime() {
        return requestTime;
    }
 
    public void setRequestTime(String requestTime) {
        this.requestTime = requestTime;
    }
 
    public String getResponseTime() {
        return responseTime;
    }
 
    public void setResponseTime(String responseTime) {
        this.responseTime = responseTime;
    }
 
    public String getCostTime() {
        return costTime;
    }
 
    public void setCostTime(String costTime) {
        this.costTime = costTime;
    }
 
    public String getStatus() {
        return status;
    }
 
    public void setStatus(String status) {
        this.status = status;
    }
 
    @Override
    public String toString() {
        return "LogRecordEntity{" +
                "id='" + id + '\'' +
                ", opName='" + opName + '\'' +
                ", path='" + path + '\'' +
                ", method='" + method + '\'' +
                ", requestIp='" + requestIp + '\'' +
                ", qualifiedName='" + qualifiedName + '\'' +
                ", inputParam='" + inputParam + '\'' +
                ", outputParam='" + outputParam + '\'' +
                ", errorMsg='" + errorMsg + '\'' +
                ", requestTime='" + requestTime + '\'' +
                ", responseTime='" + responseTime + '\'' +
                ", costTime='" + costTime + '\'' +
                ", status='" + status + '\'' +
                '}';
    }
}

1.5、创建HttpRequestUtil工具类

  • 创建一个获取HTTP请求和响应对象的工具类,在SpringBoot框架中,可以通过RequestContextHolder类获取到HTTP请求属性对象,通过该对象可以获取到Request、Response对象。
java 复制代码
package com.spring.boot.demo.util;
 
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
/**
 * @author Js
 * @version 1.0.0
 * @Date: 2023/11/2 23:03
 * @Description HTTP请求的工具类,用于获取Request、Response相关信息
 */
public final class HttpRequestUtil {
 
    /**
     * 从 SpringBoot 中获取 Request 请求对象
     * @return 返回当前请求的 Request 对象
     */
    public static HttpServletRequest getRequest() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            return null;
        }
        ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
        return attributes.getRequest();
    }
 
    /**
     * 从 SpringBoot 中获取 Response 请求对象
     * @return 返回当前请求的 Response 对象
     */
    public static HttpServletResponse getResponse() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            return null;
        }
        ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
        return attributes.getResponse();
    }
 
}

1.6、定义AOP切面

java 复制代码
package com.spring.boot.demo.config;
 
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.spring.boot.demo.anno.LogRecord;
import com.spring.boot.demo.pojo.LogRecordEntity;
import com.spring.boot.demo.util.HttpRequestUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
 
import javax.servlet.http.HttpServletRequest;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.UUID;
 
/**
 * @author Js
 * @version 1.0.0
 * @Date: 2023/11/2 12:52
 * @Description 自定义日志切面
 */
// 标记当前类是一个切面类
@Aspect
// 将当前类放入IOC容器
@Component
public class LogAspect {
 
    /**
     * 创建线程局部变量
     */
    private ThreadLocal<LogRecordEntity> threadLocal = new ThreadLocal<>();
 
    /**
     * 定义切入点,这里我们使用AOP切入自定义【@LogRecord】注解的方法
     */
    @Pointcut("@annotation(com.spring.boot.demo.anno.LogRecord)")
    public void pointCut() {}
 
    /**
     * 前置通知,【执行Controller方法之前】执行该通知方法
     */
    @Before("pointCut()")
    public void beforeAdvice() {
        System.out.println("前置通知......"); // TODO delete
    }
 
    /**
     * 后置通知,【Controller方法执行完成,返回方法的返回值之前】执行该通知方法
     */
    @After("pointCut()")
    public void afterAdvice() {
        System.out.println("后置通知......"); // TODO delete
    }
 
    /**
     * 环绕通知,执行Controller方法的前后执行
     * @param joinPoint 连接点
     */
    @Around("pointCut()")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        // 请求开始时间
        String requestTime = String.valueOf(System.currentTimeMillis());
        System.out.println("环绕通知之前....."); // TODO delete
        // 获取当前请求对象
        HttpServletRequest request = HttpRequestUtil.getRequest();
        if (request == null) {
            return null;
        }
        // 获取请求相关信息
        LogRecordEntity entity = new LogRecordEntity();
        entity.setId(UUID.randomUUID().toString().replace("-", ""));
        entity.setPath(request.getRequestURI());
        entity.setMethod(request.getMethod());
        entity.setRequestIp(request.getRemoteHost());
        entity.setRequestTime(requestTime);
 
        // 反射获取调用方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        if (method.isAnnotationPresent(LogRecord.class)) {
            // 获取注解信息
            LogRecord annotation = method.getAnnotation(LogRecord.class);
            entity.setOpName(annotation.opName());
        }
        // 获取全限定类名称
        String name = method.getName();
 
        // 获取请求参数
        String inputParam = JSONObject.toJSONString(joinPoint.getArgs());
        entity.setInputParam(inputParam);
        // 设置局部变量
        threadLocal.set(entity);
 
        // 调用Controller方法
        Object ret = joinPoint.proceed();
        System.out.println("环绕通知之后....."); // TODO delete
        return ret;
    }
 
    /**
     * 返回值通知,Controller执行完成之后,返回方法的返回值时候执行
     * @param ret 返回值的名称
     */
    @AfterReturning(pointcut = "pointCut()", returning = "ret")
    public Object afterReturning(Object ret) {
        System.out.println("返回值通知......ret=" + ret); // TODO delete
        // 获取日志实体对象
        LogRecordEntity entity = this.getEntity();
        String outputParam = JSON.toJSONString(ret);
        entity.setOutputParam(outputParam); // 保存响应参数
        entity.setStatus("成功"); // 设置成功标识
        // TODO 这里就可以做一些持久胡操作,例如:保存日志到数据表里面
        // 一定要删除 ThreadLocal 变量
        threadLocal.remove();
        System.out.println(entity); // TODO delete
        return ret;
    }
 
    /**
     * 异常通知,当Controller方法执行过程中出现异常时候,执行该通知
     * @param ex 异常名称
     */
    @AfterThrowing(pointcut = "pointCut()", throwing = "ex")
    public void throwingAdvice(Throwable ex) {
        System.out.println("异常通知......"); // TODO delete
        // 获取日志实体对象
        LogRecordEntity entity = this.getEntity();
        StringWriter errorMsg = new StringWriter();
        ex.printStackTrace(new PrintWriter(errorMsg, true));
        entity.setErrorMsg(errorMsg.toString()); // 保存响应参数
        entity.setStatus("失败"); // 设置成功标识
        // TODO 这里就可以做一些持久胡操作,例如:保存日志到数据表里面
        // 一定要删除 ThreadLocal 变量
        threadLocal.remove();
        System.out.println(entity); // TODO delete
    }
 
    /****************************************************/
 
    private LogRecordEntity getEntity() {
        // 获取局部变量
        LogRecordEntity entity = threadLocal.get();
        long start = Long.parseLong(entity.getRequestTime());
        long end = System.currentTimeMillis();
        // 获取响应时间、耗时
        entity.setCostTime((end - start) + "ms");
        entity.setResponseTime(String.valueOf(end));
        return entity;
    }
}

1.7、编写测试类

java 复制代码
package com.spring.boot.demo.controller;
 
import com.spring.boot.demo.anno.LogRecord;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
/**
 * @author Js
 * @version 1.0.0
 * @Date: 2023/11/2 22:58
 * @Description
 */
@RestController
@RequestMapping("/api/aop")
public class LogController {
 
    @LogRecord(opName = "测试日志", desc = "测试日志描述内容")
    @GetMapping("/log")
    public String demo() {
        System.out.println("开始执行业务逻辑代码......");
        return "success.";
    }
 
    @LogRecord(opName = "测试日志", desc = "测试日志描述内容")
    @GetMapping("/error")
    public String error() {
        System.out.println("开始执行业务逻辑代码......");
        int i = 10 / 0;
        return "success.";
    }
 
}

1.8、运行测试

启动工程,浏览器分别访问两个地址【http://127.0.0.1:8081/api/aop/log】和【http://127.0.0.1:8081/api/aop/error】,查看控制台日志输出。

到此,SpringBoot利用AOP和自定义注解实现日志记录就成功啦;目前是直接在控制台打印,也可以将其信息保存到数据库中;

相关推荐
hudson2022几秒前
work_mem: 这是一个陷阱!
后端·postgresql
hrhcode几秒前
【java工程师快速上手go】三.Go Web开发(Gin框架)
java·spring boot·golang
敖正炀1 分钟前
CountDownLatch 详解
java
Nturmoils2 分钟前
实时决策时代,工业物联网需要什么样的数据库?
数据库·后端
海兰3 分钟前
【Spring AI】从一个MCP小实例开始
java·人工智能·spring
用户8356290780514 分钟前
Python 实现 Word 页眉页脚添加与自定义设置
后端·python
Rick199310 分钟前
Spring Boot自动装配原理
java·spring boot·后端
我命由我1234516 分钟前
Android Jetpack Compose - 组件分类:布局组件、交互组件、文本组件
android·java·java-ee·kotlin·android studio·android jetpack·android-studio
神奇小汤圆19 分钟前
Elasticsearch 与 JVM:生产环境调优实战指南
后端
Devin~Y19 分钟前
大厂内容社区面试实录:从 Spring Boot 微服务到 AI RAG 问答(附详细解析)
java·spring boot·redis·elasticsearch·spring cloud·微服务·kafka