当前位置: 首页 > news >正文

MyBatis核心:Mapper接口凭什么能直接操作数据库?

从“奇怪”的接口说起

如果你刚接触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(Class<T> type) { return configuration.getMapper(type, this); }

这里只是转交给Configuration:

// Configuration.java public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); }

最终来到了MapperRegistry:

// MapperRegistry.java public <T> T getMapper(Class<T> type, SqlSession sqlSession) { final MapperProxyFactory<T> 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 MapperProxyFactory<T> { private final Class<T> mapperInterface; protected T newInstance(MapperProxy<T> mapperProxy) { // 使用JDK动态代理 return (T) Proxy.newProxyInstance( mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy ); } public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } }

到这里已经很清楚了:MyBatis使用JDK的Proxy.newProxyInstance为Mapper接口生成了代理对象,代理逻辑封装在MapperProxy中。

第三步:MapperProxy——核心代理逻辑

// MapperProxy.java public class MapperProxy<T> 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是整个转换的核心,它负责:

  1. 解析方法的签名(参数、返回值)

  2. 解析方法上的注解(或XML配置)

  3. 将方法调用转换为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 │ ← 执行SQL(selectOne/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等直接获取SQL

  • XML:通过命名空间+方法名去解析后的XML映射中查找SQL

两种方式殊途同归,最终都得到SQL语句和映射规则。

3. 为什么是JDK动态代理而不是CGLIB?

Mapper是接口,JDK动态代理天然支持接口代理。如果使用CGLIB(针对类的代理),反而会增加不必要的复杂度。JDK动态代理足够满足需求,且是Java原生支持,性能和稳定性都有保障。

总结

问题答案
Mapper接口有实现类吗?没有,开发者不需要写实现类
实际执行的是谁?动态生成的代理对象(MapperProxy)
代理怎么生成?JDK动态代理:Proxy.newProxyInstance()
方法调用怎么变成SQL?MapperMethod解析方法签名和SQL,调用SqlSession执行
核心设计模式动态代理模式

下次有人问你MyBatis的Mapper接口为什么能直接操作数据库,你可以自信地回答:

MyBatis在启动时扫描Mapper接口,为每个接口创建一个MapperProxyFactory。当我们调用sqlSession.getMapper()时,工厂通过JDK动态代理生成一个代理对象。代理对象内部使用MapperMethod解析方法上的注解或XML配置,将方法调用转换为对应的SQL语句,最终通过SqlSession和Executor完成数据库操作。整个过程开发者无需编写任何实现类,只需定义接口即可。

这就是MyBatis框架设计的精妙之处——把繁琐留给框架,把简洁留给开发者

http://www.jsqmd.com/news/512238/

相关文章:

  • 市政道路工程防滑性能优的花岗岩路沿石多少钱 - 工业推荐榜
  • 为雪女-斗罗大陆-造相Z-Turbo开发智能体(Agent):自动化角色设计工作流
  • 星露谷农场规划器完整指南:3步打造你的完美虚拟农场
  • IndexTTS2 V23优化升级:V23版本情感控制全面升级,效果更自然
  • JVM调优介绍 + 面试题标准答案(Java高级工程师专用)
  • 2026年西安、北京等地靠谱的文旅策划品牌企业推荐,哪家性价比高 - 工业设备
  • FRCRN降噪效果对比展示:电话录音与现场采访的清晰化处理
  • 分析西安靠谱文旅规划机构,中旅建设计性价比高值得选吗? - 工业品牌热点
  • Qwen3-32B-Chat镜像结构详解:/workspace目录设计、模型路径、依赖包预装清单
  • Qwen3-32B-Chat百度开发者实操:使用Postman调试Qwen3-32B API接口全流程
  • 大数据基于java的财经新闻文本挖掘分析与爬虫可视化应用
  • Z-Image-GGUF实操手册:基于Qwen3文本编码器的中英文提示词编写指南
  • OWL ADVENTURE项目实战:从零搭建一个微信小程序-图像识别应用
  • SiameseAOE中文-base商业应用:替代传统规则引擎实现低成本ABSA自动化
  • YOLO12惊艳效果:老电影修复帧中字幕区域检测与背景自适应擦除
  • STM32远程升级系统(Bootloader + 上位机)
  • 如何选购口碑好的旅游景区规划品牌企业 - 工业品网
  • 九州旅游通卡闲置了,用可可收一键秒回收,不浪费一分权益 - 可可收
  • PyTorch 2.5入门实战:开箱即用镜像部署全流程
  • 如何在麒麟系统ky10.aarch64上安全升级OpenSSH到10.0p1(附配置优化建议)
  • NMN抗衰科普:2026年十款优质品牌推荐榜首盼生派C9NMN,选对不迷茫 - 速递信息
  • springboot+nodejs+vue3的中小学英语学习训练与测评系统
  • 剖析2026年深圳好用的就业规划机构,国企就业规划机构排行榜揭晓 - myqiye
  • CogVideoX-2b安全特性:数据不出本地的企业级优势
  • ESP-IDF+VSCode开发环境搭建避坑指南:解决‘nvs.h‘找不到的终极方案
  • 保姆级教程:在CentOS 7上为你的OpenVPN搭建FreeRADIUS+Google Authenticator认证后端
  • 2026年道闸系统厂家推荐:北京英龙国瑞科技,百胜/威捷/栅栏/直杆道闸全品类覆盖 - 品牌推荐官
  • Archery权限管理实战:如何配置RD、PM、DBA多角色协作流程?
  • 收藏 | 从提示词工程到Skills封装革命,小白也能轻松驾驭大模型
  • GTC 2026| “千万缺口”之下,NVIDIA把AI嵌入了医疗行业