《Spring Boot 高级》3.1 JDK 动态代理与 CGLIB

从 AopProxy 接口与 DefaultAopProxyFactory.createAopProxy 的判定分支出发,讲清 Spring 何时用 JDK 动态代理、何时用 CGLIB,proxyTargetClass 与 spring.aop.proxy-target-class 如何改变选择,以及 CGLIB 内联进 spring-core 后两种代理的字节码差异与各自硬限制。

本节目标:从 AopProxy 接口与 DefaultAopProxyFactory.createAopProxy 的判定分支出发,讲清 Spring 在什么条件下用 JDK 动态代理、什么条件下用 CGLIB,以及两种代理在字节码层面的差异与硬限制。
适用版本:Spring Boot 4.1.x(Java 21)

3.1 JDK 动态代理与 CGLIB

实战卷回答的是「怎么给一个 Service 加切面」,本节回答「加完切面之后,容器里那个 Bean 到底变成了什么」。这是理解后面两节(切面顺序、代理失效边界)的地基。

先固定一个贯穿本章的场景:一个图书借阅服务 BorrowService,我们要给它加一个审计切面,记录每次借书、还书的入参与耗时。

public interface BorrowService {
    BorrowRecord borrow(String userId, String isbn);
    void giveBack(String userId, String isbn);
}

@Service
public class BorrowServiceImpl implements BorrowService {
    // 省略仓储依赖与实现
}

加了 @Aspect 之后,ctx.getBean(BorrowService.class) 拿到的不再是 BorrowServiceImpl 实例,而是一个代理。这一节就是要看清这个代理是谁、怎么选出来的。

3.1.1 代理从哪来:AopProxy 与两个实现

Spring AOP 的代理创建入口是 org.springframework.aop.framework.AopProxy,接口只有三个方法(javap 于 spring-aop-7.0.9.jar 核实):

public interface org.springframework.aop.framework.AopProxy {
  public abstract java.lang.Object getProxy();
  public abstract java.lang.Object getProxy(java.lang.ClassLoader);
  public abstract java.lang.Class<?> getProxyClass(java.lang.ClassLoader);
}

它有两个实现,都在同一个包下:

实现类关键技术声明(javap 核实)
JdkDynamicAopProxyjava.lang.reflect.Proxyimplements AopProxy, InvocationHandler, Serializable
CglibAopProxyorg.springframework.cglib.proxy.Enhancerimplements AopProxy, Serializable

两个实现都持有同一个 AdvisedSupport,它保存着目标对象(TargetSource)、拦截器链、以及 proxyTargetClass 等开关。换句话说,选 JDK 还是选 CGLIB 只影响「怎么生成字节码」,不影响「拦截哪些方法、跑哪些 advice」——后者由 AdvisedSupport 统一决定。这是 Spring AOP 把「代理方式」与「通知语义」解耦的关键设计。

JdkDynamicAopProxy 本身就是一个 InvocationHandler,所以它既是「代理工厂」也是「调用处理器」。它生成代理的那行代码(spring-aop-7.0.9-sources.jar 核实):

return Proxy.newProxyInstance(determineClassLoader(classLoader),
        this.cache.proxiedInterfaces, this);

注意第三个参数是 this——所以每次接口方法调用都会进到 JdkDynamicAopProxy.invoke(...),由它去查拦截器链。

3.1.2 DefaultAopProxyFactory 的选择规则

真正做选择的是 org.springframework.aop.framework.DefaultAopProxyFactory#createAopProxy(AdvisedSupport)。它的判定分支不长,值得逐字读(spring-aop-7.0.9-sources.jar 核实):

public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
    if (config.isOptimize() || config.isProxyTargetClass() || !config.hasUserSuppliedInterfaces()) {
        Class<?> targetClass = config.getTargetClass();
        if (targetClass == null && config.getProxiedInterfaces().length == 0) {
            throw new AopConfigException("TargetSource cannot determine target class: " +
                    "Either an interface or a target is required for proxy creation.");
        }
        if (targetClass == null || targetClass.isInterface() ||
                Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) {
            return new JdkDynamicAopProxy(config);
        }
        return new ObjenesisCglibAopProxy(config);
    }
    else {
        return new JdkDynamicAopProxy(config);
    }
}

把它翻译成判定表:

条件结果
optimize=true 或 proxyTargetClass=true 或没有用户指定的接口进入「类代理」分支
↳ 但目标类型是 null / 接口 / 已是 JDK 代理 / Lambda仍退回 JdkDynamicAopProxy
↳ 否则ObjenesisCglibAopProxy
其余(proxyTargetClass=false 且指定了接口)JdkDynamicAopProxy

两个容易踩的认知偏差:

  1. 「有接口就一定走 JDK 代理」是错的。 只有当 proxyTargetClass=false 时才成立。一旦 proxyTargetClass=true,即使目标实现了接口,也会走 CGLIB 类代理。
  2. 默认生成的是 ObjenesisCglibAopProxy,不是 CglibAopProxy。 ObjenesisCglibAopProxy 是 CglibAopProxy 的子类,用 Objenesis 绕过构造器来实例化代理对象。这是很多「讲 AOP」的文章漏掉的细节。

3.1.3 proxyTargetClass 与 Boot 的默认值

proxyTargetClass 定义在 org.springframework.aop.framework.ProxyConfig(javap 核实有 setProxyTargetClass(boolean) / isProxyTargetClass()),AdvisedSupport 继承它。

裸 Spring 的 @EnableAspectJAutoProxy 默认是 proxyTargetClass=false:

// org.springframework.context.annotation.EnableAspectJAutoProxy(spring-context-7.0.9 核实)
boolean proxyTargetClass() default false;
boolean exposeProxy() default false;

但 Spring Boot 把这个默认值反过来了。org.springframework.boot.autoconfigure.aop.AopAutoConfiguration(spring-boot-autoconfigure-4.1.1-sources.jar 核实)里:

@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ConditionalOnBooleanProperty(name = "spring.aop.proxy-target-class", matchIfMissing = true)
static class CglibAutoProxyConfiguration { }

matchIfMissing = true 意味着 spring.aop.proxy-target-class 不配也是 true。从 spring-configuration-metadata.json 读到的默认值也印证了这点:

spring.aop.auto               default = true    "Add @EnableAspectJAutoProxy."
spring.aop.proxy-target-class default = true    "Whether subclass-based (CGLIB) proxies ..."

所以结论很明确:Boot 应用默认就是 CGLIB 类代理。这与「有接口走 JDK」的直觉相反,也是为什么在 Boot 里给 @Transactional 方法打断点,看到的是 XxxService$$SpringCGLIB$$0 而不是 $Proxy0。

想改回 JDK 代理,只需一行配置:

spring:
  aop:
    proxy-target-class: false

反过来,想强制 CGLIB 又不想依赖默认值,可以显式写 spring.aop.proxy-target-class: true,或在自己配置类上加 @EnableAspectJAutoProxy(proxyTargetClass = true)。

3.x 差异:spring.aop.* 两个属性在 3.5.x 与 4.1.x 语义一致。@ConditionalOnBooleanProperty 自 3.5 起引入(官方 3.5 Release Notes 明列为新增注解),到 4.1 已广泛用于自动配置,它比 @ConditionalOnProperty(havingValue = "true") 更直接地表达布尔语义。

3.1.4 CGLIB 已内联进 spring-core

如果你按老资料去 pom.xml 里找 cglib:cglib 依赖,会一无所获——Spring 早已把 CGLIB 的源码重打包进 spring-core。核实方法:

unzip -l ~/.m2/repository/org/springframework/spring-core/7.0.9/spring-core-7.0.9.jar \
  | rg 'org/springframework/cglib/proxy/Enhancer.class'

能看到 org/springframework/cglib/proxy/Enhancer.class 与 org/springframework/cglib/core/ReflectUtils.class。也就是说:

  • 包名从 net.sf.cglib 改成了 org.springframework.cglib,避免与用户引入的原版 CGLIB 冲突。
  • 你不需要、也不应该再单独引 cglib:cglib 或 cglib-nodep。
  • spring-boot-starter-aspectj(4.0 起由 spring-boot-starter-aop 改名,见官方 4.0 Migration Guide)带来的是 AspectJ 的切面注解与织入支持,不是 CGLIB——CGLIB 一直在 spring-core 里。

代理类名同样被 Spring 接管。org.springframework.cglib.core.SpringNamingPolicy(spring-core-7.0.9-sources.jar 核实)把标签定死为 $$SpringCGLIB$$:

private static final String SPRING_LABEL = "$$SpringCGLIB$$";

于是 CGLIB 代理的类名形如 com.example.audit.BorrowServiceImpl$$SpringCGLIB$$0。

3.1.5 两种代理的字节码差异与限制

