
从“奇怪”的接口说起如果你刚接触MyBatis一定会有一个疑问为什么定义一个没有任何实现类的Mapper接口就能直接操作数据库public interface UserMapper { Select(SELECT * FROM user WHERE id #{id}) User selectById(Long id); }然后这样使用UserMapper mapper sqlSession.getMapper(UserMapper.class); User user mapper.selectById(1L);这里没有实现类没有JDBC代码甚至没有SQL字符串拼接——接口方法直接“调用”就完成了数据库查询。这背后到底是什么原理今天我们就来彻底搞清楚这个问题。核心答案动态代理一句话总结MyBatis通过JDK动态代理为Mapper接口生成了代理对象代理对象内部将接口方法调用转换为数据库操作。当你调用sqlSession.getMapper(UserMapper.class)时MyBatis并没有返回一个你写的实现类的实例而是返回了一个动态生成的代理对象。对这个代理对象的方法调用会被拦截并转发给一个InvocationHandler来处理。源码拆解一步步揭开真相我们从入口开始看看MyBatis是怎么创建这个代理对象的。第一步getMapper 的调用链// DefaultSqlSession.java Override public T T getMapper(ClassT type) { return configuration.getMapper(type, this); }这里只是转交给Configuration// Configuration.java public T T getMapper(ClassT type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); }最终来到了MapperRegistry// MapperRegistry.java public T T getMapper(ClassT type, SqlSession sqlSession) { final MapperProxyFactoryT mapperProxyFactory knownMappers.get(type); if (mapperProxyFactory null) { throw new BindingException(Type type is not known to the MapperRegistry.); } try { return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException(Error getting mapper instance. Cause: e, e); } }关键点knownMappers中保存了每个Mapper接口对应的MapperProxyFactory这个工厂专门负责创建该接口的代理对象。第二步MapperProxyFactory 创建代理// MapperProxyFactory.java public class MapperProxyFactoryT { private final ClassT mapperInterface; protected T newInstance(MapperProxyT mapperProxy) { // 使用JDK动态代理 return (T) Proxy.newProxyInstance( mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy ); } public T newInstance(SqlSession sqlSession) { final MapperProxyT mapperProxy new MapperProxy(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } }到这里已经很清楚了MyBatis使用JDK的Proxy.newProxyInstance为Mapper接口生成了代理对象代理逻辑封装在MapperProxy中。第三步MapperProxy——核心代理逻辑// MapperProxy.java public class MapperProxyT implements InvocationHandler, Serializable { Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 如果方法是Object类的方法如toString、hashCode直接执行 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } // 核心执行Mapper方法 return cachedInvoker(method).invoke(proxy, method, args, sqlSession); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } }调用userMapper.selectById(1L)时实际执行的是MapperProxy.invoke()方法。cachedInvoker(method)会返回一个MapperMethodInvoker最终由MapperMethod来完成真正的数据库操作。第四步MapperMethod——方法与SQL的桥梁MapperMethod是整个转换的核心它负责解析方法的签名参数、返回值解析方法上的注解或XML配置将方法调用转换为SqlSession的操作// MapperMethod.java public class MapperMethod { private final SqlCommand command; // 封装了SQL类型和ID private final MethodSignature method; // 封装了方法签名信息 public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case SELECT: // 处理参数 Object param method.convertArgsToSqlCommandParam(args); // 根据返回类型选择不同的执行方法 if (method.returnsVoid() method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result null; } else if (method.returnsMany()) { result executeForMany(sqlSession, param); } else if (method.returnsMap()) { result executeForMap(sqlSession, param); } else if (method.returnsCursor()) { result executeForCursor(sqlSession, param); } else { Object param method.convertArgsToSqlCommandParam(args); result sqlSession.selectOne(command.getName(), param); } break; case INSERT: case UPDATE: case DELETE: // 处理增删改... break; } return result; } }看到这里就明白了selectById(1L)最终会被转换成sqlSession.selectOne(接口全限定名.方法名, 1L)这和我们手动使用SqlSession的方式完全一致。一张图看懂整个流程调用: userMapper.selectById(1L) │ ▼ ┌──────────────────┐ │ MapperProxy │ ← JDK动态代理生成的代理对象 │ .invoke() │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ MapperMethod │ ← 解析方法签名、参数、SQL │ .execute() │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ SqlSession │ ← 执行SQLselectOne/selectList等 │ .selectOne() │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Executor │ ← 执行器缓存、事务管理 └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ JDBC执行SQL │ │ 返回结果集 │ └──────────────────┘为什么Mapper接口不能直接实例化现在我们可以回答这个问题了Mapper接口本身没有实现类它的实例是MyBatis在运行时动态生成的代理对象。编译期Mapper接口只是一个普通的Java接口没有实现类运行期MyBatis扫描到Mapper接口后通过JDK动态代理创建代理对象调用时代理对象拦截方法调用转发给MapperMethod处理最终通过SqlSession执行数据库操作深入思考设计背后的考量1. 为什么不用实现类如果让我们手动写实现类每个方法都需要写JDBC代码重复且繁琐public class UserMapperImpl implements UserMapper { Override public User selectById(Long id) { // 每个方法都要写获取连接、PreparedStatement、设置参数、执行查询、映射结果... // 大量重复代码 } }动态代理模式让MyBatis在运行时自动完成这些重复工作开发者只需关注接口定义和SQL映射。2. 为什么能支持XML和注解两种配置MapperMethod在初始化时会同时解析注解和XML配置注解通过Select、Insert等直接获取SQLXML通过命名空间方法名去解析后的XML映射中查找SQL两种方式殊途同归最终都得到SQL语句和映射规则。3. 为什么是JDK动态代理而不是CGLIBMapper是接口JDK动态代理天然支持接口代理。如果使用CGLIB针对类的代理反而会增加不必要的复杂度。JDK动态代理足够满足需求且是Java原生支持性能和稳定性都有保障。总结问题答案Mapper接口有实现类吗没有开发者不需要写实现类实际执行的是谁动态生成的代理对象MapperProxy代理怎么生成JDK动态代理Proxy.newProxyInstance()方法调用怎么变成SQLMapperMethod解析方法签名和SQL调用SqlSession执行核心设计模式动态代理模式下次有人问你MyBatis的Mapper接口为什么能直接操作数据库你可以自信地回答MyBatis在启动时扫描Mapper接口为每个接口创建一个MapperProxyFactory。当我们调用sqlSession.getMapper()时工厂通过JDK动态代理生成一个代理对象。代理对象内部使用MapperMethod解析方法上的注解或XML配置将方法调用转换为对应的SQL语句最终通过SqlSession和Executor完成数据库操作。整个过程开发者无需编写任何实现类只需定义接口即可。这就是MyBatis框架设计的精妙之处——把繁琐留给框架把简洁留给开发者。