MyBatis初探:揭示初始化阶段的核心流程与内部机制

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: MyBatis初探:揭示初始化阶段的核心流程与内部机制

前言

初始化阶段主要是完成 XML 配置文件和注解配置信息的读取,创建全局单例的 Configuration 配置对象,完成各部分的初始化工作,最底层封装的 XPath 进行解析,具体的创建过程需要三个核心类来完成解析。

核心类

创建 Configuration 的核心类和作用如下:

类名 作用
XmlConfigBuilder 加载解析主配置文件
XmlMapperBuilder 加载解析 Mapper 映射文件中非 SQL 节点部分
XmlStatementBuilder 加载解析 Mapper 映射文件中 SQL 节点部分

三个类共同完成全局配置文件的加载解析,来构建 Configuration 配置对象。这三个类看起来并没有使用到建造者模式的流式风格,但是借鉴了建造者模式的思想,在 CacheBuilder 里面的 build 方法是典型的建造者模式加载核心配置文件来创建全局的配置对象。

其他的核心类如下:

类名 作用
Configuration 单例对象,存在于程序的整个生命周期,包含所有的配置信息(初始化核心就是创建该对象)
MapperRegistry Mapper 接口动态代理的注册中心,注册的就是 Java 接口
MapperProxy 动态代理类,实现了 InvocationHandler 接口,用于为接口生成动态代理实例,实例就是 MapperProxy 类型的。
MapperProxyFactory 用于生成 MapperProxy 实例
ResultMap 解析映射配置文件中的 resultMap 节点,内部包含一个 ResultMapping 类型的 List,里面就封装了 id、result 等子元素
MappedStatement 存储映射文件中的 insert、select 等 SQL 语句
SqlSource 映射文件中的 SQL 语句会解析成 SqlSource 对象,解析 SqlSource 后得到的 SQL 语句只包含占位符,可以直接交给 DB 执行

Configuration

包含 MyBatis 全部的配置信息、全局单例、生命周期贯穿整个 MyBatis 的生命周期,应用级生命周期。

public class Configuration {
    // 环境信息
    protected Environment environment;
    // 是否启用行内嵌套语句
    protected boolean safeRowBoundsEnabled = false;
    protected boolean safeResultHandlerEnabled = true;
    // 是否启用下划线转驼峰命名的属性
    protected boolean mapUnderscoreToCamelCase = false;
    // 延迟加载
    protected boolean aggressiveLazyLoading = true;
    // 是否允许单条 SQL 返回多个数据集(取决于驱动的兼容性)default:true 
    protected boolean multipleResultSetsEnabled = true;
    // 允许 JDBC 生成主键。需要驱动器支持。如果设为了 true,这个设置将强制使用被生成的主键,有一些驱动器不兼容不过仍然可以执行。default:false
    protected boolean useGeneratedKeys = false;
    protected boolean useColumnLabel = true;
    // 是否开启二级缓存
    protected boolean cacheEnabled = true;
    protected boolean callSettersOnNulls = false;
    protected boolean useActualParamName = true;
    // 日志打印所有的前缀 
    protected String logPrefix;
    protected Class<? extends Log> logImpl;
    protected Class<? extends VFS> vfsImpl;
    protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
    protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
    // 设置触发延迟加载的方法
    protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new String[]{"equals", "clone", "hashCode", "toString"}));
    protected Integer defaultStatementTimeout;
    protected Integer defaultFetchSize;
    // 执行器类型,有 simple、resue 及 batch
    protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
    // 指定 MyBatis 应如何自动映射列到字段或属性
    protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
    protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
    protected Properties variables = new Properties();
    protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
    // MyBatis 每次创建结果对象的新实例时,它都会使用对象工厂(ObjectFactory)去构建 POJO
    protected ObjectFactory objectFactory = new DefaultObjectFactory();
    protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
    // 延迟加载的全局开关
    protected boolean lazyLoadingEnabled = false;
    // 指定 Mybatis 创建具有延迟加载能力的对象所用到的代理工具
    protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
    protected String databaseId;
    /**
         * Configuration factory class.
         * Used to create Configuration for loading deserialized unread properties.
         *
         * @see <a href='https://code.google.com/p/mybatis/issues/detail?id=300'>Issue 300 (google code)</a>
         */
    // 插件集合
    protected Class<?> configurationFactory;
    // Mapper 接口的动态代理注册中心
    protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
    protected final InterceptorChain interceptorChain = new InterceptorChain();
    // TypeHandler 注册中心
    protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
    // TypeAlias 别名注册中心
    protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
    protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
    // Mapper 文件中增删改查操作的注册中心
    protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
    // Mapper 文件中配置 cache 节点的二级缓存
    protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");
    // Mapper 文件中配置的所有 resultMap 对象,key 为命名空间 + ID
    protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
    protected final Map<String, ParameterMap> parameterMaps = new StrictMap<ParameterMap>("Parameter Maps collection");
    // Mapper 文件中配置 KeyGenerator 的 insert 和 update 节点,key 为命名空间 + ID
    protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<KeyGenerator>("Key Generators collection");
    // 加载到的所有 *Mapper.xml 文件
    protected final Set<String> loadedResources = new HashSet<String>();
    // Mapper 文件中配置的 SQL 元素,key 为命名空间 + ID
    protected final Map<String, XNode> sqlFragments = new StrictMap<XNode>("XML fragments parsed from previous mappers");
    protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<XMLStatementBuilder>();
    protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<CacheRefResolver>();
    protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<ResultMapResolver>();
    protected final Collection<MethodResolver> incompleteMethods = new LinkedList<MethodResolver>();
    public Configuration(Environment environment) {
        this();
        this.environment = environment;
    }
    // 注册默认的别名
    public Configuration() {
        typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
        typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
        typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
        typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
        typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
        typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
        typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
        typeAliasRegistry.registerAlias("LRU", LruCache.class);
        typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
        typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
        typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
        typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
        typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
        typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
        typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
        typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
        typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
        typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
        typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
        typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);
        typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
        typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
        languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
        languageRegistry.register(RawLanguageDriver.class);
    }
    // 相关属性的 get/set 方法,和其他方法,省略
}

初始化过程

入口程序

public class DaoTest {
    private SqlSession sqlSession;
    @Before
    public void before() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        sqlSession = sessionFactory.openSession();
        inputStream.close();
    }
}

SqlSessionFactoryBuilder#build

public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
}
// XMLConfigBuilder#parse 方法是配置解析的主要方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
        // 构建 XMLConfigBuilder 对象
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        // 建造者模式,XMLConfigBuilder 对象的 parse 方法就可以构造 Configuration 对象,屏蔽了所有实现细节
        // 并且将返回的 Configuration 对象作为参数构造 SqlSessionFactory 对象
        // SqlSessionFactory 的默认实现类 DefaultSqlSessionFactory 拿到了配置对象之后,就具备生产 SqlSession 的能力了
        return build(parser.parse());
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
        ErrorContext.instance().reset();
        try {
            inputStream.close();
        } catch (IOException e) {
            // Intentionally ignore. Prefer previous error.
        }
    }
}
// DefaultSqlSessionFactory 是 SqlSessionFactory 的默认实现
public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

XMLConfigBuilder#parse

XMLConfigBuilder#parse 方法是配置解析的核心方法。

public Configuration parse() {
    // 1.判断是否已经解析过,不重复解析
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    // 2.读取主配置文件的 Configuration 节点下面的配置信息,parseConfiguration 方法完成解析的流程
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}

XMLConfigBuilder#parseConfiguration

XMLConfigBuilder#parseConfiguration 方法完成全部的配置解析主流程。解析核心配置文件的关键方法,读取节点的信息,并通过对应的方法去解析配置,解析到的配置全部会放在 Configuration 里面。

private void parseConfiguration(XNode root) {
    try {
        Properties settings = settingsAsPropertiess(root.evalNode("settings"));
        // 解析<properties>节点
        propertiesElement(root.evalNode("properties"));
        // 解析<settings>节点
        loadCustomVfs(settings);
        // 解析<typeAliases>节点
        typeAliasesElement(root.evalNode("typeAliases"));
        // 解析<plugins>节点
        pluginElement(root.evalNode("plugins"));
        // 解析<objectFactory>节点
        objectFactoryElement(root.evalNode("objectFactory"));
        // 解析<objectWrapperFactory>节点
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        // 解析<reflectorFactory>节点
        reflectorFactoryElement(root.evalNode("reflectorFactory"));
        // 将settings填充到configuration
        settingsElement(settings);
        // 解析<environments>节点
        environmentsElement(root.evalNode("environments"));
        // 解析<databaseIdProvider>节点
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        // 解析<typeHandlers>节点
        typeHandlerElement(root.evalNode("typeHandlers"));
        // 解析<mappers>节点,里面会使用XMLMapperBuilder
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

我们看到这个方法的解析流程都很类似,最后面引入了 XMLMapperBuilder 解析节点,我们在前面挑一个解析 typeAliases 的看看细节,其他的就不逐个分析了,然后看 XMLMapperBuilder 解析阶段的分析。

XMLConfigBuilder#typeAliasesElement

typeAliasesElement 解析别名节点,typeAliases 有两种配置方式,如下所示:

<typeAliases>
    <package name="world.xuewei.entity"/>
    <typeAlias alias="Product" type="world.xuewei.entity.Product"/>
</typeAliases>
private void typeAliasesElement(XNode parent) {
    if (parent != null) {
        // 1. 非空才会处理,依次遍历所有节点
        for (XNode child : parent.getChildren()) {
            // 2. 处理 package 类型配置
            if ("package".equals(child.getName())) {
                // 2.1 获取包名
                String typeAliasPackage = child.getStringAttribute("name");
                // 2.2 注册包名,将包名放到 typeAliasRegistry 里面,里面拿到包名之后还会进一步处理
                // 最后会放到 TypeAliasRegistry.TYPE_ALIASES 这个 Map 里面去
                configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
            } else {
                // 3. 处理 typeAlias 类型配置
                // 3.1 获取别名
                String alias = child.getStringAttribute("alias");
                // 3.2 获取类名
                String type = child.getStringAttribute("type");
                try {
                    Class<?> clazz = Resources.classForName(type);
                    // 3.3 下面的注册逻辑其实比前面注册包名要简单,注册包名要依次处理包下的类,也会调用 registerAlias 方法,
                    // 这里直接处理类,别名没有配置也没关系,里面会生成一个 getSimpleName 或者根据 Alias 注解去取别名
                    if (alias == null) {
                        typeAliasRegistry.registerAlias(clazz);
                    } else {
                        typeAliasRegistry.registerAlias(alias, clazz);
                    }
                } catch (ClassNotFoundException e) {
                    // 4.其他类型直接报错
                    throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
                }
            }
        }
    }
}
XMLConfigBuilder#mapperElement

XMLConfigBuilder#mapperElement 方法解析节点,这个方法包含解析 mapper 节点的主流程,但是解析的细节还看不到,解析的细节在后面的 XMLMapperBuilder#parse 里面,我们先通过这个方法看一下解析的主流程,mappers 有多种配置方式,如下所示:

<mappers>
    <!-- 直接映射到相应的 mapper 文件 -->
    <mapper resource="mybatis/mapper/EmployeeMapper.xml"/>
    <mapper url="xx"/>
    <mapper class="yy"/>
    <package name="world.xuewei.mapper"/>
</mappers>

解析配置文件的 mappers 子节点,方法主要是实现一个大体框架,按照 resource->url->class 的优先级读取配置,具体的解析细节是依赖于 XMLMapperBuilder 来实现的, XMLMapperBuilder 通过 parse 方法屏蔽了细节,内部完成解析过程。

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        // 1. 节点非空,遍历子节点逐个处理,因为 mapperElement(root.evalNode("mappers")) 解析的 mappers 里面可能有多个标签
        for (XNode child : parent.getChildren()) {
            // 1.1 处理 package 类型的配置
            if ("package".equals(child.getName())) {
                // 1.2 按照包来添加,扫包之后默认会在包下找与 Java 接口名称相同的 mapper 映射文件,name 就是包名
                String mapperPackage = child.getStringAttribute("name");
                configuration.addMappers(mapperPackage);
            } else {
                // 1.3 一个一个 Mapper.xml 文件的添加,resource、url 和 class 三者是互斥的,resource 优先级最高
                String resource = child.getStringAttribute("resource");
                String url = child.getStringAttribute("url");
                String mapperClass = child.getStringAttribute("class");
                if (resource != null && url == null && mapperClass == null) {
                    // 1.4 按照 resource 属性实例化 XMLMapperBuilder 来解析 xml 配置文件
                    ErrorContext.instance().resource(resource);
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                    // 1.5 解析配置,因为 XMLMapperBuilder 继承了 BaseBuilder,BaseBuilder 内部持有 Configuration 对象
                    // 因此 XMLMapperBuilder 解析之后直接把配置设置到 Configuration 对象
                    mapperParser.parse();
                } else if (resource == null && url != null && mapperClass == null) {
                    // 1.6 按照 url 属性实例化 XMLMapperBuilder 来解析 xml 配置文件
                    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) {
                    // 1.7 按照 class 属性实例化 XMLMapperBuilder 来解析 xml 配置文件
                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                } else {
                    // resource、url 和 class 三者是互斥的,配置了多个或者不配置都抛出异常
                    throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                }
            }
        }
    }
}

结合代码注释和配置文件的结构,流程还是比较清晰的,这里面会将 mappers 里面的节点信息全部注册到 Configuration 所持有的 MapperRegistry 对象里面去, MapperRegistry 是 Mapper 接口动态代理的注册中心,总而言之 Configuration 里包含所有的配置信息,最后一种情况使用的是 configuration.addMapper 方法,其实前面几种情况最后也是走的这个方法,这里我们看到了主体流程,下面我们开始看解析的具体过程分析。

XMLMapperBuilder#parse

XMLMapperBuilder#parse 进入了 XMLMapperBuilder 解析 mapper 节点的详细流程,最后把 mapper 节点信息保存到 Configuration 的 MapperRegistry 里面去,并且会对 Mapper.xml 映射文件的内存进行解析,映射文件的内容是非常复杂的,我们慢慢跟进看。

public void parse() {
    // 1. 首先判断是否已经加载过了,没有加载才继续加载
    // loadedResources 是一个 set 集合,保存了已经加载的映射文件,如果一个配置在 mappers 里面写了两次,那么第二次就不加载了
    if (!configuration.isResourceLoaded(resource)) {
        // 2. 处理 mapper 子节点
        // 这里使用 XPathParser 来解析 xml 文件,XPathParser 在 XMLMapperBuilder 构造方法执行的时候就已经初始化好了
        configurationElement(parser.evalNode("/mapper"));
        // 3. 将解析过的文件添加到已经解析过的 set 集合里面
        configuration.addLoadedResource(resource);
        // 4. 注册 mapper 接口
        bindMapperForNamespace();
    }
    // 5. 处理解析失败的节点
    // 把加载失败的节点重新加载一遍,因为这些节点可能在之前解析失败了,比如他们继承的节点还未加载导致
    // 因此这里把失败的部分再加载一次,之前加载失败的节点会放在一个 map 里面
    // 6. 处理解析失败的 ResultMap 节点
    parsePendingResultMaps();
    // 7. 处理解析失败的 CacheRef 节点
    parsePendingChacheRefs();
    // 8. 处理解析失败的 Sql 语句节点
    parsePendingStatements();
}

XMLMapperBuilder#configurationElement

configurationElement 方法是解析 mapper 映射文件的主流程,我们可以看到每种类型节点的解析过程,注意第八步是解析 SQL 信息,我们前面说过,XMLMapperBuilder 解析 mapper 文件但是不解析 SQL 节点,SQL 节点是由 XmlStatementBuilder 来负责的,后面我们会看到 XmlStatementBuilder 的作用。

