Java-19 深入浅出MyBatis 代理模式:从 Java 动态代理到 Mapper 接口的底层原理

代理模式:从 Java 动态代理到 MyBatis Mapper 接口的底层原理

一、代理模式是什么

代理模式,英文是 Proxy Pattern。

它的核心思想是:不直接访问真实对象,而是通过一个代理对象间接访问真实对象

代理对象和真实对象通常实现同一个接口。调用方表面上调用的是代理对象,但代理对象内部会把请求转发给真实对象。在转发前后,代理对象可以额外做一些增强逻辑,例如日志、权限校验、事务控制、缓存、远程调用、延迟加载等。

简单理解:

text 复制代码
调用方 -> 代理对象 -> 真实对象

代理模式是一种对象结构型设计模式。它关注的不是对象如何创建,而是如何控制对象之间的访问关系。

在 Java 后端开发中,代理模式非常常见。Spring AOP、Spring 事务、MyBatis Mapper 接口、RPC 框架、远程调用客户端,本质上都大量使用了代理思想。


二、代理模式的主要角色

代理模式一般包含三个核心角色。

1. 抽象主题 Subject

抽象主题定义真实对象和代理对象的共同接口。

调用方只依赖这个接口,不关心背后到底是真实对象还是代理对象。

java 复制代码
public interface Person {
    void doSomething();
}

2. 真实主题 RealSubject

真实主题是真正执行业务逻辑的对象。

java 复制代码
public class Bob implements Person {

    @Override
    public void doSomething() {
        System.out.println("Bob do something!");
    }
}

3. 代理对象 Proxy

代理对象也实现同一个接口,内部持有真实对象的引用。

它可以在调用真实对象之前或之后执行额外逻辑。

text 复制代码
代理对象 = 控制访问 + 增强逻辑 + 转发调用

三、代理模式的常见分类

1. 远程代理 Remote Proxy

远程代理用于访问位于不同地址空间的对象。

调用方看起来像是在调用本地对象,实际上代理对象会把请求转换成网络请求,发送到远程服务。

典型例子:

text 复制代码
RMI
RPC
Dubbo
Feign
gRPC Client

本质上,调用方拿到的不是远程对象本身,而是一个本地代理。


2. 虚拟代理 Virtual Proxy

虚拟代理用于延迟创建开销较大的对象。

只有在真正需要使用真实对象时,才创建它。

典型例子:

text 复制代码
图片懒加载
大文件延迟加载
复杂对象延迟初始化

例如,一个图片对象加载成本很高,页面初始化时可以先创建代理对象,等用户真正查看图片时再加载真实图片。


3. 保护代理 Protection Proxy

保护代理用于控制访问权限。

代理对象会在调用真实对象前进行权限判断。

典型例子:

text 复制代码
文件权限控制
接口鉴权
角色访问控制

例如,普通用户只能读取文件,管理员才能删除文件,这种访问控制可以放在代理对象中。


4. 智能引用代理 Smart Reference Proxy

智能引用代理会在访问真实对象时附加额外操作。

典型例子:

text 复制代码
日志记录
调用计数
缓存
事务控制
资源释放
性能监控

Spring AOP 和 Spring 声明式事务就可以理解为智能引用代理的典型应用。


四、代理模式的优点

1. 职责分离

真实对象只关注自己的核心业务逻辑。

代理对象负责处理访问控制、日志、事务、缓存等横切逻辑。

这样可以避免把大量非核心逻辑写进业务类中。


2. 增强功能

代理对象可以在不修改真实对象代码的情况下,对原有方法进行增强。

例如:

text 复制代码
方法执行前打印日志
方法执行后记录耗时
方法异常时统一处理
方法执行前开启事务
方法执行后提交事务

这就是 AOP 的基础思想。


3. 控制访问

代理对象可以决定是否允许访问真实对象。

例如:

text 复制代码
权限校验
参数校验
限流
熔断
缓存命中直接返回

调用方不需要知道这些控制逻辑,只需要正常调用接口。


4. 支持延迟加载

对于创建成本高的对象,可以通过代理对象实现按需创建。

这样可以减少系统启动成本和资源占用。


五、代理模式的缺点

1. 类数量增加

静态代理需要为每个真实对象编写对应的代理类,类数量会增加。

