⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://www.iocoder.cn/Fight/mianshiguanweishenme-MyBatis-de-Mapper-jiekoubuxuyaoxieshixianlei/ 「芋道源码」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

这道题面试官真正想筛什么

「MyBatis Mapper 接口为什么不需要写实现类」——这题在面试里几乎人手必答,但能答到加分位的不到 20%。

国内 Java 项目几乎是 MyBatis(+ MyBatis-Plus)的统治版图——SSM 时代起步、Spring Boot 时代延续,用 JPA / Hibernate 的项目肉眼可见地少。这一题相当于在筛你对国内 80% Java 项目背后那套 ORM 机制的理解深度——所以面试官特别喜欢问。

题面考动态代理,实际在筛三件事:

  1. 你懂不懂 JDK 动态代理的工作机制(Proxy + InvocationHandler,不是只听过名字);
  2. 你清不清楚 namespace + id 这个 MyBatis 的核心约定(怎么把接口和 SQL 连起来);
  3. 你翻没翻过 MyBatis 源码(能不能说出几个核心类的职责和位置)。

候选人按答到的深度分三档:30 分(动态代理三个字)、60 分(讲清桥梁约定)、90 分(报得出五个核心类 + 解释为什么 JDK 而非 CGLIB)。下面按段位拆。

L1:30 秒答案过基础线

MyBatis 在运行时用 JDK 动态代理为 Mapper 接口生成代理对象。开发者调用方法时被代理拦截,根据「接口全限定名 + 方法名」找到对应的 SQL 元数据,委托给 SqlSession 执行。

——这是基础线,30 分。所有候选人都会答到这。面试官听完通常会接:「那它是怎么把接口方法和 XML 里的 SQL 关联起来的?」这就进入 L2 战场。

L2:2 分钟答案讲清桥梁

L2 的关键是讲清接口和 XML 是靠什么对上的。

核心约定一句话:

XML 的 namespace = 接口全限定名;SQL 标签的 id = 接口方法名;两者拼接 = MappedStatement 的 key。

完整调用链路两个阶段:

阶段一:代理创建(容器启动时)

  1. XMLMapperBuilder 解析每个 Mapper XML,按 namespace 把接口注册到 MapperRegistry;
  2. 每个 SQL 标签生成一个 MappedStatement,以 namespace.id 为 key 存入 Configuration.mappedStatements;
  3. 调 sqlSession.getMapper(UserMapper.class) 时,通过 MapperProxyFactory.newInstance() 创建代理;
  4. 代理对象持有 MapperProxy(实现了 InvocationHandler)。

阶段二:方法调用

  1. JVM 把 mapper.selectById(1L) 转给 MapperProxy.invoke();
  2. invoke 把 Method 封装为 MapperMethod(带缓存,避免每次都解析);
  3. MapperMethod 拼出 statementId = "com.xxx.UserMapper.selectById";
  4. 委托给 SqlSession 的对应方法(selectList / insert / …)执行 SQL。

讲清这两个阶段 + 命名约定,60 分。

关键深一层的洞察:Mapper 接口里没有任何业务实现代码,但调用照样能拿到结果——这背后是「接口 + 配置约定 = 实现」的模板方法模式极致用法。能讲到这一层,面试官就知道你不是死记调用链。

L3:5 分钟答案显源码深度

L3 加分位的核心:报得出五个核心类 + 解释为什么 JDK 而非 CGLIB + 知道 Spring 怎么集成。

五个核心类速记

类名 职责
MapperProxy 实现 InvocationHandler,拦截 Mapper 接口方法调用
MapperProxyFactory 工厂,通过 JDK 动态代理创建 Mapper 代理对象
MapperMethod 封装方法信息(SQL 类型、参数、返回值),路由到 SqlSession
MapperRegistry 维护 Class<T> → MapperProxyFactory<T> 的映射
MappedStatement 一条 SQL 的完整元数据(SQL 文本、参数映射、结果映射)

能按顺序说出这五个,面试官立刻知道你读过源码——直接加 20 分。

MapperProxy.invoke() 的核心就三行逻辑:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1. Object 方法(toString/hashCode/equals)走原方法,别走代理
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 2. 把 Method 封装成 MapperMethod(带缓存)
MapperMethod mapperMethod = cachedMapperMethod(method);
// 3. 路由到 SqlSession
return mapperMethod.execute(sqlSession, args);
}

为什么用 JDK 动态代理而不是 CGLIB

维度 JDK 动态代理 CGLIB
工作机制 基于接口生成代理 基于继承生成子类
Mapper 适配 ✅ 天然契合 ⚠️ 需要额外绕一层
性能 现代 JVM 下差距很小 原始反射时代略快
依赖 JDK 内置 需引入 CGLIB 库

结论一句话:MyBatis Mapper 天然是接口,JDK 动态代理是最直接、最轻量、最符合实现哲学的选择——CGLIB 也不是技术上完全做不到,但要为接口场景额外搭一层语义,没必要。

