【源码解读之 Mybatis】【基础篇】--第篇:Mapper 接口的动态代理机制解析
2026/7/25 16:31:20 网站建设 项目流程

【源码解读之 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),实际上会触发MapperProxyinvoke方法。让我们看看它的实现(简化源码):```java// MapperProxy.java(简化版,带中文注释)public class MapperProxy<T> implements InvocationHandler, Serializable { private final SqlSession sqlSession; private final Class<T> mapperInterface; public MapperProxy(SqlSession sqlSession, Class<T> 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(假装能执行 SQL)class MockSqlSession { // 模拟数据库:id -> 用户名 private Map<Integer, 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.Entry<Integer, 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 语句 ID(MyBatis 中是从 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方法,但通过InvocationHandlerinvoke方法,我们拦截了调用,并“翻译”成对MockSqlSession的操作。---## 五、进阶:MapperMethod 的“翻译”过程在真实 MyBatis 中,MapperMethod会做两件事:1. **解析 SQL 命令类型**:根据方法名或注解判断是select,insert,update,delete。2. **解析参数**:将方法参数(如id)转换为 SQL 参数绑定。让我们看一个更接近真实源码的简化版MapperMethod:```java// 示例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 { Map<String, 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通过statementIdConfiguration中获取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 语句、参数映射等),调用SqlSessionselectOneinsert等方法,最终执行数据库操作。**动态代理的好处**:- **零侵入**:我们只需要定义接口,无需编写实现类,MyBatis 自动完成 SQL 映射。- **灵活性**:可以在invoke方法中添加日志、缓存、事务等增强功能。- **性能优化**:MapperMethod会被缓存,避免重复解析。**一句话总结**:MyBatis 的 Mapper 接口动态代理,本质上是将 Java 接口的方法调用,通过 JDK 动态代理“翻译”为对SqlSession的数据库操作调用,从而让接口成为 SQL 的代言人。希望这篇文章能帮你揭开 MyBatis 动态代理的神秘面纱。如果你对源码中的MappedStatement` 解析或参数绑定机制感兴趣,欢迎继续关注后续篇章!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询