记录级注解如何实现操作日志自动记录?

wen java案例 52

本文目录导读:

记录级注解如何实现操作日志自动记录?

  1. 方案一:基于 Spring AOP + SpEL(最推荐,通用性强)
  2. 方案二:基于函数式接口 + 回调(更灵活,适用于复杂场景)
  3. 总结对比
  4. 进阶优化点(生产环境必看)

这是一个很经典的企业级开发问题,我先帮你梳理一下核心思路,然后给出两种主流实现方案的具体代码示例

核心思路: “记录级注解”通常指的是在方法(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),它实现了“声明式编程”,开发人员只需在方法上加一行注解,即可自动记录日志,是最通用、最标准的实现方式。
  • 在方案一无法满足(比如需要复杂计算)时,才考虑方案二作为补充。

进阶优化点(生产环境必看)

  1. 异步写入:日志保存绝对不能影响主流程,使用 @Async + 线程池,或者发送到 MQ(消息队列),再由消费端批量入库。
  2. 避免循环依赖:AOP 切面里不要注入自己切面的 Service,否则会出现循环依赖。
  3. 性能开销:SpEL 解析有性能损耗,建议在 AOP 内部做结果缓存(如用 @Cacheable),或者对表达式编译结果进行本地缓存(CachedExpressionEvaluator)。
  4. 记录变更前数据:对于更新操作,如果还想记录修改前的值(将 A 改为 B”),可以在 @RecordLog 中增加 "#oldUser" 参数,由调用方传入,或通过 AOP 方法执行前查询数据库获取。

如果你需要某个部分的详细代码(比如SpEL缓存优化、异步写入数据库的线程池配置),我可以继续补充。

抱歉,评论功能暂时关闭!