MyBatis3.X源码分析(一二级缓存机制等)

编码基础

  • 具备MyBatis基础XML使用知识;
  • 具备二级缓存开启使用知识
  • 具备dom/dom4j解析XML知识;
  • 具备Java反射、JDK动态代理基础知识;
  • 了解装饰器模式、代理模式、工厂方法模式
  • 本文相关调试源码获取
  • properties标签:用来引入properties配置文件;
  • typeAliases标签:可以为某个类型指定一个别名;
  • environments标签:环境信息,用来配置不同环境的数据源配置;
  • mapper标签:用以配置Mapper接口和mapper.xml的映射关系。

阅读本文你将收获

  1、MyBatis运行机制(原理)
  2、MyBatis SqlSession是全局的吗?
  3、一级缓存和二级缓存(缓存的装饰模式)源码原理分析
  4、MyBatis 执行器原理分析
  5、服务集群后MyBatis存在怎样的问题?
  6、StrictMap 如何实现多个key与单个value映射HashMap集合原理?

温馨提示:篇幅较长,备好Coffee ~

阶段拆分

  • 我将分一下几个阶段进行分析:
    • 配置文件加载解析阶段(核心配置、mapper解析、二级缓存加载);
    • 构建SqlSession执行器初始化阶段
    • 获取MapperProxy代理接口阶段(对应获取mapper接口);
    • 一二级缓存以及方法执行器原理分析阶段

  分别对应一下几句代码<单击获取完整调试代码>:

            // 读取核心配置文件流
            Reader resourceAsReader = Resources.getResourceAsReader("mybatis-config.xml");
            // 1.配置文件加载解析阶段
            SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsReader);
            // 2.构建SqlSession一级缓存初始化阶段
            SqlSession sqlSession = build.openSession();
            // 3.构建MapperProxy代理接口阶段
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            // 4.一二级缓存以及方法执行器原理分析阶段(入口为MapperProxy.invoke方法)
            UserDo user = userMapper.getUser(1);
            System.out.println("第一次查询:" + user.getName());
            System.out.println("第二次查询:" + userMapper.getUser(1));

一阶段:配置文件加载解析

 public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
	  // 省略try-catch-finally部分代码
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
  }

——> public XMLConfigBuilder(Reader reader, String environment, Properties props) {
    this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
  }

——> public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) {
    commonConstructor(validation, variables, entityResolver);
    this.document = createDocument(new InputSource(reader));
  }
