
先看效果:一个接口,真的能被调用
你有没有好奇过,在使用 MyBatis 时,下面这行代码是如何工作的?
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
String res = userDao.queryUserName("10001");
IUserDao 明明只是一个接口,没有实现类,但它就是能被成功调用并返回结果。这背后的魔法,正是 MyBatis 框架的核心机制之一。今天,我们就通过一个手写版本的 yanbatis-02 项目,把映射器从注册到调用的完整流程彻底拆解一遍。
测试用例(入口)
理解一个复杂流程,最好的方式是从一个完整的测试用例出发,然后逆向跟踪。我们先来看这段最外层代码:
@Test
public void test_MapperProxyFactory() {
// 1. 注册 Mapper
MapperRegistry mapperRegistry = new MapperRegistry();
mapperRegistry.addMappers("com.yanx.yanbatis.test.dao");
// 2. 从 SqlSession 工厂获取 Session
DefaultSqlSessionFactory factory = new DefaultSqlSessionFactory(mapperRegistry);
SqlSession sqlSession = factory.openSession();
// 3. 获取映射器对象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
// 4. 测试验证
String res = userDao.queryUserName("10001");
log.info("测试结果:{}", res);
}
这个测试用例清晰地展示了使用 Mapper 的四个标准步骤。下面,我们就沿着这段代码,一步步跟进到具体的实现源码中。
1 注册 Mapper:new MapperRegistry() + addMappers(...)
1.1 创建注册器
首先,我们创建了一个 MapperRegistry 对象。
MapperRegistry mapperRegistry = new MapperRegistry();
它的核心数据结构定义在文件 binding/MapperRegistry.java 中:
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
怎么理解呢?
你可以把 knownMappers 想象成一个“映射器登记册”。它的作用就是记录:某个 Mapper 接口 对应着 哪个代理工厂。这是后续一切动态代理操作的基础。
1.2 扫包注册
接下来,通过包路径进行批量注册。
mapperRegistry.addMappers("com.yanx.yanbatis.test.dao");
addMappers 方法的实现如下:
public void addMappers(String packageName) {
Set<Class<?>> mapperSet = ClassScanner.scanPackage(packageName);
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
这里主要做了三件事:
ClassScanner.scanPackage(packageName):扫描指定包路径下的所有类。
- 遍历扫描到的所有类。
- 对每个类调用
addMapper 方法尝试进行注册。
1.3 真正注册发生在 addMapper
扫包只是收集,真正的注册逻辑在 addMapper 方法里,这也是整个注册过程的核心。
private <T> void addMapper(Class<T> mapperClass) {
if (mapperClass.isInterface()) {
if (hasMapper(mapperClass)) {
throw new RuntimeException("Type " + mapperClass + " is already known to the MapperRegistry.");
}
MapperProxyFactory<T> proxyFactory = new MapperProxyFactory<>(mapperClass);
knownMappers.put(mapperClass, proxyFactory);
}
}
我们来逐行分析:
mapperClass.isInterface():这是关键!它只注册接口,这也是 MyBatis “面向接口编程”模式能够成立的前提。
hasMapper(mapperClass):检查是否已经注册过,防止重复。
new MapperProxyFactory<>(mapperClass):为这个接口创建一个专属的代理工厂。
knownMappers.put(...):完成登记,将 IUserDao.class 与 MapperProxyFactory<IUserDao> 关联起来。
此时,你可以理解为: 系统已经“认识” IUserDao 这个接口了,并且知道未来应该通过哪个工厂来为它创建代理对象。对于想深入理解Java中框架设计模式的同学,这一步是理解工厂模式与接口绑定的绝佳案例。
2 创建 SqlSessionFactory 并 openSession
注册完 Mapper,下一步是获取数据库会话。
DefaultSqlSessionFactory factory = new DefaultSqlSessionFactory(mapperRegistry);
SqlSession sqlSession = factory.openSession();
2.1 DefaultSqlSessionFactory 很简单:就是把 registry 存起来
它的实现非常直接:
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final MapperRegistry mapperRegistry;
public DefaultSqlSessionFactory(MapperRegistry mapperRegistry) {
this.mapperRegistry = mapperRegistry;
}
@Override
public SqlSession openSession() {
return new DefaultSqlSession(mapperRegistry);
}
}
- 构造时注入
mapperRegistry:这样 SessionFactory 就持有了所有已注册的 Mapper 信息。
openSession():创建 DefaultSqlSession 实例,并把 mapperRegistry 传递进去。
2.2 DefaultSqlSession:对外提供两项核心能力
DefaultSqlSession 作为会话的具体实现,主要对外提供两个核心方法:
getMapper(...):获取 Mapper 接口的代理对象。
selectOne(...):执行具体的数据库查询(在当前手写版本中,我们先做简化返回)。
3 获取 Mapper:sqlSession.getMapper(IUserDao.class)
现在,我们终于来到了魔法开始的地方。
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
3.1 先进入:DefaultSqlSession#getMapper
DefaultSqlSession 的 getMapper 方法并没有自己做太多事情。
@Override
public <T> T getMapper(Class<T> type) {
return mapperRegistry.getMapper(type, this);
}
它只是把事情“转交”给了我们之前注册 Mapper 的 MapperRegistry。注意,这里把 this(即当前的 SqlSession 对象)也传了进去。为什么?因为后续代理对象执行方法时,需要用到这个 SqlSession 来真正操作数据库。
3.2 进入:MapperRegistry#getMapper(type, sqlSession)
核心逻辑转移到了注册中心。
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new RuntimeException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new RuntimeException("Error getting mapper instance. Cause: " + e, e);
}
}
knownMappers.get(type):从“登记册” knownMappers 中,取出 IUserDao 对应的那个代理工厂 MapperProxyFactory。
- 如果没找到(即未注册),直接抛出异常。这也解释了为什么第一步的
addMappers 必不可少。
mapperProxyFactory.newInstance(sqlSession):调用代理工厂创建代理对象,并返回给你。
到这里,重点来了:newInstance 里面到底发生了什么?动态代理的奥秘就在其中。
4 代理工厂:MapperProxyFactory#newInstance(核心)
这是产生“魔法”效果的关键代码。
public T newInstance(SqlSession sqlSession) {
MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface);
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[]{mapperInterface},
mapperProxy
);
}
让我们分解一下:
new MapperProxy<>(sqlSession, mapperInterface):创建一个 InvocationHandler(调用处理器)。后续所有对代理对象的方法调用,都会被它拦截处理。它内部持有了 SqlSession,以便之后执行真正的 selectOne 操作。
Proxy.newProxyInstance(...):这是 JDK 动态代理的标准 API 调用。
classLoader:指定用哪个类加载器来加载生成的代理类。
interfaces:指定要代理的接口列表,这里就是 IUserDao。
InvocationHandler:指定代理逻辑,也就是我们刚刚创建的 MapperProxy 实例。
这行代码执行完之后:
你拿到的 userDao 并不是一个真正的实现类实例,而是一个由 JDK 在运行时动态生成的代理对象。你对它进行的任何方法调用,都会被 MapperProxy#invoke(...) 方法拦截。
5 真正执行:userDao.queryUserName(“10001”) 会走到哪里?
现在,我们执行测试的最后一步:
String res = userDao.queryUserName("10001");
这句代码会触发代理对象的调用处理器,也就是执行 MapperProxy#invoke 方法。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
return sqlSession.selectOne(method.getName(), args);
}
}
这个逻辑非常清晰:
method.getDeclaringClass():获取这个方法属于哪个类。
- 如果该方法属于
Object 类(比如 toString, hashCode, equals 等):则使用默认逻辑执行。
- 否则,就认为这是 Mapper 接口中定义的方法:于是调用
sqlSession.selectOne(method.getName(), args) 去执行。
请注意这里的一个简化点:
statement(SQL语句标识)直接使用了方法名 method.getName(),也就是 queryUserName。在实际的 MyBatis 中,这通常是 namespace + methodName,并且与 XML 文件或注解中的 SQL 配置进行映射。但在我们当前的手写版本中,目标是先跑通“代理 → Session 执行”这条主链路。
6 SqlSession 执行(当前版本是“模拟返回”)
方法调用被转发到了 DefaultSqlSession#selectOne(statement, parameter):
@Override
public <T> T selectOne(String statement, Object parameter) {
return (T) ("你被代理了!" + "方法:" + statement + " 入参:" + parameter);
}
在这个极简版本中,我们并没有连接数据库或解析 XML。它只是简单地拼接了一个字符串作为返回结果,目的是验证整个调用链路是否正确。
所以,最终变量 res 的值会是类似这样的字符串:你被代理了!方法:queryUserName 入参:[Ljava.lang.Object;@xxxx
小提示: 你可能会注意到,MapperProxy.invoke 把 args(一个 Object[] 数组)整个传给了 selectOne,所以输出的 parameter 是数组对象的 toString。如果想更直观,可以改为 args[0],等我们在后续章节实现更真实的数据库操作时再优化也不迟。
7 最后打印日志
log.info("测试结果:{}", res);
到此为止,整个最小化闭环已经完成:
- 注册 Mapper 接口
- 通过动态代理生成代理对象
- 接口方法调用被代理对象拦截
- 转交给 SqlSession 执行
- 返回模拟结果
这个过程完美诠释了 MyBatis 映射器背后的核心机制,对于进行开源实战和框架学习非常有帮助。
一张“脑内调用栈”:把链路串起来
最后,让我们把整个流程串联起来,形成一张清晰的“脑内调用栈”。
从你写下的这一句开始:
userDao.queryUserName("10001");
实际发生的调用链是:
Proxy(生成的代理类).queryUserName(...)
- →
MapperProxy.invoke(proxy, method, args)
- →
DefaultSqlSession.selectOne(“queryUserName”, args)
- → 返回拼接的字符串
而 userDao 这个代理对象的来源是:
sqlSession.getMapper(IUserDao.class)
- →
MapperRegistry.getMapper(IUserDao, sqlSession)
- →
MapperProxyFactory.newInstance(sqlSession)
- →
Proxy.newProxyInstance(...)
希望通过这次手写 MyBatis 映射器核心流程的解析,能让你对 MyBatis 乃至其他 ORM 框架中“接口-代理”的设计模式有更深刻的理解。技术学习的乐趣,往往就藏在这些看似“魔法”的底层实现细节之中。如果你想了解更多关于框架设计和 Java 生态的深度解析,欢迎来云栈社区交流探讨。