如果接口很多、方法很多,维护成本会明显上升。


2. 调用链变长

调用方不再直接访问真实对象,而是通过代理对象访问。

这会让调用链路变长,排查问题时需要理解代理层逻辑。


3. 可能带来性能开销

动态代理通常会使用反射、方法分发、拦截器链等机制。

在大多数业务系统中,这点开销可以接受,但在极端高性能场景下需要关注。


六、静态代理示例

先定义一个接口:

java 复制代码
package icu.wzk.proxy;

public interface Person {
    void doSomething();
}

定义真实对象:

java 复制代码
package icu.wzk.proxy;

public class Bob implements Person {

    @Override
    public void doSomething() {
        System.out.println("Bob do something!");
    }
}

定义代理对象:

java 复制代码
package icu.wzk.proxy;

public class PersonStaticProxy implements Person {

    private final Person target;

    public PersonStaticProxy(Person target) {
        this.target = target;
    }

    @Override
    public void doSomething() {
        System.out.println("Before calling doSomething");
        target.doSomething();
        System.out.println("After calling doSomething");
    }
}

测试代码:

java 复制代码
package icu.wzk.proxy;

public class StaticProxyTest {

    public static void main(String[] args) {
        Person bob = new Bob();

        Person proxy = new PersonStaticProxy(bob);

        proxy.doSomething();
    }
}

输出结果:

text 复制代码
Before calling doSomething
Bob do something!
After calling doSomething

静态代理的特点是:代理类在编译期就已经写死。

它的优点是简单、直观。

它的缺点也很明显:如果有很多接口、很多方法,就需要写大量代理类,扩展性较差。


七、JDK 动态代理示例

JDK 动态代理可以在运行时生成代理对象,不需要手动编写代理类。

它的核心类和接口是:

text 复制代码
java.lang.reflect.Proxy
java.lang.reflect.InvocationHandler

1. 接口 Person

java 复制代码
package icu.wzk.proxy;

public interface Person {
    void doSomething();
}

2. 真实对象 Bob

java 复制代码
package icu.wzk.proxy;

public class Bob implements Person {

    @Override
    public void doSomething() {
        System.out.println("Bob do something!");
    }
}

3. JDK 动态代理处理器

java 复制代码
package icu.wzk.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class JdkDynamicProxy implements InvocationHandler {

    private final Object target;

    public JdkDynamicProxy(Object target) {
        this.target = target;
    }

    public Object getProxy() {
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                this
        );
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before calling " + method.getName());

        Object result = method.invoke(target, args);

        System.out.println("After calling " + method.getName());

        return result;
    }
}

这里需要注意一个细节:

java 复制代码
Object result = method.invoke(target, args);

不要把返回值强转成 Person

因为被代理的方法可能返回任意类型,也可能是 void。如果方法是 voidmethod.invoke 的返回值就是 null

所以这里应该使用 Object 接收返回值。


4. 测试代码

java 复制代码
package icu.wzk.proxy;

public class JdkDynamicProxyTest {

    public static void main(String[] args) {
        Person bob = new Bob();

        System.out.println("直接调用真实对象:");
        bob.doSomething();

        System.out.println("========================");

        System.out.println("通过代理对象调用:");
        JdkDynamicProxy jdkDynamicProxy = new JdkDynamicProxy(bob);
        Person proxyBob = (Person) jdkDynamicProxy.getProxy();

        proxyBob.doSomething();
    }
}

输出结果:

text 复制代码
直接调用真实对象:
Bob do something!
========================
通过代理对象调用:
Before calling doSomething
Bob do something!
After calling doSomething

八、JDK 动态代理的调用过程

这段代码是关键:

java 复制代码
Person proxyBob = (Person) jdkDynamicProxy.getProxy();
proxyBob.doSomething();

表面上看,调用的是 proxyBob.doSomething()

但实际上,proxyBob 并不是 Bob 对象,而是 JDK 在运行时生成的代理对象。

当调用 proxyBob.doSomething() 时,JDK 会把这次调用转发到 InvocationHandlerinvoke 方法中。

调用链可以理解为:

text 复制代码
proxyBob.doSomething()
        ↓
JDK 代理对象拦截方法调用
        ↓
