Spring AOP 动态代理机制:从静态代理到 JDK 与 CGLIB 的实现

Spring AOP 动态代理机制:从静态代理到 JDK 与 CGLIB 的实现

用日志和业务逻辑解耦的案例,从静态代理的手动编码痛点出发,逐步演进至JDK动态代理与CGLIB字节码增强,对比两种机制的底层实现差异与适用场景,深入理解Spring AOP底层的实现方式。

Spring提供了对面向切面编程(AOP)的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务(transaction)管理)进行内聚性的开发。应用对象只实现它们应该做的——完成业务逻辑——仅此而已。它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。

Spring 的 AOP 是通过动态代理来实现的。代理是一种设计模式,但是这里说到的静态代理和动态代理不是GoF23种设计模式分类中的一种,而是 Java 领域中按代理类生成时机来划分的实现方式,属于代理模式的具体技术实践,而非 GoF 定义的分类。

代理模式

A (调用方) -> C(代理) -> B(目标)

调用方不直接调用使用 B,而是通过代理 C 来间接使用 B,这就是代理。

代理模式的作用

  1. 解决程序中 A 对象和 B 对象无法直接交互的问题

举例:类似婚介所,男方和女方互不相识,通过中介(代理)相互认识

  1. 给程序中某块代码做功能增强或解耦

举例:还是婚介所,男方自己找的对象自己不满意,分手后,通过婚介(代理对象)寻找,找到原对象的增强版,各方面更为满意

  1. 给程序中某块代码提供保护

举例:依然是婚介所,男方本来是登报征婚,结果手机被骚扰电话打爆,出于保护的目的,选择了婚介所(代理),让婚介所把关,有合适的再联系。

上面的男方就是目标对象,婚介所就是代理对象,代理对象代替目标征婚,这就是代理。

静态代理

耦合日志的代码

假设我们要实现一个非常简单的登陆功能,用户名是 admin,密码是 123456

代码如下

1.首先定义 LoginService 接口

LoginService.javajava
package top.dhbxs.demo.springaop.staticproxy.service;

/**
 * @author dhbxs
 * @since 2026/4/19
 */
public interface LoginService {

    boolean login(String username, String password);
}

2.实现接口

LoginServiceImpl.javajava
package top.dhbxs.demo.springaop.staticproxy.service.impl;

import lombok.extern.slf4j.Slf4j;
import top.dhbxs.demo.springaop.staticproxy.service.LoginService;

/**
 * @author dhbxs
 * @since 2026/4/19
 */
@Slf4j
public class LoginServiceImpl implements LoginService {

    @Override
    public boolean login(String username, String password) {
        log.info("开始登陆"); // 日志:非核心业务代码
        log.info("{} 正在登陆系统", username); // 日志:非核心业务代码
        boolean flag = "admin".equals(username) && "123456".equals(password); // 核心业务代码
        if (!flag) {
            log.error("{} 登陆失败", username); // 日志:非核心业务代码
        } else {
            log.info("{} 登陆成功", username); // 日志:非核心业务代码
        }
        return flag;
    }
}

3.编写测试类测试功能

LoginServiceTest.javajava
package top.dhbxs.demo.springaop.staticproxy.service;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import top.dhbxs.demo.springaop.staticproxy.service.impl.LoginServiceImpl;

@SpringBootTest
class LoginServiceTest {

    @Test
    void login() {
        LoginService loginService = new LoginServiceImpl();

        boolean flag = loginService.login("admin", "123456");
        System.out.println("flag = " + flag);
    }
}

4.输出如下

console.loglog
2026-04-19T22:17:05.145+08:00  INFO 28308 --- [SpringAOP] [           main] t.d.d.s.s.service.impl.LoginServiceImpl  : 开始登陆
2026-04-19T22:17:05.145+08:00  INFO 28308 --- [SpringAOP] [           main] t.d.d.s.s.service.impl.LoginServiceImpl  : admin 正在登陆系统
2026-04-19T22:17:05.146+08:00  INFO 28308 --- [SpringAOP] [           main] t.d.d.s.s.service.impl.LoginServiceImpl  : admin 登陆成功
flag = true

以上就是非常简单直接的一个登陆逻辑,可以在 LoginServiceImpl 代码中看到有很多非业务相关的日志记录的代码,和业务逻辑是深度耦合的。

接下来使用代理模式中的静态代理来优化解耦。

静态代理解耦

