在应用开发中,日志记录对于监控、调试和追踪用户行为至关重要。Spring Boot 虽然内置了强大的日志框架,但在某些情况下,我们可能需要更细粒度的日志管理。Spring AOP 提供了一种灵活的方式来实现方法级别的日志记录,而无需侵入业务代码。本文将介绍如何通过 Spring AOP 切面来实现这一功能。
什么是Spring AOP?
Spring AOP 是一个面向切面的编程(AOP)框架,它允许开发者将横切关注点(如日志记录、事务管理等)与业务逻辑分离。通过使用 Spring AOP,我们可以在不修改业务代码的情况下,为应用程序添加日志记录功能。
Spring AOP 详细可以看这篇文章 Spring AOP入门:为初学者准备的指南-优快云博客
日志切面的设计
在我们的示例中,我们定义了一个 SysLog
注解,用于标记需要记录日志的方法。接着,我们创建了一个日志切面 LogAspect
,它会拦截所有带有 SysLog
注解的方法,并记录日志信息。
SysLog
注解
首先,我们定义了一个 SysLog
注解,它可以用来标记需要记录日志的方法:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
String value() default "";
}
这个注解非常简单,它只有一个 value
属性,用于存储日志的描述信息。
日志切面 LogAspect
接下来,我们创建了 LogAspect
切面类,它会拦截所有带有 SysLog
注解的方法:
@Aspect
@Component
public class LogAspect {
// 省略其他成员变量和方法
@Around("@annotation(sysLog)")
public Object logAround(ProceedingJoinPoint joinPoint, SysLog sysLog) throws Throwable {
// 日志记录逻辑
}
}
在这个切面中,我们使用了 @Around
注解来定义一个环绕通知,它会在目标方法执行前后记录日志信息。
日志记录的实现
在 LogAspect
切面类中,我们实现了 logAround
方法,它会在目标方法执行前后记录日志信息:
@Around("@annotation(sysLog)")
public Object logAround(ProceedingJoinPoint joinPoint, SysLog sysLog) throws Throwable {
Log log = new Log();
log.setTimestamp(new Date());
log.setDescription(sysLog.value());
log.setMethodName(joinPoint.getSignature().getName());
log.setParameters(Arrays.toString(joinPoint.getArgs()));
try {
Object result = joinPoint.proceed();
log.setLevel("INFO");
log.setMessage("方法 " + joinPoint.getSignature().getName() + " 执行成功。");
return result;
} catch (Exception e) {
log.setLevel("ERROR");
log.setMessage("方法 " + joinPoint.getSignature().getName() + " 执行过程中发生异常。");
log.setException(e.toString());
throw e;
} finally {
logService.save(log);
}
}
在这个环绕通知中,我们首先创建了一个 Log
对象,并设置了日志的基本信息,如时间戳、描述、方法名和参数。然后,我们执行目标方法,并根据执行结果记录日志信息。如果目标方法执行过程中发生异常,我们会记录异常信息。
测试
完整代码
Maven依赖
在 pom.xml
文件中添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
SQL脚本
创建日志表的 SQL 脚本:
DROP TABLE IF EXISTS `logs`;
CREATE TABLE `logs` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '描述',
`level` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '日志级别',
`timestamp` datetime NOT NULL COMMENT '时间戳',
`message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '日志消息',
`class_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '类名',
`method_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '方法名',
`parameters` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '参数',
`user_identifier` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户标识',
`exception` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '异常信息',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '日志表' ROW_FORMAT = DYNAMIC;
日志实体类 Log
我们定义了一个 Log
实体类,用于存储日志信息:
/**
* 日志实体类
*
* @author 王闻薪
*/
@Data
@TableName("logs")
public class Log implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 描述
*/
@TableField("description")
private String description;
/**
* 日志级别
*/
@TableField("level")
private String level;
/**
* 时间戳
*/
@TableField("timestamp")
private Date timestamp;
/**
* 日志消息
*/
@TableField("message")
private String message;
/**
* 类名
*/
@TableField("class_name")
private String className;
/**
* 方法名
*/
@TableField("method_name")
private String methodName;
/**
* 参数
*/
@TableField("parameters")
private String parameters;
/**
* 用户标识
*/
@TableField("user_identifier")
private String userIdentifier;
/**
* 异常信息
*/
@TableField("exception")
private String exception;
}
这个实体类映射到数据库中的 logs
表,用于存储日志的详细信息。
切面代码
日志切面类 LogAspect
:
package org.example.demo.aspectj;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.example.demo.annotation.SysLog;
import org.example.demo.entity.Log;
import org.example.demo.service.ILogService;
import org.example.demo.utils.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Date;
/**
* 日志切面类
* 用于拦截标注有@SysLog注解的方法,并记录日志信息。
*
* @author 王闻薪
*/
@Slf4j
@Aspect // 标记为切面类
@Component // 将此切面类交给Spring管理
public class LogAspect {
@Autowired
private ILogService logService;
@Autowired
private TokenService tokenService;
/**
* 环绕通知
* 拦截带有@SysLog注解的方法,并在方法执行前后进行日志记录。
*
* @param joinPoint 连接点对象,包含方法的调用信息。
* @param sysLog 注解对象,包含日志描述信息。
* @return 方法执行结果。
* @throws Throwable 可能抛出的异常。
*/
@Around("@annotation(sysLog)")
public Object logAround(ProceedingJoinPoint joinPoint, SysLog sysLog) throws Throwable {
// 创建日志对象
Log log = new Log();
log.setLevel("INFO");
log.setTimestamp(new Date());
log.setUserIdentifier(getUserId().toString());
log.setDescription(sysLog.value());
// 记录方法调用前的日志
logBefore(joinPoint, log);
try {
// 继续执行目标方法
Object result = joinPoint.proceed();
// 记录方法调用后的日志
logAfter(joinPoint, result, log);
log.setMessage("方法 " + joinPoint.getSignature().getName() + " 执行成功。");
return result;
} catch (Exception e) {
// 记录异常日志
logException(joinPoint, e, log);
log.setLevel("ERROR");
log.setMessage("方法 " + joinPoint.getSignature().getName() + " 执行过程中发生异常。");
throw e; // 重新抛出异常
} finally {
// 保存日志到数据库
logService.save(log);
}
}
/**
* 记录方法调用前的日志信息。
*
* @param joinPoint 连接点对象,包含方法的调用信息。
* @param log 日志对象,用于存储日志信息。
*/
private void logBefore(ProceedingJoinPoint joinPoint, Log log) {
log.setClassName(joinPoint.getSignature().getDeclaringTypeName());
log.setMethodName(joinPoint.getSignature().getName());
log.setParameters(Arrays.toString(joinPoint.getArgs()));
log.setTimestamp(new Date()); // 设置日志时间
}
/**
* 记录方法调用后的日志信息。
*
* @param joinPoint 连接点对象,包含方法的调用信息。
* @param result 方法执行结果。
* @param log 日志对象,用于存储日志信息。
*/
private void logAfter(ProceedingJoinPoint joinPoint, Object result, Log log) {
log.setMessage(log.getMessage() + result);
}
/**
* 记录方法执行过程中的异常信息。
*
* @param joinPoint 连接点对象,包含方法的调用信息。
* @param e 捕获的异常对象。
* @param log 日志对象,用于存储日志信息。
*/
private void logException(ProceedingJoinPoint joinPoint, Exception e, Log log) {
log.setException(e.toString());
log.setMessage(log.getMessage() + " 异常信息:" + e.getMessage());
}
/**
* 获取当前用户ID。
*
* @return 用户ID。
*/
private Long getUserId() {
return tokenService.getUserId();
}
}
结论
通过使用 Spring AOP 和注解,我们可以灵活地为 Spring Boot 应用程序添加日志记录功能,而无需修改业务逻辑代码。这不仅提高了代码的可维护性,还使得日志记录变得更加方便和强大。