——> private Document createDocument(InputSource inputSource) {
    try {
    // 使用Java提供的Dom工具解析XML配置文件,Dom与Dom4J类似,都是直接将文件整个读取到内存中进行解析,适用于小型文件解析,Dom4J就是它演变过来的
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      factory.setValidating(validation);
	  // 省略部分代码……
      DocumentBuilder builder = factory.newDocumentBuilder();
      builder.setEntityResolver(entityResolver);
      builder.setErrorHandler(new ErrorHandler() {// 省略……);
      return builder.parse(inputSource);
    } catch (Exception e) {
      throw new BuilderException("Error creating document instance.  Cause: " + e, e);
    }
  }

  首先,使用Java提供XML解析技术Dom解析核心配置文件,解析得到树状结构的Document

  Dom一次性将XML文件读取到内存,故而使用它的getElements族方法使我们可以随心所欲的获取想要的值,但也因此,它仅适用于小型Xml文件的解析。

  同时,我们发现在XMLConfigBuilder.parse() 中完成核心配置文件的解析工作,解析的结果最终是一个Configuration对象,随后进行build() 构建出SqlSessionFactory工厂。

  XMLConfigBuilder.parse()最终调用parseConfiguration进行解析。

  private void parseConfiguration(XNode root) {
    try {
      // 标签被解析成Properties对象保存到Configuration中
      propertiesElement(root.evalNode("properties"));
      // typeAlias解析成以 别名-class 形式的HashMap集合 同样存入Configuration
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectionFactoryElement(root.evalNode("reflectionFactory"));
      settingsElement(root.evalNode("settings"));
      // environments根据Dom读取到的配置信息,构建事务管理器以及DataSource,同样存入Configuration
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      // 解析mappers标签
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

  这里我们只分析demo中用到的配置标签的的解析过程。

MyBatis mapper.xml解析全过程

  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) { // 逐条解析
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource); // 校验资源路径准确性
            InputStream inputStream = Resources.getResourceAsStream(resource); // 读取成流
            // 与解析核心配置文件一样,使用Dom技术解析mapper.xml,通过Document进行操作
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse(); // 最终在parse中完成mapper.xml文件的解析工作
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

  解析标签得到了所有的mapper.xml与接口的映射表,随后MyBatis又借助Dom解析技术,将mapper.xml文件解析成Document,这一系列准备工作完成后,进行mapper.xml的解析,解析resultMap、parameterMap、sql等一系列标签。

  public void parse() {
  	// 这里使用HashSet缓存已解析的mapper文件,所有的Mapper文件都只解析一次。
    if (!configuration.isResourceLoaded(resource)) {
      // 解析mapper文件
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      // 绑定命名空间,如果你继续分析你会发现mapper.xml最终使用名为knownMappers HashMap集合进行存储
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingChacheRefs();
    parsePendingStatements();
  }
  
  private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      // 初始化二级缓存(之后讲解)
      cacheElement(context.evalNode("cache"));
      // 解析parameterMap标签
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      // 解析resultMap标签
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      // 解析sql标签
      sqlElement(context.evalNodes("/mapper/sql"));
      // 动态SQL语句解析
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
    }
  }

  SQL语句解析过程比较复杂,篇幅有限,这里我们只需要知道,SQL语句最终将被解析成MappedStatement对象,添加到名为mappedStatements的StrictMap集合中,MappedStatement与Mapper接口方法与mapper.xml中的select|insert|update|delete标签之间一 一对应。

StrictMap 如何实现多个key与单个value映射HashMap集合?

  MyBatis中 StrictMap集合是对HashMap的重写,原有HashMap一一对应,MyBatis将 idnamespace + id的键,都put了进去,指向同一个MappedStatement对象。

  如果shortKey键值存在,就填充为占位符对象Ambiguity,属于覆盖操作。

    public V put(String key, V value) {
      if (containsKey(key)) { // key存在时,抛出异常
        throw new IllegalArgumentException(name + " already contains value for " + key);
      }
      // key是否为完全限定名
      if (key.contains(".")) { 
        final String shortKey = getShortName(key);
        // 若id不存在,则put真实Value
        if (super.get(shortKey) == null) {
          super.put(shortKey, value);
        } else { // 否则,覆盖put占位符对象Ambiguity,标识存在多个同名方法id。
          super.put(shortKey, (V) new Ambiguity(shortKey));
        }
      }
      // 非完全限定名,直接设置值
      return super.put(key, value);
    }

  什么意思呢?类似于一下两种不同格式的代码可以获取到相同的MappedStatement对象。

UserDo user  = sqlSession.selectOne("getUser", 1);
UserDo user  = sqlSession.selectOne("org.lmx.dao.mapper.UserMapper", 1);

  不同namespace空间下的id(不同mapper.xml中sql 标签id),能否相同?

    public V get(Object key) {
      V value = super.get(key);
      if (value == null) {
        throw new IllegalArgumentException(name + " does not contain value for " + key);
      }
      // 当get出来是Ambiguity对象,则表示存在多个同名方法id
      if (value instanceof Ambiguity) {
        throw new IllegalArgumentException(((Ambiguity) value).getSubject() + " is ambiguous in " + name
            + " (try using the full name including the namespace, or rename one of the entries)");
      }
      return value;
    }

  查看shortKey的get方法,我们可以得出结论:当namespace名称空间不同,
  1、而id不同时,namespace+id或者id方式都可以获取到MappedStatement
  2、而id相同时,使用namespace+id可以获取Sql,如果只用id获取,那么,将得到异常IllegalArgumentException。

二级缓存的初始化

  我们回到解析mapper.xml文件时的cacheElement()方法,这里我们以EhCache为例。<单击一键,代码尽显>

    <!-- 来自核心配置文件:开启二级缓存 -->
    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>
    <!-- 来自UserMapper.xml:配置EhCache -->
	<cache type="org.mybatis.caches.ehcache.EhcacheCache"/><!-- 来自mybatis-ehcache依赖 -->
  private void cacheElement(XNode context) throws Exception {
    if (context != null) { // 如果有配置二级缓存
      // 获取二级缓存类型,MyBatis支持多种类型的二级缓存,默认采用PerpetualCache,其是HashMap的实现
      String type = context.getStringAttribute("type", "PERPETUAL");
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      // 回收策略,装饰上面的缓存, 默认LRU
      String eviction = context.getStringAttribute("eviction", "LRU");
      // Ctrl+Alt+U 查看EhcacheCache的类图,它也是Cache接口的
      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
      Long flushInterval = context.getLongAttribute("flushInterval");
      Integer size = context.getIntAttribute("size");
      boolean readWrite = !context.getBooleanAttribute("readOnly", false);
      boolean blocking = context.getBooleanAttribute("blocking", false);
      Properties props = context.getChildrenAsProperties();
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }
  
——> public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    typeClass = valueOrDefault(typeClass, PerpetualCache.class);
    evictionClass = valueOrDefault(evictionClass, LruCache.class);
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(typeClass)
        .addDecorator(evictionClass)
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build(); 
    configuration.addCache(cache); 
    currentCache = cache;
    return cache;
  }

  缓存被构建后将被添加到核心配置类中,MyBatis如何初始化EhcacheCache缓存的呢?

  public Cache build() {
    setDefaultImplementations();
    // 根据缓存类型,构建缓存对象。implementation就是org.mybatis.caches.ehcache.EhcacheCache的Class对象
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);
    // issue #352, 不要将装饰器应用于自定义缓存
    if (PerpetualCache.class.equals(cache.getClass())) {
      for (Class<? extends Cache> decorator : decorators) {
      	// 反射调用构造方法构建一个新的PerpetualCache
        cache = newCacheDecoratorInstance(decorator, cache);
        setCacheProperties(cache);
      }
      cache = setStandardDecorators(cache);
    // 非LoggingCache,使用LoggingCache对EhcacheCache进行装饰
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
      cache = new LoggingCache(cache); 
    }
    return cache;
  }
  
  ——> private Cache newBaseCacheInstance(Class<? extends Cache> cacheClass, String id) {
  	// getBaseCacheConstructor通过反射获取类的构造函数。
    Constructor<? extends Cache> cacheConstructor = getBaseCacheConstructor(cacheClass);
    try {
      return cacheConstructor.newInstance(id); // 调用构造函数
    } catch (Exception e) {
      throw new CacheException("Could not instantiate cache implementation (" + cacheClass + "). Cause: " + e, e);
    }
  }

  到此,我们知晓了MyBatis二级缓存初始化的完整过程。

  所有二级缓存都是Cache的实现类,在解析mapper.xml的时,MyBatis根据配置的缓存类的完整路径使用反射技术调用其构造方法进行初始化。

  若没有配置缓存,则使用默认的PerpetualCache,其通过HashMap的实现缓存,这个我们后续讲解具体缓存过程。

  若是第三方缓存组件还会被LoggingCache进行装饰(装饰器模式)。

  最终,EhCache/LoggingCache/PerpetualCache将会存入名为cache的MappedStatement属性中,与MappedStatement对象绑定。

XML配置文件解析阶段总结

  总结一下文件解析阶段主要做了哪些事情:

SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsReader);

   1、第一步,通过Dom技术将核心配置文件解析成Document;

   2、第二步,依照顺序解析核心配置文件,例如,properties中的属性配置、settings中的二级缓存、typeAliases中的类型映射、以及environments中的数据源配置等;

   3、第三步,解析核心配置文件的,并找到其配置的mapper.xml文件,进行解析;

   4、第四步,同样通过Dom技术将mapper.xml文件读取成树形结构的Document;

   5、第五步,依次解析mapper中的所有标签,例如,cache二级缓存初始化、parameterMap、resultMap、select|insert|update|delete等。

   6、其中,依靠反射技术初始化二级缓存,并使用装饰器模式将其装饰成LoggingCache,若开启没有指定缓存类型,则使用默认的PerpetualCache作为二级缓存;每个的Sql将被解析成MappedStatement对象存入StrictMap 集合,StrictMap 对HashMap进行了拓展,是实现了两个Key与单个Value的映射的HashMap

   7、最后,构建出DefaultSqlSessionFactory工厂。

