Java案例怎么实现AOP切面?

wen java案例 20

Java案例实战:如何实现AOP切面编程?——从原理到项目落地

目录导读

  1. AOP核心概念与适用场景
    • 什么是切面、连接点、通知、切入点
    • 哪些业务场景需要AOP(日志、事务、权限)
  2. 主流实现方案对比
    • Spring AOP vs AspectJ
    • 代理模式选择:JDK动态代理 vs CGLIB
  3. 手写第一个AOP例子:日志切面(Spring Boot)
    • 依赖配置与注解启用
    • 定义切面类并声明通知(@Before/@After/@Around)
  4. 进阶实战:自定义注解驱动权限校验
    • 创建@PermissionCheck注解
    • 在切面中解析注解并实现逻辑
  5. 常见问题与性能优化
    • 同一类内部调用失效怎么办?
    • 如何避免过多切面导致性能下降?
  6. 深层问答环节
    • Q1:JDK代理和CGLIB何时选用?
    • Q2:如果多个切面同时作用于同一方法,执行顺序如何控制?

AOP核心概念与适用场景

AOP(Aspect-Oriented Programming,面向切面编程) 是一种编程范式,它允许开发者将横切关注点(如日志记录、事务管理、安全校验)从业务逻辑中剥离出来,独立成“切面模块”,从而避免代码冗余和耦合。

Java案例怎么实现AOP切面?

关键术语需要先透彻理解:

  • 切面(Aspect):横切关注点的模块化,例如LoggingAspect
  • 连接点(Join Point):程序执行过程中的特定位置,如方法调用、异常抛出。
  • 通知(Advice):切面在特定连接点执行的动作,有@Before@After@Around@AfterReturning@AfterThrowing五种类型。
  • 切入点(Pointcut):通过表达式定义“哪些连接点需要执行通知”。
  • 引入(Introduction):为对象添加新方法或属性。
  • 目标对象(Target):被通知的对象。
  • 织入(Weaving):将切面应用到目标对象的过程(编译期、加载期或运行期)。

经典适用场景
| 场景 | 原因 |
| --- | --- |
| 日志记录 | 避免每个方法手动插入日志代码 |
| 事务管理 | 数据层操作统一加事务控制 |
| 权限校验 | 全局拦截未授权调用 |
| 性能监控 | 统计方法执行时间 |
| 缓存管理 | 针对查询方法自动管理缓存 |


主流实现方案对比:Spring AOP vs AspectJ

特性 Spring AOP AspectJ
实现基础 基于动态代理(JDK或CGLIB) 基于字节码修改(编译期/加载期织入)
性能 运行时生成代理,略低 编译期增强,性能更好
支持粒度 仅方法级别连接点 支持字段、构造器等更多连接点
复杂度 低,与Spring深度集成 较高,需单独依赖或配置

企业级开发中,Spring AOP已覆盖95%需求,若需拦截私有方法或属性访问,才考虑引入AspectJ,本文以Spring AOP + 注解驱动为例。

代理模式选择规则

  • 目标类实现了接口 → Spring优先使用JDK动态代理。
  • 目标类没有实现接口 → 使用CGLIB生成子类代理。
  • 可在@EnableAspectJAutoProxy(proxyTargetClass = true)中强制CGLIB。

手写第一个AOP例子:日志切面

环境与依赖

确保pom.xml包含:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

启动类增加@EnableAspectJAutoProxy注解。

定义切面类

@Aspect
@Component
public class LogAspect {
    // 使用@Pointcut定义切入点:匹配所有service包下的任意方法
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void servicePointcut() {}
    // @Before:方法执行前打印入参
    @Before("servicePointcut()")
    public void logBefore(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        System.out.println("[AOP] 调用方法:" + joinPoint.getSignature().getName() 
                           + ",参数:" + Arrays.toString(args));
    }
    // @Around:封装方法执行,可控制返回值
    @Around("servicePointcut()")
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed(); // 执行原方法
        long duration = System.currentTimeMillis() - start;
        System.out.println("[AOP] 方法" + pjp.getSignature().getName() 
                           + "执行耗时:" + duration + "ms");
        return result;
    }
    // @AfterReturning:方法正常返回后记录结果
    @AfterReturning(pointcut = "servicePointcut()", returning = "result")
    public void logAfterReturning(JoinPoint joinPoint, Object result) {
        System.out.println("[AOP] 返回结果:" + result);
    }
}

