- 文档
- 教程
- 后端
【免费下载链接】CodeGuide
:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!
本文基于 CodeGuide 仓库中《面经手册 · 第35篇》的完整论述展开,并辅以仓库内《Mybatis 手撸专栏》(第2章映射器代理工厂、第3章映射器注册)与《Mybatis 接口没有实现类为什么可以执行增删改查》源码分析等资料进行交叉印证。读完本文,你将能说清
getMapper到PreparedStatement.execute之间的每一层职责、MapperMethod如何路由 SQL 类型、Mapper 方法为何不能重载,以及 namespace 的三重身份,从"知道动态代理"升级为"真懂动态代理"。
一、从一段"平平无奇"的代码说起
如果你用过 MyBatis,一定写过这样的代码:
UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.findById(1L);看起来平平无奇,但仔细想想——UserMapper是一个接口,没有实现类,findById方法也没有方法体,那这行代码到底是怎么执行的?谁来实现了这个接口?方法调用怎么就变成了 SQL 执行?
这就是 MyBatis 最巧妙的设计:用 JDK 动态代理为 Mapper 接口生成了实现类,让你以面向接口的方式操作数据库,而不需要写任何实现代码。
但面试官不会只问"动态代理"四个字就放过你。MapperProxy 是什么时候创建的?invoke 方法里做了什么?MapperMethod 又是怎么路由到 SqlSession 的?方法能不能重载?namespace 到底绑定了什么?——这些才是区分"背过八股"和"真看过源码"的分水岭。
二、面试题:谢飞机的"再战"
谢飞机,小记!,上次背了#{}和${}的区别,这次信心满满又来了。
面试官:谢飞机,MyBatis 的 Mapper 接口没有实现类,它是怎么工作的?
谢飞机:动态代理!MyBatis 用 JDK 动态代理给 Mapper 接口生成了代理对象。
面试官:嗯,那代理对象的 invoke 方法里做了什么?
谢飞机:呃... 好像是调了 SqlSession 的方法?
面试官:中间还有 MapperMethod,你知道它的作用吗?
谢飞机:MapperMethod... 就是封装了一下?
面试官:封装了什么?SqlSession 那么多方法,它怎么知道该调 select 还是 insert?
谢飞机:这个... 根据方法名?
面试官:Mapper 接口的方法能重载吗?为什么?
谢飞机:应该... 不能?因为... 同名方法会冲突?
面试官:冲突在哪?你说清楚。
谢飞机:我... 再见!ヾ( ̄▽ ̄)
这轮对话暴露出的四个追问点——代理对象怎么创建、invoke 里做了什么、MapperMethod 怎么路由、为什么不能重载——正是本文要逐一拆解的核心。
三、Mapper 接口工作原理全链路
1. 从 getMapper 说起
一切从sqlSession.getMapper(UserMapper.class)开始:
SqlSession.getMapper(UserMapper.class) → Configuration.getMapper(UserMapper.class, sqlSession) → MapperRegistry.getMapper(UserMapper.class, sqlSession) → MapperProxyFactory.newInstance(sqlSession) → Proxy.newProxyInstance(...) ← JDK 动态代理创建代理对象最终返回的不是UserMapper的实现类实例,而是一个JDK 动态代理对象,它的 InvocationHandler 是MapperProxy。
这条链路在仓库的源码分析资料中被完整还原过:DefaultSqlSession.getMapper→Configuration.getMapper→MapperRegistry.getMapper→MapperProxyFactory.newInstance,可参见 Mybatis 接口没有实现类为什么可以执行增删改查。
2. 调用链路总览
代理对象拿到手之后,每次调用接口方法都会走这样一条调用链:
userMapper.findById(1L) → MapperProxy.invoke() ← 代理拦截 → MapperMethod.execute() ← 方法路由 → SqlSession.selectOne() ← 具体 SQL 执行 → Executor.query() ← 执行器 → StatementHandler.query() ← 语句处理器 → PreparedStatement.execute() ← JDBC 执行一句话概括:接口方法调用 → 代理拦截 → 方法路由 → SqlSession 执行 SQL。
四、源码追踪:从注册到执行
1. MapperRegistry — Mapper 的注册中心
MyBatis 启动时,会解析所有的 Mapper 接口和 XML 映射文件,把每个接口注册到MapperRegistry中:
// org.apache.ibatis.binding.MapperRegistry public class MapperRegistry { // 核心:接口 → MapperProxyFactory 的映射 private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>(); public <T> void addMapper(Class<T> type) { if (type.isInterface()) { if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } try { // 为每个接口创建一个 MapperProxyFactory knownMappers.put(type, new MapperProxyFactory<>(type)); } catch (Exception e) { throw new BindingException("Error adding mapper.", e); } } } @SuppressWarnings("unchecked") public <T> T getMapper(Class<T> type, SqlSession sqlSession) { final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) 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.", e); } } }关键发现:MapperRegistry内部维护了一个knownMappers集合,Key 是 Mapper 接口的 Class 对象,Value 是MapperProxyFactory。每个 Mapper 接口对应一个工厂,由工厂负责创建代理对象。注意addMapper中还有一层守护逻辑:只有type.isInterface()为 true 的接口才会被注册,这从注册源头就锁定了 Mapper 必须是接口的事实。
注册的触发入口有两种:
- XML 方式:
XMLMapperBuilder.bindMapperForNamespace()在解析完 XML 后,尝试用 namespace 反查接口类,若存在则调用configuration.addMapper(boundType)完成注册(详见 源码分析文档 中的bindMapperForNamespace片段); - 包扫描方式:
<mappers><package name="..."/></mappers>或 Spring 的MapperScannerConfigurer扫描指定包,逐个调用addMapper。
2. MapperProxyFactory — 代理对象的工厂
// org.apache.ibatis.binding.MapperProxyFactory public class MapperProxyFactory<T> { private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>(); public MapperProxyFactory(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } @SuppressWarnings("unchecked") protected T newInstance(MapperProxy<T> mapperProxy) { // JDK 动态代理:创建实现 mapperInterface 接口的代理对象 return (T) Proxy.newProxyInstance( mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy ); } public T newInstance(SqlSession sqlSession) { // 创建 MapperProxy(InvocationHandler) final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); // 创建代理对象 return newInstance(mapperProxy); } }关键发现:
methodCache是一个ConcurrentHashMap,缓存了Method → MapperMethod的映射。同一个方法不会重复创建MapperMethod对象。Proxy.newProxyInstance()是 JDK 动态代理的核心 API,它要求目标必须是接口,这正是 Mapper 只能是接口的原因。- 这里还藏着一个工厂方法模式的应用:
newInstance(SqlSession)负责组装MapperProxy,再委托给newInstance(MapperProxy)创建代理对象。仓库的《Mybatis 手撸专栏》第2章正是围绕这一对类展开的简化实现,可以对照阅读 第2章:创建简单的映射器代理工厂。
3. MapperProxy — 代理拦截的核心
// org.apache.ibatis.binding.MapperProxy public class MapperProxy<T> implements InvocationHandler, Serializable { private final SqlSession sqlSession; private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache; public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; this.methodCache = methodCache; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 如果调用的是 Object 的方法(toString、hashCode、equals 等),直接执行 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } // 从缓存获取或创建 MapperMethod,然后执行 return cachedMapperMethod(method).execute(sqlSession, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } private MapperMethod cachedMapperMethod(Method method) { return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); } }关键发现:
invoke是代理拦截的入口。每次调用 Mapper 接口的方法,都会走进这个方法。- 如果调用的是
Object类的方法(如toString()),不走代理逻辑,直接反射调用。在更高版本源码中,还额外增加了对接口 default 方法的判断(isDefaultMethod+MethodHandles.Lookup调用),这一细节在仓库的 源码分析文档 的MapperProxy完整代码中有体现。 - 核心逻辑就一行:
cachedMapperMethod(method).execute(sqlSession, args)——找到对应的MapperMethod,把SqlSession和参数传给它执行。 MapperMethod有缓存(computeIfAbsent原子写入),不会每次调用都新建。
4. MapperMethod — 方法路由器
// org.apache.ibatis.binding.MapperMethod public class MapperMethod { private final SqlCommand command; private final MethodSignature method; public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) { this.command = new SqlCommand(config, mapperInterface, method); this.method = new MethodSignature(config, mapperInterface, method); } public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.insert(command.getName(), param); break; case UPDATE: param = method.convertArgsToSqlCommandParam(args); result = sqlSession.update(command.getName(), param); break; case DELETE: param = method.convertArgsToSqlCommandParam(args); result = sqlSession.delete(command.getName(), param); break; case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { // 默认:返回单个对象 param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); } break; case FLUSH: result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + command.getName()); } // 返回值校验 if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException("Mapper method '" + command.getName() + "' attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); } return result; } }关键发现:
MapperMethod内部有两个重要组件:SqlCommand(SQL 命令信息)和MethodSignature(方法签名信息)。execute方法根据command.getType()决定调用SqlSession的哪个方法(select/insert/update/delete)。- 对于 SELECT,还要根据返回值类型(单个对象、列表、Map、游标、ResultHandler)做进一步路由:
returnsMany走executeForMany,returnsMap走executeForMap,returnsCursor走executeForCursor,都对应SqlSession上不同的查询 API。 command.getName()返回的是namespace.id格式的全限定名,这就是定位MappedStatement的唯一标识。- execute 末尾还有一道返回值防线:如果方法返回基本类型(如
int)但结果为 null,直接抛BindingException,避免拆箱 NPE 出现在业务层。
这里注意一个细节差异:INSERT/UPDATE/DELETE 的SqlSession.insert/update/delete返回的是影响行数(int),而 MapperMethod 中部分版本还会用rowCountResult做包装,将行数转换为方法声明的返回类型(Integer/Long/Boolean 均可),说明路由层对返回值做了弹性适配。
5. SqlCommand — SQL 命令的元信息
// org.apache.ibatis.binding.MapperMethod.SqlCommand public static class SqlCommand { private final String name; // MappedStatement 的 ID = namespace + "." + 方法名 private final SqlCommandType type; // SQL 类型:SELECT / INSERT / UPDATE / DELETE public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) { final String methodName = method.getName(); final String declaringClass = method.getDeclaringClass().getName(); String statementName = declaringClass + "." + methodName; // ← 关键:全限定名 if (configuration.hasStatement(statementName)) { this.name = statementName; } else { // 兜底:用接口全限定名 + 方法名 String debugStatementName = mapperInterface.getName() + "." + methodName; this.name = debugStatementName; } this.type = configuration.getMappedStatement(this.name).getSqlCommandType(); } }关键发现:statementName的构造方式是接口全限定名 + "." + 方法名,这正好对应 XML 中<select id="findById">和namespace="com.example.UserMapper"的组合。这就是 Mapper 接口方法和 XML 映射 SQL 的绑定纽带。
SqlCommandType枚举(SELECT/INSERT/UPDATE/DELETE/FLUSH 等)是在解析 XML 时根据标签类型写入MappedStatement的,execute的 switch 分支正是依据它完成"方法调用 → SqlSession API"的路由分发。
五、方法重载问题
1. 为什么 Mapper 接口方法不能重载?
Java 允许接口方法重载:
public interface UserMapper { List<User> findByIds(List<Long> ids); List<User> findByIds(Long[] ids); // 编译通过,但运行时会出问题 }但 MyBatis 中不允许方法重载。原因要从MappedStatement的 ID 构造规则说起。
2. 根因分析
MappedStatement的唯一标识 =namespace + "." + 方法名。
接口:com.example.UserMapper 方法1:List<User> findByIds(List<Long> ids) → MappedStatement ID: com.example.UserMapper.findByIds 方法2:List<User> findByIds(Long[] ids) → MappedStatement ID: com.example.UserMapper.findByIds ↑ 两个方法生成了相同的 ID!MyBatis 在解析 XML 映射文件时,每个<select>/<insert>/<update>/<delete>标签都会生成一个MappedStatement对象,以namespace.id作为唯一 Key 存入Configuration的mappedStatements集合(StrictMap):
// org.apache.ibatis.session.Configuration protected final Map<String, MappedStatement> mappedStatements = new StrictMap<>("Mapped Statements collection"); public void addMappedStatement(MappedStatement ms) { mappedStatements.put(ms.getId(), ms); // 如果 ID 重复,StrictMap 会抛异常 }StrictMap不允许重复 Key:
// org.apache.ibatis.session.Configuration.StrictMap @Override public V put(String key, V value) { if (containsKey(key)) { throw new IllegalArgumentException(name + " already contains value for " + key); } return super.put(key, value); }两种冲突场景:
- XML 方式:两个重载方法对应同一个
<select id="findByIds">,ID 重复 → 启动时抛异常。 - 注解方式:两个重载方法都加了
@Select,生成的MappedStatementID 相同 → 启动时抛异常。
仓库的面经手册第34篇在总结设计模式与绑定规则时也特别点出:namespace + 方法名(例如com.example.UserMapper.findById)是 MyBatis 定位语句的 ID 规则,这也是 Mapper 接口方法不能重载的原因——ID 必须唯一,见 第34篇《MyBatis 工作原理是什么?》。
3. 结论
Mapper 接口方法不能重载,因为MappedStatement的 ID 只由namespace + 方法名组成,不包含参数签名。重载方法同名会导致 ID 冲突。
六、接口绑定的两种方式
1. XML 绑定
通过 XML 映射文件的namespace指定 Mapper 接口的全限定名:
<!-- UserMapper.xml --> <mapper namespace="com.example.mapper.UserMapper"> <select id="findById" resultType="User"> SELECT * FROM user WHERE id = #{id} </select> <insert id="insert"> INSERT INTO user(name, age) VALUES(#{name}, #{age}) </insert> </mapper>// UserMapper.java public interface UserMapper { User findById(Long id); int insert(User user); }绑定规则:namespace= 接口全限定名,id= 方法名。
2. 注解绑定
通过在接口方法上添加注解直接编写 SQL:
// UserMapper.java public interface UserMapper { @Select("SELECT * FROM user WHERE id = #{id}") User findById(Long id); @Insert("INSERT INTO user(name, age) VALUES(#{name}, #{age})") int insert(User user); @Update("UPDATE user SET name = #{name} WHERE id = #{id}") int update(User user); @Delete("DELETE FROM user WHERE id = #{id}") int delete(Long id); }四个核心注解:
| 注解 | 对应 SQL 类型 | 示例 |
|---|---|---|
@Select | SELECT | @Select("SELECT * FROM user WHERE id = #{id}") |
@Insert | INSERT | @Insert("INSERT INTO user(name) VALUES(#{name})") |
@Update | UPDATE | @Update("UPDATE user SET name = #{name}") |
@Delete | DELETE | @Delete("DELETE FROM user WHERE id = #{id}") |
注解方式还支持@Options(配置主键回写等)和@ResultMap(引用结果映射)。注解方式的解析入口是MapperAnnotationBuilder:MapperRegistry.addMapper在为接口创建MapperProxyFactory后,紧接着就会用MapperAnnotationBuilder.parse()解析接口上的注解 SQL,注册对应的MappedStatement。
3. 注解 vs XML:怎么选?
| 对比项 | 注解方式 | XML 方式 |
|---|---|---|
| SQL 可见性 | SQL 和接口在一起,一目了然 | SQL 集中在 XML,需要切换查看 |
| 动态 SQL | 支持(<script>标签),但很丑 | 天然支持(if/choose/foreach/where 等) |
| 复杂查询 | 多表关联、嵌套查询写起来痛苦 | 动态 SQL 标签灵活组合,可读性好 |
| SQL 长度 | 短 SQL 简洁,长 SQL 臃肿 | 长短都无影响 |
| 维护性 | 改 SQL 需改 Java 文件,重新编译 | 改 SQL 只改 XML,热更新方便 |
| 调试 | 不方便断点 | 可以查看解析后的完整 SQL |
实际项目推荐:
- 简单的单表 CRUD → 可以用注解,减少 XML 文件数量。
- 复杂的多表关联、动态条件查询 → 必须用 XML。
- 企业项目中绝大多数用 XML,因为业务 SQL 通常较复杂,动态 SQL 是刚需。
注解中的动态 SQL(不推荐,但确实支持):
@Select("<script>" + "SELECT * FROM user" + "<where>" + " <if test='name != null'>AND name = #{name}</if>" + " <if test='age != null'>AND age = #{age}</if>" + "</where>" + "</script>") List<User> findUsers(@Param("name") String name, @Param("age") Integer age);这种写法可读性极差,不如直接用 XML。
七、namespace 的作用
1. namespace 的三重身份
namespace 不仅仅是一个"命名空间",它在 MyBatis 中扮演三个重要角色:
角色一:SQL 隔离
不同 namespace 下可以有同名的 SQL ID,互不冲突:
<!-- UserMapper.xml --> <mapper namespace="com.example.mapper.UserMapper"> <select id="findById">SELECT * FROM user WHERE id = #{id}</select> </mapper> <!-- OrderMapper.xml --> <mapper namespace="com.example.mapper.OrderMapper"> <select id="findById">SELECT * FROM order WHERE id = #{id}</select> </mapper>两个findById不冲突,因为完整 ID 分别是com.example.mapper.UserMapper.findById和com.example.mapper.OrderMapper.findById。
角色二:绑定 Mapper 接口
当 namespace 等于某个 Mapper 接口的全限定名时,XML 中的 SQL 就和该接口的方法绑定:
namespace = "com.example.mapper.UserMapper" + id = "findById" → MappedStatement ID = "com.example.mapper.UserMapper.findById" → 对应 UserMapper.findById() 方法如果 namespace 写错了(和接口全限定名不一致),启动不会报错,但运行时getMapper调用方法会抛BindingException,找不到对应的MappedStatement。从源码看,bindMapperForNamespace()用Resources.classForName(namespace)反查接口类时若找不到类,会静默忽略(ClassNotFoundException被 catch 掉),这就是"启动不报错、运行才炸"的根源。
角色三:唯一标识 MappedStatement
Configuration中的mappedStatements集合以namespace.id作为 Key,namespace 保证了全局唯一性。
2. namespace 的常见错误
<!-- ❌ 错误:namespace 和接口全限定名不匹配 --> <mapper namespace="com.example.dao.UserDao"> <select id="findById">...</select> </mapper> <!-- 接口是 com.example.mapper.UserMapper --> <!-- 调用 userMapper.findById() → 找不到 MappedStatement → BindingException --><!-- ❌ 错误:namespace 重复 --> <!-- 两个 XML 文件 namespace 相同,ID 重复会报错 --><!-- ✅ 正确:namespace = 接口全限定名 --> <mapper namespace="com.example.mapper.UserMapper"> <select id="findById">...</select> </mapper>3. namespace 与跨 namespace 引用
MyBatis 支持跨 namespace 引用结果映射和 SQL 片段:
<!-- CommonMapper.xml --> <mapper namespace="com.example.mapper.CommonMapper"> <sql id="userColumns">id, name, age, email</sql> </mapper> <!-- UserMapper.xml --> <mapper namespace="com.example.mapper.UserMapper"> <select id="findAll" resultType="User"> SELECT <include refid="com.example.mapper.CommonMapper.userColumns"/> FROM user </select> </mapper>跨 namespace 引用使用完整的namespace.sqlId格式。
八、用"手写 MyBatis"验证核心思想
如果你觉得直接啃官方源码还是有距离,CodeGuide 仓库中的《Mybatis 手撸专栏》用渐进式方式重现了这一整套设计,非常适合对照验证:
- 第2章:创建简单的映射器代理工厂 用最小的代码量实现了
MapperProxy(实现InvocationHandler)与MapperProxyFactory(封装Proxy.newProxyInstance)。其invoke中那句"你的被代理了!" + sqlSession.get(mapperInterface.getName() + "." + method.getName()),正是对"接口全限定名 + 方法名作为 Key 定位操作"这一核心思想的直白演示——先用 Map 模拟 SqlSession,后续章节再逐步换成真实数据库操作。 - 第3章:实现映射器的注册和使用 补上了
MapperRegistry:knownMappers(Map<Class<?>, MapperProxyFactory<?>>)缓存接口与代理工厂的映射,addMapper注册、getMapper获取代理,DefaultSqlSession.getMapper直接委托给mapperRegistry.getMapper(type, this)——与官方MapperRegistry的结构如出一辙。
对照这三章代码,你会发现官方源码里"注册中心 + 代理工厂 + 代理拦截"的三层结构,其实每层都只做一件事:注册时记录"接口 → 工厂",获取时用"工厂 → 代理",调用时由代理 → 路由 → 执行。
九、常见面试追问
Q1:Mapper 接口能通过 new 创建实例吗?
不能。Mapper 接口是纯接口,没有实现类,new UserMapper()会编译报错。只能通过sqlSession.getMapper()或@Autowired(Spring 集成后)获取代理对象。
如果强行 new 了一个实现类,那也不是 MyBatis 管理的,无法执行 SQL。
Q2:@Mapper 和 @MapperScan 有什么区别?
@Mapper:标注在单个 Mapper 接口上,告诉 MyBatis-Spring 这个接口需要注册为 Bean。每个接口都要加,接口多了很繁琐。
@Mapper public interface UserMapper { ... }@MapperScan:标注在 Spring Boot 启动类或配置类上,指定包路径,自动扫描该包下所有 Mapper 接口并注册。推荐方式,一个注解搞定所有 Mapper。
@MapperScan("com.example.mapper") @SpringBootApplication public class Application { ... }本质:@MapperScan是批量版的@Mapper,底层都调用了MapperFactoryBean来创建代理对象并注册到 Spring 容器。展开说:@MapperScan通过@Import(MapperScannerRegistrar.class)引入MapperScannerRegistrar,它创建ClassPathMapperScanner扫描指定包,将每个接口的 BeanDefinition 替换为MapperFactoryBean,最终MapperFactoryBean.getObject()返回getSqlSession().getMapper(this.mapperInterface)得到的代理对象——也就是走回MapperRegistry.getMapper → MapperProxyFactory.newInstance这条老路。完整的 Spring 整合分析可参考 面经手册 · 第43篇《MyBatis 和 Spring 怎么整合?》。
Q3:一个 Mapper 接口能对应多个 XML 文件吗?
不能。MyBatis 不允许同一个 namespace 出现在多个 XML 文件中。如果两个 XML 的 namespace 相同,启动时会因为MappedStatementID 冲突报错。
如果一个接口的 SQL 太多,建议按业务拆分成多个接口(如UserReadMapper和UserWriteMapper),而不是一个接口对应多个 XML。
Q4:MapperProxy 为什么用 JDK 动态代理而不是 CGLIB?
因为 Mapper 是接口,JDK 动态代理专门用于接口代理,是 Java 原生支持的方案,不需要额外依赖。CGLIB 用于代理类(代理没有接口的类),而 Mapper 接口本身没有实现类,CGLIB 反而用不上。
MyBatis 的设计理念就是面向接口编程,所以 JDK 动态代理是最自然的选择。
Q5:getMapper 每次调用都创建新的代理对象吗?
是的。每次调用sqlSession.getMapper()都会通过MapperProxyFactory.newInstance()创建一个新的代理对象。但MapperMethod有缓存(methodCache),不会重复创建。
在 Spring 集成中,MapperFactoryBean是单例的,Spring 容器中每个 Mapper 只有一个代理实例。
十、总结
记住三个核心要点: 1. Mapper 接口的执行链路: 接口方法调用 → MapperProxy.invoke()(JDK动态代理) → MapperMethod.execute()(方法路由,根据SQL类型分发) → SqlSession.selectXxx/insert/update/delete(执行SQL) 2. Mapper 接口方法不能重载: MappedStatement 的 ID = namespace + 方法名 重载方法同名 → ID 冲突 → 启动报错 3. namespace 的三重作用: SQL 隔离(不同namespace可以有同名ID) + 绑定接口(namespace = 接口全限定名) + 唯一标识(namespace.id 是 MappedStatement 的全局唯一Key)面试回答模板:
Mapper 接口没有实现类,MyBatis 通过 JDK 动态代理为它生成代理对象。调用流程是:
SqlSession.getMapper()从MapperRegistry中找到对应的MapperProxyFactory,由工厂创建MapperProxy代理实例。当调用接口方法时,MapperProxy.invoke()拦截调用,根据 Method 对象从缓存获取或创建MapperMethod,MapperMethod内部的SqlCommand持有 SQL 类型和MappedStatement的 ID,execute方法根据 SQL 类型路由到SqlSession对应的方法执行。Mapper 接口方法不能重载,因为
MappedStatement的 ID 只由 namespace + 方法名组成,不包含参数签名,重载方法会生成相同 ID 导致冲突。接口绑定有 XML 和注解两种方式,XML 通过 namespace 绑定接口,注解通过
@Select/@Insert等直接在方法上定义 SQL。实际项目推荐 XML,因为动态 SQL 支持更好,复杂查询更易维护。
延伸阅读:本文只拆到了MapperMethod路由这一层。再往下走,SqlSession的实现类DefaultSqlSession会把请求交给Executor(执行器),经过StatementHandler(语句处理器)、ParameterHandler(参数处理器)、ResultSetHandler(结果集处理器)最终落到 JDBC 的PreparedStatement.execute()。如果你想把这条链路继续看完,可以在仓库中按顺序研读《面经手册》第34篇《MyBatis 工作原理是什么?从 SqlSessionFactory 到 SqlSession 全链路解析》与《Mybatis 手撸专栏》后续章节(执行器、参数处理器、结果集处理器、缓存、插件),那将是一条从"会用"到"会写"的完整进阶路线。
- 文档
- 教程
- 后端
【免费下载链接】CodeGuide
:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!
相关推荐
MyBatis binding 模块源码解析:Mapper 接口动态代理与 SQL 绑定的底层实现
MyBatis binding 模块源码解析:Mapper 接口动态代理与 SQL 绑定的底层实现 导读 本文围绕 MyBatis 基础支持层中的 bindin
文档教程知识库Workflow Designer:企业级可视化流程设计工具如何提升业务效率5倍?
Workflow Designer:企业级可视化流程设计工具如何提升业务效率5倍? Workflow Designer 是一款基于 React 和 AntV G
MyBatis 初始化原理全解析:从 SqlSessionFactoryBuilder 到 Mapper 绑定的启动链路
MyBatis 初始化原理全解析:从 SqlSessionFactoryBuilder 到 Mapper 绑定的启动链路 导读 与 Spring 框架的 IoC
文档教程知识库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考