private void configurationElement(XNode context) {
    try {
        // 1. 获取 namespace 属性(对应 Java 接口的全路径名称)不能为空
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.equals("")) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        // 2. 把 namespace 属性交给建造助手 builderAssistant
        builderAssistant.setCurrentNamespace(namespace);
        // 3. 解析 cache-ref 节点
        cacheRefElement(context.evalNode("cache-ref"));
        // 4. 重点:解析 cache 节点,和缓存相关
        cacheElement(context.evalNode("cache"));
        // 5. 解析 parameterMap 节点(已废弃)
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        // 6. 重点:解析 resultMap 节点
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        // 7. 重点:解析 SQL 节点
        sqlElement(context.evalNodes("/mapper/sql"));
        // 8. 重点:解析 SQL 语句,解析 select、insert、update、delete 节点
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
        // 异常处理
        throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
    }
}

XMLMapperBuilder#bindMapperForNamespace

bindMapperForNamespace 负责将 mapper 映射文件对应的 Java 接口类(这个接口类的名称就是 mapper 文件里面的 namespace)注册到 Configuration 的 MapperRegistry 里面,这代表该接口已经注册。

private void bindMapperForNamespace() {
    // 1. 获取命名空间,在 bindMapperForNamespace 前面的 configurationElement 方法将 namespace 已经 set 到 builderAssistant 了,这里直接取出
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
        Class<?> boundType = null;
        try {
            // 1.1 通过命名空间获取 Mapper 接口的 Class 对象
            boundType = Resources.classForName(namespace);
        } catch (ClassNotFoundException e) {
            // ignore, bound type is not required
        }
        if (boundType != null) {
            // 1.2 是否已经注册过该 mapper 接口?
            if (!configuration.hasMapper(boundType)) {
                // Spring may not know the real resource name so we set a flag
                // to prevent loading again this resource from the mapper interface
                // look at MapperAnnotationBuilder#loadXmlResource
                // 1.3 如果没有注册,将命名空间添加至 configuration.loadedResource 集合中
                configuration.addLoadedResource("namespace:" + namespace);
                // 1.4 将 mapper 接口添加到 mapper 注册中心
                configuration.addMapper(boundType);
            }
        }
    }
}

XMLMapperBuilder#parsePendingResultMaps

处理之前加载失败的 resultMap。

private void parsePendingResultMaps() {
    // 1. 之前加载失败的 resultMap 会保存在这个集合中,这里需要做的就是遍历这个集合并处理
    Collection<ResultMapResolver> incompleteResultMaps = configuration.getIncompleteResultMaps();
    synchronized (incompleteResultMaps) {
        Iterator<ResultMapResolver> iter = incompleteResultMaps.iterator();
        while (iter.hasNext()) {
            try {
                // 2. 遍历并且调用 resolve 处理即可,如果异常直接忽略,因为最后还会重试的
                iter.next().resolve();
                // 3. 处理完就从集合移除
                iter.remove();
            } catch (IncompleteElementException e) {
                // ResultMap is still missing a resource...
            }
        }
    }
}

parsePendingChacheRefs(),parsePendingStatements() 和 parsePendingResultMaps 方法是类似的,就不多解析了,这一步我们主要是梳理 XMLMapperBuilder#parse 方法,并对里面几个关键的方法做了大概的跟踪,但是底层的实现逻辑我们暂时不跟进,否则内容太多。

XMLMapperBuilder#buildStatementFromContext

前面的 XMLMapperBuilder 我们跟了一部分源码,看到了 XMLMapperBuilder 解析 mapper 映射文件的主体流程,现在我们看 XmlStatementBuilder 是如何解析 mapper 中的 SQL 语句的,解析 SQL 语句属于解析 mapper 文件的一部分,我们 XMLMapperBuilder#configurationElement 的最后一个流程方法 buildStatementFromContext 里面跟进去,就可以看到他的身影,我们跟进去看看主流程。

buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
// 解析 SQL 语句节点并注册到 Configuration 对象
private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
        try {
            // XMLStatementBuilder 和 XMLMapperBuilder 一样都是继承自 BaseBuilder,BaseBuilder 持有 Configuration 对象,因此
            //直接解析,然后将配置信息 set 进去即可
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            // 如果解析异常,就把对象添加到未完成的 Map 集合里面
            configuration.addIncompleteStatement(statementParser);
        }
    }
}

XMLStatementBuilder#parseStatementNode

XMLStatementBuilder#parseStatementNode 是解析 SQL 节点的核心方法,该方法是解析 SQL 节点的主流程,包括四种 SQL 节点,解析完毕之后会将 SQL 语句信息封装之后,注册到 Configuration 对象里面的 mappedStatements 集合里面去。

public void parseStatementNode() {
    // 1. 获取 sql 节点的 id
    String id = context.getStringAttribute("id");
    // 2. 获取 databaseId
    String databaseId = context.getStringAttribute("databaseId");
    // 不符合就返回
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
        return;
    }
    // 3. 获取 sql 节点的各种属性
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultType = context.getStringAttribute("resultType");
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    // 4. 根据 sql 节点的名称获取操作的类型 SqlCommandType(INSERT, UPDATE, DELETE, SELECT)
    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    // 5. 获取操作的各种配置,比如缓存,resultOrdered 之类的
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    // 6. flushCache 默认值是 !isSelect,如果是查询语句,没有配置 flushCache 就是 false,不是查询语句,没有配置 flushCache 就是 true
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
    // Include Fragments before parsing
    // 7. 在解析 sql 语句之前先解析 <include> 节点
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());
    // Parse selectKey after includes and remove them.
    // 8. 在解析 sql 语句之前,处理 <selectKey> 子节点,并在 xml 节点中删除
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    // 9. 解析 sql 语句是解析 mapper.xml 的核心,实例化 sqlSource,使用 sqlSource 封装 sql 语句
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    // 10. 获取 resultSets 属性
    String resultSets = context.getStringAttribute("resultSets");
    // 11. 获取主键信息 keyProperty
    String keyProperty = context.getStringAttribute("keyProperty");
    // 12. 获取主键信息 keyColumn
    String keyColumn = context.getStringAttribute("keyColumn");
    // 13. 根据 <selectKey> 获取对应的 SelectKeyGenerator 的 id
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    // 14. 获取 keyGenerator 对象,如果是 insert 类型的 sql 语句,会使用 KeyGenerator 接口获取数据库生产的 id;
    if (configuration.hasKeyGenerator(keyStatementId)) {
        keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
        keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
                                                   configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
            ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
    }
    // 15. 通过 MapperBuilderAssistant 类型的 builderAssistant 实例化 MappedStatement,并注册至 configuration 对象
    // 相当于自身主要复杂属性的解析和读取,构造对象的过程交给助手来做
    // 注意前面解析了 mapper 节点之后是注册到 mapperRegistry,那是注册 mapper 节点信息,注册的实际上是对应的 java 接口
    // 这里是注册到 MappedStatement,注册的是 sql 语句信息,但是二者都是保存在 Configuration 对象里面的Map集合
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
                                        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
                                        resultSetTypeEnum, flushCache, useCache, resultOrdered,
                                        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

MapperBuilderAssistant#addMappedStatement

在 XMLStatementBuilder#parseStatementNode 的最后一步,通过调用 builderAssistant.addMappedStatement 方法来完成 MappedStatement 对象实例化和注册到 Configuration 对象的 mappedStatements 集合,sql 语句解析之后,对应到的 java 的数据结构类是 MappedStatement,它是保存 sql 语句的数据结构。sql 语句的节点的全部配置,都能够在 MappedStatement 中找到对应的属性。

到此 XmlConfigBuilder、XmlMapperBuilder 和 XmlStatementBuilder 三者分工协作,依次完成了主配置文件、mapper 映射文件(不含 sql 信息)和 mapper 文件 sql 节点信息配置的加载,最终都把这些信息放到了 Configuration 这个大配置对象里面去了,到此处虽然还有很多细节没有分析,但是基本上初始化的流程已经看得出个大概了。 下面是代码细节:

