Bootstrap

SSM源码分析之Mybatis06-MyBatis总结

Mybatis源码分析06-MyBatis总结

前言

前面我们分析了mybatis的源码以及手写了两个版本的微型mybatis,相信大家对mybatis源码一定有了更深入的了解了。
分享一句名言:学而不思则罔,思而不学则殆!
这节我们来思考一些问题。以及对mybatis集成spring的jar包做一个分析!

mybatis源码回顾

分析mybatis源码的过程中,我拿出几个问题,不妨一起思考一下?

  1. org.apache.ibatis.binding.MapperProxy#invoke 这个类的53行什么时候执行?

在这里插入图片描述

 /**
   * Backport of java.lang.reflect.Method#isDefault()
   */
  private boolean isDefaultMethod(Method method) {
    return ((method.getModifiers()
        & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC)
        && method.getDeclaringClass().isInterface();
  }

判断一下当前类是接口,并且,类的修饰符可以是abstract或public或static~

  1. TestMapper 作者为什么要设计这样的形式来做?为什么不是一个class而是一个interface?
public interface TestMapper {
   Test selectByPrimaryKey(Integer userId);
}

因为这个类要来做代理,只需要获取其中的方法名称,方法参数即可,不需要实现类来做多余的事,并且在spring注入的时候也是接口。

  1. org.apache.ibatis.executor.BaseExecutor#queryFromDatabase 322行这行代码的意义

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);//322
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }
		

查找一下这个值在哪里put的?
ctrl+h搜索一下EXECUTION_PLACEHOLDER:
在这里插入图片描述
看canload()在哪里引用的?
在这里插入图片描述
这里还是调用,我们再看重写的这个方法deferLoad
在这里插入图片描述
看看CachingExecutor:
在这里插入图片描述
既然这里没有put,我们看一下DefaultResultSetHandler:
在这里插入图片描述
找到了!
这里是判断了一下是否是有缓存,有缓存,则调用deferLoad方法赋值
在这里插入图片描述
回到322行源码,我们再看看这里的逻辑,key不能为空,并且,当前的key对象的value不能是这个占位符,才可以load():
在这里插入图片描述
占位符:
在这里插入图片描述
什么情况下存在占位符呢?就是doQuery()执行的过程中,比如执行了100s,在这个过程中,告诉别人说,当前这个key的状态是: i’m basy! 这样就有效的防止并发~

怎么理解这种解决并发的策略呢?举个栗子:

双十一,大家在抢单同一样商品,如果这件商品没有做热备(热加载)的话,所有的并发请求都要直接走数据库了,数据库毫无疑问会崩溃!
在这里插入图片描述
所以,我们可以采取占位符的方式:
比如用户一最先访问到product1:
那么我们在Cache里put(pid=1,placeholder),表示product1正在被操作
如果此时有用户二访问product1:
则首先判断get(pid=1) ==placeholder
有占位符则表示此商品正在被操作,其他用户必须排队等待了!

通过占位符防止高并发的这种方式可以说是简单粗暴
如果有10万的dps,那么通过此举可以阻止99999的请求,只允许一个请求访问数据库!

题外:限流策略还有很多,比如查询到占位符,sleep(100)或可以使用布隆过滤

  1. lazy loading是怎么做到的?
    不妨看源码org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createResultObject():
 private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
    this.useConstructorMappings = false; // reset previous mapping result
    final List<Class<?>> constructorArgTypes = new ArrayList<Class<?>>();
    final List<Object> constructorArgs = new ArrayList<Object>();
    Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
    if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
      final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
      for (ResultMapping propertyMapping : propertyMappings) {
        // issue gcode #109 && issue #149
        if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
          resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
          break;
        }
      }
    }
    this.useConstructorMappings = (resultObject != null && !constructorArgTypes.isEmpty()); // set current mapping result
    return resultObject;
  }

懒加载:如果当前的ResultMapping的下一个id不为空,并且是懒加载,那么使用代理创建的Object。
在这里插入图片描述
之前章节我们讲到过这个配置。两种代理模式,一种是cglib代理,另一种是jdk代理。

  1. plugin功能如何实现?

整个过程四个重要的类我们需要关注:

  • InterceptorChain
  • Plugin
  • Invocation
  • Interceptor

简单的实现Plugin:
a) 先实现Interceptor 接口

@Intercepts(value={
        @Signature(
                type = Executor.class, // 只能是: StatementHandler | ParameterHandler | ResultSetHandler | Executor 类或者子类
                method = "query", // 表示:拦截Executor的query方法
                args = {  // query 有很多的重载方法,需要通过方法签名来指定具体拦截的是那个方法
                        MappedStatement.class,
                        Object.class,
                        RowBounds.class,
                        ResultHandler.class
                }
                /**
                 * type:标记需要拦截的类
                 * method: 标记是拦截类的那个方法
                 * args: 标记拦截类方法的具体那个引用(尤其是重载时)
                */
        )})
