Spring Boot 3 集成Spring AOP实现系统日志记录

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 本文介绍了如何在Spring Boot 3中集成Spring AOP实现系统日志记录功能。通过定义`SysLog`注解和配置相应的AOP切面,可以在方法执行前后自动记录日志信息,包括操作的开始时间、结束时间、请求参数、返回结果、异常信息等,并将这些信息保存到数据库中。此外,还使用了`ThreadLocal`变量来存储每个线程独立的日志数据,确保线程安全。文中还展示了项目实战中的部分代码片段,以及基于Spring Boot 3 + Vue 3构建的快速开发框架的简介与内置功能列表。此框架结合了当前主流技术栈,提供了用户管理、权限控制、接口文档自动生成等多项实用特性。

Spring Boot 3 集成Spring AOP实现系统日志记录

前言

在Spring AOP中,JoinPoint和ProceedingJoinPoint都是关键的接口,用于在切面中获取方法的相关信息以及控制方法的执行。它们的主要区别在于它们在AOP通知中的使用方式和功能。

  1. 功能定位
  • JoinPoint:代表了程序执行流程中的一个特定点,如方法的调用、异常的抛出等。JoinPoint主要用于获取连接点的信息,如方法名、参数、目标对象等,但不能控制方法的执行。
  • ProceedingJoinPoint:是JoinPoint的子接口,除了能获取连接点的信息外,还能控制方法的执行。它主要用于环绕通知(Around advice),通过调用proceed()方法来继续执行原始方法。
  1. 应用场景
  • JoinPoint:适用于四大通知类型(前置通知、后置通知、异常通知和最终通知),主要用于记录运行的数据和获取连接点的基本信息。
  • ProceedingJoinPoint:主要用于环绕通知,因为它提供了proceed()方法来继续执行原始方法。在环绕通知中,你可以在执行原始方法前后添加额外的逻辑。

使用方法

  1. JoinPoint
  • 获取方法调用时传入的参数:使用Object[] args = joinPoint.getArgs();
  • 获取被通知的目标对象:使用Object target = joinPoint.getTarget();
  • 获取代理方法的信息:使用MethodSignature signature = (MethodSignature) joinPoint.getSignature();
  • 获取增强方法中的返回值:在@AfterReturning注解的方法中使用Object ret参数。
  • 获取增强方法中的异常对象:在@AfterThrowing注解的方法中使用Throwable e参数。
  1. ProceedingJoinPoint
  • 继续执行被通知的方法:在环绕通知中,使用Object result = joinPoint.proceed();来继续执行原始方法。
  • 获取方法参数和目标对象:使用与JoinPoint相同的方法,如Object[] args = joinPoint.getArgs();Object target = joinPoint.getTarget();
  • 在环绕通知中执行额外逻辑:在调用proceed()方法前后添加任何你需要的逻辑,如性能测量、安全检查等。

示例代码

// JoinPoint 示例

@Before("execution(* com.example.myapp.MyService.*(..))")

public void logBefore(JoinPoint joinPoint) {

   System.out.println("Method " + joinPoint.getSignature().getName() + " called with arguments: " + Arrays.toString(joinPoint.getArgs()));

}

// ProceedingJoinPoint 示例

@Around("execution(* com.example.myapp.MyService.*(..))")

public Object measurePerformance(ProceedingJoinPoint joinPoint) throws Throwable {

   long start = System.currentTimeMillis();

   Object result = joinPoint.proceed(); // 继续执行原始方法

   long executionTime = System.currentTimeMillis() - start;

   System.out.println("Method " + joinPoint.getSignature().getName() + " executed in " + executionTime + "ms");

   return result;

}

ProceedingJoinPoint 不能接收异常通知。在Spring AOP中,异常通知(@AfterThrowing)用于在目标方法抛出异常时执行的通知,它通常接收一个JoinPoint对象来获取连接点的信息,但不能接收ProceedingJoinPoint对象。只有环绕通知(@Around)可以接收ProceedingJoinPoint对象,该对象提供了继续执行原始方法的能力(通过调用proceed()方法),并且可以在方法执行前后添加额外的逻辑,包括异常处理。因此,对于异常通知来说,它只能使用JoinPoint来获取相关信息,而不能使用ProceedingJoinPoint。要统计方法的执行时间,通常需要使用环绕通知(@Around),因为它允许你在方法执行前后都添加逻辑。

