SpringAOP两种代理机制

Spring AOP 代理机制详解:CGLIB vs JDK Proxy

前言

在使用 Spring AOP 进行调试时,你是否遇到过这样的困惑:明明断点打在了业务方法上,Step Over 后却跳进了一堆 CGLIB$$ 开头的类?这背后就是 Spring AOP 的代理机制在起作用。本文将深入介绍 Spring AOP 的两种代理方式:JDK 动态代理CGLIB 代理


什么是 AOP 代理?

Spring AOP 的核心思想是在不修改原始代码的情况下,为方法调用织入额外的逻辑(如日志、事务、权限校验等)。为了实现这一点,Spring 需要为目标对象创建一个代理对象,由代理对象拦截方法调用,执行切面逻辑后再调用原始方法。

Spring 提供了两种创建代理的方式:

特性 JDK 动态代理 CGLIB 代理
实现原理 基于 java.lang.reflect.Proxy,生成接口的实现类 基于字节码生成库,生成目标类的子类
前提条件 目标类必须实现接口 目标类不能是 final
代理对象类型 接口类型 目标类的子类类型
注入方式 只能用接口类型声明注入 接口类型和实现类类型都可以
性能 方法调用通过反射,略慢 方法调用通过 FastClass 机制,略快
Spring Boot 默认 ❌ 非默认(1.x 时代的默认) ✅ 默认(2.x 起)

JDK 动态代理

原理

JDK 动态代理要求目标类必须实现至少一个接口。Spring 会使用 java.lang.reflect.Proxy 生成一个实现了相同接口的代理类,在代理类中拦截方法调用。

示例

定义接口:

public interface OrderService {
    String createOrder(String productId, int quantity);
}

实现类:

@Service
public class OrderServiceImpl implements OrderService {

    @Override
    public String createOrder(String productId, int quantity) {
        String orderId = UUID.randomUUID().toString();
        System.out.println("创建订单: " + orderId);
        return orderId;
    }
}

切面:

@Aspect
@Component
public class LogAspect {

    @Around("execution(* com.example.service.impl.*.*(..))")
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        String methodName = pjp.getSignature().getName();
        System.out.println("[LOG] 方法开始: " + methodName);
        Object result = pjp.proceed();
        System.out.println("[LOG] 方法结束: " + methodName + ", 返回: " + result);
        return result;
    }
}

启用 JDK Proxy:

# application.properties
spring.aop.proxy-target-class=false

注入时必须使用接口类型:

@RestController
public class OrderController {

    // ✅ 正确:使用接口类型
    @Autowired
    private OrderService orderService;

    // ❌ 错误:使用实现类类型会报 BeanNotOfRequiredTypeException
    // @Autowired
    // private OrderServiceImpl orderService;
}

JDK Proxy 的调用链路

客户端调用 orderService.createOrder()
    → JDK Proxy ($Proxy42)
        → InvocationHandler.invoke()
            → LogAspect.logAround()
                → OrderServiceImpl.createOrder()  // 实际业务方法

手动实现 JDK 动态代理(帮助理解原理)

public class JdkProxyDemo {

    public static void main(String[] args) {
        OrderService target = new OrderServiceImpl();

        OrderService proxy = (OrderService) Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            new Class[]{OrderService.class},
            (proxyObj, method, methodArgs) -> {
                System.out.println("[Proxy] 前置处理: " + method.getName());
                Object result = method.invoke(target, methodArgs);
                System.out.println("[Proxy] 后置处理: " + method.getName());
                return result;
            }
        );

        proxy.createOrder("P001", 2);
    }
}

输出:

[Proxy] 前置处理: createOrder
创建订单: xxxx-xxxx-xxxx
[Proxy] 后置处理: createOrder

CGLIB 代理

原理

CGLIB(Code Generation Library)通过字节码生成技术,在运行时动态生成目标类的子类作为代理。它不要求目标类实现接口,但要求目标类和目标方法不能是 final 的(因为 final 无法被继承/重写)。

示例

不实现接口的类也可以被代理:

@Service
public class NotificationService {

    public void sendEmail(String recipient, String content) {
        System.out.println("发送邮件给: " + recipient);
    }

    public void sendSms(String phone, String content) {
        System.out.println("发送短信给: " + phone);
    }
}

切面同样生效:

@Aspect
@Component
public class LogAspect {

    @Around("execution(* com.example.service.NotificationService.*(..))")
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("[LOG] 方法开始: " + pjp.getSignature().getName());
        Object result = pjp.proceed();
        System.out.println("[LOG] 方法结束: " + pjp.getSignature().getName());
        return result;
    }
}

Spring Boot 2.x+ 默认就是 CGLIB,无需额外配置。

注入时接口类型和实现类类型都可以:

@RestController
public class NotificationController {

