本文目录导读:

这是一个很经典的企业级开发问题,我先帮你梳理一下核心思路,然后给出两种主流实现方案的具体代码示例。
核心思路: “记录级注解”通常指的是在方法(Service层或Controller层)上添加一个自定义注解,通过AOP(面向切面编程) 拦截该方法执行,在方法执行前后,利用 SpEL(Spring表达式语言) 解析注解中的参数(如方法参数、返回值),动态拼装出日志内容,最后异步写入数据库。
基于 Spring AOP + SpEL(最推荐,通用性强)
这是最标准的方案,不侵入业务代码,支持动态获取方法参数。
自定义注解
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RecordLog {
// 操作模块(如:用户管理)
String module();
// 操作类型(如:新增、修改、删除)
String type();
// 日志内容描述(支持SpEL表达式)
// "新增了用户:{#user.name}"
String content();
}
AOP 切面实现(核心)
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class RecordLogAspect {
// 用来解析 SpEL 表达式
private ExpressionParser parser = new SpelExpressionParser();
@Around("@annotation(recordLog)")
public Object around(ProceedingJoinPoint joinPoint, RecordLog recordLog) throws Throwable {
Object result = null;
boolean success = true;
String errorMsg = null;
long startTime = System.currentTimeMillis();
try {
result = joinPoint.proceed(); // 执行原方法
} catch (Throwable e) {
success = false;
errorMsg = e.getMessage();
throw e;
} finally {
// 不管成功失败都要记录日志
try {
// 1. 解析 SpEL 并获得最终的日志内容
String content = parseContent(joinPoint, recordLog.content());
// 2. 组装日志对象(这里简化,实际用 Builder/构造方法)
OperationLog log = new OperationLog();
log.setModule(recordLog.module());
log.setType(recordLog.type());
log.setContent(content);
log.setSuccess(success);
log.setErrorMsg(errorMsg);
log.setDuration(System.currentTimeMillis() - startTime);
log.setCreateTime(new Date());
// 3. 异步写入数据库(这里模拟,建议使用 @Async 或 消息队列)
saveLogAsync(log);
} catch (Exception e) {
// 日志记录失败不能影响主业务,仅打印日志
System.err.println("记录操作日志失败: " + e.getMessage());
}
}
return result;
}
/**
* 解析 SpEL 表达式
*/
private String parseContent(ProceedingJoinPoint joinPoint, String expression) {
if (!expression.contains("#")) {
return expression; // 没有 SpEL 表达式,直接返回
}
// 获取方法参数名和参数值
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String[] paramNames = signature.getParameterNames();
Object[] args = joinPoint.getArgs();
// 创建 SpEL 上下文
EvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < paramNames.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
// 解析
return parser.parseExpression(expression).getValue(context, String.class);
}
// 这里简化,实际应该注入一个 Service 并异步调用
private void saveLogAsync(OperationLog log) {
System.out.println("保存日志: " + log);
}
}
使用示例
@Service
public class UserService {
@RecordLog(
module = "用户管理",
type = "新增",
content = "新增用户:{T(java.time.LocalDateTime).now()}, 用户名: {#user.name}"
)
public User createUser(User user) {
// 执行数据库操作...
return user;
}
@RecordLog(
module = "用户管理",
type = "修改",
content = "修改用户 ID: {#id}, 原姓名: {#oldName}, 新姓名: {#user.name}"
)
public User updateUser(Long id, String oldName, User user) {
// 执行更新
return user;
}
}
关键点:
- SpEL 支持
#参数名访问方法参数,甚至支持更复杂的表达式。 - 异步处理:日志写入不能阻塞主流程。
基于函数式接口 + 回调(更灵活,适用于复杂场景)
需要复杂的业务逻辑拼装(比如需要查询数据库、计算多个字段),简单的 SpEL 可能不够,这时可以用函数式接口作为注解的属性。
自定义注解 + 函数式接口
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RecordLogFunctional {
String module();
String type();
// 指定一个实现了 LogContentGenerator 接口的 Bean 名称
String contentGenerator();
}
// 函数式接口:日志内容生成器
@FunctionalInterface
public interface LogContentGenerator {
String generate(ProceedingJoinPoint joinPoint, Object result, Throwable exception);
}
切面实现
@Aspect
@Component
public class RecordLogFunctionalAspect {
@Autowired
private ApplicationContext applicationContext;
@Around("@annotation(recordLog)")
public Object around(ProceedingJoinPoint joinPoint, RecordLogFunctional recordLog) throws Throwable {
// 1. 获取内容生成器
LogContentGenerator generator = applicationContext.getBean(recordLog.contentGenerator(), LogContentGenerator.class);
Object result = null;
Throwable exception = null;
try {
result = joinPoint.proceed();
} catch (Throwable e) {
exception = e;
throw e;
} finally {
// 2. 调用生成器获取日志内容
String content = generator.generate(joinPoint, result, exception);
// 3. 保存日志(同上)
saveLog(recordLog, content, exception);
}
return result;
}
}
具体业务实现
@Component("userCreateLogGenerator")
public class UserCreateLogGenerator implements LogContentGenerator {
@Override
public String generate(ProceedingJoinPoint joinPoint, Object result, Throwable exception) {
// 假设第一个参数是 User
Object[] args = joinPoint.getArgs();
User user = (User) args[0];
// 可以在这里查询其他数据,做复杂拼装
return String.format("新增用户[%s]成功,角色为%s", user.getName(), "管理员");
}
}
使用
@Service
public class UserService {
@RecordLogFunctional(
module = "用户管理",
type = "新增",
contentGenerator = "userCreateLogGenerator"
)
public User createUser(User user) {
// ...
}
}
总结对比
| 特性 | AOP + SpEL | 函数式接口回调 |
|---|---|---|
| 复杂度 | 低,注解即可使用 | 中,需要额外实现 Bean |
| 灵活性 | 高,可动态拼接方法参数 | 极高,可执行任意 Java 代码 |
| 维护成本 | 低,SpEL 表达式写在注解里 | 中,需要维护多个 Generator 类 |
| 适用场景 | 80%的通用场景,CRUD | 复杂业务逻辑,比如需要读取缓存、计算多个摘要 |
- 推荐首选方案一(AOP + SpEL),它实现了“声明式编程”,开发人员只需在方法上加一行注解,即可自动记录日志,是最通用、最标准的实现方式。
- 在方案一无法满足(比如需要复杂计算)时,才考虑方案二作为补充。
进阶优化点(生产环境必看)
- 异步写入:日志保存绝对不能影响主流程,使用
@Async+ 线程池,或者发送到 MQ(消息队列),再由消费端批量入库。 - 避免循环依赖:AOP 切面里不要注入自己切面的 Service,否则会出现循环依赖。
- 性能开销:SpEL 解析有性能损耗,建议在 AOP 内部做结果缓存(如用
@Cacheable),或者对表达式编译结果进行本地缓存(CachedExpressionEvaluator)。 - 记录变更前数据:对于更新操作,如果还想记录修改前的值(将 A 改为 B”),可以在
@RecordLog中增加"#oldUser"参数,由调用方传入,或通过 AOP 方法执行前查询数据库获取。
如果你需要某个部分的详细代码(比如SpEL缓存优化、异步写入数据库的线程池配置),我可以继续补充。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播