Spring Boot 中实现自定义注解记录接口日志功能

请添加图片描述

👨🏻‍💻 热爱摄影的程序员
👨🏻‍🎨 喜欢编码的设计师
🧕🏻 擅长设计的剪辑师
🧑🏻‍🏫 一位高冷无情的全栈工程师
欢迎分享 / 收藏 / 赞 / 在看!

【需求】

在 Spring Boot 项目中,我们经常需要记录接口的日志,比如请求参数、响应结果、请求时间、响应时间等。为了方便统一处理,我们可以通过自定义注解的方式来实现接口日志的记录。

【解决】

👉 项目地址:Gitee-ControllerLog

  1. 创建自定义注解 Log.java,用于标记需要记录日志的接口。

注解本质上是一个接口,它继承自 java.lang.annotation.Annotation。但是,你通常不需要显式地继承这个接口,因为当使用 @interface 关键字时,编译器会自动为你处理。

  • @Target 注解用于指定注解可以应用的 Java 元素类型,比如类、方法、字段等。详细的元素类型可以参考 ElementType 枚举类。
  • @Retention 注解用于指定注解的生命周期,有三个值可选:SOURCE、CLASS 和 RUNTIME。
    • SOURCE 表示注解仅存在于源代码中,编译时会被忽略;
    • CLASS 表示注解会被编译到 class 文件中,但在运行时会被忽略;
    • RUNTIME 表示注解会被编译到 class 文件中,并在运行时保留,因此可以通过反射读取注解信息。
  • @Documented 注解用于指定注解是否包含在 JavaDoc 中。
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {/*** 模块名称*/String title() default "";/*** 功能*/BusinessType businessType() default BusinessType.OTHER;/*** 操作人类别*/OperatorType operatorType() default OperatorType.MANAGE;/*** 是否保存请求的参数*/boolean isSaveRequestData() default true;/*** 是否保存响应的参数*/boolean isSaveResponseData() default true;/*** 排除指定的请求参数*/String[] excludeParamNames() default {};
}
  1. 创建操作日志记录处理切面 LogAspect.java,用于处理接口日志的记录。

处理注解通常涉及编写一个能够识别并响应注解的组件。这可以通过多种方式实现,比如使用 Spring AOP、自定义 BeanPostProcessor、或直接在配置类中通过反射读取注解信息。

如果你希望在不修改业务代码的情况下,通过切面(Aspect)来增强被注解的方法,可以使用 Spring AOP。

  • @Aspect 注解用于定义一个切面,它包含切入点和通知。
  • @Before 注解用于指定一个前置通知,在目标方法执行之前执行。
  • @AfterReturning 注解用于指定一个返回通知,在目标方法正常返回后执行。
  • @AfterThrowing 注解用于指定一个异常通知,在目标方法抛出异常后执行。
  • @Around 注解用于指定一个环绕通知,可以在目标方法执行前后执行自定义的行为。
  • @Pointcut 注解用于定义一个切入点,可以在多个通知中共用。
  • @AutoConfiguration 注解用于自动配置 Bean。
@Slf4j
@Aspect
@AutoConfiguration
public class LogAspect {/*** 排除敏感属性字段*/public static final String[] EXCLUDE_PROPERTIES = {"password", "oldPassword", "newPassword", "confirmPassword"};/*** 计时 key*/private static final ThreadLocal<StopWatch> KEY_CACHE = new ThreadLocal<>();/*** 处理请求前执行** @param joinPoint     切点* @param controllerLog 注解*/@Before(value = "@annotation(controllerLog)")public void doBefore(JoinPoint joinPoint, Log controllerLog) {StopWatch stopWatch = new StopWatch();KEY_CACHE.set(stopWatch);stopWatch.start();}/*** 处理请求后执行** @param joinPoint     切点* @param controllerLog 注解* @param jsonResult    返回结果*/@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {handleLog(joinPoint, controllerLog, null, jsonResult);}/*** 处理日志,持久化到数据库** @param joinPoint     切点* @param controllerLog 注解* @param e             异常* @param jsonResult    返回结果*/protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {try {Logs operLog = new Logs();operLog.setStatus(BusinessStatus.SUCCESS.ordinal());// 请求的地址String ip = ServletUtils.getClientIP();operLog.setOperIp(ip);operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));if (e != null) {operLog.setStatus(BusinessStatus.FAIL.ordinal());operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));}// 设置方法名称String className = joinPoint.getTarget().getClass().getName();String methodName = joinPoint.getSignature().getName();operLog.setMethod(className + "." + methodName + "()");// 设置请求方式operLog.setRequestMethod(ServletUtils.getRequest().getMethod());// 处理设置注解上的参数getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);// 设置消耗时间StopWatch stopWatch = KEY_CACHE.get();stopWatch.stop();operLog.setCostTime(stopWatch.getTime());// 发布事件保存数据库SpringUtils.context().publishEvent(operLog);} catch (Exception exp) {// 记录本地异常日志log.error("异常信息:{}", exp.getMessage());exp.printStackTrace();} finally {KEY_CACHE.remove();}}/*** 获取注解中对方法的描述信息 用于Controller层注解** @param joinPoint  切点* @param log        注解* @param operLog    操作日志* @param jsonResult 返回结果* @throws Exception 异常*/public void getControllerMethodDescription(JoinPoint joinPoint, Log log, Logs operLog, Object jsonResult) throws Exception {// 设置action动作operLog.setBusinessType(log.businessType().ordinal());// 设置标题operLog.setTitle(log.title());// 设置操作人类别operLog.setOperatorType(log.operatorType().ordinal());// 是否需要保存request,参数和值if (log.isSaveRequestData()) {// 获取参数的信息,传入到数据库中。setRequestValue(joinPoint, operLog, log.excludeParamNames());}// 是否需要保存response,参数和值if (log.isSaveResponseData() && ObjectUtil.isNotNull(jsonResult)) {operLog.setJsonResult(StringUtils.substring(JsonUtils.toJsonString(jsonResult), 0, 2000));}}/*** 获取请求的参数,放到log中** @param joinPoint         切点* @param operLog           操作日志* @param excludeParamNames 排除字段* @throws Exception 异常*/private void setRequestValue(JoinPoint joinPoint, Logs operLog, String[] excludeParamNames) throws Exception {Map<String, String> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());String requestMethod = operLog.getRequestMethod();if (MapUtil.isEmpty(paramsMap) && HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);operLog.setOperParam(StringUtils.substring(params, 0, 2000));} else {MapUtil.removeAny(paramsMap, EXCLUDE_PROPERTIES);MapUtil.removeAny(paramsMap, excludeParamNames);operLog.setOperParam(StringUtils.substring(JsonUtils.toJsonString(paramsMap), 0, 2000));}}/*** 参数拼装** @param paramsArray       参数数组* @param excludeParamNames 排除字段* @return 操作参数*/private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {StringJoiner params = new StringJoiner(" ");if (ArrayUtil.isEmpty(paramsArray)) {return params.toString();}for (Object o : paramsArray) {if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {String str = JsonUtils.toJsonString(o);Dict dict = JsonUtils.parseMap(str);if (MapUtil.isNotEmpty(dict)) {MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);MapUtil.removeAny(dict, excludeParamNames);str = JsonUtils.toJsonString(dict);}params.add(str);}}return params.toString();}/*** 判断是否需要过滤** @param o 对象* @return 是否过滤*/@SuppressWarnings("rawtypes")public boolean isFilterObject(final Object o) {Class<?> clazz = o.getClass();if (clazz.isArray()) {return MultipartFile.class.isAssignableFrom(clazz.getComponentType());} else if (Collection.class.isAssignableFrom(clazz)) {Collection collection = (Collection) o;for (Object value : collection) {return value instanceof MultipartFile;}} else if (Map.class.isAssignableFrom(clazz)) {Map map = (Map) o;for (Object value : map.values()) {return value instanceof MultipartFile;}}return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse || o instanceof BindingResult;}
}

注意,在处理日志,持久化到数据库时,使用了 SpringUtils.context().publishEvent(operLog); 发布事件保存数据库,即 Spring 的事件发布机制来异步地保存操作日志到数据库。

当这个事件被发布后,Spring 会寻找所有注册的并且能处理这个事件的监听器。这些监听器通常会实现 ApplicationListener 接口,并且在其 onApplicationEvent 方法中处理这个事件。

这种方式的好处是,事件的发布和处理是异步的,不会阻塞主线程的执行,从而提高了程序的性能。