JdkDynamicProxy.invoke(proxy, method, args)
        ↓
method.invoke(target, args)
        ↓
Bob.doSomething()

所以,动态代理真正重要的不是代理类本身,而是这个统一入口:

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

所有被代理接口的方法调用,最终都会进入这个 invoke 方法。

这也是 MyBatis Mapper 接口可以不用写实现类的关键基础。


九、JDK 动态代理的限制

JDK 动态代理有一个重要限制:

text 复制代码
只能代理接口,不能直接代理普通类。

因为 JDK 动态代理生成的代理类,本质上是实现了目标对象的接口。

如果一个类没有实现接口,就不能直接使用 JDK 动态代理。

如果想代理普通类,通常会使用 CGLIB、Byte Buddy 这类字节码增强技术。

简单对比:

text 复制代码
JDK 动态代理:基于接口
CGLIB:基于继承生成子类

Spring AOP 中,如果目标对象实现了接口,默认可以使用 JDK 动态代理;如果没有接口,则通常会使用 CGLIB。


十、MyBatis 中的代理模式

MyBatis 是代理模式非常典型的应用场景。

在 MyBatis 中,我们通常只需要写 Mapper 接口:

java 复制代码
public interface UserMapper {

    User selectById(Long id);
}

但我们并没有写 UserMapperImpl 实现类。

正常情况下,Java 接口不能直接调用,因为接口没有方法实现。

但是在 MyBatis 中,我们可以这样使用:

java 复制代码
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.selectById(1L);

这里的 userMapper 并不是一个真实的手写实现类,而是 MyBatis 在运行时生成的代理对象。


十一、MyBatis 获取 Mapper 代理对象的过程

当我们调用:

java 复制代码
sqlSession.getMapper(UserMapper.class);

底层大致会经过这样的流程:

text 复制代码
SqlSession.getMapper(...)
        ↓
Configuration.getMapper(...)
        ↓
MapperRegistry.getMapper(...)
        ↓
MapperProxyFactory.newInstance(...)
        ↓
Proxy.newProxyInstance(...)
        ↓
生成 Mapper 接口的 JDK 动态代理对象

也就是说,MyBatis 会为 Mapper 接口创建一个 JDK 动态代理对象。

这个代理对象实现了 Mapper 接口。

所以我们才能像调用普通对象一样调用 Mapper 方法。


十二、MapperProxyFactory 的作用

MyBatis 中有一个关键类叫 MapperProxyFactory

下面是简化后的代码,用来说明核心逻辑:

java 复制代码
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class MapperProxyFactory<T> {

    private final Class<T> mapperInterface;

    private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();

    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    public Class<T> getMapperInterface() {
        return mapperInterface;
    }

    public Map<Method, MapperMethod> getMethodCache() {
        return methodCache;
    }

    @SuppressWarnings("unchecked")
    protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(
                mapperInterface.getClassLoader(),
                new Class[]{mapperInterface},
                mapperProxy
        );
    }

    public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<>(
                sqlSession,
                mapperInterface,
                methodCache
        );

        return newInstance(mapperProxy);
    }
}

这段代码里最关键的是:

java 复制代码
Proxy.newProxyInstance(
        mapperInterface.getClassLoader(),
        new Class[]{mapperInterface},
        mapperProxy
);

它生成了一个实现 mapperInterface 接口的代理对象。

其中第三个参数 mapperProxy 是一个 InvocationHandler

这意味着:以后调用 Mapper 接口中的任意方法,都会进入 MapperProxy.invoke 方法。


十三、MapperProxy 的作用

MapperProxy 是 MyBatis Mapper 接口代理的核心。

它实现了 InvocationHandler 接口。

简化后的代码如下:

java 复制代码
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MapperProxy<T> implements InvocationHandler, Serializable {

    private final SqlSession sqlSession;

    private final Class<T> mapperInterface;

    private final Map<Method, MapperMethod> methodCache;

    public MapperProxy(SqlSession sqlSession,
                       Class<T> mapperInterface,
                       Map<Method, MapperMethod> methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
            }

            if (isDefaultMethod(method)) {
                return invokeDefaultMethod(proxy, method, args);
            }
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }

        final MapperMethod mapperMethod = cachedMapperMethod(method);

        return mapperMethod.execute(sqlSession, args);
    }

    private MapperMethod cachedMapperMethod(Method method) {
        return methodCache.computeIfAbsent(
                method,
                k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())
        );
    }
}

