【源码解读之 Mybatis】【基础篇】--第篇:Mapper 接口的动态代理机制解析

📅 2026/7/25 16:31:22
【源码解读之 Mybatis】【基础篇】--第篇:Mapper 接口的动态代理机制解析
【源码解读之 Mybatis】【基础篇】–第篇Mapper 接口的动态代理机制解析前言Mpper 接口的“魔法”是什么你是否好奇过在 MyBatis 中为什么我们只需要定义一个 Mapper 接口比如UserMapper然后调用userMapper.findById(1)就能直接执行 SQL 并返回结果这背后没有复杂的继承或实现类只有一行sqlSession.getMapper(UserMapper.class)。答案就藏在JDK 动态代理中。今天我们就用通俗的语言和代码拆解 MyBatis 如何通过动态代理让接口“活”起来。—## 一、动态代理的“剧本”接口 代理类想象一下你是一个导演要拍一部电影执行 SQL。你不需要自己演实现类只需要写个剧本接口然后找个替身代理对象来替你完成动作。在 MyBatis 中这个替身就是MapperProxy它负责拦截接口方法调用然后“翻译”成 SQL 执行。### 1.1 关键角色-Mapper 接口剧本定义了方法签名如selectById。-MapperProxy替身实现InvocationHandler接口在invoke方法中处理逻辑。-MappedStatement剧本的详细注释包含了 SQL、参数映射、结果映射等。-SqlSession执行 SQL 的“片场”负责真正的数据库操作。—## 二、从getMapper开始代理对象的诞生我们平时写的代码是这样的javaSqlSession sqlSession sqlSessionFactory.openSession();UserMapper userMapper sqlSession.getMapper(UserMapper.class);User user userMapper.findById(1);getMapper 方法会调用 Configuration.getMapper()最终交给 MapperRegistry 创建代理。核心代码如下简化版java// MapperRegistry.javapublic T getMapper(Class type, SqlSession sqlSession) { // 获取该接口对应的 MapperProxyFactory工厂模式 MapperProxyFactory mapperProxyFactory (MapperProxyFactory) knownMappers.get(type); if (mapperProxyFactory null) { throw new BindingException(“Type type is not known to the MapperRegistry.”); } // 通过工厂创建代理实例 return mapperProxyFactory.newInstance(sqlSession);}MapperProxyFactory是一个工厂专门为每个 Mapper 接口创建代理对象。它的newInstance方法会用 JDK 的Proxy.newProxyInstance生成一个实现该接口的代理对象。---## 三、核心机制MapperProxy 如何“拦截”方法调用当我们在代理对象上调用方法时比如userMapper.findById(1)实际上会触发MapperProxy的invoke方法。让我们看看它的实现简化源码java// MapperProxy.java简化版带中文注释public class MapperProxyT implements InvocationHandler, Serializable { private final SqlSession sqlSession; private final ClassT mapperInterface; public MapperProxy(SqlSession sqlSession, ClassT mapperInterface) { this.sqlSession sqlSession; this.mapperInterface mapperInterface; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 1. 如果是 Object 类的方法如 toString, hashCode直接调用原生方法 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } // 2. 如果是接口的默认方法Java 8也特殊处理 if (method.isDefault()) { return invokeDefaultMethod(proxy, method, args); } // 3. 核心将方法调用转为 MapperMethod 对象然后执行 MapperMethod mapperMethod cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); } private MapperMethod cachedMapperMethod(Method method) { // 缓存 MapperMethod 对象避免重复解析 MapperMethod mapperMethod methodCache.get(method); if (mapperMethod null) { mapperMethod new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()); methodCache.put(method, mapperMethod); } return mapperMethod; }}**关键点解析**- 当调用findById时invoke方法会创建一个MapperMethod对象它包含了该方法对应的 SQL 语句从MappedStatement获取和参数处理逻辑。- 然后调用mapperMethod.execute(sqlSession, args)真正执行 SQL。---## 四、实战代码示例手写简化版动态代理为了让你更直观理解我写了一个简化版的 MyBatis 动态代理示例可运行java// 示例1手写简化版 MyBatis 动态代理import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.Map;// 1. 定义 Mapper 接口interface UserMapper { String findById(Integer id); String findByName(String name);}// 2. 模拟 SqlSession假装能执行 SQLclass MockSqlSession { // 模拟数据库id - 用户名 private MapInteger, String db new HashMap(); public MockSqlSession() { db.put(1, Alice); db.put(2, Bob); } public String selectOne(String statement, Object parameter) { // 模拟根据 SQL 语句和参数查询 if (selectById.equals(statement)) { return db.get(parameter); } else if (selectByName.equals(statement)) { // 简单模拟反转查询 for (Map.EntryInteger, String entry : db.entrySet()) { if (entry.getValue().equals(parameter)) { return Found: entry.getKey(); } } return null; } return null; }}// 3. 实现 MapperProxyclass MyMapperProxy implements InvocationHandler { private MockSqlSession sqlSession; public MyMapperProxy(MockSqlSession sqlSession) { this.sqlSession sqlSession; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 将方法名映射为 SQL 语句 IDMyBatis 中是从 XML 或注解获取 String statementId method.getName(); // 简化直接使用方法名 return sqlSession.selectOne(statementId, args[0]); }}// 4. 主程序演示动态代理public class DynamicProxyDemo { public static void main(String[] args) { MockSqlSession sqlSession new MockSqlSession(); // 创建代理对象 UserMapper userMapper (UserMapper) Proxy.newProxyInstance( UserMapper.class.getClassLoader(), new Class[]{UserMapper.class}, new MyMapperProxy(sqlSession) ); // 调用代理方法 String result1 userMapper.findById(1); System.out.println(findById(1) result1); // 输出Alice String result2 userMapper.findByName(Bob); System.out.println(findByName(Bob) result2); // 输出Found: 2 }}**运行结果**findById(1) AlicefindByName(Bob) Found: 2**核心理解**代理对象userMapper并没有实现findById方法但通过InvocationHandler的invoke方法我们拦截了调用并“翻译”成对MockSqlSession的操作。---## 五、进阶MapperMethod 的“翻译”过程在真实 MyBatis 中MapperMethod会做两件事1. **解析 SQL 命令类型**根据方法名或注解判断是select,insert,update,delete。2. **解析参数**将方法参数如id转换为 SQL 参数绑定。让我们看一个更接近真实源码的简化版MapperMethodjava// 示例2模拟 MapperMethod 的 SQL 执行逻辑import java.lang.reflect.Method;import java.util.HashMap;import java.util.Map;class MapperMethodDemo { // 模拟 MappedStatement包含 SQL 信息 static class MappedStatement { String sqlType; // SELECT, INSERT 等 String sql; // 原始 SQL简化版 MappedStatement(String sqlType, String sql) { this.sqlType sqlType; this.sql sql; } } // 模拟 Configuration保存 MappedStatement static class Configuration { MapString, MappedStatement mappedStatements new HashMap(); Configuration() { // 模拟从 XML 解析的 SQL mappedStatements.put(com.example.UserMapper.findById, new MappedStatement(SELECT, SELECT * FROM user WHERE id ?)); mappedStatements.put(com.example.UserMapper.findByName, new MappedStatement(SELECT, SELECT * FROM user WHERE name ?)); } MappedStatement getMappedStatement(String id) { return mappedStatements.get(id); } } // MapperMethod 核心根据 SQL 类型调用 SqlSession static class MapperMethod { private final String statementId; private final MappedStatement mappedStatement; MapperMethod(Class? mapperInterface, Method method, Configuration config) { // 生成 statementId如 com.example.UserMapper.findById this.statementId mapperInterface.getName() . method.getName(); this.mappedStatement config.getMappedStatement(statementId); } Object execute(MockSqlSession sqlSession, Object[] args) { // 根据 SQL 类型执行不同操作 if (SELECT.equals(mappedStatement.sqlType)) { // 简化只处理单个参数 return sqlSession.selectOne(statementId, args[0]); } else if (INSERT.equals(mappedStatement.sqlType)) { // 实际会调用 sqlSession.insert() System.out.println(执行插入操作...); return 1; // 影响行数 } return null; } } // 测试 public static void main(String[] args) throws Exception { Configuration config new Configuration(); MockSqlSession sqlSession new MockSqlSession(); // 模拟 MapperProxy 的 invoke 流程 Method method UserMapper.class.getMethod(findById, Integer.class); MapperMethod mapperMethod new MapperMethod(UserMapper.class, method, config); Object result mapperMethod.execute(sqlSession, new Object[]{1}); System.out.println(执行结果: result); // 输出Alice }}**运行结果**执行结果: Alice**关键点**MapperMethod通过statementId从Configuration中获取MappedStatement然后根据 SQL 类型SELECT/UPDATE 等调用SqlSession的对应方法。这就是“方法调用 → SQL 执行”的桥梁。---## 六、总结动态代理的“魔法”三部曲通过以上分析我们可以总结 MyBatis Mapper 接口动态代理的完整流程1. **创建代理对象**sqlSession.getMapper(UserMapper.class)通过Proxy.newProxyInstance生成一个实现了UserMapper接口的代理对象。2. **拦截方法调用**当调用代理对象的方法时MapperProxy.invoke()被触发将方法名和参数封装成MapperMethod。3. **执行 SQL**MapperMethod.execute()根据方法对应的MappedStatement包含 SQL 语句、参数映射等调用SqlSession的selectOne、insert等方法最终执行数据库操作。**动态代理的好处**- **零侵入**我们只需要定义接口无需编写实现类MyBatis 自动完成 SQL 映射。- **灵活性**可以在invoke方法中添加日志、缓存、事务等增强功能。- **性能优化**MapperMethod会被缓存避免重复解析。**一句话总结**MyBatis 的 Mapper 接口动态代理本质上是将 Java 接口的方法调用通过 JDK 动态代理“翻译”为对SqlSession的数据库操作调用从而让接口成为 SQL 的代言人。希望这篇文章能帮你揭开 MyBatis 动态代理的神秘面纱。如果你对源码中的MappedStatement 解析或参数绑定机制感兴趣欢迎继续关注后续篇章