MyBatis流程及源码分析(二)

        扑街前言:上篇文章描述了mybatis 的大部分内容,从架构到传统的调用,那么本次就说一下代理调用的内容,还有上篇有提到过的插件内容,最后我会总结一下mybatis的大体流程,面试的话可以看下,不看源码还是可以背一下的嘛。


目录

MyBatis的代理调用

解析引入sql配置文件的package标签

代理生成及调用

mybatis的插件原理

拦截器的配置和解析

拦截器的初始化

有哪些对象会被拦截?

mybatis的总结和大体流程梳理


MyBatis的代理调用

        mybatis 的代理调用,其实就是结合Spring 注解后不用dao层接口实现类的实现方法,但是我这里就不详细说spring 集成mybatis 的内容了,后面关于spring 的文章再详细说。回到正题,我们先下载我资源中的mybatis 源码,再知道先关的测试类,扎到代理测试的方法。代码如下。

        我们就不再重复上篇的内容了,直接看SqlSessionFactoryBuilder().build 方法的调用,XMLConfigBuilder 中parseConfiguration 解析方法调用的mapperElement 方法和之前的解析有一定的却别。

@Test
public void test2() throws IOException {

  // 1. 通过类加载器对配置文件进行加载,加载成了字节输入流,存到内存中 注意:配置文件并没有被解析
  InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

  // 2. (1)解析了配置文件,封装configuration对象 (2)创建了DefaultSqlSessionFactory工厂对象
  SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

  // 3.问题:openSession()执行逻辑是什么?
  // 3. (1)创建事务对象 (2)创建了执行器对象cachingExecutor (3)创建了DefaultSqlSession对象
  SqlSession sqlSession = sqlSessionFactory.openSession();

  // 4. JDK动态代理生成代理对象
  UserMapper mapperProxy = sqlSession.getMapper(UserMapper.class);

  // 5.代理对象调用方法
  User user = mapperProxy.findByCondition(1);
  User user2 = mapperProxy.findByCondition(1);

  System.out.println(user);
  System.out.println("MyBatis源码环境搭建成功....");

  sqlSession.close();

}

解析引入sql配置文件的package标签

        上篇我们说了相关的mapper 标签引入sql 配置文件解析内容,那么这次我们就看package 标签的引入,代码我就不全部展示了,直接看对应的方法,configuration 的addMappers 方法,注意这里的入参就是我们配置文件package 标签的name 属性值,也就是dao层接口的包名。

        下面我们跟进具体的方法,一步一步跟到最后就是MapperRegistry 类的addMappers 方法,代码如下。这里首先是构建了一个ResolverUtil 工具类,然后通过工具类和包名,获取到所有的接口对象,并放入set 集合,最后循环迭代集合,将每一个接口添加MapperRegistry 中。

        还是在下面,我们接着看addMapper 方法,这里就是将接口放入了MapperRegistry 类的knownMappers 私有常量map集合属性中,相当于进行了一个缓存,这里的value 值对应的就是接口相关的代理工厂对象,这个方法的最后一步就是构建了一个MapperAnnotationBuilder 对象,然后用其parse 方法将接口对应的配置文件进行解析,并存入了configuration 的集合属性mappedStatements 属性中,这里跟之前解析sql 配置文件是一样的,有一定的区别就是多了一个获取sql 配置文件的逻辑,就是用过接口全路径,将.java 后缀换成了.xml 后缀。

public void addMappers(String packageName, Class<?> superType) {
  ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
  // 根据package名称,加载该包下Mapper接口文件(不是映射文件)
  resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
  // 获取加载的Mapper接口
  Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
  for (Class<?> mapperClass : mapperSet) {
    // 将Mapper接口添加到MapperRegistry中
    addMapper(mapperClass);
  }
}
public <T> void addMapper(Class<T> type) {
  if (type.isInterface()) {
    // 如果Map集合中已经有该mapper接口的映射,就不需要再存储了
    if (hasMapper(type)) {
      throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
    }
    boolean loadCompleted = false;
    try {
      // 将mapper接口以及它的代理对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
      knownMappers.put(type, new MapperProxyFactory<>(type));
      // It's important that the type is added before the parser is run
      // otherwise the binding may automatically be attempted by the
      // mapper parser. If the type is already known, it won't try.

      // 用来解析注解方式的mapper接口
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      // 解析注解方式的mapper接口
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

代理生成及调用

        当代理工厂创建完成,并将相关的xml 文件解析完成之后,后面的openSession 方法和之前的没有任何差别,就是创建事务对象和执行器对象。我们现在要看的是代理对象的生成,这里是调用了sqlSession.getMapper 方法,我们跟进代码最后可以跟到MapperRegistry 的getMapper 方法,代码如下。

        下面可以看到首先是通过接口对象获取knownMappers 集合中的代理工厂,然后通过代理工厂去调用newInstance 方法,这里我们可以继续跟进,代码如下展示,最后可以看到就是通过jdk 动态代理方法,生成代理对象,其代理对象就是MapperProxy 对象。

@SuppressWarnings("unchecked")
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);
  }
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
  // 使用JDK动态代理方式,生成代理对象
  return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

public T newInstance(SqlSession sqlSession) {
  // InvocationHandler接口的实现类
  final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
  return newInstance(mapperProxy);
}

        MapperProxy 既然是代理对象,那么我们就可以直接看它的invoke 方法,而invoker 方法其实也就是调用了cachedInvoker 方法,我们真正要看的就是cachedInvoker 方法生成的DefaultMethodInvoker 代理对象的invoker 方法,我们这里一步一步跟进最后可以跟到MapperMethod 类的execute 方法,代码如下。

        这里代码太多,我就不全部展示了,看下代替逻辑,首先是通过接口的方法类型,这个方法类型就是接口对应的配置文件中的sql 对象也就是MappedStatement 对象的配置标签属性,这里有inset、update、delete、select,而且select 还会分出种种不同的情况。

        目前我们就只看selectOne 的调用,其实跟进去会发现到了这一步就是回到了传统方法的调用方式了,后面的逻辑基本上是一模一样的。这里就不看了,有兴趣的话可以看下我之前的文章。


mybatis的插件原理

        上述基本上就是结束了mybatis 的所有调用方式,现在就说下其插件原理,我们自己先编译一个插件,在我上传的资源源码中是由一个MyPlugin 类的插件的, 可以看到首先是需要用@Intercepts 注解进行修饰,@Intercepts 中存入的又是@Signature 注解,注意这里是可以存入过个@Signature 注解的,因为@Intercepts 中的属性是一个Signature 数组。然后就是实现了Interceptor 接口。

        这里我们再逐个分析,首先是@Intercepts 注解,这个很简单就是为了存入多个@Signature 注解,其次是@Signature 注解,type 对应在拦截对象、method 对应着拦截方法、args 对应着方法参数,再然后就是Interceptor 接口,这个接口就是不同拦截方法的实现,具体如下代码展示。也就说要编译一个拦截器需要做的就是这些事情。

 

@Intercepts({
    @Signature(type = StatementHandler.class,
               method = "prepare",
               args = {Connection.class,Integer.class})
})
public class MyPlugin implements Interceptor {

  /**
   * 拦截方法:每次执行目标方法时,都会进入到intercept方法中
   * @param invocation :多个参数的封装类
   * @return
   * @throws Throwable
   */
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    // 增强逻辑:将执行的sql进行记录(打印)
    StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
    BoundSql boundSql = statementHandler.getBoundSql();
    String sql = boundSql.getSql();

    System.out.println("拦截方法,记录Sql:" + sql);

    return invocation.proceed();
  }

  /**
   * 将目标对象生成代理对象,添加到拦截器链中
   * @param target :目标对象
   * @return
   */
  @Override
  public Object plugin(Object target) {
    // wrap 将目标对象,基于JDK动态代理生成代理对象
    return Plugin.wrap(target,this);
  }

  /**
   * 设置属性
   * @param properties 插件初始化的时候,会设置的一些值的属性集合
   */
  @Override
  public void setProperties(Properties properties) {
    System.out.println("插件配置的初始化参数:" + properties);

  }
}

拦截器的配置和解析

        既然编译好了一个拦截器,那么我们也应该配置到相关配置文件中,这里我们可以直接去看解析配置文件的地方,也就是XMLConfigBuilder 的parseConfiguration 方法并找到解析plugins 标签的pluginElement 方法,这里我们就能知道,配置拦截器就是使用plugins 标签,具体的配置见下图,注意这里配置的是数据源的配置文件,不是sql 的配置文件,可以从解析的根目录看出。

        我们看下解析的具体内容,先上代码。相当简单的一个逻辑,解析标签、获取属性值、反射初始化、构建对象、存入configuration 的插件集合属性interceptorChain 属性中。就这样解析就结束了。

 

