深度剖析Mybatis的动态代理机制
深度剖析Mybatis的动态代理机制
Mybatis的动态代理机制是其整个框架的灵魂和魔法核心。理解它,你就不再是简单地“使用”Mybatis,而是真正“看透”了Mybatis。为什么我们只写一个接口,不需要写实现类,就能调用数据库?答案全在动态代理里。
此教程将为你深度剖析动态代理,从“为什么”出发,结合例子和代码,让你彻底征服这个概念。
Mybatis动态代理机制深度剖析与实战
引子:一个神奇的现象
在我们之前的学习中,我们都遵循一个模式:
- 定义一个
UserMapper.java接口。 - 在
UserMapper.xml或注解中编写SQL。 - 通过
sqlSession.getMapper(UserMapper.class)获取一个mapper对象。 - 然后直接调用
mapper.findById(1)等方法,就拿到了数据库的数据。
奇怪之处在于:我们从未编写过UserMapper接口的任何实现类(implements UserMapper)!
那么,sqlSession.getMapper(...)返回的这个mapper对象到底是什么?它为什么能执行数据库操作?
答案就是:Mybatis通过Java的动态代理技术,在运行时为你的UserMapper接口动态地创建了一个代理实现类。
第一部分:深度剖C析 —— 为什么需要动态代理?
1. 生活中的“代理”:租房中介
在深入技术之前,我们先用一个生活中的例子来理解“代理”的思想。
- 你 (调用者/程序员):想租一套房子。
- 房东 (目标对象/数据库):有很多房源,但你可能不认识他们,也不想一个个去谈。
- 租房中-介 (代理对象):它没有自己的房子,但它知道所有房东的信息。它有一个标准的“租房”服务流程。
你的租房过程是这样的:
- 你找到中介,告诉他你的需求(“我要一个两室一厅,在市中心”)。你并没有直接和房东对话。
- 中介接到你的需求后,会在内部进行一系列操作:筛选房源、联系房东、带你看房、协商价格...
- 最后,中介把符合你要求的房子的钥匙交给你。
在这个过程中,中介就是你的代理。它帮你完成了所有与房东打交道的繁琐工作,你只需要和中介这一个标准接口打交道即可。
2. 技术世界的“代理”:Mybatis的角色
现在,我们把角色映射到Mybatis中:
- 你 (调用者/程序员):想从数据库查询一个用户。
- 数据库 (目标对象):存储着数据,但操作它很繁琐(创建连接、写SQL、处理结果集、关闭资源...)。
- Mybatis生成的代理对象 (代理对象):它本身不包含业务逻辑,但它封装了所有与数据库交互的JDBC细节。
你调用Mybatis的过程是这样的:
- 你调用
mapper.findById(1)方法。你操作的是一个接口,你并不知道(也不需要知道)它的具体实现是什么。 - 这个调用实际上被Mybatis的代理对象拦截了。
- 代理对象在内部开始工作:
- 找到与
findById方法绑定的SQL语句。 - 获取一个数据库连接。
- 执行SQL,并将参数
1设置进去。 - 处理返回的
ResultSet,将其封装成一个User对象。 - 关闭连接等资源。
- 找到与
- 最后,代理对象将封装好的
User对象返回给你。
3. 核心优势:为什么动态代理这么牛?
-
解耦合与面向接口编程:
程序员的核心工作变成了定义清晰的接口(UserMapper)和声明要做什么(SQL语句)。我们完全不需要关心底层JDBC的实现细节。这使得我们的代码非常干净,只关注业务,不关注技术实现。这是面向接口编程思想的完美体现。 -
通用与可扩展 (AOP思想的雏形):
代理对象可以在执行真正的数据库操作之前和之后,加入一些通用的逻辑。比如:- 之前:开启事务、检查缓存、记录日志。
- 之后:提交/回滚事务、处理异常、将结果放入缓存。
这些逻辑对所有Mapper方法都是通用的,Mybatis通过代理机制,将这些通用功能“织入”了你的调用流程中,而你对此毫无感知。这就是面向切面编程 (AOP) 的核心思想。
第二部分:动手实践 —— 探究代理对象的真面目
光说不练假把式。我们来亲手编写代码,看看sqlSession.getMapper(...)返回的到底是个什么“怪物”。
1. 项目结构与代码准备
我们沿用之前的项目结构和代码,只需创建一个新的测试类即可。
-
项目结构
mybatis-cache-demo/ └── src/ └── main/ ├── java/ │ └── com/ │ └── example/ │ ├── ... (entity, mapper) │ └── test/ │ └── ProxyTest.java // 【新增】我们的代理探究测试类 └── resources/ └── ... (mybatis-config.xml, mappers/UserMapper.xml) -
创建
ProxyTest.java(核心探究代码)// src/main/java/com/example/test/ProxyTest.java package com.example.test; import com.example.mapper.UserMapper; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Proxy; public class ProxyTest { public static void main(String[] args) throws IOException { // 1. 加载Mybatis配置,创建SqlSessionFactory String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 2. 获取SqlSession SqlSession session = sqlSessionFactory.openSession(); // 3. 这就是魔法发生的地方! UserMapper userMapper = session.getMapper(UserMapper.class); // 4. 探究userMapper的真实身份! System.out.println("userMapper 的实际类型: " + userMapper.getClass().getName()); // 5. 验证它是否是JDK动态代理生成的对象 boolean isProxy = Proxy.isProxyClass(userMapper.getClass()); System.out.println("userMapper 是一个代理对象吗? " + isProxy); // 6. 正常调用方法,感受代理的威力 System.out.println("\n--- 调用代理对象的方法 ---"); System.out.println(userMapper.findById(1)); // 7. 关闭session session.close(); } }
2. 运行与结果分析
当你运行ProxyTest.java后,控制台会打印出类似下面的信息:
userMapper 的实际类型: com.sun.proxy.$Proxy4 <-- 看!它不是UserMapperImpl,而是一个奇怪的$Proxy类!
userMapper 是一个代理对象吗? true <-- 证实了它是一个代理对象!
--- 调用代理对象的方法 ---
User对象被创建了!(A new User object was created!)
User(id=1, username=admin, password=...)
结果分析:
com.sun.proxy.$Proxy4:这是Java的java.lang.reflect.Proxy类在运行时动态生成的一个类的名字。这个类在内存中被创建,它实现了UserMapper接口,但它的方法体并不是空的,而是包含了我们前面讨论的那些复杂的数据库交互逻辑。Proxy.isProxyClass(...)返回true,用代码的方式石锤了userMapper就是一个代理对象。
第三部分:底层机制 —— JDK动态代理是如何工作的?
Mybatis默认使用JDK动态代理。要理解它的工作原理,你需要知道两个核心的Java类:
java.lang.reflect.Proxy: 这是创建代理对象的工厂类。最核心的方法是Proxy.newProxyInstance(...)。java.lang.reflect.InvocationHandler: 这是一个接口,它只有一个方法invoke(Object proxy, Method method, Object[] args)。这是代理对象的核心逻辑所在。当你调用代理对象的任何方法时,最终都会被转发到这个invoke方法里来。
Mybatis的getMapper方法内部,大致做了如下事情(伪代码):
public <T> T getMapper(Class<T> type) {
// 1. 创建一个我们自己的InvocationHandler,Mybatis里它叫MapperProxy
MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, type);
// 2. 调用JDK的API,动态创建一个代理对象
return (T) Proxy.newProxyInstance(
type.getClassLoader(), // 使用被代理接口的类加载器
new Class[] { type }, // 告知代理对象要实现哪些接口 (这里就是UserMapper)
mapperProxy // 告知代理对象,当方法被调用时,执行哪个InvocationHandler的逻辑
);
}
而Mybatis的MapperProxy的invoke方法内部逻辑(伪代码)大概是这样的:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1. 如果调用的是Object的方法(如toString),就直接执行
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 2. 从缓存中查找这个方法是否已经处理过
MapperMethod mapperMethod = findFromCache(method);
// 3. 执行这个MapperMethod(这才是真正干活的)
return mapperMethod.execute(sqlSession, args);
}
MapperMethod.execute内部会根据SQL的类型(SELECT, INSERT等)去调用sqlSession的不同方法(selectOne, insert等),完成最终的数据库操作。
总结一下这个调用链:
mapper.findById(1) -> 代理对象的findById -> MapperProxy.invoke() -> MapperMethod.execute() -> sqlSession.selectOne() -> ...JDBC操作... -> 返回结果
企业级思考与总结
-
性能考量:动态代理是基于Java反射机制的,相比直接调用,会有一定的性能开销。但在数据库I/O这种耗时操作面前,这点性能开销几乎可以忽略不计。框架带来的开发效率和代码可维护性的提升,远比这点性能开销重要。
-
代理的选择 (JDK vs CGLIB):
- JDK动态代理 (Mybatis默认):要求被代理的类必须实现接口。
- CGLIB代理:不要求实现接口,它是通过创建被代理类的子类来实现的。所以如果你的目标类没有实现接口,Mybatis(以及Spring)会自动切换到CGLIB。
-
理解动态代理是理解Spring AOP的基础:Spring框架中强大的声明式事务 (
@Transactional)、日志、安全等切面功能,其底层实现的核心技术也是动态代理。今天你彻底搞懂了Mybatis的代理,将来学习Spring AOP就会易如反掌。
最终记忆要点:
- 为什么? 为了解耦,实现面向接口编程,让程序员只关心业务接口和SQL,而不用管繁琐的JDBC实现。
- 是什么?
sqlSession.getMapper()返回的是一个由JDK动态代理在运行时生成的代理对象,它实现了你的Mapper接口。 - 怎么做? Mybatis内部创建了一个
InvocationHandler(叫做MapperProxy),当你调用接口方法时,会被这个Handler的invoke方法拦截,invoke方法内部封装了所有数据库操作的逻辑。
希望这份从“为什么”到“是什么”再到“怎么做”的深度剖析,能让你对Mybatis的动态代理机制有一个透彻的、牢固的理解。

浙公网安备 33010602011771号