Mybatis源码剖析 -- 二级缓存

一、思考一个问题

假设 Mybatis 一级缓存和二级缓存同时开启,那么到底是生效一级缓存还是二级缓存呢?

  • 答案:二级缓存是构建在⼀级缓存之上的,在收到查询请求时,MyBatis 首先会查询二级缓存,若二级缓存未能命中,再去查询⼀级缓存,⼀级缓存没有,再查询数据库。
  • 所以实际上是这样的:二级缓存 -> 一级缓存 -> 数据库
  • 与一级缓存不同,二级缓存和具体的命名空间(namespace)绑定,⼀个 Mapper 中有⼀个 Cache,相同 Mapper 中的 MappedStatement 共用⼀个 Cache,⼀级缓存则是和 SqlSession 绑定。

二、启用二级缓存

  1. 在 sqlMapConfig.xml 中开启全局二级缓存配置
    注意官方文档中的这句解释,自己意会:全局性的开启或关闭所有映射器配置文件中已配置的任何缓存,默认:true
    
    
        
    
    
  2. 在需要使用二级缓存的 Mapper 配置文件中配置标签
  3. 在具体 CURD 标签上配置 useCache=true
    注意:这个配置默认就是 true
    
    

三、cache 标签解析

  1. 根据之前的 mybatis 源码剖析,xml 的解析工作主要交XMLConfigBuilder.parse()方法来实现,最终通过mapperElement(root.evalNode("mappers"));获取到 Mapper 配置文件,那么 cache 标签也应该在这里被解析
    private void mapperElement(XNode parent) throws Exception {
        if (parent != null) {
            // 遍历子节点
            for (XNode child : parent.getChildren()) {
                // 如果是 package 标签,则扫描该包
                if ("package".equals(child.getName())) {
                    // 获取  节点中的 name 属性
                    String mapperPackage = child.getStringAttribute("name");
                    // 从指定包中查找 mapper 接口,并根据 mapper 接口解析映射配置
                    configuration.addMappers(mapperPackage);
                // 如果是 mapper 标签,
                } else {
                    // 获得 resource、url、class 属性
                    String resource = child.getStringAttribute("resource");
                    String url = child.getStringAttribute("url");
                    String mapperClass = child.getStringAttribute("class");
    
                    // resource 不为空,且其他两者为空,则从指定路径中加载配置
                    if (resource != null && url == null && mapperClass == null) {
                        ErrorContext.instance().resource(resource);
                        // 获得 resource 的 InputStream 对象
                        InputStream inputStream = Resources.getResourceAsStream(resource);
                        // 创建 XMLMapperBuilder 对象
                        XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                        // 执行解析
                        mapperParser.parse();
                        // url 不为空,且其他两者为空,则通过 url 加载配置
                    } else if (resource == null && url != null && mapperClass == null) {
                        ErrorContext.instance().resource(url);
                        // 获得 url 的 InputStream 对象
                        InputStream inputStream = Resources.getUrlAsStream(url);
                        // 创建 XMLMapperBuilder 对象
                        XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                        // 执行解析
                        mapperParser.parse();
                        // mapperClass 不为空,且其他两者为空,则通过 mapperClass 解析映射配置
                    } else if (resource == null && url == null && mapperClass != null) {
                        // 获得 Mapper 接口
                        Class mapperInterface = Resources.classForName(mapperClass);
                        // 添加到 configuration 中
                        configuration.addMapper(mapperInterface);
                        // 以上条件不满足,则抛出异常
                    } else {
                        throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                    }
                }
            }
        }
    }
    
  2. 其中mapperParser.parse();方法就是解析配置文件的
    public void parse() {
        // 判断当前 Mapper 是否已经加载过
        if (!configuration.isResourceLoaded(resource)) {
            // 解析 `` 节点
            configurationElement(parser.evalNode("/mapper"));
            // 标记该 Mapper 已经加载过
            configuration.addLoadedResource(resource);
            // 绑定 Mapper
            bindMapperForNamespace();
        }
    
        // 解析待定的  节点
        parsePendingResultMaps();
        // 解析待定的  节点
        parsePendingCacheRefs();
        // 解析待定的 SQL 语句的节点
        parsePendingStatements();
    }
    
  3. 解析 节点
    // 解析 `` 节点
    private void configurationElement(XNode context) {
        try {
            // 获得 namespace 属性
            String namespace = context.getStringAttribute("namespace");
            if (namespace == null || namespace.equals("")) {
                throw new BuilderException("Mapper's namespace cannot be empty");
            }
            // 设置 namespace 属性
            builderAssistant.setCurrentNamespace(namespace);
            // 解析  节点
            cacheRefElement(context.evalNode("cache-ref"));
            // 解析  节点
            cacheElement(context.evalNode("cache"));
            // 已废弃!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除,这里不会记录。
            parameterMapElement(context.evalNodes("/mapper/parameterMap"));
            // 解析  节点们
            resultMapElements(context.evalNodes("/mapper/resultMap"));
            // 解析  节点们
            sqlElement(context.evalNodes("/mapper/sql"));
            // 解析     节点们
    private void buildStatementFromContext(List list) {
        if (configuration.getDatabaseId() != null) {
            buildStatementFromContext(list, configuration.getDatabaseId());
        }
        buildStatementFromContext(list, null);
        // 上面两块代码,可以简写成 buildStatementFromContext(list, configuration.getDatabaseId());
    }
    
    private void buildStatementFromContext(List list, String requiredDatabaseId) {
        //遍历