历史角度也能解释:MyBatis 起家时(2010 年前后)的主流就是 JDK 代理 + 接口约定,那个时代 CGLIB 主要用来代理 Spring 的非接口 Bean——两套代理用在两套场景,各得其所。

Spring 集成的额外环节(高分位)

很多人讲 MyBatis 代理只到 sqlSession.getMapper(),但 Spring 项目里还有一层:

  1. @MapperScan 触发 MapperScannerConfigurer;
  2. 它把每个 Mapper 接口注册成 FactoryBean(具体是 MapperFactoryBean);
  3. Spring 容器创建 Bean 时调 MapperFactoryBean.getObject();
  4. getObject() 内部最终还是调 sqlSession.getMapper(mapperInterface) 拿到代理;
  5. 这个代理被注册成 Spring Bean,可以用 @Autowired 注入。

能讲出 MapperFactoryBean 这个名字 + 解释「为什么 Mapper 能被 @Autowired 注入」,是高分位标志。

直接掉分的几种答法

  • 「MyBatis 帮我生成了 Mapper 实现类的 .class 文件」——错。JDK 动态代理是运行时内存生成 $Proxy0 这种类,不写盘。
  • 「就是反射调用方法」——没说到点上,反射只是动态代理的实现细节。
  • 「Spring 的依赖注入帮我做了」——把 Spring 的 IoC 和 MyBatis 的代理混了,根本机制是 MyBatis 自己的。
  • 「Mapper 接口和 XML 是 Spring 自动关联的」——错。是 MyBatis 自己解析的(XMLMapperBuilder),Spring 只是触发了注册。
  • 「getMapper() 每次返回新代理」——错。MapperProxyFactory 缓存代理类,但每次 getMapper 会 new 一个 MapperProxy 实例(轻量)。

高频追问怎么接

追问 1:方法名和 XML 的 id 不一致会怎样?
答:启动不报错(接口和 XML 独立解析)。运行时调用该方法抛 BindingException: Invalid bound statement (not found): com.xxx.UserMapper.xxx。排查思路:先检查 namespace 是否等于全限定名,再检查方法名是否等于 id。

追问 2:用 @Select 注解还需要 XML 吗?
答:不需要。MyBatis 解析注解直接生成 MappedStatement,代理机制和 XML 方式完全一样。

追问 3:同一个方法能既用注解又用 XML 吗?
答:不能,启动报错——重复定义。但同一个 Mapper 里不同方法可以一部分注解、一部分 XML,没问题。

追问 4:MyBatis 启动时怎么扫描 XML?
答:靠配置——XML 路径在 mybatis-config.xml 的 <mappers> 节,或者 Spring Boot 的 mybatis.mapper-locations(默认扫 classpath*:/mapper/**/*.xml)。

追问 5:MapperProxy 里的 methodCache 是 Map<Method, MapperMethod>,缓存这个有什么意义?
答:避免每次方法调用都重新解析 Method 的注解、参数、返回类型——这些信息在类加载后是不变的。这是高频调用场景的关键优化。

追问 6:MyBatis-Plus 的 BaseMapper 是怎么基于这套机制扩展的?
答:MP 的 BaseMapper<T> 接口里定义了 selectById / insert / updateById 这些通用方法,但它没改 MyBatis 的代理机制——只是在容器启动时通过 SqlInjector 往 Configuration.mappedStatements 里多塞了一批”动态生成的 MappedStatement”(每个 BaseMapper 方法对应一条)。所以 MapperProxy 拦截到 selectById 调用时一样能按 namespace + id 找到对应 SQL,就像它们写在 XML 里一样。

一句话总结 MP 和 MyBatis 的关系:MP 不是另一套 ORM,它就是 MyBatis 上面套了一层”通用 SQL 自动生成器”——理解了 MyBatis 的代理机制,MP 的源码改起来就没什么神秘感。

一句话收口

Mapper 不需要实现类,是因为 MyBatis 在运行时用 JDK 动态代理生成了代理对象。MapperProxy 拦截调用,按 namespace + id 找到 MappedStatement,通过 MapperMethod 路由到 SqlSession 执行 SQL。

整套机制的本质是「接口 + 配置约定 = 实现」——这是模板方法模式的极致用法。

这题答到 30 分容易,答到 90 分靠报得出 5 个核心类 + 解释 JDK vs CGLIB + 知道 MapperFactoryBean。

文章目录
  1. 1. 这道题面试官真正想筛什么
  2. 2. L1:30 秒答案过基础线
  3. 3. L2:2 分钟答案讲清桥梁
  4. 4. L3:5 分钟答案显源码深度
    1. 4.0.1. 五个核心类速记
    2. 4.0.2. 为什么用 JDK 动态代理而不是 CGLIB
    3. 4.0.3. Spring 集成的额外环节(高分位)
  • 5. 直接掉分的几种答法
  • 6. 高频追问怎么接
  • 7. 一句话收口