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剥离日志
好,理论说完了,咱们直接上手。还是刚才那个订单服务,我们用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