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

简介: 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;
}


相关文章
|
7月前
|
Java 数据库连接 mybatis
MyBatis 插件机制
插件是一种常见的扩展方式,大多数开源框架也都支持用户通过添加自定义插件的方式来扩展或者改变原有的功能,MyBatis中也提供的有插件,虽然叫插件,但是实际上是通过拦截器(Interceptor)实现的。
30 0
|
5天前
|
Java 关系型数据库 数据库连接
MyBatis 执行流程分析
MyBatis 执行流程分析
10 2
|
17天前
|
SQL Java 数据库连接
深入源码:解密MyBatis数据源设计的精妙机制
深入源码:解密MyBatis数据源设计的精妙机制
30 1
深入源码:解密MyBatis数据源设计的精妙机制
|
1月前
|
存储 缓存 Java
什么!?实战项目竟然撞到阿里面试的原题!???关于MyBatis Plus的缓存机制
什么!?实战项目竟然撞到阿里面试的原题!???关于MyBatis Plus的缓存机制
|
2月前
|
XML Java 数据库连接
一文搞懂MyBatis初始化机制
对于任何框架而言,在使用前都要进行一系列的初始化,MyBatis也不例外。本章将通过以下几点详细介绍MyBatis的初始化过程。
13 0
|
3月前
|
SQL Java 数据库连接
|
9月前
|
SQL XML 存储
一.吃透Mybatis源码-Mybatis初始化
Mybatis是Java 项目开发使用率非常高的一款持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。 同时Mybatis也是面试过程中被高频问到的一门技术,今天我就带大家一起来对Mybatis的重要原理及其源码进行一个分析。
|
4月前
|
SQL 缓存 Java
MyBatis Plus插件机制与执行流程原理分析
MyBatis Plus插件机制与执行流程原理分析
238 0
|
4月前
|
缓存 Java 数据库连接
Hibernate或MyBatis:ORM映射、缓存机制等知识讲解梳理
Hibernate或MyBatis:ORM映射、缓存机制等知识讲解梳理
45 0
|
6月前
|
关系型数据库 MySQL Java
Mybatis-Plus使用案例(包括初始化以及常用插件)
Mybatis-Plus使用案例(包括初始化以及常用插件)
57 0