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

一、SpringBoot记录日志

文章目录

    • 一、SpringBoot记录日志
      • 1.1、环境搭建
      • 1.2、配置FastJson
      • 1.3、自定义LogRecord注解
      • 1.4、定义日志实体类
      • 1.5、创建HttpRequestUtil工具类
      • 1.6、定义AOP切面
      • 1.7、编写测试类
      • 1.8、运行测试

1.1、环境搭建

  • 搭建SpringBoot工程。
  • 引入【spring-boot-starter-parent】依赖。
  • 引入【spring-boot-starter-web】依赖。
  • 引入【spring-boot-starter-aop】依赖。
  • 引入【fastjson】依赖。
<!-- 引入 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

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切面会拦截这个方法,并且记录请求日志信息。
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、定义日志实体类

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

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对象。
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切面

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、编写测试类

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和自定义注解实现日志记录就成功啦;目前是直接在控制台打印,也可以将其信息保存到数据库中;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/115914.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python-列表推导式、生成器表达式

一、列表推导式 列表推导式&#xff1a;用一句话来生成列表 语法&#xff1a;[结果 for循环 判断] 筛选模式&#xff1a; 二、生成器表达式

百度智能云千帆大模型平台黑客马拉松报名开启!

比赛简介 创造是生成式 AI 的核心。无论是智能导购带来的线上购物体验升级&#xff0c;还是主图生成带来的素材生产效率提升&#xff0c;又或是游戏场景的快速设置、智能 NPC 的全新交互、数字广告的精准推荐和个性化定制&#xff0c;亦或者是为学生提供更符合真实的口语练习环…

适用于 Linux 的 WPF:Avalonia

许多年前&#xff0c;在 WPF 成为“Windows Presentation Foundation”并将 XAML 作为 .NET、Windows 等的 UI 标记语言引入之前&#xff0c;有一个代号为“Avalon”的项目。Avalon 是 WPF 的代号。XAML 现在无处不在&#xff0c;XAML 标准是一个词汇规范。 Avalonia 是一个开…

一文弄懂synchronized

简述 synchronized是什么? synchronized 关键字是一种同步锁&#xff0c;它可以保证在一个时刻只有一个线程可以执行某段代码。synchronized 关键字可以用在方法、代码块、静态方法和静态代码块上。 synchronized怎么用&#xff1f; synchronized是Java中用于实现线程同步…

婚庆策划小程序制作教程

本文将向你展示如何使用小程序制作平台&#xff0c;轻松制作婚庆策划小程序。只需按照以下步骤操作&#xff0c;你就能拥有自己的专业级婚庆策划小程序。 登录小程序制作平台 首先&#xff0c;你需要登录小程序制作平台。在浏览器搜索乔拓云&#xff0c;然后使用你的账号登录。…

Promise的并发控制 - 从普通并发池到动态并发池

一、场景 给你一个有200个URL的数组&#xff0c;通过这些URL来发送请求&#xff0c;要求并发请求数不能超过五个。 这是一道很常考的面试题&#xff0c;接下来让我们来学习一下Promise并发控制 二、普通并发池的实现 主要思路就是&#xff0c;判断当前队列是否满&#xff0c;…

【word技巧】ABCD选项如何对齐?

使用word文件制作试卷&#xff0c;如何将ABCD选项全部设置对齐&#xff1f;除了一直按空格或者Tab键以外&#xff0c;还有其他方法吗&#xff1f;今天分享如何将ABCD选项对齐。 首先&#xff0c;我们打开【替换和查找】&#xff0c;在查找内容输入空格&#xff0c;然后点击全部…

基于野狗算法的无人机航迹规划-附代码

基于野狗算法的无人机航迹规划 文章目录 基于野狗算法的无人机航迹规划1.野狗搜索算法2.无人机飞行环境建模3.无人机航迹规划建模4.实验结果4.1地图创建4.2 航迹规划 5.参考文献6.Matlab代码 摘要&#xff1a;本文主要介绍利用野狗算法来优化无人机航迹规划。 1.野狗搜索算法 …

微信小程序:实现多个按钮提交表单