二阶段:构建SqlSession一级缓存初始化

  通过DefaultSqlSessionFactory.openSession可以对SqlSession进行初始化,接下来我们看看它里面都做了哪些工作。

SqlSession sqlSession = build.openSession();

  public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }

  getDefaultExecutorType获取默认的执行器类型,执行器是MyBatis实现二级缓存的关键。我们来看看它的默认值是什么。

  在Configuration.getDefaultExecutorType下方有setDefaultExecutorType,Ctrl+Alt+H查看其被应用的列表:
setDefaultExecutorType应用列表

configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));

  最终,我们找到了它的默认值"SIMPLE",请记住它。继续推衍openSessionFromDataSource。

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      // 在一阶段我们有提到解析了事务以及数据源,这里直接从核心配置类中进行获取事务相关配置信息,并进行初始化
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 随后初始化了默认的执行器(execType)
      final Executor executor = configuration.newExecutor(tx, execType);
      // 最后,构建DefaultSqlSession,在DefaultSqlSession中可以看到非常多眼熟的执行方法,它们依据执行器才得以执行
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

执行器初始化

  执行器分类:
  1、BatchExecutor: 用于批处理的 Executor;
  2、ResuseExecutor: 相同的 SQL 会复用 Statement;
  3、SimpleExecutor:默认的 Executor、每个 SQL 执行时都会创建新的 Statement;
  4、CachingExecutor: 可缓存数据的 Executor,用代理模式包装了其它类型的 Executor。注意,CachingExecutor只是在原有执行器的基础上实现了缓存,所以他的执行器功能还是需要依仗于原有执行器来实现,故需入参原有执行器executor。

  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) { // 当开启二级缓存后,使用CachingExecutor对默认执行器进行装饰
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

  上文中我们提到executorType的默认值是"SIMPLE",因此默认将创建SimpleExecutor执行器。

  随后,检验了cacheEnabled值,在上文中可能没有进行详细介绍,其是段解析核心配置文件中的的一个配置项,是二级缓存的开关,默认关闭。在一阶段解析核心配置文件时得到解析,解析后将赋值给Configuration的cacheEnabled。(newExecutor()属于Configuration)

  Ok,也就是说,当开启二级缓存后,MyBatis将会使用CachingExecutor对默认执行器进行装饰,可想而知Mybatis有多喜爱(装饰器模式)。

  执行器根本作用是与使用jdbc与数据库进行交互,当然其内部也实现了缓存机制,距离物理数据最近的称为“一级缓存”,其次是“二级缓存”,依次进行划分。

MyBatis SqlSession是全局的吗?

  分析到这里,我们可以非常明确的回答这个问题,No。

  SqlSesion有SqlSessionFactory进行构建,每个SqlSession中都包含了一个执行器,执行器负责与数据库打交道,因此SqlSession不是也不能是全局的,而是是线程独享的。

  多个接口并行访问数据库时,将会拥有多个SqlSession对象

  若是全局的,则完全满足不了用户的请求,程序相当于是单线程的Run。

三阶段:获取MapperProxy代理接口

  二阶段构建出DefaultSqlSession后,我们就可以使用sqlSession.getMapper()获取到对应的接口,并且通过执行器Sql可以执行Sql语句,接下来我们就分析一下,方法何以被调用,Sql如何被执行?

UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
——>
  public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
  }
  ——>>
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }
  	——>>>
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
	    // knownMappers 1阶段中我们有提到,存放Mapper文件缓存中,根据类型获取到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工厂实例化的接口
	      return mapperProxyFactory.newInstance(sqlSession);
	    } catch (Exception e) {
	      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
	    }
	  }
	  	——>>>>>
	    public T newInstance(SqlSession sqlSession) {
		    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
		    return newInstance(mapperProxy);
		}
		——>>>>>>
	  	protected T newInstance(MapperProxy<T> mapperProxy) {
    		return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  		}

  调用链执行到这里,发现Proxy.newProxyInstance 关键字,可以得知MapperProxy必然是JDK动态代理的实现类,查看其具体实现。

  果然,MapperProxy实现了InvocationHandler接口。

  也就是说,当我们调用被代理的Mapper接口是,将会执行invoke方法。

					UserDo user = userMapper.getUser(1);
public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) { 
      try {
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    // 根据一阶段解析得到的信息,拼接Sql。
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    // 通过执行器进行调用
    return mapperMethod.execute(sqlSession, args);
  }

  private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      // 这里将解析得到的散乱信息,构建成MapperMethod,细节不进行展示
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      // 使用ConcurrentHashMap类型的methodCache缓存装配后的MapperMethod对象。
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }
}

四阶段:方法执行器以及一二级缓存原理分析

  MapperMethod被构建好后,说明一切准备就绪,就可以调用execute()执行Sql语句了。

  我们假设,开启二级缓存,并使用EhCache作为二级缓存,那么在二阶段时,CachingExecutor以及SimpleExecutor都将被初始化,并且CachingExecutor对SimpleExecutor进行了装饰。

  在开始阅读以下代码之前,你需要牢记这一点!

  继续讲解,invoke最终将调用的MapperMethod的execute()执行Sql。

  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    if (SqlCommandType.INSERT == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
    } else if (SqlCommandType.UPDATE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
    } else if (SqlCommandType.DELETE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
    } else if (SqlCommandType.SELECT == command.getType()) {
      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 {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
      }
    } else if (SqlCommandType.FLUSH == command.getType()) {
        result = sqlSession.flushStatements();
    } else {
      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;
  }

  在execute中只做了一件事情,那就是判断Sql的类型,根据类型调用不同sqlSession(SqlSession中封装了执行器)的方法,这里我们以SqlCommandType.SELECT.returnsMap为例。<单击一键,代码尽显>

  private <K, V> Map<K, V> executeForMap(SqlSession sqlSession, Object[] args) {
    Map<K, V> result;
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      // RowBounds用于分页控制,默认为Integer.MAX_VALUE
      RowBounds rowBounds = method.extractRowBounds(args);
      result = sqlSession.<K, V>selectMap(command.getName(), param, method.getMapKey(), rowBounds);
    } else {
      result = sqlSession.<K, V>selectMap(command.getName(), param, method.getMapKey());
    }
    return result;
  }