1.创建一个代理类,实现相同的 LoginService 接口

2.修改原来的 LoginServiceImpl 接口

LoginServiceImpl.javajava
package top.dhbxs.demo.springaop.staticproxy.service.impl;

import lombok.extern.slf4j.Slf4j;
import top.dhbxs.demo.springaop.staticproxy.service.LoginService;

/**
 * @author dhbxs
 * @since 2026/4/19
 */
@Slf4j
public class LoginServiceImpl implements LoginService {

    @Override
    public boolean login(String username, String password) {

        boolean flag = "admin".equals(username) && "123456".equals(password); // 核心业务代码

        return flag;
    }
}

3.修改测试类,调用代理类实现业务

LoginServiceTest.javajava
package top.dhbxs.demo.springaop.staticproxy.service;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import top.dhbxs.demo.springaop.staticproxy.service.proxy.LoginServiceProxy;

@SpringBootTest
class LoginServiceTest {

    @Test
    void login() {
        LoginService loginService = new LoginServiceProxy(); // 创建代理对象

        boolean flag = loginService.login("admin", "123456"); // 调用代理对象方法
        System.out.println("flag = " + flag);
    }
}

修改后,我们通过 LoginServiceProxy 实现相同的 LoginService 接口,然后调用方直接调用我们的 Proxy 代理对象,由代理对象通义打印日志,并由代理对象调用最终目标方法,实现日志功能和业务逻辑解耦合。

但是这种静态代理需要我们一个个手动创建代理类,调用代理对象,还是太麻烦。所以就有了动态代理。

动态代理

  • JDK 动态代理
    • 基于接口实现,代理类和目标类都实现同一个接口。代理类是基于反射机制生成。类名一般是 $Proxy 前缀开头 + 数字
    • 核心 API:package java.lang.reflect 下的 class Proxy(生成代理对象)、InvocationHandler(定义增强逻辑)
    • 调用逻辑:A -> C(代理对象 -> h( InvocationHandler.invoke() )) -> B(目标)
  • CGLIB 动态代理
    • 基于继承实现,代理类是目标类的子类。代理类是通过第三方插件,字节码生成工具动态生成的内部类。
    • 无需接口:直接继承目标类生成代理,目标类可以无任何接口
    • 核心 API:Enhancer(生成代理对象)、MethodInterceptor( 定义增强逻辑)

JDK 动态代理

新建一个 jdkproxy.service 包,把之前的 LoginService 接口和 LoginServiceImpl 实现类复制到新包下

然后新建测试类,源码如下

创建步骤

使用 JDK 给一个目标对象动态创建代理对象,分为三步

1. 先创建目标对象

LoginService loginService = new LoginServiceImpl(); 我们要给 loginServiceImpl 这个类(目标类)做功能增强,先 new 这个类的实体对象。然后将实体类类型改为 LoginService 这个接口,这里就是面向对象里的多态了。因为 JDK 动态代理是基于接口实现的,所以目标类需要实现一个接口,也就是 LoginService 接口,后续的代理类也会实现这个接口。

2. 创建代理对象

JDK 动态代理主要使用 package java.lang.reflect 下的 class Proxy 中的 newProxyInstance()(翻译为 新建代理实例,也就是新建代理对象) 方法来生成代理对象。

本质上,Proxy.newProxyInstance() 这个方法做了两件事:

  • 在内存中动态的生成了一个类的字节码 class
  • new 了一个对象。通过内存中生成的代理类的字节码,实例化了代理对象

这个 newProxyInstance(类加载器,代理类实现哪些接口,调用处理器) 需要传入三个参数

  1. 第一个参数:ClassLoader loader

告诉 JDK,要用哪个类加载器加载动态生成的代理类的字节码,一般我们就用目标对象所属类的类加载器,这里就是 loginService.getClass().getClassLoader()

在 Spring 中,使用哪个类的类加载器由一个静态方法来确定,在 Spring-Core 中的 package org.springframework.util包下的 public abstract class ClassUtils 这个抽象类里,有一个静态方法 getDefaultClassLoader()

源码如下:

Spring 采用的是三级降级策略,优先使用当前线程上下文类加载器 ThreadContextClassLoader(TCCL),如果失败,降级使用 Spring 自己的类加载器,如果还是白,继续降级,使用兜底方案,使用系统类加载器,如果依然失败,则返回 null交给调用者自己解决(oh well, maybe the caller can live with null...)。

  1. 第二个参数:Class<?> interfaces

