找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1886

积分

0

好友

250

主题
发表于 11 小时前 | 查看: 2| 回复: 0

手写Mybatis映射器注册与调用流程图

先看效果:一个接口,真的能被调用

你有没有好奇过,在使用 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);
    }
}

这里主要做了三件事:

  1. ClassScanner.scanPackage(packageName):扫描指定包路径下的所有类。
  2. 遍历扫描到的所有类。
  3. 对每个类调用 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.classMapperProxyFactory<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

DefaultSqlSessiongetMapper 方法并没有自己做太多事情。

@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
    );
}

让我们分解一下:

  1. new MapperProxy<>(sqlSession, mapperInterface):创建一个 InvocationHandler(调用处理器)。后续所有对代理对象的方法调用,都会被它拦截处理。它内部持有了 SqlSession,以便之后执行真正的 selectOne 操作。
  2. 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.invokeargs(一个 Object[] 数组)整个传给了 selectOne,所以输出的 parameter 是数组对象的 toString。如果想更直观,可以改为 args[0],等我们在后续章节实现更真实的数据库操作时再优化也不迟。

7 最后打印日志

log.info("测试结果:{}", res);

到此为止,整个最小化闭环已经完成:

  1. 注册 Mapper 接口
  2. 通过动态代理生成代理对象
  3. 接口方法调用被代理对象拦截
  4. 转交给 SqlSession 执行
  5. 返回模拟结果

这个过程完美诠释了 MyBatis 映射器背后的核心机制,对于进行开源实战和框架学习非常有帮助。


一张“脑内调用栈”:把链路串起来

最后,让我们把整个流程串联起来,形成一张清晰的“脑内调用栈”。

从你写下的这一句开始:

userDao.queryUserName("10001");

实际发生的调用链是:

  1. Proxy(生成的代理类).queryUserName(...)
  2. MapperProxy.invoke(proxy, method, args)
  3. DefaultSqlSession.selectOne(“queryUserName”, args)
  4. → 返回拼接的字符串

userDao 这个代理对象的来源是:

  1. sqlSession.getMapper(IUserDao.class)
  2. MapperRegistry.getMapper(IUserDao, sqlSession)
  3. MapperProxyFactory.newInstance(sqlSession)
  4. Proxy.newProxyInstance(...)

希望通过这次手写 MyBatis 映射器核心流程的解析,能让你对 MyBatis 乃至其他 ORM 框架中“接口-代理”的设计模式有更深刻的理解。技术学习的乐趣,往往就藏在这些看似“魔法”的底层实现细节之中。如果你想了解更多关于框架设计和 Java 生态的深度解析,欢迎来云栈社区交流探讨。




上一篇:深入解析Mybatis代理机制:手写映射工厂与动态代理实现
下一篇:Linux内核开发者因穿西装被开除?技术尊严与企业文化的冲突与反思
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-28 20:44 , Processed in 0.395103 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表