Mybatis映射接口的动态代理实现原理
在上一节中,我们介绍了MyBatis的核心配置文件加载流程,Mybatis核心配置文件加载流程详解
在文中,我们介绍了MyBatis在加载配置文件的过程中会针对每个接口类都生成一个相应的MapperProxyFactory
动态代理工厂类。
在MapperRegistry类中有一个叫做knownMappers的map缓存,其键为映射接口的Class对象,值为MapperProxyFactory对象,其有一个mapperInterface属性用来保存需要创建代理对象的接口类。
在MyBatis中,我们通过调用sqlSession.getMapper
方法可以获取映射接口的动态代理对象,然后调用该映射接口定义的方法执行数据库操作时,就会去映射配置文件中找到该方法对应的SQL语句进行执行。
那么,MyBatis是如何实现上述流程的呢?接下来,我们就从源码中来一探究竟。
下面列出了测试主方法:
public class MybatisDemo {
private static UserMapper userMapper;
public static void main(String[] args) throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
userMapper = sqlSession.getMapper(UserMapper.class);
User u1 = userMapper.selectUserById(1);
System.out.println(u1);
}
}
因为动态代理的生成步骤是在sqlSession.getMapper
方法中,因此,我们先进入DefaultSqlSession
类下的到getMapper
方法的源码中来看一下:
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
这里会调用Configuration类
下的getMapper
方法,type
即为映射接口类。然后我们进入到Configuration
类下的getMapper
方法:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
这里调用了本文初所提及的MapperRegistry
类下的getMapper
方法,再进入:
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. Cause: " + e, e);
}
}
这里,我们就看到了本文初介绍的,MyBatis在MapperRegistry
类中会维护一个knownMappers
的Map缓存,用来存储在解析核心配置文件的过程中,为每个映射接口生成的代理工厂类对象。
因此,这里就是根据被调用的映射接口,直接从该缓存中获取相应的MapperProxyFactory
动态代理工厂类对象,如果获取不到则直接报错提示。
在获取到mapperProxyFactory
后,调用newInstance
方法来创建一个mapperProxy
动态代理对象。接下来,我们进入到MapperProxyFactory
类中观察一下它的实现逻辑,以及newInstance
方法:
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class<T> getMapperInterface() {
return mapperInterface;
}
public Map<Method, MapperMethod> getMethodCache() {
return methodCache;
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
在MapperProxyFactory
中包含了两个属性:
mapperInterface
:需要创建代理对象的映射接口methodCache
:是一个map缓存,其键为映射接口的方法对象,值为这个方法对应的MapperMethodInvoker
。实际上,SQL的执行最终会由MapperMethodInvoker
方法来完成,后面会详细说明。
再观察MapperProxyFactory
中的两个重载的newInstance
方法,可以看到最终重载方法会通过执行Proxy.newProxyInstance
来创建映射接口的动态代理对象。而基于JDK动态代理的原理我们知道,代理对象方法的执行最终是在其实现的InvocationHandler
接口的invoke
方法中被执行的。而观察上面的代码不难发现,MapperProxy
类必定是InvocationHandler
接口的实现类:
果然,MapeprProxy
类实现了InvocationHandler
接口,并在创建MapperProxy
时,MapperProxyFactory
会将其持有的methodCache
传递给MapperProxy
,因此methodCache
的实际读写是由MapperProxy
来完成的。
下面来看一下MapperProxy
实现的invoke
方法:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (method.isDefault()) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 从methodCache中根据方法对象获取MapperMethodInvoker来执行sql
// 如果获取不到,则创建一个MapperMethodInvoker并添加到methodCache中,再执行Sql
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {
return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
通过上面的代码我们可以知道之前提及的methodCache
属性的作用,其实就是缓存了每个方法相应的MapperMethod
对象,而MapperMethod
对象是做什么用的呢?接下来,我们继续进入到execute
方法:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case 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:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
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:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
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;
}
喔!这里我们看到几个case标签不就是对应到我们执行SQL的几种类别的操作吗,看来代理对象方法的执行最终会进入到MapperMethod
的execute
方法里,并根据方法对应SQL的类别执行相应的处理逻辑。那么是如何根据映射接口的方法来得到SQL的执行类别呢?
很明显,就是要根据映射接口的方法来获取到映射配置文件中相应的SQL标签,而这种关联就要利用到MappedStatement
对象了。在前文MyBatis的核心配置文件加载过程中,我们详细分析了,在MyBatis加载的过程中,会对映射配置文件中的每一个<select>
、<insert>
、<update>
、<delete>
标签根据其namespace+id值生成一个MappedStatement
对象,存储在Configuration
中。因此,要实现上面的关联,只需要根据映射接口类名+方法名,从Configuration
中获取到相应的MappedStatement
对象即可。
那么MyBatis的实际实现是不是正如我们上面的分析所料呢?答案就在MapperMethod
的构造方法中:
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
在该构造方法中,会根据传入的映射接口的Class对象,映射接口被调用方法以及配置类Configuration
来创建SqlCommand
和MethodSignature
对象。
- SqlCommand主要是保存和映射接口被调用方法所关联的MappedStatement的信息;
- MethodSignature主要是存储映射接口被调用方法的参数信息和返回值信息。
来看一下SqlCommand
的构造方法:
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
// 获取映射接口被调用方法的方法名
final String methodName = method.getName();
// 获取被调用方法的接口的Class对象
final Class<?> declaringClass = method.getDeclaringClass();
// 获取和映射接口被调用方法关联的MappedStatement对象
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
if (ms == null) {
if (method.getAnnotation(Flush.class) != null) {
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
// 将MappedStatement的id值赋值给SqlCommand的name字段
name = ms.getId();
// 将MappedStatement的Sql命令类别赋值给SqlCommand的type字段
// 比如SELECT、INSERT、UPDATE、DELETE
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
构造函数中主要做了这些事情:
- 先获取和被调用方法关联的MappedStatement对象;
- 然后将MappedStatement的id字段赋值给SqlCommand的name字段;
- 最后将MappedStatement的sqlCommandType字段赋值给SqlCommand的type字段。
这样一来,SqlCommand就具备了和被调用方法关联的MappedStatement的信息。那么如何获取和被调用方法关联的MappedStatement对象呢,继续看resolveMappedStatement() 的实现,如下所示:
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
// 根据接口全限定类名+"."+方法名拼接出MappedStatement的id
String statementId = mapperInterface.getName() + "." + methodName;
if (configuration.hasStatement(statementId)) {
// 根据statementId获取configuration中保存的MappedStatement对象
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;
}
}
终于在这里,我们就可以利用源码来佐证我们上面的分析了!实际上,MyBatis就是根据被调用映射接口的类名+方法名拼接成statementId,并利用该statementId从Configuration
对象中获取到相应的MappedStatement
对象。这也就是为什么在MyBatis中老生常谈的问题:映射配置文件中的<select>
等标签id要跟映射接口的方法名保持一致,且映射配置文件的<mappers namespace=xxx>
namespace属性要写成映射接口的全限定类名。
上面代码中有一处递归调用逻辑,可能比较难以理解。这里简单介绍下,实际上这么做的目的是:
在MyBatis的3.4.2及以前的版本,只会根据映射接口的全限定名 + “.” + 方法名和声明被调用方法的接口的全限定名 + “.” + 方法名去Configuration的mappedStatements缓存中获取MappedStatement,那么按照这样的逻辑,如果是某些继承映射接口的子接口类是无法获取到MappedStatement的,因此在MyBatis的3.4.3及之后的版本中,采用了resolveMappedStatement() 方法中的逻辑,以支持继承了映射接口的接口对应的SqlCommand也能和映射接口对应的MappedStatement相关联。
对于SqlCommand的分析到此为止。
至此,回到上面定位到MapperMethod
类中的execute
方法中,我们就知道了MyBatis对于映射接口的动态代理实现原理,以及执行流程。最终在execute
方法中,会根据被调用方法关联的SQL执行类别,将方法传入的参数进行转换,最终在SqlSession中调用相应的方法来执行SQL语句。
总结一下本文的主要内容如下:
-
动态代理实现原理
在MyBatis启动加载核心配置文件时,会对每个映射接口生成一个MapperProxyFactory
对象,并保存在MapperRegistry
对象的knownMappers
缓存中,便于后续使用。当调用SqlSession.getMapper
方法来获取映射接口的代理对象时,会从MapperRegistry
对象的knownMappers
缓存中获取该映射接口对应的动态代理工厂类,该工厂类会通过Proxy.newProxyInstance
方法来创建一个代理对象。同时,会创建一个MapperProxy
对象在invoke
方法中执行真正的被调用方法执行逻辑。在invoke
方法中,会获取到该调用方法对应的MapperMethod
对象,并在其execute
方法中结合SqlSession
来执行具体的增删改查逻辑。 -
MyBatis中的动态代理是对接口的代理
在MyBatis的JDK动态代理中,是不存在被代理对象的,是对接口的代理。MapperProxy实现了InvocationHandler接口,因此MapperProxy在MyBatis的JDK动态代理中扮演调用处理器的角色,即调用映射接口的方法时,实际上是调用的MapperProxy实现的invoke() 方法,又因为不存在被代理对象,所以在MapperProxy的invoke() 方法中,并没有去调用被代理对象的方法,而是会基于映射接口和被调用方法的方法对象生成MapperMethod并执行MapperMethod的execute() 方法,即调用映射接口的方法的请求会发送到MapperMethod。
可以理解为映射接口的方法由MapperMethod代理。
最后,用一张图归纳一下MyBatis中的动态代理执行流程,如下所示: