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日志并进行多维度分析。
目录
相关文章
|
2月前
|
Java 中间件
SpringBoot入门(6)- 添加Logback日志
SpringBoot入门(6)- 添加Logback日志
102 5
|
14天前
|
缓存 前端开发 Java
【Spring】——SpringBoot项目创建
SpringBoot项目创建,SpringBootApplication启动类,target文件,web服务器,tomcat,访问服务器
|
2月前
|
监控 Java 数据库连接
详解Spring Batch:在Spring Boot中实现高效批处理
详解Spring Batch:在Spring Boot中实现高效批处理
255 12
|
2月前
|
安全 Java 测试技术
详解Spring Profiles:在Spring Boot中实现环境配置管理
详解Spring Profiles:在Spring Boot中实现环境配置管理
102 10
|
1月前
|
负载均衡 Java 开发者
深入探索Spring Cloud与Spring Boot:构建微服务架构的实践经验
深入探索Spring Cloud与Spring Boot:构建微服务架构的实践经验
144 5
|
2月前
|
Java 中间件
SpringBoot入门(6)- 添加Logback日志
SpringBoot入门(6)- 添加Logback日志
54 1
|
2月前
|
XML Java 数据库连接
SpringBoot集成Flowable:打造强大的工作流管理系统
在企业级应用开发中,工作流管理是一个核心组件,它能够帮助我们定义、执行和管理业务流程。Flowable是一个开源的工作流和业务流程管理(BPM)平台,它提供了强大的工作流引擎和建模工具。结合SpringBoot,我们可以快速构建一个高效、灵活的工作流管理系统。本文将探讨如何将Flowable集成到SpringBoot应用中,并展示其强大的功能。
387 1
|
2月前
|
JavaScript NoSQL Java
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
59 0
|
Java 应用服务中间件
SpringBoot集成使用jsp(超详细)
SpringBoot集成使用jsp(超详细)
SpringBoot集成使用jsp(超详细)
|
3月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,包括版本兼容性、安全性、性能调优等方面。
205 1