然而,我即想要记录异常,又想统计执行时间。我们可以使用JoinPoint来获取相关信息,分别使用 @AfterReturning@AfterThrowing 来统计执行时间,利用 @Pointcut 来定义切点,你需要一种方法来在方法开始时记录开始时间,并在方法结束时(无论是正常结束还是异常结束)记录结束时间。由于 @AfterReturning@AfterThrowing 是独立的通知,它们无法直接共享状态,因此你需要一种机制来在它们之间传递开始时间。一个常见的做法是使用 ThreadLocal 变量来存储开始时间,因为 ThreadLocal 变量对于每个线程都是独立的,所以可以在不同的通知之间共享数据而不会发生冲突。

下面是一个使用 @Pointcut@AfterReturning@AfterThrowing 来统计执行时间的示例,其中使用了 ThreadLocal 来存储开始时间:

/**

* 日志切面

*

* @author harry

* @公众号 Harry技术

*/

@Slf4j

@Aspect

@Component

public class WebLogAspect {

   /**

    * 线程绑定变量,用于记录请求的开始时间

    */

   private static final ThreadLocal<Long> START_TIME_THREAD_LOCAL = new ThreadLocal<>();

   /**

    * 配置织入点

    */

   @Pointcut("@annotation(cn.harry.common.annotation.SysLog)")

   public void logPointCut() {

   }

   @Before("logPointCut()")

   public void recordStartTime(JoinPoint joinPoint) {

       START_TIME_THREAD_LOCAL.set(System.currentTimeMillis());

   }

   /**

    * 处理完请求后执行

    *

    * @param joinPoint 切点

    */

   @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")

   public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {

       // 执行时间

       long time = System.currentTimeMillis() - START_TIME_THREAD_LOCAL.get();

       handleLog(joinPoint, null, jsonResult, time);

       START_TIME_THREAD_LOCAL.remove();

   }

   /**

    * 拦截异常操作

    *

    * @param joinPoint 切点

    * @param e         异常

    */

   @AfterThrowing(value = "logPointCut()", throwing = "e")

   public void doAfterThrowing(JoinPoint joinPoint, Exception e) {

       // 执行时间

       long time = System.currentTimeMillis() - START_TIME_THREAD_LOCAL.get();

       handleLog(joinPoint, e, null, time);

       START_TIME_THREAD_LOCAL.remove();

   }

在这个示例中:

  1. @Pointcut 定义了一个切点表达式 logPointCut(),它匹配 @annotation(cn.harry.common.annotation.SysLog) 注解下的所有方法。
  2. @Before 注解的方法 recordStartTime 在切点匹配的方法执行之前被调用,用于记录开始时间,并将其存储在 ThreadLocal 变量中。
  3. @AfterReturning 注解的方法 doAfterReturning 在切点匹配的方法正常执行后被调用,用于计算并打印执行时间,并从 ThreadLocal 变量中清除开始时间。
  4. @AfterThrowing 注解的方法 doAfterThrowing 在切点匹配的方法抛出异常后被调用,同样用于计算并打印执行时间(包括异常发生前的时间),并从 ThreadLocal 变量中清除开始时间。

请注意,ThreadLocal 变量必须在使用完毕后清除,以避免潜在的内存泄漏和错误的数据共享。在这个示例中,我们在每个通知的末尾都调用了 startTimeThreadLocal.remove() 来清除开始时间。

项目实战

Harry技术为例: https://gitee.com/harry-tech/harry.git

引入依赖:

<dependency>

   <groupId>org.springframework.boot</groupId>

