代理模式:从 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。如果方法是 void,method.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 会把这次调用转发到 InvocationHandler 的 invoke 方法中。
调用链可以理解为:
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 后端框架中最重要、最常见的底层思想之一。