这段代码的核心逻辑是:

java 复制代码
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);

也就是说,Mapper 接口方法最终并不是由某个 UserMapperImpl 执行的,而是被转换成了一个 MapperMethod,然后通过 SqlSession 执行。


十四、Mapper 接口方法是如何变成 SQL 执行的

假设我们有如下 Mapper 接口:

java 复制代码
public interface UserMapper {

    User selectById(Long id);
}

调用代码:

java 复制代码
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.selectById(1L);

真实调用链大致是:

text 复制代码
userMapper.selectById(1L)
        ↓
JDK 动态代理对象拦截调用
        ↓
MapperProxy.invoke(proxy, method, args)
        ↓
根据 method 获取 MapperMethod
        ↓
MapperMethod.execute(sqlSession, args)
        ↓
SqlSession.selectOne / selectList / insert / update / delete
        ↓
Executor 执行 SQL
        ↓
StatementHandler 处理 JDBC Statement
        ↓
ParameterHandler 设置 SQL 参数
        ↓
ResultSetHandler 封装查询结果
        ↓
返回 User 对象

这就是 MyBatis 中 Mapper 接口不需要实现类的原因。

因为真正执行 SQL 的不是 Mapper 接口实现类,而是 MyBatis 通过代理对象接管了接口方法调用。


十五、MapperMethod 的作用

MapperMethod 可以理解为 Mapper 接口方法和 SQL 执行之间的适配器。

它负责分析当前方法应该执行什么类型的 SQL。

例如:

text 复制代码
selectById -> SELECT
insertUser -> INSERT
updateUser -> UPDATE
deleteById -> DELETE

它还会处理方法参数、返回值类型、SQL 命令类型等信息。

例如:

java 复制代码
User selectById(Long id);

MyBatis 需要知道:

text 复制代码
这是 SELECT 还是 INSERT?
SQL 语句 id 是什么?
参数如何绑定?
返回值是单个对象还是集合?
是否返回 Optional?
是否返回 Map?

这些都不是 Mapper 接口自己完成的,而是由 MyBatis 在运行时解析并执行。


十六、为什么 MyBatis 要缓存 MapperMethod

MapperProxyFactory 中有一个缓存:

java 复制代码
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();

这个缓存的作用是避免每次调用 Mapper 方法时都重新解析方法元信息。

因为 Mapper 方法的结构是固定的。

例如:

java 复制代码
User selectById(Long id);

这个方法对应的 SQL 命令类型、参数结构、返回值类型,在运行期间不会频繁变化。

所以 MyBatis 会在第一次调用时创建 MapperMethod,后续调用直接从缓存中获取。

这是一种典型的性能优化。


十七、MyBatis 代理模式的本质

MyBatis 使用代理模式解决了一个核心问题:

text 复制代码
只有接口,没有实现类,如何执行数据库操作?

答案是:

text 复制代码
运行时为 Mapper 接口创建代理对象。

这个代理对象实现了 Mapper 接口。

当调用 Mapper 方法时,代理对象拦截方法调用,然后根据方法信息找到对应的 SQL,最终通过 SqlSession 和 Executor 执行数据库操作。

所以 MyBatis 的 Mapper 机制可以总结为:

text 复制代码
Mapper 接口不是实现类。
Mapper 接口是 SQL 调用入口。
Mapper 代理对象负责接管接口方法调用。
MapperProxy 负责把方法调用转成 SQL 执行。

十八、代理模式和 MyBatis 的关系总结

代理模式在 MyBatis 中主要体现在以下几个方面:

1. Mapper 接口没有实现类

开发者只需要定义接口:

java 复制代码
public interface UserMapper {
    User selectById(Long id);
}

不需要手写:

java 复制代码
public class UserMapperImpl implements UserMapper {
    // ...
}

实现类由 MyBatis 在运行时通过 JDK 动态代理生成。


2. Mapper 方法调用被统一拦截

所有 Mapper 接口方法的调用都会进入:

java 复制代码
MapperProxy.invoke(...)

