在上一篇文章[源码]mybatis之JDBC处理器(一)StatementHandler与ParamHandler中,重点在于捋顺StatementHandler创建statement、设置参数,这两个步骤执行流程的逻辑,而忽略了其中参数处理的细节。而且由于我们之前的探究都是使用sqlSession直接调用的方式去做的,所以也没有涉及到全部的参数处理过程(缺少参数转换过程)。为了探究参数处理的整体流程,在这篇文章中,我们将先介绍另外一种执行sql的方式(涉及Mapper动态代理及参数转换)。为参数处理的整体逻辑梳理做准备。
当我们使用mybatis执行SQL的时候, 除了之前提及的直接调用,如下所示:
SqlSession session = sqlSessionFactory.openSession())
User user = new User();
user.setId(1);
user.setUsername("小张");
session.selectList("com.swag.pojo.UserMapper.findUserById(user)
还有另外一种方式来执行SQL,如下所示:
SqlSession session = sqlSessionFactory.openSession())
User user = new User();
user.setId(1);
user.setUsername("小张");
UserMapper mapper = session.getMapper(UserMapper.class);
List<User> users = mapper.findUser(user);
@Mapper
public interface UserMapper {
@Select("select id,username,age,phone,`desc` from user where id=#{id}")
public List<User> findUserById(User user);
}
我们通过之前的学习已经知道,与数据库进行交互,mybatis会开启会话(sqlSession),有了会话就可以调用查询了。
第一种调用方式与sqlSession的关系一目了然,拿到sqlSession直接调用其selectList方法开始查询之旅。而第二种方式,调用sqlSession的方法相对来说比较隐晦,而且我们的UserMapper明明是一个接口,为何能直接调用它的方法得到结果,是谁在我们不知道的地方实现了它的方法?
不要急,先进行一波粗犷的debug,来看下从sqlSession中获得的对象UserMapper是什么?得到的结果如下图红框部分所示——MapperProxy@540335f…,显然,从sqlSession中获得的UserMapper的实现类,是一个代理对象。
NO.1|创建Mapper代理对象
为了探究这个过程,我们先要了解session.getMapper(UserMapper.class)里面做了哪些处理,以至于最终能够获得这个代理对象。
SqlSession是一个接口,所以我们进入到它的实现类—DefaultSqlSession中来查看这个方法。DefaultSqlSession.getMapper这个方法从Configuration对象中,根据Mapper接口,获取了Mapper代理对象 。
public class DefaultSqlSession implements SqlSession:
@Override
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);}
}
在Configuration的getMapper方法中,调用了其持有的MapperRegistry的getMapper方法。
Configuration:
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
我们进入到了MapperRegistry的getMapper方法中,可以看见,他首先会根据Mapper接口的类型,从Map集合—knownMappers【注1】中取出相应的Mapper代理对象工厂。随后调用拿到的Mapper代理对象工厂产生代理对象。而mapperProxyFactory.newInstance(sqlSession)这个方法又进行了怎样的操作,我们接着向下去看。
【注1】:在mybatis加载配置文件的过程中, 会拿到所有mapper接口以及它的代理对象存储到knownMappers这个Map集合中,key为mapper接口类型,value为代理对象工厂,以便此时使用。这里不是本次的重点,暂作了解。
public class MapperRegistry:
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 根据Mapper接口的类型,从Map集合中获取Mapper代理对象工厂
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 通过MapperProxyFactory生产MapperProxy,通过MapperProxy产生Mapper代理对象
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
在mapperProxyFactory的 newInstance(sqlSession)方法中,创建了一个关键的对象MapperProxy,在newInstance的重载方法newInstance(mapperProxy)中传入了这个对象,newInstance(mapperProxy)这个方法里面,有着明显的JDK动态代理痕迹,代理对象在这里创建并返回。这与我们最开始debug得到的答案完全相符——从sqlSession中获得的UserMapper的实现类,确实是一个代理对象。
public class MapperProxyFactory<T> :
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
// 使用JDK动态代理方式,生成代理对象
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
// 创建基于JDK实现的Mapper代理对象
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
接下来看下MapperProxy这个关键类,果然实现了InvocationHandler,完全符合动态代理api。
public class MapperProxy<T> implements InvocationHandler, Serializable
NO.2|通过Mapper代理对象调用方法
那么在调用mapper.findUser(user)又会发生什么呢,相信你心里也有了一些猜想。首先,这里的mapper是一个代理对象,所以在通过代理对象调用方法的时候一定会调用MapperProxy的invoke方法,我们大胆猜测,mybatis在invoke方法中做了处理,调用了sqlSession的方法,从而可以进行SQL的执行。具体是不是这样呢,还要继续向下探究。
可以肯定的是通过mapper代理对象调用方法的时候,一定会走MapperProxy的invoke方法,所以这里是执行的入口,探究之旅也从这里开始。
可以看见在MapperProxy的invoke方法中,调用了MapperMethod的execute(sqlSession, args)方法。
public class MapperProxy<T> implements InvocationHandler, Serializable:
private final Map<Method, MapperMethod> 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);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 执行sqlsession的方法
return mapperMethod.execute(sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {
//判断methodCache中以method为key有没有值存在, 如果没有创建一个MapperMethod对象放进去
return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
MapperMethod是整个代理机制的核心类,我们一直心心念念的sqlSession的操作就是在这个类中进行了封装。所以有必要先来认识一下这个类以及它的成员变量,方便我们后续的理解。
它的成员变量有两个,一个是SqlCommand【注2】命令类型,还有一个是MethodSignature【注3】方法签名。
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
}
【注2】:SqlCommand命令类型,SqlCommand的构造方法中会根据mapperInterface.getName() + “.” + method.getName()生成statementId,然后根据statementId从configuration中取出对应的MappedStatement(每个sql语句会被封装成一个MappedStatement),进而通过ms.getSqlCommandType()获得此次SQL命令的类型—增、删、改、查。
【注3】:MethodSignature方法签名,里面定义了关于方法的相关信息,具体是什么可以看下面的注释部分。
public MethodSignature(Configuration configuration, Method method) throws BindingException {
this.returnType = method.getReturnType(); // 返回值类型
this.returnsVoid = void.class.equals(this.returnType); // 返回值是否为void
this.returnsMany = (configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray()); // 是否返回多个结果
this.mapKey = getMapKey(method); // method是否使用了mapKey注解
this.returnsMap = (this.mapKey != null); // 返回值是否是Map类型
this.hasNamedParameters = hasNamedParams(method); // 参数是否自定义了别名
this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class); // 是否使用mybatis的物理分页
this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class); // 是否有resutlHandler
this.params = Collections.unmodifiableSortedMap(getParams(method, this.hasNamedParameters)); // 请求参数解析
}
认识了MapperMethod这个类之后,就要探究它的execute方法的执行逻辑了。在execute方法中,会先判断SQL语句的命令类型—增、删、改、查。选择不同的执行逻辑。
而增删改查执行逻辑大致相同,先是调用method.convertArgsToSqlCommandParam(args)进行参数转换(在介绍参数处理的整个流程的时候会详细介绍图片)然后就是调用sqlSession对应的增删改方法。
而当进行查询操作的时候,可以看到根据返回类型的不同,会执行不同的方法,然而殊途同归,最终都会先调用convertArgsToSqlCommandParam进行参数转化,然后调用sqlSession的不同的查询api。
我们这里仅以需要返回多个结果,也就是返回参数是List的情况为例,进行探究。当返回结果为List时,会调用executeForMany(sqlSession, args)方法。
public class MapperMethod:
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;
}
可以清楚的看见在executeForMany方法中, 进行了参数转换后,调用了sqlSession.selectList()得到结果,最后根据method定义的返回类型进行数据封装。终于,我们在Mapper代理对象执行方法的过程中,成功的找到了sqlSession,可以用它与数据库交互了。
public class MapperMethod:
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.<E>selectList(command.getName(), param);
}
if (!method.getReturnType().isAssignableFrom(result.getClass())) {
if (method.getReturnType().isArray()) {
return convertToArray(result);
} else {
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}
NO.3|小结
mybatis在session.getMapper(UserMapper.class);
的时候获取代理对象工厂,使用代理对象工厂创建MapperProxy,并利用MapperProxy创建JDK动态代理对象,然后返回代理对象。
mybatis在mapper.findUser(user);
执行的时候,其实是用代理对象来执行方法,在执行的过程中,会调用MapperProxy的invoke方法,invoke方法中会通过MapperMethod调用sqlSession的方法操作SQL。
截止到这里,当调用mapper.findUser(user)时mybatis在干什么?相信大家已经清楚了。
这里除了编程知识分享,同时也是我成长脚步的印记, 期待与您一起学习和进步,了解更多源码知识,可以扫描下方二维码关注我的公众号:程序媛swag。