86、重构与AOP:用面向切面编程重构横切关注点

说实话,横切关注点这词听着挺唬人。但说白了,就是那些散落在系统各处的「杂务」——日志、权限校验、事务管理、性能监控。我见过太多项目,这些代码像牛皮癣一样贴在业务逻辑上,撕都撕不掉。

我记得有一次接手一个老项目,光一个下单接口,日志打印就占了40行。业务逻辑反而被挤得没地方站。你想想看,这要是改个需求,得翻多少垃圾代码?

今天我们就聊聊,怎么用AOP把这堆「横切关注点」从业务代码里剥离出去。

什么是横切关注点?

先看一段典型的「脏代码」:

public class OrderService {
    public void createOrder(Order order) {
        // 日志
        logger.info("开始创建订单:" + order.getId());
        
        // 权限校验
        if (!SecurityContext.hasPermission("ORDER_CREATE")) {
            throw new SecurityException("无权限");
        }
        
        // 事务开始
        TransactionManager.begin();
        
        try {
            // 真正的业务逻辑——就这3行
            orderRepository.save(order);
            inventoryService.deduct(order.getProductId(), order.getQuantity());
            notificationService.send(order.getUserId(), "订单已创建");
            
            // 事务提交
            TransactionManager.commit();
        } catch (Exception e) {
            TransactionManager.rollback();
            throw e;
        }
        
        // 日志
        logger.info("订单创建完成:" + order.getId());
    }
}

看到了吗?业务逻辑只占20%,剩下80%全是横切关注点。我在项目中遇到过类似的情况,一个Controller里塞了六七个切面逻辑,改一个权限需求,得动三个文件。

AOP的核心思想

AOP说白了就一句话:把「杂务」抽出去,让业务代码只做业务

它的核心概念其实不多:

  • 切面(Aspect):封装横切关注点的模块,比如日志切面、事务切面
  • 连接点(Join Point):程序执行过程中的某个点,比如方法调用、异常抛出
  • 通知(Advice):在连接点上执行的动作,比如前置通知、后置通知
  • 切入点(Pointcut):定义哪些连接点需要被拦截

我习惯用一张图来理解它们的关系:

AOP核心概念关系图 业务代码(OrderService.createOrder) 连接点(Join Point) 连接点(Join Point) 连接点(Join Point) 切入点(Pointcut) 匹配规则:execution(* OrderService.*(..)) 切面(Aspect) @Before 前置通知 @After 后置通知 @Around 环绕通知 @AfterThrowing 异常通知

重构实战:用AOP剥离日志

好,理论说完了,咱们直接上手。还是刚才那个订单服务,我们用AOP把日志抽出来。

第一步:定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
    String value() default "";
}

第二步:编写切面

@Aspect
@Component
public class LoggingAspect {
    
    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
    
    @Around("@annotation(loggable)")
    public Object logExecution(ProceedingJoinPoint joinPoint, Loggable loggable) throws Throwable {
        String methodName = joinPoint.getSignature().toShortString();
        String bizName = loggable.value().isEmpty() ? methodName : loggable.value();
        
        logger.info("【{}】开始执行,参数:{}", bizName, joinPoint.getArgs());
        
        long start = System.currentTimeMillis();
        try {
            Object result = joinPoint.proceed();
            long elapsed = System.currentTimeMillis() - start;
            logger.info("【{}】执行成功,耗时:{}ms,结果:{}", bizName, elapsed, result);
            return result;
        } catch (Exception e) {
            logger.error("【{}】执行失败,异常:{}", bizName, e.getMessage(), e);
            throw e;
        }
    }
}

第三步:改造业务代码

@Service
public class OrderService {
    
    @Loggable("创建订单")
    public void createOrder(Order order) {
        // 只有业务逻辑,干干净净
        orderRepository.save(order);
        inventoryService.deduct(order.getProductId(), order.getQuantity());
        notificationService.send(order.getUserId(), "订单已创建");
    }
}

核心变化:

  • 业务代码从40行缩减到5行
  • 日志逻辑集中管理,改一处全系统生效
  • 新增方法只需要加个注解,零侵入

更多横切关注点的重构

日志只是开胃菜。我整理了一个表格,看看常见的横切关注点怎么用AOP处理:

横切关注点 传统做法 AOP做法 注解示例
日志记录 每个方法手动写logger.info @Loggable 切面 @Loggable("创建订单")
权限校验 if语句判断权限 @RequirePermission 切面 @RequirePermission("ORDER_CREATE")
事务管理 try-catch手动begin/commit/rollback @Transactional 切面 @Transactional(rollbackFor = Exception.class)
性能监控 System.currentTimeMillis() 手动计算 @Monitored 切面 @Monitored
缓存处理 手动查缓存、写缓存 @Cacheable 切面 @Cacheable("orders")
参数校验 if (param == null) 满天飞 @Validated 切面 @Validated

我的习惯:每个横切关注点单独一个切面类,不要混在一起。比如日志切面只做日志,权限切面只做权限。这样每个切面职责单一,测试也方便。

避坑指南

我曾经踩过的坑:

  • 切面顺序问题:多个切面同时作用在一个方法上时,执行顺序很重要。比如事务切面应该包在日志切面外面,否则日志里记录的事务状态可能是错的。用@Order注解控制顺序。
  • 内部调用失效:同一个类里的方法互相调用,AOP不会生效。因为代理对象没经过。我习惯把需要AOP的方法抽到另一个Service里,或者自己注入自己。
  • 不要滥用AOP:不是所有东西都适合用AOP。简单的工具方法、纯数据计算,用AOP反而增加复杂度。我一般只对「跨多个模块的通用逻辑」用AOP。

总结

AOP重构横切关注点,说白了就是「把杂务关进切面的笼子里」。业务代码只关心业务,日志、权限、事务这些事,交给切面去操心。

我个人习惯在项目初期就定义好通用的切面注解。这样后续开发新功能时,团队成员只需要加个注解,不用重复写那些模板代码。代码量减少60%以上,维护成本直线下降。

嗯,如果你现在正对着一个满屏日志和权限判断的Service发愁,不妨试试AOP。从日志开始,一步步把那些横切关注点「请」出去。


公众号:蓝海资料掘金营,微信deep3321