    // ✅ 都可以正常工作
    @Autowired
    private NotificationService notificationService;
    
    @Autowired
    private NotificationServiceImpl notificationService;
}

CGLIB 的调用链路

客户端调用 notificationService.sendEmail()
    → CGLIB 代理 (NotificationService$$EnhancerBySpringCGLIB$$a1b2c3d4)
        → MethodInterceptor.intercept()
            → LogAspect.logAround()
                → NotificationService.sendEmail()  // 实际业务方法

手动实现 CGLIB 代理(帮助理解原理)

public class CglibProxyDemo {

    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(NotificationService.class);
        enhancer.setCallback((MethodInterceptor) (obj, method, methodArgs, proxy) -> {
            System.out.println("[CGLIB] 前置处理: " + method.getName());
            Object result = proxy.invokeSuper(obj, methodArgs);
            System.out.println("[CGLIB] 后置处理: " + method.getName());
            return result;
        });

        NotificationService proxy = (NotificationService) enhancer.create();
        proxy.sendEmail("user@example.com", "Hello");
    }
}

输出:

[CGLIB] 前置处理: sendEmail
发送邮件给: user@example.com
[CGLIB] 后置处理: sendEmail

Spring Boot 为什么默认选择 CGLIB?

在 Spring Boot 1.x 时代,默认使用 JDK 动态代理。但从 Spring Boot 2.0 开始,默认改为了 CGLIB。这个决策的核心原因是:

JDK Proxy 的"类型注入陷阱":

// 假设 UserService 接口有实现类 UserServiceImpl

// ✅ JDK Proxy 下正确的写法
@Autowired
private UserService userService;

// ❌ JDK Proxy 下会报错!
// 因为代理对象是 $Proxy 类型,不是 UserServiceImpl 类型
@Autowired
private UserServiceImpl userService;
// 报错: BeanNotOfRequiredTypeException:
// Bean named 'userServiceImpl' is expected to be of type 'UserServiceImpl'
// but was actually of type 'com.sun.proxy.$Proxy42'

这个问题对新手来说非常困惑,而 CGLIB 代理生成的是目标类的子类,天然兼容两种注入方式,所以 Spring Boot 团队选择了 CGLIB 作为默认方案。


调试技巧:如何跳过代理类

使用 CGLIB 代理后,调试时 Step Over/Into 会进入大量代理类代码。以下是解决方案:

方案一:断点打在方法体内部

public String createOrder(String productId, int quantity) {
    String orderId = UUID.randomUUID().toString(); // ← 断点打这里,而不是方法签名
    return orderId;
}

方案二:IntelliJ IDEA 配置 Step Filter

Settings → Build, Execution, Deployment → Debugger → Stepping

勾选 "Do not step into the classes",添加:

net.sf.cglib.*
org.springframework.cglib.*
com.sun.proxy.*
org.springframework.aop.*

同时勾选 "Skip synthetic methods"

配置后,调试时会自动跳过代理类,直接到达业务代码。


常见问题 FAQ

Q1: 实现了接口的类,Spring Boot 还是用 CGLIB 吗?

是的。 Spring Boot 2.x+ 默认 proxy-target-class=true,无论是否实现接口,都使用 CGLIB。除非你手动设置 spring.aop.proxy-target-class=false

Q2: CGLIB 有什么限制?

  • 目标类不能是 final(无法生成子类)
  • 目标方法不能是 finalprivate(无法被重写)
  • 构造函数会被调用两次(目标对象一次,代理对象一次),Spring 4.0+ 通过 Objenesis 优化了这个问题

Q3: 两者性能差异大吗?

在现代 JVM 和 Spring 版本下,差异可以忽略不计。CGLIB 使用 FastClass 机制避免了反射调用,在高频调用场景下甚至略优于 JDK Proxy。

Q4: 什么时候应该用 JDK Proxy?

一般不需要刻意选择。但如果你的项目有以下需求,可以考虑 JDK Proxy:

  • 严格遵循面向接口编程,希望强制所有注入都使用接口类型
  • 需要减少生成的字节码类数量(JDK Proxy 不生成额外的 class 文件)

总结

维度 JDK 动态代理 CGLIB 代理
实现方式 接口代理 子类代理
是否需要接口 ✅ 必须 ❌ 不需要
final 类/方法 可以代理(只要接口方法不是 final) ❌ 不能代理
注入灵活性 只能用接口类型 接口和实现类都可以
Spring Boot 默认 1.x 默认 2.x+ 默认
调试体验 较好(代理类较简单) 需配置 Step Filter

实际开发建议:保持 Spring Boot 默认的 CGLIB 代理即可,配合 IDE 的 Step Filter 功能解决调试体验问题。专注于业务代码的编写,代理机制交给框架处理。

posted @ 2026-04-02 10:20  cwp0  阅读(29)  评论(0)    收藏  举报