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,勉强可以接受。但真实项目里有 UserMapper、OrderMapper、ProductMapper... 几十个 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 替你生成 UserMapperImpl、OrderMapperImpl... 所有实现类,每个方法体都套用这个模板。你什么都不用写。

动态代理解决的核心问题: 把"接口实现类的编写工作"从编译期的人工重复劳动,变成运行时由 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() 反射创建实例(无参构造) JDKProxyFactory 中 cacheAdapter.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 的远程调用客户端,它们的核心原理说都是同一件事。

相关推荐
晒太羊的猫2 分钟前
hot100子串
java·算法·leetcode
开开心心就好26 分钟前
本地文件搜索工具,双击Ctrl就能呼出搜索
java·服务器·前端·支持向量机·ffmpeg·音视频·模拟退火算法
独隅31 分钟前
Claude 高效使用指南深度评测
java·开发语言·claude·claude code
ByteCoder.38 分钟前
班前十分钟的清单,按体检引导单来写
java·服务器·前端·助播虾
海的透彻40 分钟前
如何让docker容器启动时用自定义的用户
java·docker·dubbo
疯狂学习GIS1 小时前
将谷歌地球引擎账号升级为Contributor与Partner层级的方法
java·数据库·人工智能
GG.WinGel1 小时前
质数数目(蓝桥杯练习题)
java·数据结构·算法·青少年编程·蓝桥杯
泡泡糖的中文规格书1 小时前
HDLbits 2.2 学习笔记 · 多路复用器
java·开发语言
John_Hui1 小时前
第 3 篇:配置 JDK 17 环境
java·开发语言·ubuntu
秋天的怀念_David1 小时前
算法一千零一夜 · 第十七夜 长街点灯
java·程序人生·算法