一、书接上文,引出问题
书接上文,我们在02篇的时候提到一个问题,现在我们再来看一下这个问题,我们先看一下mybatis查询的这段核心代码。
// 读取配置文件转化为流
InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = factory.openSession();
//这⾥不调⽤SqlSession的api,⽽是获得了接⼝对象,调⽤接⼝中的⽅法。使用JDK动态代理产生代理对象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
// 代理对象执行方法的时候,调用的最终是底层的invoke方法
List<User> userList = userDao.findAll();
我们看到下面你使用 sqlSession.getMapper(IUserDao.class);的时候创建出一个IUserDao 的对象,而IUserDao 是个接口,接口不能在下面调用方法,所以其实这里最后返回的是个这个接口的实现类,他是一个多态的体现。
所以我们可以推断出来sqlSession.getMapper(IUserDao.class);这一步一定是创建了实现类,但是问题又来了,我们在编写代码的时候,我作为程序员是从头到尾没有创建这个实现类的,那这个实现类是哪里来的呢。经过03篇的预热我们已经知道代理模式,其中的动态代理是可以在字节码层面创建类的,不需要我们自己去创建,其实这里的本质就是使用动态代理来创建的这个实现类。使用的就是动态字节码技术,在虚拟机运行的时候创建,随着虚拟机的运行结束而消失,仅此而已。所以这个由虚拟机创建的,你是看不到的他直接在运行时创建,生命周期和虚拟机一致。所以程序员看不到。我知道你可能不信,所以我打断点看看就知道了。
我们看到就是一个代理类,类型是MapperProxy,你给我记死这个类型。
所以我们上面的猜测大致无二了。那么他是怎么创建的呢,还有他创建出代理类是怎么就能调用我们那个findAll方法执行查询呢,他们是怎么关联上去的呢。
这种从头到尾你没看见类,但是确实存在的我们第一反应就应该是动态代理,02篇我们详细的讲了动态代理,我们这里就来试一试能不能完成实际操作。
二、动态代理,完成执行
首先我们准备一个mapper文件UserMapper.xml,
<mapper namespace="com.yx.dao.IUserDao">
<select id="findAll" resultType="com.yx.domain.User" statementType="CALLABLE">
SELECT * FROM `user`
</select>
<select id="findOne" resultType="com.yx.domain.User" parameterType="com.yx.domain.User">
select * from User where id = #{id}
</select>
</mapper>
然后我们开始方法的实现。
动态代理是这么个结构语法:Proxy.newProxyInstance(classLoader,接口,InvocationHandler实现);
第一个参数类加载器,随处都能获取一个,这个无妨。
第二个参数是接口,也就是我们要代理的接口,这个能拿到,问题也不大。
第三个参数是这个接口的实现,我们先来实现一下。
InvocationHandler接口实现:
class MyInvocationHandler implements InvocationHandler{
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
}
OK,到了这里我们是完成了这个实现,以后在你生成动态代理类的时候,只要你调用其中的方法,
就会回调到这里的invoke方法里面,那既然最后的落脚地在这里,我们就把对应的实现sql操作也实现在这里,
也就是执行一下sql<mybatis中执行sql用的是sqlsession所以我们就把这个东西作为变量传进来,用构造方法传递。
所以类就变成了这样。
class MyInvocationHandler implements InvocationHandler{
private SqlSession sqlSession;
public MyInvocationHandler(SqlSession sqlSession) {
this.sqlSession = sqlSession;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return sqlSession.selectList(daoClass.getName() + "." + method.getName());
}
}
sqlSession.selectList();这个方法执行的时候需要传递一个statement的唯一标识,额就是我们前面说的那个
namespace.id也就是你的dao在配置文件里面的namespace配置和那个sql标签的id,但是目前为止我们拿不到那个
namespace,倒是这个id其实就是方法名,mybatis约定的就是你这个sql的id要和接口方法名一致,我们可以从method.getName里面直接获取到。
所以我们还需要这个namespace,其实这个我们写过mapper文件的都知道,这个就是dao接口的类的全路径,所以我们需要他,需要就是依赖,依赖就要注入,注入就是变量+构造,于是类进一步演化成为这样。
class MyInvocationHandler implements InvocationHandler{
private SqlSession sqlSession;
private Class daoClass;
public MyInvocationHandler(SqlSession sqlSession, Class daoClass) {
this.sqlSession = sqlSession;
this.daoClass = daoClass;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if("findAll".equals(method.getName())){
return sqlSession.selectList(daoClass.getName() + "." + method.getName());
}
// 这里就是写个样品,实际上判断更加通用,我就是区分一下方法的执行,因为有的可能执行update,就是这么个意思,实际源码会传更多东西进来,这里我就是意思意思怎么处理。
if("findOne".equals(method.getName())){
return sqlSession.selectList(daoClass.getName() + "." + method.getName(),((User)args[0]).getId()).get(0);
}
return null;
}
}
OK,到这里我们好像一切都很完善,三个参数我们都齐了,那么就来吧。
动态代理传参调用
public void test2() throws IOException {
// 读取配置文件转化为流
InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = factory.openSession();
/**
第一个参数我们就随便借用一个类加载器,也就是当前类的类加载器随便就行
第二个参数就是我们要代理的接口,传入数组,这是参数类型要求
第三个参数就是我们的这个InvocationHandler的实现类
最后返回值就是我们这个接口的动态代理的代理类,也就是我们的IDao接口的最后的实现类,此时就满足代理的要求了,就模拟了 mybatis的逻辑了。
*/,
IUserDao userDao = (IUserDao) Proxy.newProxyInstance(MybatisTest.class.getClassLoader(),
new Class[]{IUserDao.class}, new MyInvocationHandler(sqlSession, IUserDao.class));
List<User> userList = userDao.findAll();
userList.forEach(user -> {
System.out.println(user.toString());
});
System.out.println("**************************************************");
User user = new User();
user.setId(1);
User userDaoOne = userDao.findOne(user);
System.out.println(userDaoOne.toString());
}
调用结果如下:
==> Preparing: SELECT * FROM `user`
==> Parameters:
<== Columns: id, name
<== Row: 1, 诸葛亮
<== Row: 2, 刘备
<== Total: 2
User{id=1, username='诸葛亮'}
User{id=2, username='刘备'}
**************************************************
==> Preparing: select * from User where id = ?
==> Parameters: 1(Integer)
<== Columns: id, name
<== Row: 1, 诸葛亮
<== Total: 1
User{id=1, username='诸葛亮'}
到此为止,我们就基本模拟了这个动态代理的意思。理解了核心思想,就是使用动态代理去做这种操作,我们看到了这个操作,我们就去源码里面看看,他到底是怎么做的,其实也大差不差。
三、Mybatis源码分析
我们的mybatis开发的核心代码就落在这一行上,我们跟进去看看他是怎么创建的代理类、
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
点进去看看:
进去到了这里:
@Override
public <T> T getMapper(Class<T> type) {
// 继续往下点
return configuration.getMapper(type, this);
}
最后来到这里,看着像是开始创建代理了。
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
// 这里是异常处理,不看了,不过可以看看mybatis的异常是怎么抛出的信息,学一学就当
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 我们看到这里返回了一个proxy,我们看到这里有个工厂类,是mapper代理的工厂,如果你还有记忆,
// 我们上面打断点的时候看到的代理类型就是mapperProxy,所以我们到这里清楚了一个事情,就是这个
// 代理类是在MapperProxyFactory这个工厂类里面创建出来的。
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
我们经过一波分析,终于来到了真相的入口。
1、MapperProxyFactory代理工厂开始分析
public class MapperProxyFactory<T> {
// 你要代理的那个接口类
private final Class<T> mapperInterface;
private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class<T> getMapperInterface() {
return mapperInterface;
}
public Map<Method, MapperMethodInvoker> getMethodCache() {
return methodCache;
}
/**
创建工厂生产类的地方,我们看到他其实就是调用了Proxy.newProxyInstance,这里就是jdk动态代理的代码
我们看到他三个参数:
1、类加载器,他随便借用了一个
2、你要代理的那个接口的class数组,我们自己实现的时候也是这样
3、mapperProxy,不出意外的话,他就是InvocationHandler的实现
**/
@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);
}
}
我们大致都分析完了,基本最后就是剩下一个MapperProxy我们猜测是InvocationHandler的实现。我们就跟着这个类看看去是不是这么回事。
/*
* Copyright 2009-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ibatis.binding;
import java.io.Serializable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import org.apache.ibatis.reflection.ExceptionUtil;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.util.MapUtil;
/**
* @author Clinton Begin
* @author Eduardo Macarron
*/
// 首先我们看到他确实是实现了InvocationHandler,这个和我们刚才分析的没有偏差
public class MapperProxy<T> implements InvocationHandler, Serializable {
// 传进来SqlSession用来执行最后的操作,
private final SqlSession sqlSession;
// 传进来接口类型,用于拼接参数
private final Class<T> mapperInterface;
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethodInvoker> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
// 这里是invoke方法,我们进去看看
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 这里是一个注意点,人家框架判断了是不是默认的方法,比如object下面那些tostring equal这种
// 是默认的就直接实现,但是我们要是我们的那些sql方法,那就需要执行sqlsession的sql操作,
// 如果是你自己写动态代理,你也得注意区分,当我们调用IUserDao.tostring的时候就不需要下去执行sql
// 要是执行findAll就跳下去执行sql
if (Object.class.equals(method.getDeclaringClass())) {
// 这里是匹配的object的方法,直接执行就行了,不用sqlsession去执行了
return method.invoke(this, args);
} else {
// 所以我们大概就知道了这里是执行sql的方法,打个标签1,跳下去代码
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
// 标签1
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
return MapUtil.computeIfAbsent(methodCache, method, m -> {
if (m.isDefault()) {
try {
// 判空处理,就不分析了
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
// 来到这里我们点进去看看,标签2,MapperMethod是我们DAO接口的方法封装
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} 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;
}
// 标签2,
private static class PlainMethodInvoker implements MapperMethodInvoker {
// 这里面包了SqlCommand里面有sql,以及sql的方法类型是Insert还是select等,以及namespace+id,就是能确定每一个sql
// 这里要给session准备的执行的
private final MapperMethod mapperMethod;
public PlainMethodInvoker(MapperMethod mapperMethod) {
super();
this.mapperMethod = mapperMethod;
}
// 这里执行了Invoke方法,也就是我们最后把方法落实到了mapperMethod.execute,参数也是sqlsession
// 看上去就是要执行sql了
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
// 点进去看看,标签3,这里把sqlsession传进去了,这里开始执行了
return mapperMethod.execute(sqlSession, args);
}
}
}
// 标签3,其实这里就能看出来,他根据之前解析的方法的类型对应执行sqlsession对应的方法进行对应的
// crud,这个方法类型之前进来的时候就处理过了,解析包装好了,因为不是主流程,就没贴代码
//这里我们也看到了我们创建出来的动态代理就和sqlsession就绑上了,这和我们之前自己模拟基本类似,就是要把sqlsession
// 作为属性构造,最终传进来去执行sql,args就是我们sql中的where的后面的参数
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:
// 查询的时候还得判断,你是返回多个还是返回一个,还是返回map,还是返回游标还是怎么样
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;
}
这里我们也看到了我们创建出来的动态代理就和sqlsession就绑上了,这和我们之前自己模拟基本类似.
所以我们总结一下就是我们使用jdk的动态代理创建了dao的代理类,然后在invoke方法里面传进去sqlsession根据方法的类型执行对应的方法,其中不同的方法里面还需要对参数之类的做处理。这就是mybatis执行代理创建实现操作的主流程。