0%

mybatis之Sql绑定

MyBatis Mapper 接口与 SQL 绑定机制全解析:从动态代理到 SQL 执行

MyBatis 的核心特性之一是 “接口无实现类却能执行 SQL”—— 开发者只需定义 Mapper 接口(如 UserMapper),并在 XML / 注解中编写对应 SQL,调用接口方法即可触发 SQL 执行。这一 “魔法” 的本质是 JDK 动态代理 + 注册中心 + 方法与 SQL 绑定 的协同机制。从 “注册→代理→绑定→执行” 四个阶段,彻底拆解 Mapper 接口与 SQL 的绑定原理。

核心问题:为什么接口能执行 SQL?

Mapper 接口本身没有实现类,MyBatis 通过以下机制实现 “接口调用→SQL 执行” 的闭环:

  1. 注册中心(MapperRegistry):初始化时扫描 Mapper 接口,将 “接口” 与 “代理工厂(MapperProxyFactory)” 绑定并注册;
  2. 动态代理(MapperProxy):调用接口方法时,通过代理工厂生成接口的动态代理对象,拦截方法调用;
  3. 方法与 SQL 绑定(MapperMethod):代理对象将接口方法解析为 “SQL 标识(namespace+methodName)” 和 “参数信息”,找到对应的 SQL;
  4. 委托执行(SqlSession):通过 SqlSession 调用底层执行器(Executor),最终执行 SQL 并返回结果。

阶段 1:初始化注册 ——MapperRegistry 管理绑定关系

MapperRegistry 是 MyBatis 的 Mapper 接口注册中心,负责在 MyBatis 初始化时,将 Mapper 接口与对应的 “代理工厂(MapperProxyFactory)” 关联并存储,为后续生成代理对象做准备。

1. 核心属性:存储接口与代理工厂的映射

1
2
3
4
5
public class MapperRegistry {
private final Configuration config; // MyBatis 全局配置
// 核心映射:key=Mapper接口(如UserMapper.class),value=代理工厂(MapperProxyFactory)
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
}

2. 注册流程:addMapper () 方法

MyBatis 初始化时(如解析 mybatis-config.xml<mappers> 标签),会调用 addMapper() 方法,完成以下操作:

  1. 校验接口合法性(仅接口可注册,排除类 / 抽象类);
  2. 创建该接口对应的 MapperProxyFactory,存入 knownMappers
  3. 解析接口的注解 SQL(如 @Select)或关联的 XML 文件,完成 SQL 与方法的绑定。
源码解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public <T> void addMapper(Class<T> type) {
// 1. 仅注册接口(非接口直接返回)
if (type.isInterface()) {
// 2. 避免重复注册
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 3. 核心:将接口与代理工厂绑定,存入 knownMappers
knownMappers.put(type, new MapperProxyFactory<>(type));

// 4. 解析 Mapper 接口(注解 SQL 或 XML SQL)
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse(); // 解析 @Select/@Insert 等注解,或关联 XML 中的 SQL

loadCompleted = true;
} finally {
// 5. 解析失败时移除注册,避免脏数据
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
关键细节:
  • MapperProxyFactory 作用:为后续生成 Mapper 接口的动态代理对象提供工厂,每个接口对应一个工厂;
  • 解析逻辑MapperAnnotationBuilder 会解析接口上的注解 SQL,或根据接口全限定名(如 com.example.UserMapper)找到对应的 XML 文件(如 UserMapper.xml),将 SQL 语句与接口方法绑定(存储在 MappedStatement 中,id 为 “接口全限定名 + 方法名”,如 com.example.UserMapper.getUser)。

3. 获取代理工厂:getMapper () 方法

当开发者通过 SqlSession.getMapper(UserMapper.class) 获取 Mapper 接口实例时,MyBatis 会调用 MapperRegistry.getMapper(),从 knownMappers 中获取对应代理工厂,生成代理对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 1. 从注册中心获取代理工厂
MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 2. 代理工厂生成代理对象(传入 SqlSession,用于后续执行 SQL)
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}

阶段 2:生成代理对象 ——MapperProxyFactory 与 MapperProxy

MapperProxyFactoryMapper 接口代理对象的工厂类,通过 JDK 动态代理生成 MapperProxy(实现 InvocationHandler),为接口方法调用提供拦截能力。

1. MapperProxyFactory:代理工厂核心逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface; // 目标 Mapper 接口
// 方法缓存:key=接口方法(Method),value=方法执行器(MapperMethodInvoker)
private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();

public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}

// 生成代理对象(核心方法)
public T newInstance(SqlSession sqlSession) {
// 1. 创建 InvocationHandler 实现类:MapperProxy
MapperProxy<T> mapperProxy = new MapperProxy<>(
sqlSession,
mapperInterface,
methodCache
);
// 2. 通过 JDK 动态代理生成接口实例
return newInstance(mapperProxy);
}

