找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5480

积分

0

好友

769

主题
发表于 2 小时前 | 查看: 7| 回复: 0

「MyBatis Mapper 接口为什么不需要写实现类」这道题在 Java 面试里几乎人人都会答,但能答到加分位的不到 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)。下面按段位拆开说。

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

从答题深度来看,三个段位的分水岭很清晰:

  • 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。

完整调用链路分两个阶段:

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

  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. invokeMethod 封装为 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 的代理机制——只是在容器启动时通过 SqlInjectorConfiguration.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 开发者一起交流实战经验。




上一篇:Wireshark抓包分析教程:过滤器设置与TCP三次握手实战
下一篇:AI Agent安全防护警钟:OpenAI总裁Greg警告企业CISO
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-8-19 21:34 , Processed in 1.339425 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表