3行注解干掉30行日志代码!Spring AOP实战全程复盘

大家好,我是大华! 今天基于AOP改了系统里一个日志记录的功能,顺便记录和分享一下。

在我的项目中,有好几个地方需要记录用户操作日志。每个方法里都手动写日志代码,这样写不仅代码重复,而且一旦要改,就得改一堆地方。

这时候 AOP(面向切面编程)就可以用来解决这个问题了。只要打个注解,日志自动记录。

核心步骤

1、定义注解 标记哪些方法要记日志。

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLog {
    String module();
    String action();
}

2、加注解 在需要的方法上加上@OperationLog

java 复制代码
@OperationLog(module = "订单", action = "更新")
public void update() { ... }

3、写切面AOP拦截带注解的方法,通过反射获取信息,自动记日志。

java 复制代码
@Around("@annotation(log)")
public Object around(ProceedingJoinPoint joinPoint, OperationLog log) {
    Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
    // 记录日志
    return joinPoint.proceed();
}

下面来看我的实际项目例子

在我的项目中,有这样一段代码:

java 复制代码
@Transactional(rollbackFor = Exception.class)
@Override
public void updateProductProcurementSummary(ProductProcurementSummarySaveReqVO updateReqVO) {
    // 校验存在
    ProductProcurementSummaryDO summaryDO = productProcurementSummaryMapper.selectById(updateReqVO.getId());
    if (summaryDO == null) {
        throw exception(PRODUCT_PROCUREMENT_SUMMARY_NOT_EXISTS);
    }
    // 更新
    ProductProcurementSummaryDO updateObj = BeanUtils.toBean(updateReqVO, ProductProcurementSummaryDO.class);
    productProcurementSummaryMapper.updateById(updateObj);

    // 记录日志
    ProductProcurementLogSaveReqVO logVO = new ProductProcurementLogSaveReqVO();
    logVO.setPlatformType(updateReqVO.getPlatformType());
    logVO.setPlatformName(updateReqVO.getPlatformName());
    logVO.setSku(updateReqVO.getSku());
    logVO.setReplenishmentSuggestion(summaryDO.getReplenishmentSuggestion());
    logVO.setSkuGroup(updateReqVO.getSkuGroup());
    logVO.setOperationManager(updateReqVO.getOperationManager());
    logVO.setStatus(updateReqVO.getStatus());
    logVO.setModuleType(PrpductProcurementLogEnum.MODULE_TYPE_1.getCode());
    logVO.setOperationType(PrpductProcurementLogEnum.OPERATION_TYPE_2.getCode());
    logVO.setNewReplenishmentSuggestion(updateReqVO.getReplenishmentSuggestion());

    String nickName = getLoginUserNickname();
    logVO.setRemark(nickName + "修改了补货数量");
    logVO.setSourceId(updateReqVO.getId());
    logVO.setPeriods(summaryDO.getPeriods());

    productProcurementLogService.createProductProcurementLog(logVO);
}

这段代码中,业务逻辑和日志记录的逻辑直接混合在一起了,会存在下面的问题:

  • 代码冗余:如果其他方法也需要记录日志,得在每个方法里都写类似的日志代码。
  • 维护困难:如果日志记录逻辑需要修改,需要修改所有相关的方法。

AOP解决方案

1、创建常量类

首先,我们创建一个常量类,用于定义模块类型和操作类型:

java 复制代码
public class ProductProcurementLogConstant {

    /**
     * 模块类型 - 备货汇总模块
     */
    public static final int MODULE_TYPE_1 = 1;

    /**
     * 操作类型 - 修改操作
     */
    public static final int OPERATION_TYPE_2 = 2;
}
2、创建注解

然后,我们创建一个自定义注解,用于标记需要记录日志的方法:

java 复制代码
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperationLog {

    /**
     * 模块类型
     */
    int moduleType() default ProductProcurementLogConstant.MODULE_TYPE_1;

    /**
     * 操作类型
     */
    int operationType() default ProductProcurementLogConstant.OPERATION_TYPE_2;

    /**
     * 操作描述
     */
    String description() default "";
}
3、实现切面
java 复制代码
@Aspect
@Component
public class OperationLogAspect {

    @Resource
    private ProductProcurementLogService productProcurementLogService;
    
    @Resource
    private ProductInventoryPlatformMapper productInventoryPlatformMapper;