// JDK 动态代理生成实例
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(), // 类加载器
new Class[]{mapperInterface}, // 目标接口(仅支持单接口)
mapperProxy // 拦截器(InvocationHandler)
);
}
}
关键技术:JDK 动态代理

JDK 动态代理要求目标对象必须是接口(这也是 MyBatis Mapper 必须是接口的原因),通过 Proxy.newProxyInstance 生成代理对象,所有接口方法调用都会被 MapperProxy.invoke() 拦截。

2. MapperProxy:代理对象的核心拦截逻辑

MapperProxy 实现 InvocationHandler 接口,是接口方法调用的实际拦截者。其核心逻辑是:区分 “Object 类方法” 和 “Mapper 接口方法”,仅拦截接口方法并委托执行器处理。

源码解析:invoke () 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class MapperProxy<T> implements InvocationHandler, Serializable {
private final SqlSession sqlSession; // 用于执行 SQL
private final Class<T> mapperInterface; // 目标接口
private final Map<Method, MapperMethodInvoker> methodCache; // 方法缓存

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 1. 拦截 Object 类的方法(如 toString()、hashCode()),直接执行
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
// 2. 拦截 Mapper 接口方法:获取方法执行器并执行
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}

// 获取方法执行器(从缓存或新建)
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
// 1. 先查缓存,避免重复创建
MapperMethodInvoker invoker = methodCache.get(method);
if (invoker != null) {
return invoker;
}

// 2. 缓存未命中,创建执行器
return methodCache.computeIfAbsent(method, m -> {
// 2.1 处理 Java 8+ 接口默认方法(如 default void test() {})
if (m.isDefault()) {
// 省略默认方法处理逻辑...
return new DefaultMethodInvoker(...);
} else {
// 2.2 处理普通接口方法:创建 PlainMethodInvoker,包装 MapperMethod
return new PlainMethodInvoker(
new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())
);
}
});
}
}
关键区分:
  • Object 类方法:如 toString()equals(),直接通过 method.invoke(this, args) 执行,不关联 SQL;
  • 普通接口方法:如 User getUser(int id),创建 PlainMethodInvoker,包装 MapperMethod(方法与 SQL 的绑定核心);
  • 默认方法:Java 8+ 接口支持的 default 方法,通过 DefaultMethodInvoker 执行,与 SQL 无关。

阶段 3:方法与 SQL 绑定 ——MapperMethod 的核心作用

MapperMethodMapper 接口方法与 SQL 语句的 “绑定器”,封装了两个关键信息:

  1. SqlCommand:SQL 的标识(id = 接口全限定名 + 方法名)和类型(INSERT/SELECT/UPDATE/DELETE);
  2. MethodSignature:接口方法的签名(返回值类型、参数类型、是否支持结果处理器等)。

通过 MapperMethod.execute() 方法,将接口方法调用转换为 SqlSession 的对应操作(如 sqlSession.selectOne()sqlSession.insert()),完成 “方法→SQL” 的绑定。

1. MapperMethod 构造与核心属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class MapperMethod {
// SQL 命令:包含 SQL 的 id(namespace+methodName)和类型
private final SqlCommand command;
// 方法签名:包含方法返回值、参数等信息
private final MethodSignature method;

public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
// 1. 解析 SQL 命令(从 Configuration 中获取 MappedStatement)
this.command = new SqlCommand(config, mapperInterface, method);
// 2. 解析方法签名
this.method = new MethodSignature(config, mapperInterface, method);
}

// SQL 命令内部类
public static class SqlCommand {
private final String name; // SQL 的 id(如 com.example.UserMapper.getUser)
private final SqlCommandType type; // SQL 类型(INSERT/SELECT 等)

public SqlCommand(Configuration config, Class<?> mapperInterface, Method method) {
// 1. 生成 SQL 的 id:接口全限定名 + 方法名
String methodName = method.getName();
String namespace = mapperInterface.getName();
String sqlId = namespace + "." + methodName;
// 2. 从 Configuration 中获取 MappedStatement(XML/注解解析后的 SQL 元信息)
MappedStatement ms = config.getMappedStatement(sqlId);
if (ms == null) {
throw new BindingException("Invalid bound statement (not found): " + sqlId);
}
// 3. 存储 SQL 名称和类型
this.name = sqlId;
this.type = ms.getSqlCommandType();
}
}

// 方法签名内部类(省略,主要解析返回值、参数等)
public static class MethodSignature { /* ... */ }
}
关键解析:
  • sqlId 的生成:如 UserMapper 接口的 getUser 方法,sqlIdcom.example.UserMapper.getUser,与 XML 中 <select id="getUser">id 完全匹配;
  • MappedStatement:MyBatis 解析 XML / 注解后生成的 SQL 元信息,包含 SQL 语句、参数映射、结果映射等,config.getMappedStatement(sqlId) 是 “方法→SQL” 绑定的关键一步。

2. 执行 SQL:execute () 方法