public class TestPlugin implements Interceptor {
    /**
     * 具体拦截的实现逻辑
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        System.out.println("----------- intercept query start.... ---------");

        MappedStatement mappedStatement = (MappedStatement)invocation.getArgs()[0];
        BoundSql boundSql = mappedStatement.getBoundSql(invocation.getArgs()[1]);
        System.out.println(String.format("plugin output sql = %s , params = %s",boundSql.getSql(),boundSql));
        // 调用方法,实际上就是拦截的方法
        Object result = invocation.proceed();

        System.out.println("----------- intercept query end.... ---------");

        return invocation.proceed();
    }

    /**
     * 插入插件
     * @param target
     * @return
     */
    @Override
    public Object plugin(Object target) {
        // 调用Plugin工具类,创建当前的类的代理类
        return Plugin.wrap(target, this);
    }

    /**
     *  设置插件属性
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {

    }
}

b) 然后在配置文件里引入即可:

在这里插入图片描述
看官网上plugins的介绍:
在这里插入图片描述
只有在初始化到以上四个类的时候会plugin ,这也就确定了@Signature的type字段。

我们在debug一下之前我们访问通过mybatis访问数据库的demo,发现容器启动,就加载plugins:

在这里插入图片描述
然后是pluginAll(处理多个plugin):
在这里插入图片描述
org.apache.ibatis.plugin#Plugin:
在这里插入图片描述
在Incocation被invoke的同时又被invoke:
在这里插入图片描述
整个过程类似于aop的动态代理:

在这里插入图片描述

mybatis-spring.jar

spring与mybatis整合(通过@Configuration将mybatis的类注册到spring)
在这里插入图片描述
思路:用mybatis programming(编程式)->managed(集成式)
怎么集成?

  • xml
  • annotation

好的,我们依照惯例,打开源码:
在这里插入图片描述
这里annotation包就是以注解的形式整合
这里的mapper包就是以编程式形式整合

我们先看annotation形式:
MapperScannerRegistrar实现于ImportBeanDefinitionRegistrar接口
ImportBeanDefinitionRegistrar是spring对暴露对外的框架,整合以注解形式的所有bean。

在这里插入图片描述
我们看代码的59行,就是先解析到org.mybatis.spring.annotation#MapperScan
然后将MapperScan的所有注解扫描,进行匹配
在这里插入图片描述

下图是spring的ImportBeanDefinitionRegistrar接口:
在这里插入图片描述
通过以上我们了解:
spring容器在处理@Configuration的时候会去调用第三方的实现类MapperScannerRegistrar调用registerBeanDefinitions去把mybatis的所有需要的类注册进来。
那么,我们同样可以自定义这个实现类!

题外话:
在这里插入图片描述
注册bean的时候,可以注册接口,spring就可以自动将所有的实现类全部注入!
题外话结束~!

回到正题:
mybatis的dao层的多个mapper被扫描到,怎么知道当前的要扫描的类就是Mapper.java?
org.mybatis.spring.mapper#isCandidateComponent
在这里插入图片描述
这里就是判断是否为接口,是否是独立的类

我们先看xml形式,实际上与annotation的形式类似:
在这里插入图片描述
在这里插入图片描述
那么是sqlsession集成方式?

在这里插入图片描述
我们说编程式与当前spring-mybatis集成最大的区别就在于:sqlsession的实现方式不同,一个是SqlSessionTemplate,另一个是DefaultSqlSession!

在这里插入图片描述
我们看debug,从这里开始不同,直接进入SqlSessionTemplate
在这里插入图片描述
SqlSessionTemplate这里做了一个代理:
在这里插入图片描述
代理做了什么事情呢?
我们看invoke方法:
在这个SqlSessionTemplate里,我们看这里是获取了sqlsession,而不是我们在demo里手动调用,这也就是为什么我们使用ssm框架,直接用mapper的方法就可以了,sqlsession其实是在双层代理的invoke里实现的!
在这里插入图片描述
再看getSqlsession:
在这里插入图片描述
看一下getSqlSession()方法的实现,在template下面,这个util里有三个方法,一个是获取sqlsession,一个是关闭sqlsession,另一个是注册SessionHolder:
在这里插入图片描述

这里注意一个问题:
之前我们总结Mapper的生命周期是method,但是现在为什么是单例(容器级别)呢?
在这里插入图片描述
mapper的作用是什么?
找sql

总结

代码地址:本节github代码地址

;