Mapper 动态代理
Mapper 动态代理:接口没有实现类,凭什么能执行 SQL
Mapper 动态代理解决的问题:我们只写了一个 UserMapper 接口,从没写实现类,userMapper.selectById(1) 却能跑出 SQL。
核心思路:MyBatis 用 JDK 动态代理在运行期给接口造代理对象,所有方法调用被拦进 MapperProxy.invoke,按「接口全限定名.方法名」定位到那条 SQL,转发给 SqlSession 执行。
整条链路四个类全在 binding 包里:MapperRegistry → MapperProxyFactory → MapperProxy → MapperMethod。
注意:3.5.6 起 MapperProxy 被重构过,网上大量教程还是老版的 cachedMapperMethod(method) 写法,现在是 cachedInvoker + MapperMethodInvoker,default 方法改用 MethodHandle 处理,下面是新版真实结构。
代码块收起展开
// 基于本地 MyBatis 仓 (D:/1ForCode/SourceCode/JavaSourceReadingLab, 3.5.x), org.apache.ibatis.binding.MapperRegistry
public class MapperRegistry {
private final Configuration config;
// 接口 Class -> 代理工厂。用 ConcurrentHashMap:注册可能在运行期懒触发,与 getMapper 并发
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new ConcurrentHashMap<>();
// ...
@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 {
// 每次 getMapper 都现造一个新代理:代理持有本次的 sqlSession,生命周期跟着会话走,不能缓存复用
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) { // 只收接口:类连注册表都进不去,「Mapper 必须是接口」从这里定死
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 先登记再解析:解析器碰到接口引用时看到「已注册」就不会再触发一次绑定,防递归
knownMappers.put(type, new MapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse(); // 解析接口注解 + 同名 XML,每条语句注册成 Configuration 里的 MappedStatement
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type); // 解析失败必须回滚登记,否则留下「已注册但没语句」的半成品
}
}
}
}
// ...
}工厂负责造代理,MapperProxy 是那个 InvocationHandler,所有接口方法调用的总入口。
代码块收起展开
// 基于本地 MyBatis 仓 (同上), org.apache.ibatis.binding.MapperProxyFactory / MapperProxy
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
// 方法级缓存放在工厂而不是代理里:工厂全应用一份,同一方法的解析结果跨 SqlSession 共享
private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();
// ...
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
// JDK 动态代理三要素:类加载器、要实现的接口、InvocationHandler。「没有实现类也有对象」的根源
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
// 代理本身极薄:就三个字段的引用,每次 new 的成本可忽略,重的解析全在 methodCache 里摊掉了
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
public class MapperProxy<T> implements InvocationHandler, Serializable {
// ...
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 {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args); // toString/hashCode 等 Object 方法打到 handler 自己身上,不当 SQL 处理
}
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t); // 剥掉反射包装的 InvocationTargetException,抛回真实异常
}
}
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
// 不直接用 map.computeIfAbsent:绕 JDK8 的 JDK-8161372——CHM 的 computeIfAbsent 即使 key 已存在也可能加锁
return MapUtil.computeIfAbsent(methodCache, method, m -> {
if (!m.isDefault()) {
// 重活在这一行:new MapperMethod 要解析方法签名 + 定位语句,全应用只做一次,之后纯查表
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
}
// default 方法不是 SQL:用 MethodHandle 的 findSpecial 直调接口默认实现
return new DefaultMethodInvoker(getMethodHandleJava9(method));
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
// ...
interface MapperMethodInvoker {
Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable;
}
private static class PlainMethodInvoker implements MapperMethodInvoker {
private final MapperMethod mapperMethod;
// ...
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args); // 普通接口方法最终全汇到这一行
}
}
private static class DefaultMethodInvoker implements MapperMethodInvoker {
private final MethodHandle methodHandle;
// ...
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
// bindTo(proxy):default 方法体里 this 指向代理,它内部再调别的接口方法仍会被 invoke 拦截走 SQL
return methodHandle.bindTo(proxy).invokeWithArguments(args);
}
}
}MapperMethod 是「方法调用 → SqlSession 调用」的翻译器:SqlCommand 记这个方法绑哪条语句、是增删改查哪种,MethodSignature 记返回类型和参数怎么转。
代码块收起展开
// 基于本地 MyBatis 仓 (同上), org.apache.ibatis.binding.MapperMethod
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
// ...
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) { // 命令类型在解析 XML/注解时就定死了,这里只做分派
case INSERT: {
// @Param 就在这层生效:多参数被 ParamNameResolver 包成 ParamMap{name=..., param1=...}
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param)); // rowCountResult 把影响行数适配成 void/int/long/boolean
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:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args); // 大结果集流式处理:逐行喂给 ResultHandler,不在内存攒 List
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args); // 返回集合/数组 -> selectList
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args); // @MapKey -> selectMap
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param); // 单条:底层还是 selectList,查出多行抛 TooManyResultsException
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());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
// 返回 int 却查不到数据:null 拆箱会 NPE,这里提前抛出带方法名的可读错误
throw new BindingException("Mapper method '" + command.getName()
+ "' attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
// ...
public static class SqlCommand {
private final String name; // MappedStatement 的 id,即「接口全限定名.方法名」
private final SqlCommandType type;
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
final String methodName = method.getName();
final Class<?> declaringClass = method.getDeclaringClass();
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration);
if (ms == null) {
if (method.getAnnotation(Flush.class) == null) {
// 经典报错的出处:XML 的 namespace/id 跟接口对不上,就死在这一行
throw new BindingException(
"Invalid bound statement (not found): " + mapperInterface.getName() + "." + methodName);
}
name = null;
type = SqlCommandType.FLUSH;
} 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) {
// 绑定规则本体:namespace 必须是接口全名、id 必须是方法名,凑出 statementId 去 Configuration 查
String statementId = mapperInterface.getName() + "." + methodName;
if (configuration.hasStatement(statementId)) {
return configuration.getMappedStatement(statementId);
}
if (mapperInterface.equals(declaringClass)) {
return null;
}
for (Class<?> superInterface : mapperInterface.getInterfaces()) { // 方法声明在父接口时沿接口树向上找:BaseMapper 式通用方法靠这里生效
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName, declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
}
// ...
}原理串讲
从 sqlSession.getMapper(UserMapper.class) 走一遍完整链路。
这个调用经 Configuration.getMapper 转到 MapperRegistry.getMapper,从 knownMappers 里取出启动时登记好的 MapperProxyFactory(登记发生在 addMapper:解析 mapper XML 的 namespace 或扫描到接口时),工厂调 Proxy.newProxyInstance 现造一个实现了 UserMapper 的代理对象返回。
此刻没有任何一行 SQL 相关逻辑被执行,代理只是个套着接口壳的 MapperProxy。
接着调 userMapper.selectById(1)。JDK 代理把调用转进 MapperProxy.invoke:先排除 Object 自带方法,然后 cachedInvoker(method) 查 methodCache。
第一次未命中,走 MapUtil.computeIfAbsent 的 lambda——普通方法 new 一个 MapperMethod 包进 PlainMethodInvoker。
MapperMethod 的构造器里两件重活一次做完:SqlCommand 用 resolveMappedStatement 拼出 com.x.UserMapper.selectById 这个 statementId 去 Configuration 里找到 MappedStatement,记下它的 id 和命令类型;MethodSignature 解析返回类型、RowBounds/ResultHandler 参数位置、建好 ParamNameResolver。
之后同一个方法的每次调用都直接查表,反射解析只付一次成本。
为什么缓存 MapperMethodInvoker 放在工厂里、代理却每次 getMapper 都 new 新的?因为两者生命周期不同:MapperMethod 只依赖接口和 Configuration,是全应用不变的解析结果,理应共享;而代理持有 sqlSession 这个有状态、非线程安全的会话对象,必须跟会话同生共死。
把重的做成全局缓存、轻的每次现造,正好是「一级工厂缓存 + 二级方法缓存」的分层。另一个细节是 cachedInvoker 不直接调 ConcurrentHashMap.computeIfAbsent 而是先 get 一把(MapUtil 的封装),为的是绕开 JDK-8161372:JDK8 的 CHM 在 key 已存在时 computeIfAbsent 也可能走加锁路径,而 Mapper 方法调用是极高频操作,命中路径必须无锁。
命中 PlainMethodInvoker 后进入 MapperMethod.execute。
command.getType() 是 SELECT,返回类型不是集合,落到 sqlSession.selectOne(command.getName(), param) 这个分支,其中 param 由 convertArgsToSqlCommandParam 转好——单参数直接透传,多参数包成 ParamMap。
到这里,「接口方法调用」彻底变成了「对 SqlSession 的调用」,后续的缓存、Executor、StatementHandler 都是 SqlSession 侧的故事,见 02-SqlSession 与缓存。
还有一条岔路值得点透:接口的 default 方法。它有真实现,不该翻译成 SQL,但也不能用 method.invoke(proxy, args) 去调——那会再次被代理拦进 invoke,无限递归栈溢出。
所以 DefaultMethodInvoker 用 MethodHandles.privateLookupIn(...).findSpecial(...) 拿到「绕过动态分派、直调接口默认实现」的 MethodHandle(语义上等价于 super 调用),再 bindTo(proxy) 让方法体里的 this 仍指向代理,这样 default 方法内部调用其它抽象方法时照样能走 SQL 流程。
JDK8 没有 privateLookupIn,只能反射破开 Lookup 的私有构造器,这就是静态块里那段兼容代码的由来。
设计取舍
- Mapper 必须是接口,不是能力不足是刻意设计:JDK 代理只支持接口,且「statementId = 接口全名.方法名」的约定让接口本身就是完整的绑定信息,实现类没有存在的必要。
- 代理无状态化:
MapperMethod不持有SqlSession,execute 时才传入——这是 methodCache 能跨会话共享的前提。 - 每次
getMapper都 new 代理不是浪费:代理就三个字段引用,真正贵的解析全在共享的 methodCache 里;反过来若缓存代理,就会把某个 sqlSession 泄漏给其它会话。 selectOne底层就是selectList取第一条,查出多行抛TooManyResultsException,条件写松了不会「静默取第一条」。- 排查
Invalid bound statement (not found)盯三处:namespace 是否等于接口全限定名、id 是否等于方法名、XML 是否真的被打进 classpath(Maven 默认不拷 src/main/java 下的 xml)。
核心记忆:Mapper 接口没有实现类,实现是 JDK 动态代理在运行期生成的。Proxy.newProxyInstance 造出代理,任何接口方法调用都被 MapperProxy.invoke 拦截,经 MapperMethod 按「namespace.方法名」找到那条 SQL,最终转发给 SqlSession。
这跟 AOP-动态代理 是同一套 JDK 代理机制,区别在 AOP 代理的是「有实现的类、织入增强」,MyBatis 代理的是「纯接口、把调用翻译成 SQL」。