橘子学Mybatis04之源码中的代理模式

一、书接上文,引出问题

书接上文,我们在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执行代理创建实现操作的主流程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值