   <artifactId>spring-boot-starter-aop</artifactId>

</dependency>

定义日志表

CREATE TABLE `sys_log` (

 `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',

 `title` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '模块标题',

 `method` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '方法名称',

 `request_method` varchar(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '请求方式',

 `operator_type` varchar(10) COLLATE utf8mb4_general_ci DEFAULT '1' COMMENT '操作类别(0 其它 1 后台用户 2 移动端用户)',

 `username` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '操作人员',

 `url` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '请求URL',

 `ip` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '主机地址',

 `location` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '操作地点',

 `param` varchar(2000) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '请求参数',

 `json_result` varchar(2000) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '返回参数',

 `status` char(1) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '0' COMMENT '操作状态(0正常 1异常)',

 `error_msg` varchar(2000) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '' COMMENT '错误消息',

 `execution_time` bigint DEFAULT NULL COMMENT '执行时间(ms)',

 `browser` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '浏览器',

 `browser_version` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '浏览器版本',

 `os` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '终端系统',

 `create_time` datetime DEFAULT NULL COMMENT '创建时间',

 `valid` int DEFAULT '1' COMMENT '有效状态,0:无效 1:有效',

 PRIMARY KEY (`id`) USING BTREE

) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='系统日志表';

创建SysLog注解

/**

* 系统日志注解

*

* @author harry

* @公众号 Harry技术

*/

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface SysLog {

   /**

    * 模块

    */

   String title() default "";

   /**

    * 功能

    */

   BusinessType businessType() default BusinessType.OTHER;

   /**

    * 是否保存请求的参数

    */

   boolean isSaveRequestData() default true;

}

日志切面

/**

* 日志切面

*

* @author harry

* @公众号 Harry技术

*/

@Slf4j

@Aspect

@Component

public class WebLogAspect {

   /**

    * 线程绑定变量,用于记录请求的开始时间

    */

   private static final ThreadLocal<Long> START_TIME_THREAD_LOCAL = new ThreadLocal<>();

   /**

    * 配置织入点

    */

   @Pointcut("@annotation(cn.harry.common.annotation.SysLog)")

   public void logPointCut() {

   }

   @Before("logPointCut()")

   public void recordStartTime(JoinPoint joinPoint) {

       START_TIME_THREAD_LOCAL.set(System.currentTimeMillis());

   }

   /**

    * 处理完请求后执行

    *

    * @param joinPoint 切点

    */

   @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")

   public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {

       long time = System.currentTimeMillis() - START_TIME_THREAD_LOCAL.get();

       handleLog(joinPoint, null, jsonResult, time);

       START_TIME_THREAD_LOCAL.remove();

   }

   /**

    * 拦截异常操作

    *

    * @param joinPoint 切点

    * @param e         异常

    */

   @AfterThrowing(value = "logPointCut()", throwing = "e")

   public void doAfterThrowing(JoinPoint joinPoint, Exception e) {

       long time = System.currentTimeMillis() - START_TIME_THREAD_LOCAL.get();

       handleLog(joinPoint, e, null, time);

       START_TIME_THREAD_LOCAL.remove();

   }

   protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, Long executionTime) {

       try {

           // 获得注解

           cn.harry.common.annotation.SysLog log = getAnnotationLog(joinPoint);

           if (log == null) {

               return;

           }

           // 获取当前的用户

           SysUser loginUser = SecurityUtils.getSysUser();

           // *========数据库日志=========*//

           SysLog sysLog = new SysLog();

           sysLog.setStatus(StatusEnums.ENABLE.getKey());

           HttpServletRequest request = ServletUtils.getRequest();

           // 请求的IP地址

           String ip = IpUtil.getIp(request);

           sysLog.setIp(ip);

           // 返回参数

           sysLog.setJsonResult(JSONUtil.toJsonStr(jsonResult));

           // 请求URL

           sysLog.setUrl(request.getRequestURI());

           if (loginUser != null) {

               sysLog.setUsername(loginUser.getUsername());

           }

           if (e != null) {

               sysLog.setStatus(StatusEnums.DISABLE.getKey());

               sysLog.setErrorMsg(StrUtil.sub(e.getMessage(), 0, 2000));

           }

           // 设置方法名称

           String className = joinPoint.getTarget().getClass().getName();

           String methodName = joinPoint.getSignature().getName();

           sysLog.setMethod(className + "." + methodName + "()");

           // 设置请求方式

           sysLog.setRequestMethod(ServletUtils.getRequest().getMethod());

           sysLog.setCreateTime(DateUtil.date());

           // 获取浏览器和终端系统信息

           String userAgentString = request.getHeader("User-Agent");

           UserAgent userAgent = UserAgentUtil.parse(userAgentString);

           // 系统信息

           sysLog.setOs(userAgent.getOs().getName());

           // 浏览器信息

           sysLog.setBrowser(userAgent.getBrowser().getName());

           sysLog.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString));

           // 处理设置注解上的参数

           getControllerMethodDescription(joinPoint, log, sysLog);

           // 执行时长

           sysLog.setExecutionTime(executionTime);

           // 保存数据库

           AsyncManager.me().execute(AsyncFactory.logTask(sysLog));

       } catch (Exception exp) {

           // 记录本地异常日志

           log.error("==前置通知异常==");

           log.error("异常信息:{}", exp.getMessage());

       }

   }

   /**

    * 获取注解中对方法的描述信息 用于Controller层注解

    *

    * @param log    日志

    * @param sysLog 操作日志

    */

   public void getControllerMethodDescription(JoinPoint joinPoint, cn.harry.common.annotation.SysLog log, SysLog sysLog) throws Exception {

       // 设置标题

       sysLog.setTitle(log.title());

       // 是否需要保存request,参数和值

       if (log.isSaveRequestData()) {

           // 获取参数的信息,传入到数据库中。

           setRequestValue(joinPoint, sysLog);

       }

   }

   /**

    * 获取请求的参数,放到log中

    *

    * @param sysLog 操作日志

    * @throws Exception 异常

    */

   private void setRequestValue(JoinPoint joinPoint, SysLog sysLog) throws Exception {

       String requestMethod = sysLog.getRequestMethod();

       if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {

           String params = argsArrayToString(joinPoint.getArgs());

           sysLog.setParam(StrUtil.sub(params, 0, 2000));

       } else {

           Map<?, ?> paramsMap = (Map<?, ?>) ServletUtils.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);

           sysLog.setParam(StrUtil.sub(paramsMap.toString(), 0, 2000));

       }

   }

   /**

    * 是否存在注解,如果存在就获取

    */

   private cn.harry.common.annotation.SysLog getAnnotationLog(JoinPoint joinPoint) throws Exception {

       Signature signature = joinPoint.getSignature();

       MethodSignature methodSignature = (MethodSignature) signature;

       Method method = methodSignature.getMethod();

       if (method != null) {

           return method.getAnnotation(cn.harry.common.annotation.SysLog.class);

       }

       return null;

   }

   /**

    * 参数拼装

    */

   private String argsArrayToString(Object[] paramsArray) {

       StringBuilder params = new StringBuilder();

       if (paramsArray != null) {

           for (Object o : paramsArray) {

               if (!isFilterObject(o)) {

                   Object jsonObj = JSONUtil.toJsonStr(o);

                   params.append(jsonObj.toString()).append(" ");

               }

           }

       }

       return params.toString().trim();

   }

   /**

    * 判断是否需要过滤的对象。

    *

    * @param o 对象信息。

    * @return 如果是需要过滤的对象,则返回true;否则返回false。

    */

   public boolean isFilterObject(final Object o) {

       return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse;

   }

}

使用

   @Operation(summary = "更新")

   @PreAuthorize("@ss.hasPermission('sys_user_edit')")

   @SysLog(title = "sys_user", businessType = BusinessType.UPDATE)

   @PutMapping

   public R<Integer> update(@RequestBody SysUser sysUser) {

       return sysUserService.updateUser(sysUser) ? R.success() : R.failed();

   }

操作记录信息

此功能在Harry技术中已经完成整合,后续会不断拓展新的功能,如邮件发送,公众号整合,大屏展示等

基于SpringBoot3+Vue3前后端分离的Java快速开发框架

平台简介

基于 JDK 17、Spring Boot 3、Spring Security 6、JWT、Redis、Mybatis-Plus、Knife4j等构建后端,基于Vue 3、Element-Plus 、TypeScript等构建前端的分离单体权限管理系统。

  • 🚀 开发框架: 使用 Spring Boot 3 和 Vue 3,以及 Element-Plus 等主流技术栈,实时更新。
  • 🔐 安全认证: 结合 Spring Security 和 JWT 提供安全、无状态、分布式友好的身份验证和授权机制。
  • 🔑 权限管理: 基于 RBAC 模型,实现细粒度的权限控制,涵盖接口方法和按钮级别。
  • 🛠️ 功能模块: 包括用户管理、角色管理、菜单管理、部门管理、字典管理等多个功能。
  • 📘 接口文档: 自动生成接口文档,支持在线调试,提高开发效率。

内置功能

  • 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
  • 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
  • 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
  • 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
  • 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
  • 参数管理:对系统动态配置常用参数。
  • 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
  • 登录日志:系统登录日志记录查询包含登录异常。
  • 系统接口:根据业务代码自动生成相关的api接口文档,引入swagger接口文档服务的工具(Knife4j)。
  • 代码生成:完善的代码生成机制.减少基础代码的编写,专注于业务逻辑实现

后端开发

Gitee仓库地址: https://gitee.com/harry-tech/harry.git

前端开发

  • 本项目是前后端分离的,还需要部署前端,才能运行起来

Gitee仓库地址: https://gitee.com/harry-tech/harry-vue.git

效果展示

Watermark 水印

暗黑模式

觉着有帮助,给个Star再走呗 ~~~~

公众号搜“Harry技术”,关注我,带你看不一样的人间烟火!

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
24天前
|
XML Java 开发者
Spring Boot中的AOP实现
Spring AOP(面向切面编程)允许开发者在不修改原有业务逻辑的情况下增强功能,基于代理模式拦截和增强方法调用。Spring Boot通过集成Spring AOP和AspectJ简化了AOP的使用,只需添加依赖并定义切面类。关键概念包括切面、通知和切点。切面类使用`@Aspect`和`@Component`注解标注,通知定义切面行为,切点定义应用位置。Spring Boot自动检测并创建代理对象,支持JDK动态代理和CGLIB代理。通过源码分析可深入了解其实现细节,优化应用功能。
|
13天前
|
监控 Java 应用服务中间件
SpringBoot是如何简化Spring开发的,以及SpringBoot的特性以及源码分析
Spring Boot 通过简化配置、自动配置和嵌入式服务器等特性,大大简化了 Spring 应用的开发过程。它通过提供一系列 `starter` 依赖和开箱即用的默认配置,使开发者能够更专注于业务逻辑而非繁琐的配置。Spring Boot 的自动配置机制和强大的 Actuator 功能进一步提升了开发效率和应用的可维护性。通过对其源码的分析,可以更深入地理解其内部工作机制,从而更好地利用其特性进行开发。
32 6
|
18天前
|
前端开发 JavaScript Java
springboot图书馆管理系统前后端分离版本
springboot图书馆管理系统前后端分离版本
36 12
|
16天前
|
JavaScript NoSQL Java
基于SpringBoot+Vue实现的大学生就业服务平台设计与实现(系统源码+文档+数据库+部署等)
面向大学生毕业选题、开题、任务书、程序设计开发、论文辅导提供一站式服务。主要服务:程序设计开发、代码修改、成品部署、支持定制、论文辅导,助力毕设!
45 6
|
16天前
|
JavaScript Java 测试技术
基于Java+SpringBoot+Vue实现的车辆充电桩系统设计与实现(系统源码+文档+部署讲解等)
面向大学生毕业选题、开题、任务书、程序设计开发、论文辅导提供一站式服务。主要服务:程序设计开发、代码修改、成品部署、支持定制、论文辅导,助力毕设!
41 6
|
16天前
|
JavaScript NoSQL Java
基于SpringBoot+Vue的班级综合测评管理系统设计与实现(系统源码+文档+数据库+部署等)
✌免费选题、功能需求设计、任务书、开题报告、中期检查、程序功能实现、论文辅导、论文降重、答辩PPT辅导、会议视频一对一讲解代码等✌
29 4
|
16天前
|
JavaScript NoSQL Java
基于SpringBoot+Vue实现的大学生体质测试管理系统设计与实现(系统源码+文档+数据库+部署)
面向大学生毕业选题、开题、任务书、程序设计开发、论文辅导提供一站式服务。主要服务:程序设计开发、代码修改、成品部署、支持定制、论文辅导,助力毕设!
30 2
|
26天前
|
缓存 安全 Java
Spring Boot 3 集成 Spring Security + JWT
本文详细介绍了如何使用Spring Boot 3和Spring Security集成JWT,实现前后端分离的安全认证概述了从入门到引入数据库,再到使用JWT的完整流程。列举了项目中用到的关键依赖,如MyBatis-Plus、Hutool等。简要提及了系统配置表、部门表、字典表等表结构。使用Hutool-jwt工具类进行JWT校验。配置忽略路径、禁用CSRF、添加JWT校验过滤器等。实现登录接口,返回token等信息。
250 12
|
24天前
|
开发框架 运维 监控
Spring Boot中的日志框架选择
在Spring Boot开发中,日志管理至关重要。常见的日志框架有Logback、Log4j2、Java Util Logging和Slf4j。选择合适的日志框架需考虑性能、灵活性、社区支持及集成配置。本文以Logback为例,演示了如何记录不同级别的日志消息,并强调合理配置日志框架对提升系统可靠性和开发效率的重要性。
|
16天前
|
JavaScript NoSQL Java
基于SpringBoot+Vue实现的冬奥会科普平台设计与实现(系统源码+文档+数据库+部署)
面向大学生毕业选题、开题、任务书、程序设计开发、论文辅导提供一站式服务。主要服务:程序设计开发、代码修改、成品部署、支持定制、论文辅导,助力毕设!
32 0