测试业务类

@Service
public class OrderService {
    public String createOrder(String userId, double amount) {
        return "订单创建成功:" + userId + " 金额:" + amount;
    }
}

输出示例

[AOP] 调用方法:createOrder,参数:[user01, 99.8]
[AOP] 方法createOrder执行耗时:2ms
[AOP] 返回结果:订单创建成功:user01 金额:99.8

进阶实战:自定义注解驱动权限校验

创建自定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionCheck {
    String value() default "";  // 权限标识
}

切面实现校验逻辑

@Aspect
@Component
public class PermissionAspect {
    // 切入点匹配所有含有@PermissionCheck的方法
    @Pointcut("@annotation(com.example.annotation.PermissionCheck)")
    public void permissionPointcut() {}
    @Around("permissionPointcut()")
    public Object checkPermission(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        PermissionCheck permissionCheck = signature.getMethod()
            .getAnnotation(PermissionCheck.class);
        String requiredPermission = permissionCheck.value();
        // 模拟从当前上下文获取用户权限列表
        List<String> userPermissions = getCurrentUserPermissions();
        if (!userPermissions.contains(requiredPermission)) {
            throw new SecurityException("无权限执行:" + requiredPermission);
        }
        return pjp.proceed(); // 权限校验通过
    }
    private List<String> getCurrentUserPermissions() {
        return Arrays.asList("read", "write");
    }
}

业务方法使用注解

@RestController
public class AdminController {
    @GetMapping("/deleteUser")
    @PermissionCheck("admin:delete")
    public String deleteUser() {
        return "用户已删除";
    }
}

优势:通过注解即可声明式控制权限,业务代码零侵入。


常见问题与性能优化

问题1:同一类内部调用失效

@Transactional
public void outerMethod() {
    this.innerMethod(); // 此时AOP不会生效!因为this是原始对象而非代理
}

解决

  • 注入自身代理:@Autowired ApplicationContext context.getBean(Xxx.class)
  • 使用AopContext.currentProxy()(需要暴露代理:@EnableAspectJAutoProxy(exposeProxy = true))。

问题2:多切面执行顺序控制

使用@Order注解指定顺序,数值越小越先执行@Before

@Aspect
@Order(1)
public class LogAspect {...}
@Aspect
@Order(2)
public class PermissionAspect {...}

性能优化建议

  • 避免在@Before@After中执行耗时操作(如数据库查询)。
  • 使用cglib-proxy时,目标类不能是final
  • 切面数量尽量控制在5个以内,过多会拖慢每次方法调用。
  • 利用编译期织入(AspectJ)可以消除运行时代理开销。

深层问答环节

Q1:JDK代理和CGLIB动态代理何时选用?性能差距大吗?

  • JDK代理:要求目标类实现接口,通过InvocationHandler拦截方法,运行效率较高,因为生成的代理类只包含接口中的方法。
  • CGLIB代理:通过继承目标类生成子类,不能代理final类和final方法,启动时需生成字节码,首次创建较慢,但运行期性能接近JDK代理。
    选型建议:若项目已全部面向接口编程,优先使用JDK代理;若存在无接口的第三方类,强制指定CGLIB,在Spring Boot 2.x+中默认已优化:当目标类实现接口时用JDK,否则用CGLIB。

Q2:如果多个切面同时作用于同一方法,执行顺序由什么决定?

  • Spring AOP:按切面类上的@Order注解(或实现Ordered接口)的数值升序执行。
    • @Before:数值越小越先执行。
    • @After@AfterReturning@AfterThrowing:数值越大越先执行(实际上往往以逆向顺序执行)。
  • AspectJ:按织入时声明的顺序或依赖declare precedence语句控制。
    最佳实践:如果不确定顺序,让日志切面排最外层(@Order(1)),事务切面排内层(@Order(100)),这样即使权限校验失败,日志依然能记录拒绝原因。

AOP切面是Java企业级开发中解耦横切逻辑的利器,从最基础的日志切面到自定义注解驱动的权限校验,核心在于理解代理机制和切入点表达式,实际项目中,合理利用@Order@Pointcut复用,并注意内部调用失效问题,即可实现高内聚低耦合的优雅代码结构。

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