代理类需要实现的一些接口。在 JDK 动态生成代理对象的时候,需要我们告诉他这个代理类要实现哪些接口。

类似前文中静态代理的的实现,代理类需要和目标类实现相同的接口,才能让调用者在调用的时候不需要关注具体调用的是哪个,从而做到无感,低耦合。如果代理类没有实现目标类的接口,那不就意味着代理类的行为和目标类完全不同,那就不是代理了,完全是个新类了。

上文代码中,使用 loginService.getClass().getInterfaces(),loginService 是目标对象,调用 getClass() 获取到的是目标对象的类,运行时是 LoginServiceImpl.class 这个实现类,然后继续调用他的 getInterfaces()获取到的是这个类实现的接口,就是 LoginService.class 接口。

getInterfaces() 返回的是数组,一个类可以多实现(implement),但是只能单继承(extend)。所以这个类实现的接口可能有多个,就需要一个数组,放到数组里传给 JDK。

  1. 第三个参数:InvocationHandler h

调用处理器,JDK 帮我们动态生成了代理类,并且这个代理类实现了我们指定的接口,此时 JDK 还不知道生成的这个代理类要做什么事,也就是接口中的方法需要我们来实现,就在这个参数里实现。

这个参数是个接口,这个接口上并没有加 @FunctionalInterface 注解,但是该接口中只有一个抽象方法 invoke(),另外一个是静态方法 invokeDefault() 所以是个隐式的函数式接口,可以使用 Lambda 表达式。

我们需要实现这个 invoke() 方法,在这个方法里,写我们需要增强目标对象的目标方法的逻辑。这个方法在代理对象调用代理方法的时候,会被 JDK 调用执行。

这个方法有三个参数 (Object proxy, Method method, Object[] args) ,proxy 就是 JDK 动态代理生成的代理对象的引用,method 是目标对象中的目标方法(要执行的目标方法就是它),args 是目标方法上的实参。

要调用目标对象的目标方法,就需要使用反射机制,传入的参数有 method(目标方法),所以可以使用方法的 invoke() 函数来调用目标方法。

上面代码中 boolean flag = (boolean) method.invoke(loginService, args) 就是在调用目标对象的目标方法。这个 invoke() 需要传入两个参数,第一个是目标对象,第二个是调用这个目标对象的目标方法时传入什么参数。

3. 调用代理对象的代理方法

通过 Proxy.newProxyInstance() 构建好代理对象后,就可以直接调用代理对象的代理方法,和调用目标对象的目标方法一样,只不过把实例对象换成了代理对象,仅此而已。

在上面示例代码中就是loginServiceProxy.login("admin", "123456")

CGLIB 动态代理

1.引入 CGLIB 依赖

pom.xmlxml
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

2.新建测试类

3.添加 jvm 参数 --add-opens java.base/java.lang=ALL-UNNAMED,因为 JDK9 开始为了增强安全性,引入了模块系统(JPMS)对反射做了严格限制。CGLIB 尝试通过反射调用 ClassLoader.defineClass() 方法来动态生成类,但该方法属于 java.lang 包且未被暴露给“未命名模块”(即你的普通 Java 项目)。所以在不加上面的 jvm 参数时会报如下错误:

console.loglog
Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(...) accessible:
module java.base does not "opens java.lang" to unnamed module

4.执行结果

console.loglog
2026-04-19T23:36:10.364+08:00  INFO 15380 --- [SpringAOP] [           main] t.d.d.s.c.service.LoginServiceTest       : 开始登陆
2026-04-19T23:36:10.365+08:00  INFO 15380 --- [SpringAOP] [           main] t.d.d.s.c.service.LoginServiceTest       : admin 正在登陆系统
2026-04-19T23:36:10.367+08:00  INFO 15380 --- [SpringAOP] [           main] t.d.d.s.c.service.LoginServiceTest       : admin 登陆成功
flag = true

总结

特性JDK 动态代理CGLIB 动态代理
依赖条件目标类必须实现接口目标类无需实现接口
实现方式实现与目标类相同的接口继承目标类
核心 APIProxy + InvocationHandlerEnhancer + MethodInterceptor
Spring AOP 适配目标类有接口时默认使用目标类无接口时自动切换使用
重复造轮子之简易RPC框架
从零手写一个 Spring Boot Starter:深入理解自动装配原理

评论区

评论加载中...