CodeGuide 深度解析:MyBatis Mapper 接口没有实现类,动态代理源码链路全拆解
2026/9/24 16:44:36 网站建设 项目流程
  • 文档
  • 教程
  • 后端

【免费下载链接】CodeGuide

:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!

项目地址:https://gitcode.com/gh_mirrors/code/CodeGuide
点击查看免费下载

本文基于 CodeGuide 仓库中《面经手册 · 第35篇》的完整论述展开,并辅以仓库内《Mybatis 手撸专栏》(第2章映射器代理工厂、第3章映射器注册)与《Mybatis 接口没有实现类为什么可以执行增删改查》源码分析等资料进行交叉印证。读完本文,你将能说清getMapperPreparedStatement.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.getMapperConfiguration.getMapperMapperRegistry.getMapperMapperProxyFactory.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)做进一步路由:returnsManyexecuteForManyreturnsMapexecuteForMapreturnsCursorexecuteForCursor,都对应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 存入ConfigurationmappedStatements集合(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 类型示例
@SelectSELECT@Select("SELECT * FROM user WHERE id = #{id}")
@InsertINSERT@Insert("INSERT INTO user(name) VALUES(#{name})")
@UpdateUPDATE@Update("UPDATE user SET name = #{name}")
@DeleteDELETE@Delete("DELETE FROM user WHERE id = #{id}")

注解方式还支持@Options(配置主键回写等)和@ResultMap(引用结果映射)。注解方式的解析入口是MapperAnnotationBuilderMapperRegistry.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.findByIdcom.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章:实现映射器的注册和使用 补上了MapperRegistryknownMappersMap<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 太多,建议按业务拆分成多个接口(如UserReadMapperUserWriteMapper),而不是一个接口对应多个 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 对象从缓存获取或创建MapperMethodMapperMethod内部的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核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!

项目地址:https://gitcode.com/gh_mirrors/code/CodeGuide
点击查看免费下载
上一篇:3分钟快速导出QQ空间历史说说的完整指南:GetQzonehistory免费工具使用教程
下一篇:5分钟搭建缠论分析系统:免费开源ChanlunX通达信插件终极指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询