MyBatis 动态代理生成实现类详解
MyBatis 通过 动态代理 技术将 Mapper 接口与 SQL 映射绑定,开发者只需定义接口,无需手动编写实现类。以下是动态代理生成实现类的核心机制和流程解析:
一、动态代理的核心类
MyBatis 的动态代理实现主要依赖以下核心类:
| 类名 | 作用 |
|---|---|
MapperProxy | 实现 InvocationHandler 接口,负责拦截接口方法调用并路由到 SQL 执行逻辑。 |
MapperProxyFactory | 工厂类,用于创建 MapperProxy 实例。 |
MapperRegistry | 管理所有 Mapper 接口与 MapperProxyFactory 的注册关系。 |
SqlSession | 提供 getMapper() 方法,触发动态代理对象的创建。 |
二、动态代理生成流程
以下为动态代理对象生成的核心步骤:
1. 注册 Mapper 接口
-
初始化阶段:MyBatis 解析
mybatis-config.xml和 Mapper 文件时,将接口的 Class 对象注册到MapperRegistry。 -
关键源码(简化):
public class MapperRegistry { private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>(); public <T> void addMapper(Class<T> type) { knownMappers.put(type, new MapperProxyFactory<>(type)); } }
2. 创建代理对象
-
调用
SqlSession.getMapper():UserMapper userMapper = sqlSession.getMapper(UserMapper.class); -
底层流程:
SqlSession从MapperRegistry获取MapperProxyFactory。MapperProxyFactory创建MapperProxy实例。- 通过
Proxy.newProxyInstance()生成代理对象。
3. 方法拦截与路由
-
MapperProxy.invoke():代理对象拦截接口方法调用,执行以下逻辑:- 判断是否为默认方法:Java 8+ 的默认方法直接调用,不代理。
- 缓存方法签名:将方法名、参数类型等转换为
MapperMethod(避免重复解析)。 - 执行 SQL:调用
MapperMethod.execute(),触发 SQL 解析和执行。
源码简析:
public class MapperProxy<T> implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 处理默认方法
if (method.isDefault()) {
return invokeDefaultMethod(proxy, method, args);
}
// 转换为 MapperMethod
MapperMethod mapperMethod = cachedMapperMethod(method);
// 执行 SQL
return mapperMethod.execute(sqlSession, args);
}
}
三、方法签名与 SQL 映射的绑定
1. 方法签名解析
-
方法唯一标识:由 接口全限定名 + 方法名 + 参数类型列表 组成。
-
示例:
public interface UserMapper { User selectUserById(int id); // 方法签名: com.example.UserMapper.selectUserById(int) List<User> selectUsersByName(String name); }
2. SQL 语句查找
-
MappedStatement:MyBatis 在初始化阶段会将每个 SQL 标签(如<select>)解析为MappedStatement,并存储在Configuration对象中。 -
查找逻辑:
String statementId = interfaceName + "." + methodName; // 如 "com.example.UserMapper.selectUserById" MappedStatement ms = configuration.getMappedStatement(statementId);
3. 参数与 SQL 绑定
- 参数转换:通过
ParamNameResolver解析方法参数名,并绑定到 SQL 的#{param}占位符。 - 动态 SQL 处理:根据参数值动态生成最终 SQL(如
<if>标签条件判断)。
四、动态代理的底层实现
1. JDK 动态代理 vs CGLIB
-
JDK 动态代理:
- MyBatis 使用 JDK 动态代理,仅支持接口代理。
- 代理对象实现
Mapper接口,并持有InvocationHandler(MapperProxy)。
-
为何不用 CGLIB:
- Mapper 接口本身不需要实现类,CGLIB 代理类的方式不适用。
2. 代理对象示例
-
生成的代理类(伪代码):
public class $Proxy123 implements UserMapper { private InvocationHandler handler; public $Proxy123(InvocationHandler handler) { this.handler = handler; } public User selectUserById(int id) { Method method = UserMapper.class.getMethod("selectUserById", int.class); return (User) handler.invoke(this, method, new Object[]{id}); } }
五、性能优化
1. 方法缓存
MapperMethod缓存:每个方法对应的MapperMethod仅创建一次,避免重复解析方法签名。- 缓存位置:
MapperProxy内部维护Map<Method, MapperMethod>。
2. 延迟加载
SqlSession延迟绑定:代理对象在执行 SQL 时才会从SqlSession中获取数据库连接。
六、示例:动态代理全流程
1. 定义接口
public interface UserMapper {
User selectUserById(int id);
}
2. XML 映射
<mapper namespace="com.example.UserMapper">
<select id="selectUserById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
3. 代理对象执行流程
// 获取代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 调用方法(触发动态代理)
User user = userMapper.selectUserById(1);
// 底层执行步骤:
// 1. MapperProxy 拦截方法调用,获取方法签名 "com.example.UserMapper.selectUserById"
// 2. 查找对应的 MappedStatement
// 3. 执行 SQL 并返回结果
七、注意事项
-
接口方法必须与 SQL 映射严格匹配:
- 若接口方法未在 XML 或注解中定义 SQL,启动时将抛出
BindingException。
- 若接口方法未在 XML 或注解中定义 SQL,启动时将抛出
-
方法重载不支持:
- MyBatis 通过方法名唯一标识 SQL,不支持同一接口中的方法重载。
-
默认方法的限制:
- 默认方法(Java 8+)不会被代理,需手动调用其他方法。
总结
MyBatis 通过 JDK 动态代理 动态生成 Mapper 接口的代理对象,核心流程包括接口注册、代理对象创建、方法拦截与 SQL 执行。代理对象通过 MapperProxy 将方法调用路由到对应的 SQL 语句,结合 MappedStatement 和 SqlSession 完成数据库操作。该机制实现了 接口与 SQL 的解耦,开发者只需关注接口定义和 SQL 编写,无需手动维护实现类,显著提升开发效率。