——>> DefaultSqlSession.selectMap
  public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) {
    final List<? extends V> list = selectList(statement, parameter, rowBounds);
    // 对结果进行ResultMap格式化处理,并返回
    final DefaultMapResultHandler<K, V> mapResultHandler = new DefaultMapResultHandler<K, V>(mapKey,
        configuration.getObjectFactory(), configuration.getObjectWrapperFactory(), configuration.getReflectorFactory());
    final DefaultResultContext<V> context = new DefaultResultContext<V>();
    for (V o : list) {
      context.nextResultObject(o);
      mapResultHandler.handleResult(context);
    }
    return mapResultHandler.getMappedResults();
  }
——>>> DefaultSqlSession.selectList
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

  一阶段提到,每个Sql标签(select|insert|update|delete)最终都将解析成MappedStatement对象,并且以id - MappedStatement方式存入Configuration中的名为mappedStatementStrictMap集合当中。

  此时DefaultSqlSession.selectList中的executor是什么执行器?

  “CachingExecutor对SimpleExecutor进行了装饰”,所以最终sqlsession中的执行器是CachingExecutor,因为我们开启了二级缓存。

  除此之外,MappedStatement还保存了EhCache

  综上所述,接下来我们查看CachingExecutor.query

  // CachingExecutor.query
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    // 通过MappedStatement 获取EhCache
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        // 检查是否存在out,MyBatis不支持带out的存储过程的缓存
        ensureNoOutParams(ms, parameterObject, boundSql);
        // 检索二级缓存,缓存未命中时,通过原生执行器执行SQL查询
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          // 调用原生执行器,也就是SimpleExecutor的qurey
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
      	  // 将其添加到二级缓存中
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    } // 未开启缓存则直接调用原生执行器SimpleExecutor
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

  如果你继续调试tcm.getObject(cache, key),他最终将调用Cache接口的getObject(),Ctrl+Alt+B可以看到EhCache对Cache的实现。

