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

news/2024/12/24 8:42:55/

请添加图片描述

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

【需求】

在 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 中。
java">@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。
java">@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 发布的事件。
java">@Async
@EventListener
public void recordLogs(Logs logs) {// 将日志保存到数据库insertLogs(logs);
}
  1. 在需要记录日志的接口上添加 @Log 注解。

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

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.ppmy.cn/news/1557696.html

相关文章

Suno Api V4模型无水印开发「高清音频WAV下载」 —— 「Suno Api系列」第6篇

历史文章 Suno AI API接入 - 将AI音乐接入到自己的产品中&#xff0c;支持120并发任务 Suno Api V4模型无水印开发「灵感模式」 —— 「Suno Api系列」第1篇 Suno Api V4模型无水印开发「自定义模式」 —— 「Suno Api系列」第2篇 Suno Api V4模型无水印开发「AI生成歌词」…

git企业开发的相关理论(二)

目录 git企业开发的相关理论&#xff08;一&#xff09; 八.修改文件 九.版本回退 十.撤销修改 情况一(还没有add) 情况二(add后还没有commit) 情况三(commit后还没有push) 十一.删除本地仓库中的文件 方法一 方法二 十二.理解分支 1.常见的分支工作流程 2.合并冲…

第79期 | GPTSecurity周报

GPTSecurity是一个涵盖了前沿学术研究和实践经验分享的社区&#xff0c;集成了生成预训练Transformer&#xff08;GPT&#xff09;、人工智能生成内容&#xff08;AIGC&#xff09;以及大语言模型&#xff08;LLM&#xff09;等安全领域应用的知识。在这里&#xff0c;您可以找…

WebRTC服务质量(09)- Pacer机制(01) 流程概述

一、前言&#xff1a; Pacer 是一种数据发送调度机制。它的主要功能是根据网络带宽限制、网络拥塞控制的反馈以及媒体的发送策略&#xff0c;对数据包的发送进行适配和节奏调度&#xff0c;以避免网络拥塞、减少丢包并保证流媒体传输的平滑性。 二、核心概念&#xff1a; 2.…

每天40分玩转Django:Django文件上传

Django文件上传 一、今日学习内容概述 学习模块重要程度主要内容基础文件上传⭐⭐⭐⭐⭐文件字段、基本配置自定义存储⭐⭐⭐⭐⭐存储后端、云存储集成文件处理⭐⭐⭐⭐图片处理、文件验证异步上传⭐⭐⭐⭐AJAX上传、进度显示 二、模型和表单设计 # models.py from django.…

Android OpenGLES2.0开发(十):FBO离屏渲染

人生是一场单程的旅行&#xff0c;即使有些遗憾我们也没有从头再来的机会&#xff0c;与其纠结无法改变的过去不如微笑着珍惜未来。 Android OpenGLES开发&#xff1a;EGL环境搭建Android OpenGLES2.0开发&#xff08;一&#xff09;&#xff1a;艰难的开始Android OpenGLES2.0…

面向对象 类函数的区别 实例方法 类方法 静态方法 抽象方法

前言&#xff1a;面向对象类方法的说明&#xff1a; 实例方法 定义&#xff1a;实例方法是在类中定义的&#xff0c;用于操作类的实例&#xff08;对象&#xff09;的属性和行为的方法。它的第一个参数通常是self&#xff08;在 Python 中&#xff09;或this&#xff08;在 Jav…

#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍06-基于子查询的SQL注入(Subquery-Based SQL Injection)

免责声明 本教程仅为合法的教学目的而准备&#xff0c;严禁用于任何形式的违法犯罪活动及其他商业行为&#xff0c;在使用本教程前&#xff0c;您应确保该行为符合当地的法律法规&#xff0c;继续阅读即表示您需自行承担所有操作的后果&#xff0c;如有异议&#xff0c;请立即停…