execute() 方法根据 SqlCommandType 调用 SqlSession 的对应方法,完成 SQL 执行,并根据 MethodSignature 处理返回值(如将 int 转为 void、将 List 转为 Optional)。

源码解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
Object param;

// 根据 SQL 类型,调用 SqlSession 对应的方法
switch (command.getType()) {
case INSERT:
// 1. 处理参数(将 args 转换为 SQL 所需的参数格式,如 Map/JavaBean)
param = method.convertArgsToSqlCommandParam(args);
// 2. 调用 SqlSession.insert(),并处理返回值(如受影响行数)
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
case UPDATE:
param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
case DELETE:
param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
case SELECT:
// 处理 SELECT 多种返回场景
if (method.returnsVoid() && method.hasResultHandler()) {
// 场景 1:返回 void,结果由 ResultHandler 处理
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
// 场景 2:返回集合(List)
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
// 场景 3:返回 Map
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
// 场景 4:返回 Cursor(流式读取)
result = executeForCursor(sqlSession, args);
} else {
// 场景 5:返回单个对象(如 User)
param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
// 处理 Optional 返回值(Java 8+)
if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
// 处理批量刷新
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}

// 处理基本类型返回值为 null 的异常(如返回 int 却返回 null)
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a primitive return type.");
}

return result;
}
关键步骤:
  1. 参数处理convertArgsToSqlCommandParam(args) 将接口方法的参数(如 getUser(int id) 中的 id=1)转换为 SQL 所需的格式(如单个值、Map、JavaBean);
  2. SQL 执行:根据 SQL 类型调用 sqlSession.insert/update/selectOne 等方法,这些方法最终委托 Executor 执行 SQL;
  3. 返回值处理rowCountResult() 将受影响行数(int)转换为方法返回值(如 void 则忽略,boolean 则判断是否 >0),并处理 Optional 等特殊返回类型。

完整流程总结:从接口调用到 SQL 执行

UserMapper.getUser(1) 为例,梳理整个绑定与执行流程:

sequenceDiagram
    participant 开发者
    participant MapperProxy 代理对象
    participant MapperMethod 方法 SQL绑定
    participant SqlSession
    participant Executor 执行器
    participant 数据库

    开发者->>MapperProxy:调用userMapper.getUser(1)
    MapperProxy->>MapperProxy:invoke() 拦截方法调用
    MapperProxy->>MapperMethod:cachedInvoker() 获取执行器
    MapperMethod->>MapperMethod:解析 SqlCommand id=com.example.UserMapper.getUser,类型=SELECT
    MapperMethod->>SqlSession:调用 sqlSession.selectOne(sqlId, 1)
    SqlSession->>Executor:委托 executor.query()
    Executor->>数据库:执行 SQL SELECT * FROM user WHERE id=1
    数据库-->>Executor:返回 ResultSet
    Executor->>SqlSession:返回映射后的 User 对象
    SqlSession-->>MapperMethod:返回 User 对象
    MapperMethod-->>MapperProxy:返回 User 对象
    MapperProxy-->>开发者:返回 User 对象

关键问题与解决方案

1. 报错 “Invalid bound statement (not found)”

  • 原因sqlId 不存在,即 Mapper 接口方法与 XML / 注解中的 SQL 未绑定;
  • 解决方案:
    1. 检查 XML 中 <select> 等标签的 id 与接口方法名一致;
    2. 检查 XML 的 namespace 与接口全限定名一致;
    3. 检查 MyBatis 配置是否扫描了该 Mapper(如 <mappers> 标签配置正确)。

2. 多参数方法如何绑定?

  • 问题:接口方法有多个参数(如 User getUser(String name, int age)),如何与 SQL 中的 #{name}#{age} 绑定?

  • 解决方案:

    1. 使用@Param注解指定参数名:

      1
      User getUser(@Param("name") String name, @Param("age") int age);
    2. MyBatis 会将参数封装为 Map,key@Param 注解值,value 为参数值,XML 中可直接使用 #{name}#{age}

3. 为什么 Mapper 接口不能有实现类?

  • 原因:MyBatis 使用 JDK 动态代理生成接口实例,而 JDK 动态代理要求目标对象必须是接口(无法为类生成代理);
  • 例外:若需自定义实现,可通过 @MapperScan 扫描实现类,但需手动关联 SQL,失去 MyBatis 自动绑定的优势。

总结

MyBatis 中 Mapper 接口与 SQL 的绑定,本质是 “注册中心管理映射关系 + 动态代理拦截方法调用 + MapperMethod 绑定方法与 SQL + SqlSession 委托执行” 的协同过程:

  1. 注册:初始化时通过 MapperRegistry 注册 “接口→代理工厂”;
  2. 代理:调用接口方法时,通过 MapperProxyFactory 生成动态代理对象;
  3. 绑定MapperProxy 拦截调用,MapperMethod 解析方法为 SQL 标识和参数;
  4. 执行:委托 SqlSessionExecutor 执行 SQL,返回结果

欢迎关注我的其它发布渠道