Mapper接口的代理实现——MapperProxy与MapperMethod
摘要
Mybatis 最优雅的设计之一,是让开发者只需要定义一个 Java 接口,框架就能自动生成其实现——userMapper.selectById(1L) 背后没有任何手写的实现类,有的只是框架在运行时动态生成的代理对象。这个代理对象通过 JDK 动态代理实现,核心由 MapperProxy(代理处理器)和 MapperMethod(方法分析器)共同完成。本文深入剖析从 getMapper(UserMapper.class) 到接口方法被执行的全过程:MapperRegistry 如何管理 Mapper 接口的注册与实例化、MapperProxy.invoke() 如何分发方法调用、MapperMethod 如何分析方法签名并映射到正确的 SqlSession 操作,以及返回值类型推断(单对象/列表/Optional/游标/Map)的完整逻辑。
第 1 章 为什么 Mapper 接口不需要实现类
1.1 传统 DAO 模式的痛点
在 Mybatis 之前(包括 Mybatis 早期版本),数据访问层通常这样写:
// 传统写法:SqlSession API 直接调用
public class UserDaoImpl implements UserDao {
private SqlSessionFactory sqlSessionFactory;
public User selectById(Long id) {
try (SqlSession session = sqlSessionFactory.openSession()) {
// 字符串 ID 容易拼错,无编译期检查
return session.selectOne("com.example.UserMapper.selectById", id);
}
}
public int insert(User user) {
try (SqlSession session = sqlSessionFactory.openSession()) {
int rows = session.insert("com.example.UserMapper.insert", user);
session.commit();
return rows;
}
}
// 每个方法都要重复 openSession/close,极度冗余
}这种写法的核心问题:
- 字符串 ID 缺少编译期检查:
"com.example.UserMapper.selectById"拼错了,编译不报错,只在运行时才炸; - SqlSession 管理冗余:每个方法都要
openSession/close,模板代码量大; - 无 IDE 支持:字符串 ID 不支持 IDE 的”查找引用”、“重命名”等重构功能;
- 接口与实现重复:
UserDao接口定义了方法,UserDaoImpl只是把字符串调用包了一层,没有任何业务逻辑,纯粹是噪声代码。
1.2 Mapper 接口代理的解决思路
Mybatis 的解决方案:让 Mapper XML 中的 namespace 与 Java 接口全限定名绑定,接口方法名与 SQL 语句 ID 绑定,然后用 JDK 动态代理自动生成接口的实现。
<!-- namespace 绑定到接口全限定名 -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- SQL ID 绑定到接口方法名 -->
<select id="selectById" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>// 接口定义(无需实现类)
public interface UserMapper {
User selectById(Long id);
int insert(User user);
}
// 使用(代理对象在 Spring 容器中像普通 Bean 一样注入)
@Autowired
private UserMapper userMapper; // 实际是 MapperProxy 代理对象
public User getUser(Long id) {
return userMapper.selectById(id); // 调用代理,最终执行 SQL
}代理的实现让 IDE 完全支持接口方法的重构(重命名接口方法时,可以同步更新 XML 中的 ID),同时消除了 DAO 实现类的模板代码。
第 2 章 MapperRegistry:Mapper 接口的注册中心
2.1 注册机制
MapperRegistry 是 Configuration 的一个字段,负责管理所有 Mapper 接口的注册信息:
public class MapperRegistry {
private final Configuration config;
// key: Mapper 接口的 Class 对象
// value: MapperProxyFactory(用于创建 MapperProxy 实例)
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
// 注册一个 Mapper 接口
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
// 防止重复注册(重复注册通常意味着配置错误)
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 创建 MapperProxyFactory 并注册
knownMappers.put(type, new MapperProxyFactory<>(type));
// 解析该 Mapper 接口对应的注解(@Select、@Insert 等)
// 或触发对应 XML 文件的解析(如果尚未解析)
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
// 解析失败,从注册表中移除,防止留下半初始化状态
knownMappers.remove(type);
}
}
}
}
// 获取 Mapper 代理实例
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);
}
}
}2.2 注册的触发时机
Mapper 接口的注册发生在 Mybatis 初始化阶段,有三种触发方式:
方式一:mybatis-config.xml 中配置 <mappers>
<!-- 按包扫描(推荐,最常用) -->
<mappers>
<package name="com.example.mapper"/>
</mappers>
<!-- 或者逐个配置(适合少量 Mapper) -->
<mappers>
<mapper resource="com/example/mapper/UserMapper.xml"/>
<mapper class="com.example.mapper.OrderMapper"/> <!-- 直接注册接口 -->
</mappers>方式二:Spring Boot + Mybatis-Spring-Boot-Starter 自动扫描
通过 @MapperScan 注解或 application.yml 的 mybatis.mapper-locations 配置,由 ClassPathMapperScanner 自动扫描并注册。
方式三:在 Mapper 接口上加 @Mapper 注解
Spring Boot 的 MybatisAutoConfiguration 会自动扫描被 @Mapper 标注的接口。
第 3 章 MapperProxyFactory:代理工厂
MapperProxyFactory 是一个轻量级工厂,每个 Mapper 接口对应一个 MapperProxyFactory 实例:
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface; // 要代理的接口类型
// 缓存已分析过的 MapperMethod(key: Method 对象,value: MapperMethodInvoker)
// 避免每次方法调用都重新分析方法签名
private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
// 创建 MapperProxy 实例(JDK 动态代理)
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
// 使用 JDK 动态代理:代理对象实现了 mapperInterface,InvocationHandler 是 mapperProxy
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[]{mapperInterface},
mapperProxy);
}
}methodCache 是关键性能优化点:MapperMethod 的构建需要通过反射分析方法签名、查找对应的 MappedStatement 等,开销不小。将已分析结果缓存在 ConcurrentHashMap 中,同一方法的后续调用直接复用,避免重复分析。
第 4 章 MapperProxy:代理处理器的核心逻辑
4.1 invoke() 方法的完整分发逻辑
MapperProxy 实现了 JDK 的 InvocationHandler 接口,是代理对象所有方法调用的入口:
public class MapperProxy<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = -4724728412955527868L;
// 当前 SqlSession(每个请求/事务对应一个 SqlSession)
private final SqlSession sqlSession;
// 要代理的接口类型
private final Class<T> mapperInterface;
// 方法缓存:避免重复分析
private final Map<Method, MapperMethodInvoker> methodCache;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 分支一:Object 类的方法(toString/hashCode/equals 等)
// 直接调用,不走 Mybatis 的 SQL 逻辑
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 分支二:接口的 default 方法(Java 8+)
// 通过 MethodHandle 调用接口的默认实现
// 分支三:普通接口抽象方法(绝大多数情况)
// 查找或创建 MapperMethodInvoker,执行 SQL
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
// 查找或创建缓存的 MapperMethodInvoker
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
// computeIfAbsent:如果 methodCache 中没有,就创建并放入
return MapUtil.computeIfAbsent(methodCache, method, m -> {
if (m.isDefault()) {
// default 方法:使用 DefaultMethodInvoker 包装 MethodHandle
try {
// Java 8 与 Java 9+ 的 MethodHandle 获取方式不同
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
// 普通抽象方法:创建 PlainMethodInvoker,包装 MapperMethod
return new PlainMethodInvoker(
new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
}4.2 default 方法的特殊处理
Java 8 引入了接口的 default 方法,让接口可以提供默认实现。在 Mapper 接口中,default 方法有时被用于组合多个查询或添加通用逻辑:
public interface UserMapper {
User selectById(Long id);
// default 方法:组合多个查询,提供更高层次的抽象
default UserWithOrders selectWithOrders(Long id) {
User user = selectById(id);
List<Order> orders = selectOrdersByUserId(id);
return new UserWithOrders(user, orders);
}
List<Order> selectOrdersByUserId(Long userId);
}当调用 userMapper.selectWithOrders(1L) 时,MapperProxy.invoke() 检测到这是 default 方法,通过 MethodHandle 调用接口中的默认实现(而不是走 Mybatis 的 SQL 执行路径)。
MethodHandle 是 Java 7 引入的方法调用机制,可以理解为”类型安全的方法引用”,比反射更高效且类型安全。对于接口 default 方法,需要通过特殊方式获取 MethodHandle:
// Java 9+ 的实现(推荐)
private MethodHandle getMethodHandleJava9(Method method) throws Exception {
final Class<?> declaringClass = method.getDeclaringClass();
return MethodHandles.lookup()
.findSpecial(declaringClass, method.getName(),
MethodType.methodType(method.getReturnType(), method.getParameterTypes()),
declaringClass)
.bindTo(proxy);
}第 5 章 MapperMethod:方法签名分析与执行分发
5.1 MapperMethod 的职责
MapperMethod 是 Mapper 接口方法与 SqlSession 操作之间的桥梁,它在初始化时完成两件事:
SqlCommand:分析方法对应的 SQL 命令类型(SELECT/INSERT/UPDATE/DELETE/FLUSH)和 Statement ID;MethodSignature:分析方法的参数列表和返回值类型,决定如何传参、如何处理返回值。
public class MapperMethod {
private final SqlCommand command; // SQL 命令信息
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);
}
// 执行:根据 SQL 类型分发到正确的 SqlSession 方法
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
// 处理参数,调用 sqlSession.insert()
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
// SELECT 的处理最复杂:根据返回值类型选择不同的 SqlSession API
if (method.returnsVoid() && method.hasResultHandler()) {
// 方法参数中有 ResultHandler:流式处理结果
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
// 返回 List 或数组
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
// 返回 Map(方法上有 @MapKey 注解)
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
// 返回 Cursor(流式游标,不一次性加载到内存)
result = executeForCursor(sqlSession, args);
} else {
// 返回单个对象(或 Optional)
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
// 刷新 BatchExecutor 的批次(批量模式专用)
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
// 如果方法声明返回类型是基本类型(int/boolean 等),但实际结果为 null,
// 抛出异常(防止 NullPointerException 自动拆箱)
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;
}
}5.2 SqlCommand:绑定 SQL 语句
SqlCommand 在初始化时解析方法对应的 MappedStatement:
public static class SqlCommand {
private final String name; // MappedStatement ID(如 "com.example.UserMapper.selectById")
private final SqlCommandType type; // SELECT / INSERT / UPDATE / DELETE / FLUSH / UNKNOWN
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
final String methodName = method.getName();
final Class<?> declaringClass = method.getDeclaringClass();
// 查找 MappedStatement:先在声明类的 namespace 中查找,再在接口 namespace 中查找
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration);
if (ms == null) {
// 如果方法有 @Flush 注解(BatchExecutor 模式用于提交批次),SqlCommandType = FLUSH
if (method.getAnnotation(Flush.class) != null) {
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
name = ms.getId();
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
// 构建 statementId:接口全限定名 + "." + 方法名
String statementId = mapperInterface.getName() + "." + methodName;
if (configuration.hasStatement(statementId)) {
return configuration.getMappedStatement(statementId);
} else if (mapperInterface.equals(declaringClass)) {
return null;
}
// 如果当前接口没找到,去父接口中查找(支持接口继承)
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
}5.3 MethodSignature:返回值类型的推断逻辑
MethodSignature 分析方法的参数和返回值,是 Mybatis 自动处理各种返回类型的关键:
public static class MethodSignature {
private final boolean returnsMany; // 返回 Collection 或数组?
private final boolean returnsMap; // 返回 Map(有 @MapKey 注解)?
private final boolean returnsVoid; // 返回 void?
private final boolean returnsCursor; // 返回 Cursor(流式游标)?
private final boolean returnsOptional; // 返回 Optional?
private final Class<?> returnType; // 方法返回类型
private final String mapKey; // @MapKey 注解指定的列名
private final Integer resultHandlerIndex; // ResultHandler 参数的位置(如有)
private final Integer rowBoundsIndex; // RowBounds 参数的位置(如有)
private final ParamNameResolver paramNameResolver; // 参数解析器
public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
// 解析返回类型(处理泛型,如 List<User>,提取 User)
Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
if (resolvedReturnType instanceof Class<?>) {
this.returnType = (Class<?>) resolvedReturnType;
} else if (resolvedReturnType instanceof ParameterizedType) {
// 泛型类型(如 List<User>):取原始类型(List)
this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
} else {
this.returnType = method.getReturnType();
}
// 判断各种返回类型标志
this.returnsVoid = void.class.equals(this.returnType);
this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType)
|| this.returnType.isArray();
this.returnsCursor = Cursor.class.equals(this.returnType);
this.returnsOptional = Optional.class.equals(this.returnType);
// 处理 @MapKey 注解
this.mapKey = getMapKey(method);
this.returnsMap = this.mapKey != null;
// 找到特殊参数的位置(RowBounds 和 ResultHandler 不作为 SQL 参数)
this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
// 构建参数解析器
this.paramNameResolver = new ParamNameResolver(configuration, method);
}
// 将方法参数数组转换为 SqlSession 能理解的参数对象
public Object convertArgsToSqlCommandParam(Object[] args) {
return paramNameResolver.getNamedParams(args);
}
}5.4 各返回类型的处理方式
单对象返回(selectOne):
// 方法签名:User selectById(Long id)
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
// selectOne 内部调用 selectList,如果 List.size() > 1 抛出 TooManyResultsException列表返回(selectList):
// 方法签名:List<User> selectAll() 或 User[] selectAll()
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
// 如果参数中有 RowBounds,传入内存分页参数
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.selectList(command.getName(), param);
}
// 如果方法返回类型是数组,将 List 转为数组
if (!method.getReturnType().isAssignableFrom(result.getClass())) {
if (method.getReturnType().isArray()) {
return convertToArray(result);
} else {
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}Map 返回(@MapKey 注解):
// 方法签名:@MapKey("id") Map<Long, User> selectAllAsMap()
// 返回以 id 为 key,User 对象为 value 的 Map
private <K, V> Map<K, V> executeForMap(SqlSession sqlSession, Object[] args) {
Map<K, V> result;
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
// sqlSession.selectMap 内部调用 selectList,然后根据 mapKey 转换为 Map
result = sqlSession.selectMap(command.getName(), param, method.getMapKey(), rowBounds);
} else {
result = sqlSession.selectMap(command.getName(), param, method.getMapKey());
}
return result;
}Optional 返回(Java 8+):
// 方法签名:Optional<User> findById(Long id)
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
// 包装成 Optional(null 变为 Optional.empty(),非 null 变为 Optional.of(result))
result = Optional.ofNullable(result);Cursor 返回(流式游标,适合大结果集):
// 方法签名:Cursor<User> selectAllCursor()
// Cursor 实现了 Iterable,可以逐行处理,不一次性加载到内存
private <T> Cursor<T> executeForCursor(SqlSession sqlSession, Object[] args) {
Cursor<T> result;
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.selectCursor(command.getName(), param, rowBounds);
} else {
result = sqlSession.selectCursor(command.getName(), param);
}
return result;
}ResultHandler 参数(流式回调处理):
// 方法签名:void selectAllWithHandler(ResultHandler<User> handler)
// 每查出一行,立即回调 handler.handleResult(),不积累到 List
private void executeWithResultHandler(SqlSession sqlSession, Object[] args) {
MappedStatement ms = sqlSession.getConfiguration().getMappedStatement(command.getName());
// 必须是 SELECT 且不是存储过程
if (!StatementType.CALLABLE.equals(ms.getStatementType())
&& void.class.equals(ms.getResultMaps().get(0).getType())) {
throw new BindingException("...");
}
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
sqlSession.select(command.getName(), param, rowBounds,
(ResultHandler) args[method.getResultHandlerIndex()]);
} else {
sqlSession.select(command.getName(), param,
(ResultHandler) args[method.getResultHandlerIndex()]);
}
}第 6 章 完整调用链路回顾
结合前几篇文章,现在可以串联 Mapper 接口调用的完整链路:
graph TD classDef client fill:#ff79c6,stroke:#282a36,color:#282a36 classDef proxy fill:#ffb86c,stroke:#282a36,color:#282a36 classDef session fill:#50fa7b,stroke:#282a36,color:#282a36 classDef executor fill:#8be9fd,stroke:#282a36,color:#282a36 classDef handler fill:#bd93f9,stroke:#282a36,color:#282a36 A["业务代码</br>userMapper.selectById(1L)"]:::client B["MapperProxy</br>invoke() 分发"]:::proxy C["MapperMethod</br>execute() 路由"]:::proxy D["DefaultSqlSession</br>selectOne()"]:::session E["CachingExecutor</br>二级缓存查询"]:::executor F["SimpleExecutor</br>一级缓存 + 数据库"]:::executor G["StatementHandler</br>prepare() + query()"]:::handler H["ParameterHandler</br>setParameters()"]:::handler I["ResultSetHandler</br>handleResultSets()"]:::handler J["数据库"]:::client A --> B --> C --> D --> E --> F --> G --> H --> J J --> I --> F --> E --> D --> B --> A
每个环节的职责:
MapperProxy.invoke():拦截接口方法调用,查找或创建MapperMethodInvoker;MapperMethod.execute():分析 SQL 类型和返回值类型,调用正确的SqlSessionAPI;DefaultSqlSession:门面,参数封装,委托给Executor;CachingExecutor:二级缓存查询/写入(通过TransactionalCache暂存);SimpleExecutor:一级缓存查询/写入,调用StatementHandler;StatementHandler.prepare():创建PreparedStatement;ParameterHandler.setParameters():绑定参数;StatementHandler.query():执行 SQL;ResultSetHandler:将ResultSet映射为 Java 对象。
第 7 章 Mapper 接口使用的进阶特性
7.1 接口继承
Mapper 接口支持继承,子接口会继承父接口的所有方法:
// 通用 CRUD 基础接口
public interface BaseMapper<T, ID> {
T selectById(ID id);
List<T> selectAll();
int insert(T entity);
int updateById(T entity);
int deleteById(ID id);
}
// 继承基础接口,添加业务特定方法
public interface UserMapper extends BaseMapper<User, Long> {
// 继承了 5 个通用方法
// 额外添加业务方法
List<User> selectByStatus(@Param("status") String status);
List<User> selectByDeptAndStatus(@Param("deptId") Long deptId,
@Param("status") String status);
}父接口的 SQL 映射同样在 XML 中定义,namespace 使用子接口的全限定名,方法 ID 直接是父接口的方法名:
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 继承自 BaseMapper 的方法 -->
<select id="selectById" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
<!-- ... -->
<!-- UserMapper 自己的方法 -->
<select id="selectByStatus" resultType="User">
SELECT * FROM users WHERE status = #{status}
</select>
</mapper>SqlCommand.resolveMappedStatement() 会先在子接口的 namespace 中查找,找不到再递归向父接口查找,支持这种继承体系。
7.2 注解方式 vs XML 方式
Mybatis 支持两种方式定义 SQL:
// 注解方式(简单 SQL 推荐,无需 XML)
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User selectById(Long id);
@Insert("INSERT INTO users (name, email) VALUES (#{name}, #{email})")
@Options(useGeneratedKeys = true, keyProperty = "id") // 返回自动生成的主键
int insert(User user);
@Update("UPDATE users SET name = #{name}, email = #{email} WHERE id = #{id}")
int update(User user);
@Delete("DELETE FROM users WHERE id = #{id}")
int delete(Long id);
// 复杂 SQL 可用 @SelectProvider(通过 Java 代码动态生成 SQL)
@SelectProvider(type = UserSqlProvider.class, method = "buildSearchSql")
List<User> search(@Param("keyword") String keyword, @Param("status") String status);
}
// SQL 提供者(适合复杂动态 SQL,但不如 XML 直观)
public class UserSqlProvider {
public String buildSearchSql(@Param("keyword") String keyword, @Param("status") String status) {
return new SQL() {{
SELECT("*");
FROM("users");
if (keyword != null && !keyword.isEmpty()) {
WHERE("name LIKE CONCAT('%', #{keyword}, '%')");
}
if (status != null) {
WHERE("status = #{status}");
}
ORDER_BY("created_at DESC");
}}.toString();
}
}注解 vs XML 的选择建议:
- 简单 CRUD(单表、无动态条件):注解方式更简洁;
- 复杂查询(多表 JOIN、多动态条件、嵌套 ResultMap):XML 方式更易读、更易维护;
- 团队规范统一最重要:混用会增加认知负担,推荐全部 XML 或全部注解,勿混用。
总结
Mapper 接口代理机制是 Mybatis 最优雅的设计,它让开发者只需定义接口,框架自动完成”接口 → SQL 执行”的全部转换:
MapperRegistry维护”Mapper 接口 →MapperProxyFactory”的注册表,在初始化时扫描并注册所有 Mapper 接口;MapperProxyFactory.newInstance()通过 JDKProxy.newProxyInstance()创建代理对象,InvocationHandler 是MapperProxy;methodCache缓存已分析的MapperMethodInvoker,避免重复反射;MapperProxy.invoke()区分三类方法:Object方法(直接调用)、default方法(MethodHandle 调用)、普通接口方法(走MapperMethod);MapperMethod.execute()根据SqlCommand.type分发到SqlSession的insert/update/delete/select方法,根据MethodSignature的返回类型信息选择selectOne/selectList/selectMap/selectCursor等变体;- 返回值类型推断是
MethodSignature的核心能力:通过returnsMany/returnsMap/returnsCursor/returnsOptional等标志位,让同一个execute()方法能处理所有返回类型变体; - Mapper 接口支持继承(父接口方法通过递归查找
MappedStatement),支持注解方式定义 SQL。
下一篇,剖析 Mybatis 与 Spring 的整合机制——SqlSessionTemplate 如何保证线程安全、Mybatis 事务如何与 Spring 事务无缝对接:09 Mybatis-Spring整合原理——SqlSessionTemplate与事务管理。
参考资料
org.apache.ibatis.binding.MapperProxy源码org.apache.ibatis.binding.MapperMethod源码org.apache.ibatis.binding.MapperProxyFactory源码org.apache.ibatis.binding.MapperRegistry源码
思考题
- Mapper 接口没有实现类,MyBatis 通过 JDK 动态代理生成实现。
MapperProxy实现了InvocationHandler,将方法调用转发为 SqlSession 的 select/insert/update/delete 操作。这意味着 Mapper 接口的方法只能返回 MyBatis 支持的类型——如果方法返回CompletableFuture<User>(异步),MyBatis 能否处理?MapperMethod内部将 Mapper 方法的返回类型映射为具体的 SqlSession 操作:返回List调用selectList,返回单个对象调用selectOne,返回Map调用selectMap。如果一个方法声明返回Optional<User>,MyBatis 3.5+ 是如何支持的?底层是先调用selectOne再包装为Optional吗?- 在 Spring 中,
@MapperScan扫描 Mapper 接口并注册为 Spring Bean。底层使用MapperFactoryBean创建代理实例。如果两个不同的 Mapper 接口定义了相同签名的方法但对应不同的 SQL(不同的 XML Namespace),Spring 容器中会有冲突吗?