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

作者:Harry技术2025.01.10 08:04浏览量:4

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

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

[TOC]

前言

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

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

使用方法

  1. JoinPoint
    • 获取方法调用时传入的参数:使用Object[] args = joinPoint.getArgs();
    • 获取被通知的目标对象:使用Object target = joinPoint.getTarget();
    • 获取代理方法的信息:使用MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    • 获取增强方法中的返回值:在@AfterReturning注解的方法中使用Object ret参数。
    • 获取增强方法中的异常对象:在@AfterThrowing注解的方法中使用Throwable e参数。
  2. 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技术中已经完成整合,后续会不断拓展新的功能,如邮件发送,公众号整合,大屏展示等

<h4 align="center">基于SpringBoot3+Vue3前后端分离的Java快速开发框架</h4>

<p align="center">
<a href="https://gitee.com/harry-tech/harry/stargazers"><img src="https://gitee.com/harry-tech/harry/badge/star.svg?theme=gvp"></a>
<a href="https://gitee.com/harry-tech/harry"><img src="https://img.shields.io/badge/harry-v2.0.0-brightgreen.svg"></a>
<a href="https://gitee.com/harry-tech/harry/blob/master/LICENSE"><img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a>
</p>

平台简介

基于 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技术”,关注我,带你看不一样的人间烟火!