维度JDK 动态代理CGLIB
生成机制Proxy.newProxyInstance 运行时生成实现接口的类Enhancer 生成目标类的子类
类名形态jdk.proxy1.$Proxy0(JDK 9+ 动态模块)XxxServiceImpl$$SpringCGLIB$$0
继承关系继承 java.lang.reflect.Proxy,实现目标接口继承目标类本身
能拦截的方法仅接口中声明的方法目标类中可被覆盖的 public / protected 实例方法
需要接口必须有接口不需要接口
final 类不受影响(不继承目标类)无法代理(不能继承 final 类)
final 方法接口方法若为 final 也不可代理无法覆盖,静默失效并打 WARN
private / static 方法不在接口里,天然不拦截不可覆盖,天然不拦截
强制转型只能转成接口类型可转成目标类类型
目标对象可持有一个独立的目标实例代理即子类实例,目标逻辑在父类
额外依赖无(JDK 自带)无(已内联进 spring-core)

其中「强制转型」这一条最容易被业务代码感知:JDK 代理的实例不能转成 BorrowServiceImpl,只能转成 BorrowService。这也是 Boot 默认改用 CGLIB 的现实理由之一——很多遗留代码直接 @Autowired 具体类,用 JDK 代理会直接启动失败。

3.1.6 怎么亲手观察用的是哪种代理

写一个测试就能看到(示例输出,需你在自己的应用里运行):

@SpringBootTest
class ProxyTypeTest {

    @Autowired
    private ApplicationContext ctx;

    @Test
    void printProxyType() {
        Object bean = ctx.getBean(BorrowService.class);
        System.out.println("class      = " + bean.getClass().getName());
        System.out.println("superclass = " + bean.getClass().getSuperclass().getName());
        System.out.println("isAopProxy = " + AopUtils.isAopProxy(bean));
        System.out.println("isJdk      = " + AopUtils.isJdkDynamicProxy(bean));
        System.out.println("isCglib    = " + AopUtils.isCglibProxy(bean));
    }
}

默认配置下的示例输出:

class      = com.example.audit.BorrowServiceImpl$$SpringCGLIB$$0
superclass = com.example.audit.BorrowServiceImpl
isAopProxy = true
isJdk      = false
isCglib    = true

把 spring.aop.proxy-target-class 改成 false 后再跑,输出会变成:

class      = jdk.proxy1.$Proxy48
superclass = java.lang.reflect.Proxy
isAopProxy = true
isJdk      = true
isCglib    = false

AopUtils 的三个静态方法(isAopProxy / isJdkDynamicProxy / isCglibProxy,javap 于 spring-aop-7.0.9.jar 核实)是排查「这个 Bean 到底被代理了没有、用哪种代理」的第一把工具,第 3.3 节会反复用到它。

3.1.7 本节要点回顾

  • 选代理只影响字节码生成方式,拦截语义由 AdvisedSupport 统一决定。
  • 判定逻辑集中在 DefaultAopProxyFactory.createAopProxy:optimize / proxyTargetClass / 无用户接口三者任一为真就走类代理分支,再按目标类型决定是否退回 JDK 代理。
  • Boot 默认 spring.aop.proxy-target-class=true,因此默认是 CGLIB;裸 Spring 默认是 JDK 代理。
  • CGLIB 已内联进 spring-core 的 org.springframework.cglib 包,无需额外依赖。
  • AopUtils.isJdkDynamicProxy / isCglibProxy 是判断代理类型的标准手段。

小结

  • 代理入口是 AopProxy,实现为 JdkDynamicAopProxy(Proxy.newProxyInstance + InvocationHandler)与 CglibAopProxy(Enhancer 生成子类,实际默认子类是 ObjenesisCglibAopProxy)。
  • 选择规则写在 DefaultAopProxyFactory.createAopProxy:isOptimize() || isProxyTargetClass() || !hasUserSuppliedInterfaces() 为真则尝试类代理,目标为接口 / JDK 代理 / Lambda 时再退回 JDK 代理。
  • proxyTargetClass 定义在 ProxyConfig,裸 Spring @EnableAspectJAutoProxy 默认 false,而 Boot 的 AopAutoConfiguration 用 matchIfMissing=true 把它默认成 true。
  • CGLIB 源码已重打包进 spring-core 的 org.springframework.cglib,代理类名带 $$SpringCGLIB$$ 标签。
  • JDK 代理只能转成接口类型、只拦截接口方法;CGLIB 能拦截可覆盖的实例方法,但受 final 类 / final 方法 / private / static 限制。
  • 判断代理类型用 AopUtils.isJdkDynamicProxy 与 isCglibProxy。

知道了「代理是谁」,下一步要看「一个切面是怎么变成拦截器链、多个切面谁先谁后」——那是 3.2 节的主题。

阅读导航:上一节:2.3 自动配置的覆盖与排除 · 下一节:3.2 切面织入与顺序 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计