  • @Async 注解用于指定异步方法,被注解的方法会在新的线程中执行。
  • @EventListener 注解用于指定一个事件监听器,它会监听所有被 Spring 发布的事件。
@Async
@EventListener
public void recordLogs(Logs logs) {// 将日志保存到数据库insertLogs(logs);
}
  1. 在需要记录日志的接口上添加 @Log 注解。

定义好注解后,你可以在任何符合 @Target 元注解指定的 Java 元素上使用它。

@Log(title = "小程序管理员", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(SysAdministratorBo bo, HttpServletResponse response) {List<SysAdministratorVo> list = sysAdministratorService.queryList(bo);ExcelUtil.exportExcel(list, "小程序管理员", SysAdministratorVo.class, response);
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【超详细实操内容】django的身份验证系统之限制用户访问的三种方式

目录 1、使用request.user.is_authenticated属性 2、装饰器login_required 3、LoginRequiredMixin类 通常情况下,网站都会对用户限制访问,例如,未登录的用户不可访问用户中心页面。Django框架中使用request.user.isauthenticated属性、装饰器loginrequired和LoginRequire…

scss配置全局变量报错[sass] Can‘t find stylesheet to import.

路径没有错误&#xff0c;使用别名即可 后又提示Deprecation Warning: Sass import rules are deprecated and will be removed in Dart Sass 3.0.0. 将import改为use 使用时在$前添加全局变量所在文件&#xff0c;即variable.

【Java 基础】-- TreeMap 和HashMap 的内存开销情况

在相同数据量的情况下&#xff0c;Java TreeMap 和 HashMap 的内存开销有所不同&#xff0c;这是由于它们底层数据结构的差异导致的。以下是两者内存开销的分析与比较&#xff1a; 1. 底层数据结构差异 1.1 HashMap HashMap 是基于哈希表实现的。每个键值对存储在一个桶中&am…

微信小程序实现二维码海报保存分享功能

首先在写这个二维码分享海报的时候试过很多方法&#xff0c;比如&#xff1a;canvas中的这个createCanvasContext创建上下文的方法&#xff0c;去网上一搜就是一大堆&#xff0c;但其实这个方法已经被废弃了。Canvas 实例&#xff0c;可通过 SelectorQuery 获取。这是绘制背景图…

基于Qlearning强化学习的机器人路线规划matlab仿真

目录 1.算法仿真效果 2.算法涉及理论知识概要 3.MATLAB核心程序 4.完整算法代码文件获得 1.算法仿真效果 matlab2022a仿真结果如下&#xff08;完整代码运行后无水印&#xff09;&#xff1a; 训练过程 测试结果 仿真操作步骤可参考程序配套的操作视频。 2.算法涉及理论…

9 RCC使用HSE、HSI配置时钟

一、时钟树 RCC&#xff1a;reset clock control,复位和时钟控制器。HSE是外部的高速时钟信号&#xff0c;可以由有源晶振或者无源晶振提供。如果使用HSE或者HSE经过PLL倍频之后的时钟作为系统时钟SYSCLK&#xff0c;当HSE故障时候&#xff0c;不仅HSE会被关闭&#xff0c;PLL…

认识数据结构之——排序

一、 插入排序&#xff1a; 直接插入排序(以排升序为例)&#xff1a; 排序思想&#xff1a; 单趟&#xff1a;记录某个位置的值&#xff0c;一个一个和前面的值比较&#xff0c;碰到更大的就往后覆盖&#xff0c;碰到更小的或者相等的就结束&#xff0c;最后将记录的值插入到…

uniapp 微信小程序 功能入口

单行单独展示 效果图 html <view class"shopchoose flex jsb ac" click"routerTo(要跳转的页面)"><view class"flex ac"><image src"/static/dyd.png" mode"aspectFit" class"shopchooseimg"&g…

【集合】Java 8 - Stream API 17种常用操作与案例详解

文章目录 Java8 Stream API 17种常用操作与案例详解1. collect()&#xff1a;将流中的元素收集到集合中2. filter()&#xff1a;根据条件过滤流中的元素3. map()&#xff1a;元素映射为另一个值4. forEach()&#xff1a;对流中的元素执行操作5. flatMap()&#xff1a;将流中的元…

加入SFTP 用户------(小白篇 2)

以下是为 SFTP 添加一个新用户&#xff08;例如 newuser&#xff09;的完整步骤&#xff1a; 1.服务器上加入SFTP------(小白篇 1) 2.加入SFTP 用户------(小白篇 2) 3.代码加入SFTP JAVA —&#xff08;小白篇3&#xff09; 4.代码加入SFTP Python —&#xff08;小白篇4…

苍穹外卖-day05redis 缓存的学习

苍穹外卖-day05 课程内容 Redis入门Redis数据类型Redis常用命令在Java中操作Redis店铺营业状态设置 学习目标 了解Redis的作用和安装过程 掌握Redis常用的数据类型 掌握Redis常用命令的使用 能够使用Spring Data Redis相关API操作Redis 能够开发店铺营业状态功能代码 功能实…

Linux之系统管理

一、相关命令 筛选 grep&#xff0c;可以用来进行筛选&#xff0c;例如对目录筛选课写成 # 过滤出带serv的 ls /usr/sbin | grep serv2. 对服务的操作 2.1 centos6版本 service 服务名 start|stop|restart|status # start&#xff1a;开启 # stop&#xff1a;停止 # restart…

什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap

在刚刚过去的 FlutterInProduction 活动里&#xff0c;Flutter 官方除了介绍「历史进程」和「用户案例」之外&#xff0c;也着重提及了未来相关的 roadmap &#xff0c;其中就有 3.27 里的 Swift Package Manager 、 Widget 实时预览 和 Dart 与 native 平台原生语言直接互操作…

vue.js滑动到顶便锁定位置

<template><div><div class"nav"></div><div class"searchBar" id"searchBar"><ul :class"searchBarFixed true ? isFixed :"> <li>区域<i class"iconfont icon-jiantouxia"…

Numpy基本操作

目录 1、生成数组的方法 1.1、生成0和1的数组 1.2、从现有数组生成 1.2.1、生成方式 1.3、生成固定范围的数组 1.4、生成随机数组 1.4.1、使用模块介绍 1.4.2、均匀分布 1.4.3、正态分布 1.4.4、正态分布创建方式 1、生成数组的方法 1.1、生成0和1的数组 import numpy…

Unity录屏插件-使用Recorder录制视频

目录 1.Recorder的下载 2.Recorder面板 2.1常规录制属性 2.2录制器配置 2.2.1添加录制器 2.2.2配置Input属性 2.2.3配置 Output Format 属性 2.2.4配置 Output File 属性 3.Recorder的使用 3.1录制Game View视频 3.1.1Recorder配置与场景搭建 3.1.2开始录制 3.1.3…

Android Vendor Overlay机制

背景介绍&#xff1a; 看Android 15版本更新时&#xff0c;"Android 15 deprecates vendor overlay"。 猜想这个vendor overlay是之前用过的settings overlay&#xff0c; 不过具体是怎么回事呢&#xff1f; 目录 Vendor Overlay介绍 Vendor Overlay工作原理 Ven…

Python 绘图魔法:用turtle库开启你的编程艺术之旅

&#x1f3e0;大家好&#xff0c;我是Yui_&#xff0c;目标成为全栈工程师~&#x1f4ac; &#x1f351;如果文章知识点有错误的地方&#xff0c;请指正&#xff01;和大家一起学习&#xff0c;一起进步&#x1f440; &#x1f680;如有不懂&#xff0c;可以随时向我提问&#…

AI开发:使用支持向量机(SVM)进行文本情感分析训练 - Python

支持向量机是AI开发中最常见的一种算法。之前我们已经一起初步了解了它的概念和应用&#xff0c;今天我们用它来进行一次文本情感分析训练。 一、概念温习 支持向量机&#xff08;SVM&#xff09;是一种监督学习算法&#xff0c;广泛用于分类和回归问题。 它的核心思想是通过…

.net core在linux导出excel,System.Drawing.Common is not supported on this platform

使用框架 .NET7 导出组件 Aspose.Cells for .NET 5.3.1 asp.net core mvc 如果使用Aspose.Cells导出excel时&#xff0c;报错 &#xff1a; System.Drawing.Common is not supported on this platform 平台特定实现&#xff1a; 对于Windows平台&#xff0c;System.Drawing.C…