public MappedStatement addMappedStatement(
    String id,
    SqlSource sqlSource,
    StatementType statementType,
    SqlCommandType sqlCommandType,
    Integer fetchSize,
    Integer timeout,
    String parameterMap,
    Class<?> parameterType,
    String resultMap,
    Class<?> resultType,
    ResultSetType resultSetType,
    boolean flushCache,
    boolean useCache,
    boolean resultOrdered,
    KeyGenerator keyGenerator,
    String keyProperty,
    String keyColumn,
    String databaseId,
    LanguageDriver lang,
    String resultSets) {
    // 1. 确保 Cache-ref 节点已经被解析过了
    if (unresolvedCacheRef) {
        throw new IncompleteElementException("Cache-ref not yet resolved");
    }
    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    // 2. 典型的建造者模式,创建 MappedStatement 对象
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);
    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
        statementBuilder.parameterMap(statementParameterMap);
    }
    // 3. build 方法创建 MappedStatement 对象
    MappedStatement statement = statementBuilder.build();
    // 4. 添加 MappedStatement 对象到全局 Configuration 配置对象的对应 Map 中
    configuration.addMappedStatement(statement);
    return statement;
}


相关文章
|
2月前
|
缓存 Java 数据库连接
mybatis复习05,mybatis的缓存机制(一级缓存和二级缓存及第三方缓存)
文章介绍了MyBatis的缓存机制,包括一级缓存和二级缓存的配置和使用,以及如何整合第三方缓存EHCache。详细解释了一级缓存的生命周期、二级缓存的开启条件和配置属性,以及如何通过ehcache.xml配置文件和logback.xml日志配置文件来实现EHCache的整合。
mybatis复习05,mybatis的缓存机制(一级缓存和二级缓存及第三方缓存)
|
3天前
|
SQL Java 数据库连接
Mybatis架构原理和机制,图文详解版,超详细!
MyBatis 是 Java 生态中非常著名的一款 ORM 框架,在一线互联网大厂中应用广泛,Mybatis已经成为了一个必会框架。本文详细解析了MyBatis的架构原理与机制,帮助读者全面提升对MyBatis的理解和应用能力。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
Mybatis架构原理和机制,图文详解版,超详细!
|
5月前
|
SQL 缓存 Java
Java框架之MyBatis 07-动态SQL-缓存机制-逆向工程-分页插件
Java框架之MyBatis 07-动态SQL-缓存机制-逆向工程-分页插件
|
6月前
|
SQL 数据库
MyBatisPlus之逻辑删除、MyBatisPlus解决并发问题的乐观锁机制
MyBatisPlus之逻辑删除、MyBatisPlus解决并发问题的乐观锁机制
80 2
|
6月前
|
XML 缓存 Java
MyBatis二级缓存解密:深入探究缓存机制与应用场景
MyBatis二级缓存解密:深入探究缓存机制与应用场景
461 2
MyBatis二级缓存解密:深入探究缓存机制与应用场景
|
6月前
|
SQL Java 数据库连接
深入源码:解密MyBatis数据源设计的精妙机制
深入源码:解密MyBatis数据源设计的精妙机制
106 1
深入源码:解密MyBatis数据源设计的精妙机制
|
6月前
|
Java 关系型数据库 数据库连接
MyBatis 执行流程分析
MyBatis 执行流程分析
56 2
|
6月前
|
存储 缓存 Java
什么!?实战项目竟然撞到阿里面试的原题!???关于MyBatis Plus的缓存机制
什么!?实战项目竟然撞到阿里面试的原题!???关于MyBatis Plus的缓存机制
|
6月前
|
XML Java 数据库连接
一文搞懂MyBatis初始化机制
对于任何框架而言,在使用前都要进行一系列的初始化,MyBatis也不例外。本章将通过以下几点详细介绍MyBatis的初始化过程。
38 0
|
1月前
|
Java 数据库连接 Maven
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和MyBatis Generator,使用逆向工程来自动生成Java代码,包括实体类、Mapper文件和Example文件,以提高开发效率。
105 2
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。