private void pluginElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      // 获取拦截器
      String interceptor = child.getStringAttribute("interceptor");
      // 获取配置的Properties属性
      Properties properties = child.getChildrenAsProperties();
      // 根据配置文件中配置的插件类的全限定名 进行反射初始化
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
      // 将属性添加到Intercepetor对象
      interceptorInstance.setProperties(properties);
      // 添加到配置类的InterceptorChain属性,InterceptorChain类维护了一个List<Interceptor>
      configuration.addInterceptor(interceptorInstance);
    }
  }
}

拦截器的初始化

        拦截器在sqlSessionFactory 构建的时候就已经解析完成了,那么其初始化就是在openSession 的逻辑中,上篇文章也提到过,但是当时没有详细说明,我们现在看下执行器的构建,在configuration.newExecutor 方法调用的时候,我们之前看到最后是一个interceptorChain.pluginAll 方法调用的代码,现在可以知道了interceptorChain 对象就是我们上面刚刚封装的拦截器集合,那么这里我们跟进去看下,上代码。

        可以看到这个方法就是迭代所有的拦截器,然后调用interceptor.plugin 方法,跟到最后可以看到Plugin 类的wrap 方法,有意思的是这里Plugin 类就是invocationHandler 的实现,那说明这也是个代理类,下面代码中也可以看到满足条件的情况下还是去生成了Plugin 类的动态代理,这里满足的条件其实就是之前拦截器上面@Signature 注解配置的type 属性。

        也就说只要是拦截Executor 执行器的都会被加载,这里注意传入参数Object target,这个最开始就是Executor 对象,但是如有拦截器进行了加载,那么这个对象就是Executor 包了一层拦截器的对象,也就说如果有多个拦截器这里会被层层包装。这里的Plugin 类的invoke 方法就是调用拦截器的intercept 方法,这里其实就已经形成了一个闭环。拦截器的逻辑基本就是这样。

public Object pluginAll(Object target) {
  for (Interceptor interceptor : interceptors) {
    target = interceptor.plugin(target);
  }
  return target;
}
public static Object wrap(Object target, Interceptor interceptor) {
  // 1.解析该拦截器所拦截的所有接口及对应拦截接口的方法
  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
  Class<?> type = target.getClass();
  // 2.获取目标对象实现的所有被拦截的接口
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  // 3.目标对象有实现被拦截的接口,生成代理对象并返回
  if (interfaces.length > 0) {
    // 通过JDK动态代理的方式实现
    return Proxy.newProxyInstance(
        type.getClassLoader(),
        interfaces,
        new Plugin(target, interceptor, signatureMap));
  }
  // 目标对象没有实现被拦截的接口,直接返回原对象
  return target;
}

 有哪些对象会被拦截?

        我们刚刚说了Executor 对象构建的时候会被拦截,那么还有哪些对象会被拦截呢,很简单在源码中全局搜一下interceptorChain.pluginAll 方法的调用就能发现,一共是四个executor、StatementHandler、parameterHandler、resultSetHandler,可以看到它们全部都是在构建的时候加载了拦截器。

 


mybatis的总结和大体流程梳理

        上述加上篇文章基本上就讲完了mybatis的所有内容,在这里总结一下吧,毕竟我目前也是需要面试了。

        mybatis的大体流程:首选将配置文件解析封装到configuration 中,并生成一个sqlSessionFactory 工厂对象,然后由工厂构建出事务对象和执行器executor还有封装sqlSession,并且在构建executor 的时候封装插件拦截器,然后用sqlSession 来获取configuration 中的sql 封装对象,然后传入执行器并调用对应的方法,执行器再去查询二级缓存,如果存在数据直接返回,不存在则去查询一级缓存,原理同二级缓存一致,没有数据则去查询数据库,查询数据库就会构建StatementHandler 对象,再由StatementHandler 对象构建parameterHandler,这里就是将sql 的入参和语句进行封装,然后由parameterHandler 强转为parameterStatement java的jdbc 底层预编译对象,然后执行sql,再由resultSetHandler 对象处理结果集返回。

        这基本上就是mybatis 的大体流程,详细的比如配置文件的解析、sql 文件的解析、sql 占位符的替换、configuration 对象的封装、代理方式的调用等等,这些都需要看源码,上面的这段话,可以应付一下初中级的面试,但是深挖还是有问题,建议跟着我的文章一起学习一下,老规矩有疑问可以留言给我,我会尽量解答,大佬看着不对的地方也可以指出,我会尽快改正。mybatis 就结束了,后面说spring 了,那又是一个难啃的骨头,愿与我同行的人越来越多,毕竟这样才能更好的交流嘛。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值