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

1886

积分

0

好友

250

主题
发表于 13 小时前 | 查看: 3| 回复: 0

Mybatis动态代理机制核心类图

你是否好奇,当我们在 MyBatis 中调用一个只有方法定义的 Mapper 接口时,背后是如何执行 SQL 的?这背后的核心魔法,正是 JDK 动态代理。通过手动实现一个简易的映射代理工厂,我们可以清晰地透视其工作原理。这不仅有助于理解 MyBatis 框架的设计精髓,也是提升 Java 核心技术能力的绝佳实践。接下来,让我们一步步搭建这个核心代理层。

定义 Mapper 接口

一切从定义一个标准的 Mapper 接口开始。这个接口声明了我们希望执行的操作,它只有方法签名,没有任何实现——这正是 MyBatis 的特色所在。

public interface IUserDao {
    /**
     * 根据用户ID查询用户名
     * @param uId 用户ID
     * @return 用户名
     */
    String queryUserName(String uId);
    /**
     * 根据用户ID查询用户年龄
     * @param uId 用户ID
     * @return 用户年龄
     */
    Integer queryUserAge(String uId);
}

实现 InvocationHandler(代理逻辑核心)

代理的逻辑在于拦截方法调用。我们创建一个 MapperProxy 类来实现 InvocationHandler 接口,它是整个代理过程的大脑,负责决定方法被调用时该做什么。

import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;

public class MapperProxy<T> implements InvocationHandler, Serializable {
    @Serial
    private static final long serialVersionUID = -6424540398559729838L;
    /**
     * SQL映射容器:
     * key格式:接口全限定名.方法名(如com.yanx.yanbatis.test.dao.IUserDao.queryUserName)
     * value:对应的SQL语句(此处用文字模拟)
     */
    private final Map<String, String> sqlSession;
    /**
     * 被代理的Mapper接口类型
     */
    private final Class<T> mapperInterface;

    public MapperProxy(Map<String, String> sqlSession, Class<T> mapperInterface) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
    }

    /**
     * 代理方法核心逻辑:拦截接口方法调用
     * @param proxy 生成的代理对象
     * @param method 被调用的方法
     * @param args 方法入参
     * @return 方法执行结果
     * @throws Throwable 异常
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 过滤Object类的默认方法(如toString、equals),直接执行原始逻辑
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else {
            // 拼接key:接口全限定名 + 方法名
            String key = mapperInterface.getName() + "." + method.getName();
            // 从容器中获取对应的SQL(此处用文字模拟)
            String sql = sqlSession.get(key);
            return "你的方法被代理执行了!" + sql;
        }
    }
}

关键点解析invoke 方法是核心。当代理对象的方法被调用时,JVM 会将调用转发到这里。我们通过方法名和接口名构造一个唯一键,然后从一个模拟的 sqlSession(即 SQL 映射容器)中取出对应的“SQL”进行执行。这就是 MyBatis 将接口方法与 XML/注解中的 SQL 绑定起来的秘密。

实现代理工厂(创建代理对象)

有了代理逻辑处理器,我们还需要一个工厂来负责生产代理对象。MapperProxyFactory 就扮演了这个角色。

import java.lang.reflect.Proxy;
import java.util.Map;

public class MapperProxyFactory<T> {
    /**
     * 目标Mapper接口类型
     */
    private final Class<T> mapperInterface;

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

    /**
     * 创建Mapper接口的代理对象
     * @param sqlSession SQL映射容器
     * @return 代理后的Mapper接口实例
     */
    @SuppressWarnings("unchecked")
    public T newInstance(Map<String, String> sqlSession) {
        // 创建代理逻辑处理器
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface);
        // 生成并返回代理对象
        return (T) Proxy.newProxyInstance(
                mapperInterface.getClassLoader(),  // 类加载器
                new Class[]{mapperInterface},      // 要代理的接口
                mapperProxy                       // 代理逻辑处理器
        );
    }
}

设计模式思考:工厂模式在这里被优雅地运用。它将代理对象的复杂创建过程封装起来,对外只提供一个简单的 newInstance 方法。这种设计模式的运用,让客户端代码与 JDK 动态代理的底层 API 解耦,极大地提升了代码的可用性和可维护性。

测试验证:让代理跑起来

理论需要实践来检验。我们编写一个测试类,模拟 MyBatis 初始化配置并调用 Mapper 接口的完整流程。

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;

public class ApiTest {
    private final Logger log = LoggerFactory.getLogger(ApiTest.class);
    @Test
    public void test_MapperProxyFactory() {
        // 1. 创建Mapper代理工厂
        MapperProxyFactory<IUserDao> mapperProxyFactory = new MapperProxyFactory<>(IUserDao.class);

        // 2. 模拟MyBatis解析XML后存储的SQL映射
        HashMap<String, String> sqlSession = new HashMap<>();
        sqlSession.put("com.yanx.yanbatis.test.dao.IUserDao.queryUserName",
                      "模拟执行 Mapper.xml 中 SQL 语句的操作:查询用户姓名");
        sqlSession.put("com.yanx.yanbatis.test.dao.IUserDao.queryUserAge",
                      "模拟执行 Mapper.xml 中 SQL 语句的操作:查询用户年龄");

        // 3. 获取代理后的Mapper接口实例
        IUserDao userDao = mapperProxyFactory.newInstance(sqlSession);

        // 4. 调用接口方法(实际执行的是代理逻辑)
        String res = userDao.queryUserName("1");

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

执行结果与流程解析

输出结果

运行上述测试,控制台将打印出预期的结果,证明我们的代理机制成功运行:

测试结果:你的方法被代理执行了!模拟执行 Mapper.xml 中 SQL 语句的操作:查询用户姓名

执行流程解析

让我们拆解一下 userDao.queryUserName("1") 这句简单调用背后发生的故事:

  1. 方法调用:我们调用 userDao.queryUserName("1"),此时 userDao 是 JDK 动态代理生成的对象,而非普通的实现类实例。
  2. 代理拦截:JVM 识别到这是代理对象,立即将调用转发给其关联的 InvocationHandler,也就是我们编写的 MapperProxy.invoke() 方法。
  3. 路由与查找:在 invoke 方法内,程序将接口全限定名和方法名拼接成唯一键 com.yanx.yanbatis.test.dao.IUserDao.queryUserName,并使用这个键从模拟的 sqlSession(HashMap)中查找对应的“SQL语句”。
  4. 执行与返回:获取到预存的 SQL 描述文本后,将其与固定前缀拼接,形成最终的返回结果,完成一次完整的代理调用。

通过这个简单的轮子,我们清晰地揭示了 MyBatis 连接接口与 SQL 的桥梁是如何搭建的。理解这个核心机制,对于排查复杂的 MyBatis 问题或进行更深度的源码分析都大有裨益。希望这次动手实践能为你打开一扇深入理解框架原理的门。云栈社区也提供了更多关于 Java 生态和架构设计的深度讨论,欢迎一起交流成长。




上一篇:手写Mybatis第一步:深度解析JDK动态代理原理与实战
下一篇:手写MyBatis核心:从注册到调用,详解Mapper接口的动态代理机制
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2026-2-28 22:14 , Processed in 0.387104 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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