MyBatis3.X源码分析(一二级缓存机制等)_第1张图片
  AbstractEhcacheCache 中通过原生Element实现缓存检索的操作。到此为止,我们知晓了二级缓存的全部逻辑,那么原生执行器SimpleExecutor超类BaseExecutor中一级缓存是如何实现的?

  // SimpleExecutor.BaseExecutor.query
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++; // 查询栈
      // 检索一级缓存,MyBati将一级缓存命名为PerpetualCache,直译为“永久缓存”,内部是HashMap的实现
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
      	// 存储过程相关:处理本地缓存中的输出数据,缓存的数据并不一定是接口想要的数据
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
      	// 从数据库查询
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

  以及缓存被命名为localCache是PerpetualCache的实现,活跃在BaseExecutor类中。并且,PerpetualCache通过HashMap实现缓存,提供了一些拓展方法。

  到此为止,根据缓存数据距离物理数据的远近我们将,CachingExecutor命名为“二级缓存”(这里只是代指实际二级缓存可以是EhCache、Redis等等),而PerpetualCache命名为“一级缓存”。

queryStack

  查询栈。每次查询之前,加一,查询返回结果后减一,如果为1,表示整个会会话中没有执行的查询语句,并根据 MappedStatement 是否需要执行清除缓存,如果是查询类的请求,无需清除缓存,如果是更新类操作的MappedStatemt,每次执行之前都需要清除缓存。

  最后,我们分析一下,MyBatis调用Jdbc与数据库交互部分的代码。

服务集群后MyBatis存在怎样的问题?

  MyBatis默认开启一级缓存,这可能会导致脏读。通常我们会使用缓存中间件搭建二级缓存来解决一级缓存带来的脏读问题,比如可选的Redis、EhCache等实现方式。

  搭建缓存系统是解决性能问题的首选项,也是王牌,往往除了缓存中间件,我们还需要在本地实现缓存,比如:JetCache、Guava的LoadingCache等亦或者是一个HashMap、List也是一种选择。

MyBatis 如何调用的JDBC与数据库进行交互的?

  从上面的分析,我们知道BaseExecutor.queryFromDatabase()完成与数据库交互部分的工作。

  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);
    try {
      // 构建原始的JDBC连接,与数据库进行交互
      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;
  }
——>>SimpleExecutor.doQuery
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null; // 我想熟悉JDBC操作数据库的你,一定不会不晓得Statement吧? 
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }
——>>>SimpleStatementHandler.query
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    String sql = boundSql.getSql();
    statement.execute(sql); // 最终通过Statement.execute执行SQL语句
    return resultSetHandler.<E>handleResultSets(statement);
  }

  调用JDBC在一级缓存中设置一个EXECUTION_PLACEHOLDER标识,这个标识主要是配合queryStack解决线程并发问题的。

MyBatis 原理总结

  1. 一阶段,利用Dom解析Mybatis核心配置文件以及mapper.xml文件,同时初始化二级缓存(这也是为什么说二级缓存是SqlSessionFactory级别缓存的原因),二级缓存可以是EhCache、Redis等。Sql标签(也就是select|insert|update|delete)被解析成MappedStatement,存入StricMap集合中。主要目的则是构建出SqlSessionFactory工厂,它可以用来构建SqlSession回话;

  2. 二阶段,通过SqlSessionFactory实例化SqlSession,同时初始化执行器以及一级缓存,若是开启二级缓存,使用二级缓存执行器装饰原生执行器(装饰器模式);

  3. 三阶段,当所有XML、接口都被解析,一二级缓存被初始化,我们就可以通过SqlSession通过JDK动态代理技术获取到接口代理;

  4. 四阶段,被代理的接口类被调用,会执行MapperProxy代理类的invoke方法,invoke方法中做了以下几个事情:

   a、根据Sql类型执行调用对应的执行器方法,类型不同Sql语法不同;

   b、组织上行参数、下行参数、接口等信息,构造执行器可识别的对象;

   c、调用执行器执行Sql语句,若是查询Sql则会先检索二级缓存CachingExecutor,未命中时,二级缓存将调用原生执行器执行SQL,原生执行器则会先检索一级缓存PerpetualCache中是否存在Sql对应的Key,若仍未命中,则建立JDBC连接与数据库进行交互。

   d、其中,一级缓存PerpetualCache默认开启,通过HashMap实现,而二级缓存则默认关闭,默认也通过PerpetualCache进行实现,但可以选择EhCache、Redis等缓存框架的实现方案。

你可能感兴趣的:(实践总结)