    @AfterReturning("@annotation(cn.iocoder.yudao.module.system.framework.productinventory.core.annotation.OperationLog)")
    public void doAfterReturning(JoinPoint joinPoint) {
        // 获取注解信息
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        OperationLog operationLog = method.getAnnotation(OperationLog.class);
        
        // 获取方法参数
        Object[] args = joinPoint.getArgs();
        if (args.length == 0) {
            return;
        }
        
        // 创建日志对象
        ProductProcurementLogSaveReqVO logVO = new ProductProcurementLogSaveReqVO();
        logVO.setModuleType(operationLog.moduleType());
        logVO.setOperationType(operationLog.operationType());
        
        // 根据参数类型处理不同的请求
        if (args[0] instanceof ProductProcurementSummarySaveReqVO) {
            handleProcurementSummary((ProductProcurementSummarySaveReqVO) args[0], logVO, operationLog);
        } else if (args[0] instanceof ProductInventoryPlatformSaveReqVO) {
            handleInventoryPlatform((ProductInventoryPlatformSaveReqVO) args[0], logVO, operationLog);
        } else {
            return; // 不支持的参数类型
        }
        
        // 设置备注
        String description = operationLog.description();
        if (description.isEmpty()) {
            description = getLoginUserNickname() + "修改了补货数量";
        } else {
            description = getLoginUserNickname() + description;
        }
        logVO.setRemark(description);
        
        // 创建日志
        productProcurementLogService.createProductProcurementLog(logVO);
    }
    
    /**
     * 处理备货汇总日志
     */
    private void handleProcurementSummary(ProductProcurementSummarySaveReqVO updateReqVO, 
                                         ProductProcurementLogSaveReqVO logVO, 
                                         OperationLog operationLog) {
        // 设置日志属性...
    }
    
    /**
     * 处理多平台补货管理日志
     */
    private void handleInventoryPlatform(ProductInventoryPlatformSaveReqVO updateReqVO, 
                                        ProductProcurementLogSaveReqVO logVO, 
                                        OperationLog operationLog) {
        // 从数据库中查询原始数据
        ProductInventoryPlatformDO platformDO = productInventoryPlatformMapper.selectById(updateReqVO.getId());
        if (platformDO == null) {
            return; // 如果找不到原始数据,则不记录日志
        }
        
        // 设置日志属性...
    }
}
4、使用注解

最后,我们在需要记录日志的方法上添加@OperationLog注解:

java 复制代码
@Override
@OperationLog(moduleType = ProductProcurementLogConstant.MODULE_TYPE_1,
             operationType = ProductProcurementLogConstant.OPERATION_TYPE_2,
             description = "修改了多平台补货数量")
public void updateProductInventoryPlatform(ProductInventoryPlatformSaveReqVO updateReqVO) {
    // 校验存在
    validateProductInventoryPlatformExists(updateReqVO.getId());
    // 更新
    ProductInventoryPlatformDO updateObj = BeanUtils.toBean(updateReqVO, ProductInventoryPlatformDO.class);
    productInventoryPlatformMapper.updateById(updateObj);
}
OK完成

使用AOP的优点

  • 关注点分离:业务和日志各管各的,代码更清晰。
  • 减少重复:加个注解就行,不用到处写日志代码。
  • 统一维护:所有日志逻辑集中在切面里,改一次全生效。
  • 无侵入:老代码不用动,加个注解就能用。

使用建议

  • 注解设计要实用,比如加上模块、操作类型、描述等。
  • 注意性能:切面里别做耗时操作,比如复杂查询。

本文首发于微信公众号「刘大华的开发笔记」我是大华,专注分享前后端开发的实战笔记。 关注我,少走弯路,一起进步! 觉得有用的话,点个爱心吧!

相关推荐
还梦呦3 分钟前
2025年09月计算机二级Java选择题每日一练——第一期
java·开发语言
与火星的孩子对话11 分钟前
Unity高级开发:反射原理深入解析与实践指南 C#
java·unity·c#·游戏引擎·lucene·反射
花开富贵ii32 分钟前
代码随想录算法训练营四十六天|图论part04
java·数据结构·算法·图论
Miraitowa_cheems36 分钟前
LeetCode算法日记 - Day 15: 和为 K 的子数组、和可被 K 整除的子数组
java·数据结构·算法·leetcode·职场和发展·哈希算法
答题卡上的情书36 分钟前
java第一个接口
java·开发语言
大得36937 分钟前
django生成迁移文件,执行生成到数据库
后端·python·django
寻月隐君41 分钟前
Rust Web 开发实战:使用 SQLx 连接 PostgreSQL 数据库
后端·rust·github
RainbowSea1 小时前
伙伴匹配系统(移动端 H5 网站(APP 风格)基于Spring Boot 后端 + Vue3 - 06
java·spring boot·后端
Keya1 小时前
MacOS端口被占用的解决方法
前端·后端·设计模式
用户9096783069431 小时前
Python 判断一个字符串中是否含有数字
后端