效果 核心步骤 通过data-type给不同按钮进行设置&#xff0c;便于很好的区分不同按钮执行不同功能 data-type"" 完整代码 wxml <form action"" bindsubmit"formSubmit"><button style"margin-bottom:5%" data-type"pa…

​C++内存模型

c语言分区:栈、堆、全局/静态存储区、常量存储区、代码区(.text段)、自由存储区 1、栈区&#xff08;stack&#xff09;— 由编译器自动分配释放&#xff0c;存放函数的参数值&#xff0c;局部变量的值等。其操作方式类似于数据结构中的栈。向下生长 2、堆区&#xff08;heap&…

超声波俱乐部分享:百度世界大会点燃AI创业者新希望

10月22日&#xff0c;2023年第十三期超声波俱乐部内部分享会在北京望京举行。本期的主题是&#xff1a;百度世界大会点燃AI创业者新希望。 到场的嘉宾有&#xff1a;超声波创始人杨子超&#xff0c;超声波联合创始人、和牛商业创始人刘思雨&#xff0c;中国国际经济交流中心研…

MFC String类的初始化学习

之前写过CString的用法&#xff1b; VC CString 编程实例图解_bcbobo21cn, cstring-CSDN博客 下面单独看一下CString的各种初始化方式&#xff1b; void CTest2View::OnDraw(CDC* pDC) {CTest2Doc* pDoc GetDocument();ASSERT_VALID(pDoc);// TODO: add draw code for nati…

vscode1.83远程连接失败

&#xff08;报错信息忘记截图了 总之卡在vscode-server.tar.gz的下载那里&#xff0c;一直404&#xff0c;删了C:\Users\Administrator\.ssh\known_hosts也不管用 看了一下vscode1.83的commitID为a6606b6ca720bca780c2d3c9d4cc3966ff2eca12&#xff0c;网友说可以通过以下网…

C++笔记之动态数组的申请和手动实现一个简单的vector

C笔记之动态数组的申请和手动实现一个简单的vector code review! 文章目录 C笔记之动态数组的申请和手动实现一个简单的vector1.C语言中动态数组的申请与使用1.动态数组的申请使用new和delete使用std::vector 1.std::vector的底层实现2.手动实现一个简单的vector:使用一个指向…

Java设计模式之观察者模式

目录 定义 结构 案例 优点 缺点 使用场景 JDK源码解析 定义 又被称为发布-订阅&#xff08;Publish/Subscribe&#xff09;模式&#xff0c;它定义了一种一对多的依赖关系&#xff0c;让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时&#xff0c;会…

iSlide2024一款基于PPT的插件工具包含38个设计辅助功能

根据使用者情况表明iSlide 是一款拥有30W素材的PPT高效设计软件&#xff0c;可提高90%工作效率&#xff0c;现全球已有超过1400万使用者&#xff0c;智能排版原创高品模板可商用图形&#xff0c;真正摆脱PPT的束缚&#xff0c;把精力用在该用的地方。我们都明白islide插件功能特…

PYTHON学习

元组不可修改&#xff1a; 元组支持下标索引。 字符串也是容器&#xff0c;不支持修改。

selenium自动化测试入门 —— 设置等待时间

time.sleep(3) 固定等待3秒 driver.implicitly_wait(10) 隐性的等待&#xff0c;对应全局 WebDriverWait( driver, timeout).until(‘有返回值的__call__()方法或函数’) 显性的等待&#xff0c;对应到元素 一、time.sleep(seconds) 固定等待 import time time.sleep(3) #…

Gopro hero5运动相机格式化后恢复案例

Gopro运动相机以稳定著称&#xff0c;旗下的Hero系列销售全球。下面我们来看一个Hero5格式化后拍了少量素材的恢复案例。 故障存储:64G MicroSD卡 Exfat文件系统 故障现象: 64G的卡没备份数据时做了格式化操作又拍了一条&#xff0c;发现数据没有备份&#xff0c;客户自行使…

Pytorch 文本情感分类案例

一共六个脚本,分别是: ①generateDictionary.py用于生成词典 ②datasets.py定义了数据集加载的方法 ③models.py定义了网络模型 ④configs.py配置一些参数 ⑤run_train.py训练模型 ⑥run_test.py测试模型 数据集https://download.csdn.net/download/Victor_Li_/88486959?spm1…