这个方法就是 MyBatis Mapper 代理的统一入口。


3. 方法调用被转换成 SQL 执行

Mapper 方法本身没有方法体。

MyBatis 会根据 Mapper 接口方法、XML 配置或注解配置,找到对应的 SQL 语句,然后执行。


4. 代理对象隐藏了复杂的 JDBC 操作

开发者调用的是:

java 复制代码
userMapper.selectById(1L);

但底层真正发生的是:

text 复制代码
获取 SQL
绑定参数
创建 Statement
执行 JDBC
处理 ResultSet
封装返回对象

代理对象把这些复杂细节全部隐藏起来。


十九、代理模式在后端开发中的常见应用

代理模式不是 MyBatis 独有的。

在 Java 后端开发中,它非常常见。

1. Spring AOP

Spring AOP 会通过代理对象增强目标方法。

典型场景:

text 复制代码
日志
权限
监控
异常处理
接口耗时统计

2. Spring 声明式事务

使用 @Transactional 时,Spring 会通过代理对象在方法执行前后处理事务。

大致逻辑是:

text 复制代码
方法执行前开启事务
方法执行成功后提交事务
方法执行异常后回滚事务

这也是代理模式的典型应用。


3. RPC 框架

RPC 框架中的客户端调用通常也是代理模式。

例如调用:

java 复制代码
userService.getUserById(1L);

表面上像本地方法调用,实际上代理对象会把请求发送到远程服务。


4. Feign

Spring Cloud OpenFeign 也是典型代理模式。

开发者只需要写接口:

java 复制代码
@FeignClient("user-service")
public interface UserClient {

    @GetMapping("/user/{id}")
    User getUser(@PathVariable Long id);
}

实际调用时,Feign 会为这个接口生成代理对象。

方法调用最终会被转换成 HTTP 请求。


二十、总结

代理模式的核心是:

text 复制代码
通过代理对象控制对真实对象的访问。

它可以在不修改真实对象代码的情况下,对方法调用进行增强、控制和转发。

静态代理需要手写代理类,简单但扩展性较差。

JDK 动态代理可以在运行时生成代理对象,适合基于接口的代理场景。

MyBatis 的 Mapper 接口机制就是代理模式的典型应用。

我们没有编写 Mapper 接口的实现类,但依然可以调用 Mapper 方法,是因为 MyBatis 在运行时通过 Proxy.newProxyInstance 创建了 Mapper 接口的代理对象。

当调用 Mapper 方法时,真正执行的不是接口本身,而是:

text 复制代码
MapperProxy.invoke(...)
        ↓
MapperMethod.execute(...)
        ↓
SqlSession
        ↓
Executor
        ↓
JDBC

所以,理解代理模式之后,再看 MyBatis、Spring AOP、Spring 事务、Feign、RPC 框架,很多底层原理都会变得更加清晰。

代理模式不是一个停留在设计模式书里的概念,而是 Java 后端框架中最重要、最常见的底层思想之一。

相关推荐
devilnumber2 小时前
Java Lambda方法引用的三类核心类型、转化逻辑与深度对比
java·开发语言
郑洁文2 小时前
基于Springboot的足球青训俱乐部管理系统的设计与实现
java·spring boot·后端·足球青训俱乐部管理系统
阿聪谈架构2 小时前
第14章:多模态AI实战 —— 让AI"看懂"图片和文档
人工智能·后端
Oneslide2 小时前
rsync 大数据量同步中断问题
后端
云烟成雨TD2 小时前
Spring AI 1.x 系列【39】MCP Java SDK 与 Spring AI 集成
java·人工智能·spring
极客先躯2 小时前
高级java每日一道面试题-2026年01月19日-实战篇[Docker]-如何配置镜像仓库的垃圾回收 (GC)?
java·运维·docker·容器
我登哥MVP2 小时前
Spring Boot 从“会用”到“精通”:自定义参数绑定原理
java·spring boot·后端·spring·servlet·maven·intellij-idea
Elias不吃糖2 小时前
AI Resume Forge:基于 LangGraph 的 AI 简历优化与模拟面试平台
java·人工智能·面试·agent开发
Pikachu8032 小时前
我在早高峰地铁里对手机吼了几句,隔壁同事直接看傻了
前端·后端