
你是否好奇,当我们在 MyBatis 中调用一个只有方法定义的 Mapper 接口时,背后是如何执行 SQL 的?这背后的核心魔法,正是 JDK 动态代理。通过手动实现一个简易的映射代理工厂,我们可以清晰地透视其工作原理。这不仅有助于理解 MyBatis 框架的设计精髓,也是提升 Java 核心技术能力的绝佳实践。接下来,让我们一步步搭建这个核心代理层。
定义 Mapper 接口
一切从定义一个标准的 Mapper 接口开始。这个接口声明了我们希望执行的操作,它只有方法签名,没有任何实现——这正是 MyBatis 的特色所在。
public interface IUserDao {
/**
* 根据用户ID查询用户名
* @param uId 用户ID
* @return 用户名
*/
String queryUserName(String uId);
/**
* 根据用户ID查询用户年龄
* @param uId 用户ID
* @return 用户年龄
*/
Integer queryUserAge(String uId);
}
实现 InvocationHandler(代理逻辑核心)
代理的逻辑在于拦截方法调用。我们创建一个 MapperProxy 类来实现 InvocationHandler 接口,它是整个代理过程的大脑,负责决定方法被调用时该做什么。
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
public class MapperProxy<T> implements InvocationHandler, Serializable {
@Serial
private static final long serialVersionUID = -6424540398559729838L;
/**
* SQL映射容器:
* key格式:接口全限定名.方法名(如com.yanx.yanbatis.test.dao.IUserDao.queryUserName)
* value:对应的SQL语句(此处用文字模拟)
*/
private final Map<String, String> sqlSession;
/**
* 被代理的Mapper接口类型
*/
private final Class<T> mapperInterface;
public MapperProxy(Map<String, String> sqlSession, Class<T> mapperInterface) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
}
/**
* 代理方法核心逻辑:拦截接口方法调用
* @param proxy 生成的代理对象
* @param method 被调用的方法
* @param args 方法入参
* @return 方法执行结果
* @throws Throwable 异常
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 过滤Object类的默认方法(如toString、equals),直接执行原始逻辑
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
// 拼接key:接口全限定名 + 方法名
String key = mapperInterface.getName() + "." + method.getName();
// 从容器中获取对应的SQL(此处用文字模拟)
String sql = sqlSession.get(key);
return "你的方法被代理执行了!" + sql;
}
}
}
关键点解析:invoke 方法是核心。当代理对象的方法被调用时,JVM 会将调用转发到这里。我们通过方法名和接口名构造一个唯一键,然后从一个模拟的 sqlSession(即 SQL 映射容器)中取出对应的“SQL”进行执行。这就是 MyBatis 将接口方法与 XML/注解中的 SQL 绑定起来的秘密。
实现代理工厂(创建代理对象)
有了代理逻辑处理器,我们还需要一个工厂来负责生产代理对象。MapperProxyFactory 就扮演了这个角色。
import java.lang.reflect.Proxy;
import java.util.Map;
public class MapperProxyFactory<T> {
/**
* 目标Mapper接口类型
*/
private final Class<T> mapperInterface;
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
/**
* 创建Mapper接口的代理对象
* @param sqlSession SQL映射容器
* @return 代理后的Mapper接口实例
*/
@SuppressWarnings("unchecked")
public T newInstance(Map<String, String> sqlSession) {
// 创建代理逻辑处理器
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface);
// 生成并返回代理对象
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(), // 类加载器
new Class[]{mapperInterface}, // 要代理的接口
mapperProxy // 代理逻辑处理器
);
}
}
设计模式思考:工厂模式在这里被优雅地运用。它将代理对象的复杂创建过程封装起来,对外只提供一个简单的 newInstance 方法。这种设计模式的运用,让客户端代码与 JDK 动态代理的底层 API 解耦,极大地提升了代码的可用性和可维护性。
测试验证:让代理跑起来
理论需要实践来检验。我们编写一个测试类,模拟 MyBatis 初始化配置并调用 Mapper 接口的完整流程。
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
public class ApiTest {
private final Logger log = LoggerFactory.getLogger(ApiTest.class);
@Test
public void test_MapperProxyFactory() {
// 1. 创建Mapper代理工厂
MapperProxyFactory<IUserDao> mapperProxyFactory = new MapperProxyFactory<>(IUserDao.class);
// 2. 模拟MyBatis解析XML后存储的SQL映射
HashMap<String, String> sqlSession = new HashMap<>();
sqlSession.put("com.yanx.yanbatis.test.dao.IUserDao.queryUserName",
"模拟执行 Mapper.xml 中 SQL 语句的操作:查询用户姓名");
sqlSession.put("com.yanx.yanbatis.test.dao.IUserDao.queryUserAge",
"模拟执行 Mapper.xml 中 SQL 语句的操作:查询用户年龄");
// 3. 获取代理后的Mapper接口实例
IUserDao userDao = mapperProxyFactory.newInstance(sqlSession);
// 4. 调用接口方法(实际执行的是代理逻辑)
String res = userDao.queryUserName("1");
// 5. 打印结果
log.info("测试结果:{}", res);
}
}
执行结果与流程解析
输出结果
运行上述测试,控制台将打印出预期的结果,证明我们的代理机制成功运行:
测试结果:你的方法被代理执行了!模拟执行 Mapper.xml 中 SQL 语句的操作:查询用户姓名
执行流程解析
让我们拆解一下 userDao.queryUserName("1") 这句简单调用背后发生的故事:
- 方法调用:我们调用
userDao.queryUserName("1"),此时 userDao 是 JDK 动态代理生成的对象,而非普通的实现类实例。
- 代理拦截:JVM 识别到这是代理对象,立即将调用转发给其关联的
InvocationHandler,也就是我们编写的 MapperProxy.invoke() 方法。
- 路由与查找:在
invoke 方法内,程序将接口全限定名和方法名拼接成唯一键 com.yanx.yanbatis.test.dao.IUserDao.queryUserName,并使用这个键从模拟的 sqlSession(HashMap)中查找对应的“SQL语句”。
- 执行与返回:获取到预存的 SQL 描述文本后,将其与固定前缀拼接,形成最终的返回结果,完成一次完整的代理调用。
通过这个简单的轮子,我们清晰地揭示了 MyBatis 连接接口与 SQL 的桥梁是如何搭建的。理解这个核心机制,对于排查复杂的 MyBatis 问题或进行更深度的源码分析都大有裨益。希望这次动手实践能为你打开一扇深入理解框架原理的门。云栈社区也提供了更多关于 Java 生态和架构设计的深度讨论,欢迎一起交流成长。