Java 动态代理深度解析:从“为什么“到“底层原理“

文章目录

    • 前言
    • 一、为什么需要动态代理?
      • [1.1 先从一个真实问题说起](#1.1 先从一个真实问题说起)
      • [1.2 没有动态代理时,你必须这么写](#1.2 没有动态代理时,你必须这么写)
      • [1.3 动态代理如何解决这个问题](#1.3 动态代理如何解决这个问题)
    • 二、三个核心概念是什么?
      • [2.1 Java 反射(Reflection)](#2.1 Java 反射(Reflection))
      • [2.2 ClassLoader 类加载器](#2.2 ClassLoader 类加载器)
      • [2.3 JDK 动态代理](#2.3 JDK 动态代理)
    • [三、如何使用?--- 以模拟 MyBatis 为例](#三、如何使用?— 以模拟 MyBatis 为例)
      • [3.1 完整代码结构](#3.1 完整代码结构)
      • [3.2 定义接口](#3.2 定义接口)
      • [3.3 实现 InvocationHandler](#3.3 实现 InvocationHandler)
      • [3.4 创建代理工厂](#3.4 创建代理工厂)
      • [3.5 模拟数据库执行层](#3.5 模拟数据库执行层)
      • [3.6 测试代码](#3.6 测试代码)
    • 总结

前言

你有没有思考过一个问题:在 MyBatis 中,你只写了一个 UserMapper 接口,从来没有写过它的实现类,却可以直接调用 userMapper.selectById(1) 查出数据库结果?

这背后隐藏的技术叫做 JDK 动态代理,它是 Java 反射机制的核心应用之一,也是 MyBatis、Spring AOP、RPC 框架的基石。本文将从"为什么需要它"出发,逐步深入到底层字节码原理,并附上可运行的完整代码。

本文运行代码链接:https://github.com/likerhood/CodeDesignWork/tree/main/codedesign0.0-0/src/main/java/com/likerhood/design/mybatisproxy


一、为什么需要动态代理?

1.1 先从一个真实问题说起

你每天都在用 MyBatis,但有没有想过这段代码为什么能运行?

java 复制代码
// UserMapper.java ------ 只有接口,没有任何实现类
public interface UserMapper {
    String selectById(int id);
    void insert(String username);
    void deleteById(int id);
}
java 复制代码
// 使用时直接调用,和普通对象没有任何区别
UserMapper userMapper = MapperProxyFactory.getMapper(UserMapper.class, sqlSession);
String user = userMapper.selectById(1);  // 能查出"张三"

整个项目里,你搜不到任何 UserMapperImpl 之类的实现类。一个没有实现类的接口,方法调用是怎么执行的?

1.2 没有动态代理时,你必须这么写

如果没有动态代理,要让 UserMapper 能用,唯一的办法是手动写实现类:

java 复制代码
// 传统做法:手动实现接口
public class UserMapperImpl implements UserMapper {

    private SqlSession sqlSession = new SqlSession();

    @Override
    public String selectById(int id) {
        // 每个方法都要手动调 sqlSession,模板代码
        return (String) sqlSession.execute("selectById", new Object[]{id});
    }

    @Override
    public void insert(String username) {
        sqlSession.execute("insert", new Object[]{username});
    }

    @Override
    public void deleteById(int id) {
        sqlSession.execute("deleteById", new Object[]{id});
    }
}

现在只有一个 UserMapper,勉强可以接受。但真实项目里有 UserMapperOrderMapperProductMapper... 几十个 Mapper,每一个都要写这样一个实现类,而且每个实现类的方法体几乎完全一样,只有方法名字符串不同:

java 复制代码
// UserMapperImpl、OrderMapperImpl、ProductMapperImpl...
// 每个方法体都长这样,区别只有方法名字符串
return (String) sqlSession.execute("selectById", new Object[]{id});
//                                  ↑ 唯一的区别

这就是问题所在:

  • 几十个 Mapper 就要写几十个实现类,代码高度重复
  • UserMapper 新增一个方法,UserMapperImpl 必须同步修改
  • 所有实现类在编译期就写死了,完全是机械劳动

1.3 动态代理如何解决这个问题

观察上面的重复代码,你会发现规律------每个方法体做的事情完全一样:拿到方法名,调用 sqlSession.execute()

既然逻辑是固定模板,为什么要人工重复写?让 JDK 在运行时自动生成这些实现类就好了。

动态代理只需要你描述这个"固定模板":

java 复制代码
// MapperProxy.java ------ 描述"所有方法都应该怎么处理"
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 无论调用哪个方法,逻辑都一样:拿方法名 → 交给 sqlSession 执行
    return sqlSession.execute(method.getName(), args);
}

然后 JDK 替你生成 UserMapperImplOrderMapperImpl... 所有实现类,每个方法体都套用这个模板。你什么都不用写。

动态代理解决的核心问题: 把"接口实现类的编写工作"从编译期的人工重复劳动,变成运行时由 JDK 自动完成。


二、三个核心概念是什么?

2.1 Java 反射(Reflection)

是什么: 在运行时"审视"和"操作"一个类的能力,包括获取它的方法、字段、构造器,并动态调用。

讲解反射的博客:https://likerhood.github.io/2026/05/09/032 java反射和注解/

先问:为什么动态代理需要反射?

$Proxy0 生成之后,它的每个方法体里只有一行固定代码:

java 复制代码
public String selectById(int id) {
    return (String) h.invoke(this, m_selectById, new Object[]{id});
}

h.invoke() 需要告诉 MapperProxy:"你现在处理的是 selectById 这个方法,参数是 1"。但 invoke() 的签名是:

java 复制代码
Object invoke(Object proxy, Method method, Object[] args)

method 这个参数就是反射的 Method 对象------它是一张方法名片 ,携带了被调用方法的所有元信息。没有反射,MapperProxy 根本不知道当前被调用的是哪个方法。

反射是什么: 在运行时"审视"一个类的能力------可以把任何一个方法包装成 Method 对象,通过它读取方法名、参数类型、返回类型,并动态调用。

java 复制代码
// 普通调用:编译期写死,调用哪个方法在编译时就确定了
String result = userMapper.selectById(1);

// 反射调用:运行时动态决定,效果完全等价
Method method = UserMapper.class.getMethod("selectById", int.class);
Object result = method.invoke(userMapper, 1);

在本案例中反射做了什么:

java 复制代码
// MapperProxy.invoke() 里的核心一行
return sqlSession.execute(method.getName(), args);
//                         ↑
// method.getName() 从 Method 对象里读出方法名字符串
// 运行时才知道是 "selectById" 还是 "insert" 还是 "deleteById"
// 这就是为什么一个 invoke() 能处理所有方法------靠反射动态读名字

Method 对象能提供的信息:

java 复制代码
Method method = UserMapper.class.getMethod("selectById", int.class);

method.getName()             // "selectById"           ← MapperProxy 用这个转发
method.getParameterTypes()   // [int.class]
method.getReturnType()       // String.class
method.getDeclaringClass()   // interface UserMapper    ← 用这个过滤 Object 方法
method.getAnnotations()      // 注解数组               ← Spring AOP 用这个判断是否要增强

常用 API 速查:

关键 API 作用 本案例使用位置
Class.forName("全类名") 运行时按名字加载一个类 ---
clazz.getMethod("方法名", 参数类型) 获取 public 方法的 Method 对象 $Proxy0 静态块中
method.invoke(对象, 参数) 动态调用方法 MapperProxy.invoke() 核心
clazz.newInstance() 反射创建实例(无参构造) JDKProxyFactorycacheAdapter.newInstance()
method.getDeclaringClass() 拿到方法所属的类 invoke() 中过滤 Object 方法

2.2 ClassLoader 类加载器

先问:为什么动态代理需要 ClassLoader?

$Proxy0 是 JDK 在内存里动态生成的,磁盘上没有这个 .class 文件。JVM 只认识被加载进方法区的类,一个类如果没被加载,就根本不存在。

问题来了:普通类可以从磁盘读 .class 文件加载,$Proxy0 的字节码只在内存里,谁来把它加载进 JVM?

答案就是你传入的 ClassLoader

ClassLoader 是什么: JVM 把字节码读入内存的"搬运工"。每个类都由某个 ClassLoader 负责加载,加载后以 Class 对象的形式存放在 JVM 方法区,你才能 new 出实例。

直接加载类和动态加载类的区别如图所示:

Java 中普通类与动态代理类(如 $Proxy0)在 JVM 底层运作机制上的三大核心差异:

  1. 出处不同(输入源)
    • 普通类 :来自静态编译期,作为 .class 文件真实存在于磁盘上。
    • 动态代理类 :没有实体文件,是在程序运行时由代码(ProxyGenerator)在内存里临时拼凑生成的字节数组(byte[])。
  2. 加载路径不同(类加载器)
    • 两者都要接受"双亲委派"机制的审查,但加载动作有别。
    • 普通类 :使用 loadClass() 进行常规的磁盘 I/O 读取。
    • 动态代理类 :因为只存在于内存,只能调用底层的 defineClass() 方法,将字节数组强行注入给加载器以获得合法身份。
  3. 诞生方式不同(JVM 内存)
    • 它们最终都会在"方法区"注册为 Class 元数据。但在"堆内存"中生成具体实例时:
    • 普通类 :通过熟悉的 new 关键字直接创建。
    • 动态代理类 :只能通过反射(newInstance)强行创建,并在其内部偷偷塞入一个核心的拦截器(InvocationHandler)。

加载任何一个类时,都先问父加载器"你有没有这个类",父加载器找不到才自己加载。这叫双亲委派 ,目的是防止你自己写一个 java.lang.String 去替换核心类。

动态代理中 ClassLoader 的特殊角色:

普通类的加载流程:磁盘有 .class 文件 → ClassLoader 读取 → 加载进方法区。

$Proxy0 根本没有 .class 文件,它的字节码是 Proxy内存中动态生成的:

这就是为什么 Proxy.newProxyInstance 需要传入 classLoader------它要告诉 JVM:用这个管理员,把我动态生成的这本"新书"放进阅览室

java 复制代码
// 获取当前线程的类加载器(动态代理常用,能加载业务类)
ClassLoader cl = Thread.currentThread().getContextClassLoader();

// 获取某个类是由哪个 ClassLoader 加载的
ClassLoader cl = UserMapper.class.getClassLoader();

// 获取系统类加载器(= AppClassLoader)
ClassLoader cl = ClassLoader.getSystemClassLoader();

// 获取父加载器(AppClassLoader → ExtClassLoader → null[BootstrapCL])
ClassLoader parent = cl.getParent();

2.3 JDK 动态代理

前两节解决了两个子问题:

  • 反射解决了:运行时如何知道调用了哪个方法、如何动态转发
  • ClassLoader 解决了:动态生成的类如何合法地进入 JVM

动态代理就是把这两者整合起来的最终执行器

Proxy.newProxyInstance 三参数,每个对应一个子问题:

java 复制代码
Proxy.newProxyInstance(
    classLoader,                    // → 交给 ClassLoader 解决"如何加载 $Proxy0"
    new Class[]{UserMapper.class},  // → 告诉 ProxyGenerator 生成什么结构的类
    mapperProxy                     // → 交给反射和 InvocationHandler 解决"方法如何转发"
)

$Proxy0 内部结构------三者协作的汇聚点:

java 复制代码
// ProxyGenerator 生成的 $Proxy0(反编译后结构)
public final class $Proxy0 extends Proxy implements UserMapper {

    // ① 反射在这里:静态块缓存每个方法的 Method 对象
    private static Method m_selectById;
    static {
        m_selectById = UserMapper.class.getMethod("selectById", int.class);
    }

    // ② ClassLoader 在这里:这个类的字节码由 classLoader.defineClass() 加载进 JVM

    // ③ InvocationHandler 在这里:构造时注入,每个方法体调用 h.invoke()
    public $Proxy0(InvocationHandler h) {
        super(h);  // h 存入父类 Proxy 的 protected 字段
    }

    @Override
    public String selectById(int id) {
        // 方法体固定模板:把"谁、什么方法、什么参数"打包扔给 h
        return (String) h.invoke(this, m_selectById, new Object[]{id});
        //                ↑ h = MapperProxy,invoke() 里用反射拿方法名转发
    }
}

三概念协作的完整时序:

复制代码
你写的代码:
  UserMapper userMapper = MapperProxyFactory.getMapper(UserMapper.class, sqlSession)
                                                   ↓
                              Proxy.newProxyInstance(classLoader,
                                                     [UserMapper.class],  ← 接口
                                                     mapperProxy)         ← 拦截器
                                                   ↓
                          JVM 内存中生成 $Proxy0,由 classLoader 加载
                                                   ↓
                              返回 $Proxy0 实例,赋值给 userMapper

你调用:
  userMapper.selectById(1)
                 ↓
  $Proxy0.selectById(1)  ← 实际执行的是动态生成的代理类
                 ↓
  MapperProxy.invoke(proxy, method[selectById], args[1])  ← InvocationHandler 拦截
                 ↓
  sqlSession.execute("selectById", [1])  ← 反射拿到方法名,转发给真正的执行层
                 ↓
  返回 "张三"

三个核心角色的分工:

角色 职责 本案例对应
代理工厂 java.lang.reflect.Proxy 生成字节码、加载类、实例化 MapperProxyFactory 调用它
拦截器接口 java.lang.reflect.InvocationHandler 定义"所有方法被调用时做什么" MapperProxy 实现它
方法元信息 java.lang.reflect.Method 携带被拦截方法的所有信息 invoke() 的第二个参数
java 复制代码
public static Object newProxyInstance(
    ClassLoader loader,      // 参数1  类加载器:来把 $Proxy0 加载进 JVM
    Class<?>[] interfaces,   // 参数2  $Proxy0 要实现具体的接口?(决定它的类型)
    InvocationHandler h      // 参数3  方法调用交给谁处理
)

三者的协作流程(对应本案例):

整体流程:

复制代码
第1步:生成字节码
       ProxyGenerator.generateProxyClass(
           "$Proxy0",
           new Class[]{UserMapper.class}
       )
       → 在内存中生成 byte[] 字节码
       → 内容就是上面那个伪代码的二进制形式

第2步:加载进JVM
       classLoader.defineClass("$Proxy0", byteCode)
       → $Proxy0 的 Class 对象进入 JVM 方法区
       → 此刻 $Proxy0 作为一个"类"正式存在于 JVM 中

第3步:实例化
       Constructor c = $Proxy0.getConstructor(InvocationHandler.class)
       c.newInstance(mapperProxy)
       → 创建 $Proxy0 实例,把 mapperProxy 注入到 h 字段

第4步:返回
       return ($Proxy0实例)
       → 强转为 UserMapper,赋值给 mapper 变量------帮我把这个流程可视化,生成美观直观的xml代码

缺少任何一个都不行:没有 ClassLoader,生成的类没人加载;没有接口,不知道要生成哪些方法;没有 Handler,生成了方法却没有任何逻辑。


三、如何使用?--- 以模拟 MyBatis 为例

3.1 完整代码结构

复制代码
mybatisproxy/
├── UserMapper.java           ← 接口(无实现类)
├── SqlSession.java           ← 模拟数据库执行层
├── MapperProxy.java          ← InvocationHandler 实现(核心)
└── MapperProxyFactory.java   ← 代理工厂

3.2 定义接口

java 复制代码
// UserMapper.java
public interface UserMapper {
    String selectById(int id);
    void insert(String username);
    void deleteById(int id);
}
// 注意:没有任何实现类!

3.3 实现 InvocationHandler

java 复制代码
// MapperProxy.java ------ 核心:所有方法调用的拦截器
public class MapperProxy implements InvocationHandler {

    private SqlSession sqlSession;  // 真正执行 SQL 的组件

    public MapperProxy(SqlSession sqlSession) {
        this.sqlSession = sqlSession;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 过滤 toString/hashCode 等 Object 自带方法
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        }
        // 核心:用方法名 + 参数 转发给 SqlSession 执行
        return sqlSession.execute(method.getName(), args);
    }
}

invoke 三个参数的含义:

java 复制代码
invoke(Object proxy, Method method, Object[] args)
//        ↓               ↓               ↓
//   代理对象本身    被调用方法的元信息    传入的参数
//   ($Proxy0)     (selectById)           ([1])

3.4 创建代理工厂

java 复制代码
// MapperProxyFactory.java
public class MapperProxyFactory {
    public static <T> T getMapper(Class<T> mapperClass, SqlSession sqlSession) {
        MapperProxy mapperProxy = new MapperProxy(sqlSession);
        return (T) Proxy.newProxyInstance(
            Thread.currentThread().getContextClassLoader(), // ① 类加载器
            new Class[]{mapperClass},                       // ② 要实现的接口
            mapperProxy                                     // ③ 拦截器
        );
    }
}

3.5 模拟数据库执行层

java 复制代码
// SqlSession.java
public class SqlSession {
    private static Map<Integer, String> database = new HashMap<>();
    static {
        database.put(1, "张三");
        database.put(2, "李四");
        database.put(3, "王五");
    }

    public Object execute(String methodName, Object[] args) {
        if ("selectById".equals(methodName)) {
            int id = (int) args[0];
            System.out.println("执行 SQL: SELECT * FROM user WHERE id = " + id);
            return database.get(id);
        }
        if ("insert".equals(methodName)) {
            int newId = database.size() + 1;
            database.put(newId, (String) args[0]);
            System.out.println("执行 SQL: INSERT INTO user VALUES (" + newId + ", '" + args[0] + "')");
            return null;
        }
        if ("deleteById".equals(methodName)) {
            database.remove((int) args[0]);
            System.out.println("执行 SQL: DELETE FROM user WHERE id = " + args[0]);
            return null;
        }
        throw new RuntimeException("未知方法: " + methodName);
    }
}

3.6 测试代码

java 复制代码
public class MyBatisProxyTest {

    /**
     * 测试1:核心流程验证
     * 验证:调用方式和有实现类时完全一样,但背后走的是动态代理
     */
    @Test
    public void test_basicProxy() {
        SqlSession sqlSession = new SqlSession();

        // getMapper 返回的是 $Proxy0,不是任何手写实现类
        UserMapper userMapper = MapperProxyFactory.getMapper(UserMapper.class, sqlSession);

        // 看起来普通,实际上每次调用都经过 MapperProxy.invoke()
        String user = userMapper.selectById(1);
        System.out.println("查到的用户: " + user);  // 张三

        userMapper.insert("赵六");
        System.out.println("插入后查询: " + userMapper.selectById(4));  // 赵六

        userMapper.deleteById(2);
        System.out.println("删除后查询: " + userMapper.selectById(2));  // null
    }

    /**
     * 测试2:验证代理对象的真实身份
     * 关键:证明 userMapper 不是任何手写类,而是 JDK 动态生成的 $Proxy0
     */
    @Test
    public void test_proxyIdentity() {
        SqlSession sqlSession = new SqlSession();
        UserMapper userMapper = MapperProxyFactory.getMapper(UserMapper.class, sqlSession);

        // $Proxy0 的真实类名
        System.out.println("真实类名: " + userMapper.getClass().getName());
        // 输出: com.sun.proxy.$Proxy0

        // 虽然是 $Proxy0,但确实实现了 UserMapper 接口
        System.out.println("instanceof UserMapper: " + (userMapper instanceof UserMapper));
        // 输出: true

        // 但它不是任何手写实现类
        // System.out.println(userMapper instanceof UserMapperImpl); // 编译报错,该类根本不存在
    }

    /**
     * 测试3:反射 Method 对象的能力
     * 展示 invoke() 中 method 参数能告诉你什么信息
     */
    @Test
    public void test_reflectionMethod() throws Exception {
        Method method = UserMapper.class.getMethod("selectById", int.class);

        System.out.println("方法名: " + method.getName());
        // 输出: selectById

        System.out.println("参数类型: " + Arrays.toString(method.getParameterTypes()));
        // 输出: [int]

        System.out.println("返回类型: " + method.getReturnType());
        // 输出: class java.lang.String

        System.out.println("所属接口: " + method.getDeclaringClass());
        // 输出: interface com.likerhood.design.mybatisproxy.UserMapper
    }

    /**
     * 测试4:ClassLoader 加载动态代理类的过程
     * 展示 $Proxy0 是被哪个 ClassLoader 加载的
     */
    @Test
    public void test_classLoader() {
        SqlSession sqlSession = new SqlSession();
        UserMapper userMapper = MapperProxyFactory.getMapper(UserMapper.class, sqlSession);

        ClassLoader proxyClassLoader = userMapper.getClass().getClassLoader();
        ClassLoader appClassLoader = Thread.currentThread().getContextClassLoader();

        System.out.println("代理类的 ClassLoader: " + proxyClassLoader);
        System.out.println("当前线程的 ClassLoader: " + appClassLoader);
        System.out.println("是否同一个: " + (proxyClassLoader == appClassLoader));
        // 输出: true ------ $Proxy0 由 AppClassLoader 加载,和普通类一样
    }
}

运行 test_basicProxy 输出:

复制代码
执行 SQL: SELECT * FROM user WHERE id = 1
查到的用户: 张三
执行 SQL: INSERT INTO user VALUES (4, '赵六')
插入后查询: 赵六
执行 SQL: DELETE FROM user WHERE id = 2
删除后查询: null

总结

动态代理并非魔法,而是三种 Java 底层技术的组合拳,每一层都有明确分工:

技术 解决的问题 在动态代理中的角色
反射 运行时如何知道调用了哪个方法 把方法包装成 [Method] 动态转发
ClassLoader 内存中生成的类如何进入 JVM 通过 defineClass() 将动态字节码注册进方法区,赋予 $Proxy0 合法身份
动态代理 如何消除重复的接口实现类 整合前两者,运行时自动生成 $Proxy0,所有方法体转交 [InvocationHandler]处理

三者的依赖关系是单向的:动态代理依赖 ClassLoader 加载类,依赖反射传递方法信息,彼此分工而不耦合。

回到最初的问题:为什么 UserMapper 没有实现类却能调用?

Proxy.newProxyInstance\]在运行时替你写了 `UserMapperImpl`,叫做 `$Proxy0`。它的每个方法体只有一行------\[h.invoke()\],把调用转发给你的 `MapperProxy`,再由 `MapperProxy` 用反射读出方法名,交给 `SqlSession` 执行 SQL。你感知不到任何代理的存在,这正是动态代理的价值所在。

掌握这套机制之后,MyBatis 的 MapperProxy、Spring AOP 的事务切面、Dubbo 的远程调用客户端,它们的核心原理说都是同一件事。

相关推荐
_阿伟_1 小时前
信息检索简单介绍
java
下次再写1 小时前
深入浅出微服务架构:从理论到Spring Boot实战
java·微服务·springboot·springcloud·架构设计·后端开发·分布式系统
进阶的猿猴1 小时前
Rsa简单实现接口到期限制(springBoot)
java·spring boot·后端
雨落在了我的手上1 小时前
初识java(二):数据类型与变量
java·开发语言
小闫BI设源码1 小时前
当20个节点选出两个Master时:Elasticsearch的致命故障与解决方案
java·elasticsearch·jenkins·php·面试宝典·深入解析
SamDeepThinking1 小时前
千万级用户购物车系统的架构设计
java·后端·架构
liwulin05061 小时前
【JAVAFX】从ORACLE JDK切换到国内的JDK以便使用JAVAFX功能
java·数据库·oracle
广师大-Wzx2 小时前
JavaWeb:后端部分
java·开发语言·spring·servlet·tomcat·maven·mybatis
dishugj2 小时前
HANA数据库常用命令总结
java·前端·数据库