「MyBatis Mapper 接口为什么不需要写实现类」这道题在 Java 面试里几乎人人都会答,但能答到加分位的不到 20%。
国内 Java 项目几乎被 MyBatis(+ MyBatis-Plus)统治:SSM 时代起步,Spring Boot 时代延续,用 JPA / Hibernate 的项目肉眼可见地少。这题本质上是在筛你对国内 80% Java 项目背后那套 ORM 机制的理解深度,所以面试官特别喜欢问。
表面上考动态代理,实际在筛三件事:
- 你懂不懂 JDK 动态代理的工作机制(
Proxy + InvocationHandler,不是只听过名字);
- 你清不清楚
namespace + id 这个 MyBatis 的核心约定(怎么把接口和 SQL 连起来);
- 你翻没翻过 MyBatis 源码,能不能说出几个核心类的职责和位置。
候选人按作答深度大致分三档:30 分(只答出「动态代理」三个字)、60 分(讲清桥梁约定)、90 分(能报出五个核心类,并能解释为什么是 JDK 而不是 CGLIB)。下面按段位拆开说。
这道题面试官真正想筛什么
从答题深度来看,三个段位的分水岭很清晰:
- 30 分档:只知道「动态代理」这个名词,说不清它怎么和 Mapper 接口、XML 对应上。
- 60 分档:能讲清楚
namespace + id 的桥梁作用,以及完整的调用链路。
- 90 分档:还能报出核心类清单,并说明 JDK 动态代理和 CGLIB 的取舍逻辑。
L1:30 秒答案过基础线
MyBatis 在运行时用 JDK 动态代理为 Mapper 接口生成代理对象。开发者调用方法时被代理拦截,根据「接口全限定名 + 方法名」找到对应的 SQL 元数据,委托给 SqlSession 执行。
这是基础线,30 分,几乎每位候选人都会答到这里。面试官听完通常会接一句:「那它是怎么把接口方法和 XML 里的 SQL 关联起来的?」这就进入 L2 战场了。所有候选人都会答到这,面试官接下来关心的,才是真正拉开差距的地方。
L2:2 分钟答案讲清桥梁
L2 的关键是讲清接口和 XML 到底是靠什么对上的。
核心约定一句话:
XML 的 namespace = 接口全限定名;SQL 标签的 id = 接口方法名;两者拼接 = MappedStatement 的 key。
完整调用链路分两个阶段:
阶段一:代理创建(容器启动时)
XMLMapperBuilder 解析每个 Mapper XML,按 namespace 把接口注册到 MapperRegistry;
- 每个 SQL 标签生成一个
MappedStatement,以 namespace.id 为 key 存入 Configuration.mappedStatements;
- 调用
sqlSession.getMapper(UserMapper.class) 时,通过 MapperProxyFactory.newInstance() 创建代理;
- 代理对象持有
MapperProxy(实现了 InvocationHandler)。
阶段二:方法调用
- JVM 把
mapper.selectById(1L) 转给 MapperProxy.invoke();
invoke 把 Method 封装为 MapperMethod(带缓存,避免每次都解析);
MapperMethod 拼出 statementId = "com.xxx.UserMapper.selectById";
- 委托给
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 项目里还有一层:
@MapperScan 触发 MapperScannerConfigurer;
- 它把每个 Mapper 接口注册成
FactoryBean(具体是 MapperFactoryBean);
- Spring 容器创建 Bean 时调
MapperFactoryBean.getObject();
getObject() 内部最终还是调 sqlSession.getMapper(mapperInterface) 拿到代理;
- 这个代理被注册成 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。
如果你对这类源码级面试题感兴趣,欢迎到 云栈社区 和更多 Java 开发者一起交流实战经验。