初始化流程的完整串联:从 XML 到 SqlSessionFactory

概述

在之前的系列中,我们深入剖析了 MappedStatementBoundSqlExecutor 等运行时核心组件的运作机制。然而,这些对象在 MyBatis 启动时是如何被凭空创建出来的?mybatis-config.xmlUserMapper.xml 又是如何被解析成 Configuration 对象中的一个个 MappedStatement?本文回到 MyBatis 生命周期的起点,完整串联从 XML 文件到 SqlSessionFactory 的初始化全链路。

MyBatis 的初始化过程是建造者模式的教科书级应用。XMLConfigBuilder 像一位总工程师,依次解析数据源、事务管理器、别名、插件等全局配置;每当遇到 <mappers> 节点,便委派 XMLMapperBuilder 去解析具体的 mapper.xmlXMLMapperBuilder 再将每个 <select><insert> 等节点交给 XMLStatementBuilder 去构建 MappedStatement。这套层层递进的建造者流水线,将分散在多个文件中的配置最终收敛到全局唯一的 Configuration 对象中,并由 SqlSessionFactoryBuilder 将其封装为 SqlSessionFactory。本文将沿着这条建造者流水线,逐层拆解每个 Builder 的解析逻辑与设计巧思。

核心要点

  • 初始化入口SqlSessionFactoryBuilder.build 创建 XMLConfigBuilder 并启动解析。
  • 建造者流水线XMLConfigBuilderXMLMapperBuilderXMLStatementBuilder 的层层委托。
  • 配置项解析propertiessettingstypeAliasesenvironmentsmappers 的逐一处理。
  • SQL 片段递归XMLIncludeTransformer 处理 <sql><include> 标签的替换机制。
  • 注解合并MapperAnnotationBuilder@Select 等注解转化为 MappedStatement

文章组织架构图

flowchart TD subgraph S1 ["1. 初始化全链路总览"] direction TB A["SqlSessionFactoryBuilder.build"] end subgraph S2 ["2. SqlSessionFactoryBuilder入口"] B["创建 XMLConfigBuilder 并调用 parse"] end subgraph S3 ["3. XMLConfigBuilder 解析顶层配置"] C["解析 properties/settings/environments/mappers 等"] end subgraph S4 ["4. XMLMapperBuilder 解析映射文件"] D["解析 namespace/resultMap/select 等"] end subgraph S5 ["5. SQL片段与Include递归替换"] E["XMLIncludeTransformer 处理 sql/include"] end subgraph S6 ["6. 注解映射解析"] F["MapperAnnotationBuilder 处理 @Select 等"] end subgraph S7 ["7. SqlSessionFactory构建与校验"] G["包装 Configuration 并创建 DefaultSqlSessionFactory"] end subgraph S8 ["8. 设计模式总结"] H["建造者/工厂/组合模式分析"] end subgraph S9 ["9. 生产事故排查"] I["案例:mapper-locations失效/别名错误"] end subgraph S10 ["10. 面试高频专题"] J["10+题含系统设计"] end A --> B --> C --> D --> E D --> F C --> G F --> G G --> H H --> I H --> J classDef topic fill:#f8f9fa,stroke:#333,stroke-width:2px,rx:5,color:#333; class S1,S2,S3,S4,S5,S6,S7,S8,S9,S10 topic;

分层详尽的文字说明

  • 总览说明:全文 10 个模块从初始化总览开始,逐步深入各层 Builder 的解析逻辑,最后通过设计模式总结、事故排查和面试完成闭环。
  • 逐模块说明:模块 1 建立初始化全链路的全局认知;模块 2-6 逐层拆解各 Builder 的内部实现;模块 7 讲解最终工厂的构建;模块 8 提炼设计思想;模块 9-10 落地实践与应试。
  • 关键结论MyBatis 的初始化是建造者模式的精妙编排------XMLConfigBuilderXMLMapperBuilderXMLStatementBuilder 层层委托,将分散的配置最终收敛为 Configuration 单例对象,再由 SqlSessionFactoryBuilder 封装为 SqlSessionFactory

1. MyBatis 初始化流程总览

MyBatis 的启动可分为两大阶段:配置解析工厂构建

  • 配置解析阶段 :将 XML、注解等外部描述转化为内存中的 Configuration 对象,该对象持有所有的 MappedStatementResultMapParameterMapCache、插件拦截链以及环境设置。
  • 工厂构建阶段 :将完整的 Configuration 对象包装进 DefaultSqlSessionFactory,成为应用创建 SqlSession 的工厂。

整个流程的入口是 SqlSessionFactoryBuilder.build(reader),它委派给 XMLConfigBuilder 完成解析,最后调用 build(configuration) 生成 SqlSessionFactory。下图为全链路流程:

flowchart TD A[应用程序] -->|1. 提供配置文件流| B(SqlSessionFactoryBuilder) B -->|2. 创建 XMLConfigBuilder| C[XMLConfigBuilder] C -->|3. 解析 mybatis-config.xml| D[XMLConfigBuilder.parse] D -->|4. 调用 parseConfiguration| E[解析 settings/properties/...] E -->|5. 遇到 mappers 节点| F[XMLMapperBuilder] F -->|6. 解析 mapper.xml| G[XMLMapperBuilder.parse] G -->|7. 遇到 select/insert 等| H[XMLStatementBuilder] H -->|8. 构建 MappedStatement| I[Configuration] D -->|9. 返回 Configuration| J[SqlSessionFactoryBuilder] J -->|10. build Configuration| K[DefaultSqlSessionFactory]

图表主旨概括 :展示从读取配置到生成 SqlSessionFactory 的完整调用链,体现建造者的逐级委托。
逐层分解

① 应用程序创建 SqlSessionFactoryBuilder 并传入 MyBatis 配置的 InputStream

② Builder 创建 XMLConfigBuilder 实例,内部持有 XPathParser 和空 Configuration 对象。

XMLConfigBuilder.parse() 方法解析 XML 根节点,并调用 parseConfiguration 处理全局节点。

④ 解析 mappers 时,根据类型(resource/url/class/package)构造对应的 XMLMapperBuilder 并调用 parse()

XMLMapperBuilder 解析映射文件中的 select|insert|update|delete,交给 XMLStatementBuilder 生成 MappedStatement

⑥ 解析全部完成后,返回装配好的 Configuration 对象。

SqlSessionFactoryBuilder.build(Configuration) 创建 DefaultSqlSessionFactory
设计原理映射

  • 建造者模式XMLConfigBuilderXMLMapperBuilderXMLStatementBuilder 分别负责不同粒度的对象构建。
  • 工厂模式SqlSessionFactoryBuilder 最终产出 SqlSessionFactory 工厂。
  • 组合模式 :动态 SQL 节点(SqlNode)在解析阶段即构建成树型结构。
    工程联系与关键结论
    理解此流程对排查"找不到 MappedStatement"错误至关重要------若某 Mapper 未被解析,极大概率是 mappers 配置或 Resource 加载出了问题。在 Spring Boot 中,SqlSessionFactoryBean 内部同样调用 SqlSessionFactoryBuilder,本质流程一致。

下文将按照该流程展开每个环节的源码与设计细节。


2. SqlSessionFactoryBuilder:入口与委托

SqlSessionFactoryBuilder 是一个轻量级的构造器,主要提供多个 build 重载方法来接收不同形式的配置源(ReaderInputStream),最终都汇聚到 build(Configuration)

源码片段:SqlSessionFactoryBuilder.build(InputStream, String, Properties)

java 复制代码
public class SqlSessionFactoryBuilder {

    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
        try {
            // 创建 XMLConfigBuilder,传入配置文件和环境 id
            XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
            // 执行解析,返回 Configuration,然后构建 DefaultSqlSessionFactory
            return build(parser.parse());
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error building SqlSession.", e);
        } finally {
            // 关闭输入流
            try {
                inputStream.close();
            } catch (IOException e) {
                // ignore
            }
        }
    }

    public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config);
    }
}

解读

  • SqlSessionFactoryBuilder 并不承担任何解析职责,它仅创建 XMLConfigBuilder 并调用 parse(),后者才是真正的配置解析器。这是典型的委托模式 ,Builder 将构建过程委派给专业的 XMLConfigBuilder
  • parse() 返回完整的 Configuration 对象后,build(Configuration) 简单地 new DefaultSqlSessionFactory(config),可见 Configuration 是 MyBatis 的核心状态容器。
  • 关闭流的逻辑保证资源释放。

其它重载方法(如只传入 InputStream,或指定 environment)本质相同,省略部分参数的会使用默认值。在 Spring Boot 整合环境中,SqlSessionFactoryBean 最终也会调用 SqlSessionFactoryBuilder.build(Configuration),因此该入口是理解所有 MyBatis 启动的钥匙。

设计启示SqlSessionFactoryBuilder 是一次性的,用完即可丢弃,因为其产物 SqlSessionFactory 已具备生产 SqlSession 的能力。这也符合建造者模式的典型用法:建造者独立于产品,完成建造后移除。


3. XMLConfigBuilder:顶层配置的解析引擎

XMLConfigBuilder 继承自 BaseBuilder,是解析 mybatis-config.xml 的核心。其 parseConfiguration 方法内部采用模板方法 风格,按顺序调用各个 private 方法,将 XML 节点逐一映射到 Configuration 对象的相应属性。

sequenceDiagram participant SB as SqlSessionFactoryBuilder participant XC as XMLConfigBuilder participant XP as XPathParser participant CF as Configuration SB->>XC: build(inputStream) XC->>XC: parse() XC->>XP: evalNode("/configuration") XC->>XC: parseConfiguration(root) loop 解析各个顶层节点 XC->>XC: propertiesElement (替换占位符) XC->>XC: settingsAsProperties -> loadCustomLogImpl, etc. XC->>CF: setSettings (autoMappingBehavior, cacheEnabled...) XC->>XC: typeAliasesElement -> CF.getTypeAliasRegistry().registerAlias XC->>XC: pluginElement -> CF.addInterceptor XC->>XC: environmentsElement -> CF.setEnvironment (含 TxFactory, DataSource) XC->>XC: mapperElement -> 委派 XMLMapperBuilder/注解解析 end XC-->>SB: Configuration

图表主旨概括 :展示 XMLConfigBuilder.parseConfiguration 如何依序处理 settingstypeAliasespluginsenvironmentsmappers 等节点,并填充 Configuration
逐层分解

  • propertiesElement:读取 <properties> 节点及外部属性文件,并执行占位符替换(${})。
  • settingsAsProperties:解析 <settings>Properties,后续调用 settingsElement 将具体值设置到 Configuration(如 cacheEnabledlazyLoadingEnabled)。
  • typeAliasesElement:注册类型别名,默认扫描 TypeAliasRegistry 中的预定义别名,同时支持 package 扫描。
  • pluginElement:实例化拦截器并添加到 Configuration.interceptorChain
  • environmentsElement:根据 default 环境 id 选取 <environment>,分别构造事务工厂和 DataSource,然后通过 Configuration.setEnvironment 设置。
  • mapperElement:处理 <mappers>,支持 resourceurlmapperClasspackage 四种方式映射。
    设计原理映射
  • 建造者模式 :每个 xxxElement 方法负责建造 Configuration 的一个局部,parseConfiguration 协调建造顺序。
  • 策略模式environments 中可根据配置选择不同的 TransactionFactory 实现(JDBC/MANAGED)和不同的数据源实现(POOLED/UNPOOLED/JNDI)。
    工程联系与关键结论
    解析顺序严格遵守 MyBatis 规定,properties 首先加载以保证后续节点可使用 ${} 占位符。mappers 最后解析是因为它依赖前面完成的别名注册、类型处理器等。若违反顺序自行拼接配置,可能导致占位符无法解析或别名未找到。

3.1 解析 mappers 节点的四种方式

XMLConfigBuilder.mapperElement(XNode parent) 中,会根据子节点的不同特征执行不同策略:

源码片段(简化)

java 复制代码
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) {
                    // 方式二:resource 相对路径
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                    mapperParser.parse();
                } else if (resource == null && url != null && mapperClass == null) {
                    // 方式三:绝对 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) {
                    // 方式四:直接指定 Mapper 接口类
                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                }
            }
        }
    }
}

解读

  • package 扫描通过 configuration.addMappers(package) 内部使用 ResolverUtil 扫描指定包下的所有接口,然后逐个调用 configuration.addMapper(mapperInterface),后者会触发 MapperAnnotationBuilder 解析注解。
  • resourceurl 都是加载 XML 映射文件,区别在于路径解析方式。前者使用类加载器加载相对路径,后者支持绝对 URL,适用于配置文件位于远程或非类路径的情况。
  • class 直接指定 Mapper 接口,会尝试通过注解或同名 XML 进行解析(MapperAnnotationBuilder 会检查是否存在同路径 XML)。
  • 无论哪种方式,最终目标都是将解析得到的 MappedStatement 注册进 Configuration

工程提示 :Spring Boot 的 mybatis.mapper-locations 属性最终会转换为 Resource[],传递给 SqlSessionFactoryBean.setMapperLocations,其底层仍是通过 XMLMapperBuilder 解析每一个匹配的资源,与上述 resource 方式本质一致。


4. XMLMapperBuilder:SQL 映射文件的解析中枢

XMLMapperBuilder 专门负责解析单个 mapper.xml 文件,是连接全局配置与具体 SQL 语句的桥梁。

sequenceDiagram participant XC as XMLConfigBuilder participant XM as XMLMapperBuilder participant XS as XMLStatementBuilder participant CF as Configuration XC->>XM: new XMLMapperBuilder(inputStream, config, resource, sqlFragments) XM->>XM: parse() XM->>CF: isResourceLoaded(resource)? 防止重复加载 XM->>XM: configurationElement(root) XM->>CF: namespace 绑定 Mapper 接口 (bindMapperForNamespace) XM->>XM: resultMapElement 解析 resultMap 节点 XM->>CF: addResultMap loop 每个 select|insert|update|delete 节点 XM->>XS: new XMLStatementBuilder(config, context) XS->>XS: parseStatementNode() XS->>CF: addMappedStatement end XM->>XM: parsePendingResultMaps / parsePendingStatements (处理引用未完成解析的)

图表主旨概括 :展示 XMLMapperBuilder.parse() 如何解析 <mapper> 根节点,处理 namespaceresultMap 以及将各语句节点委托给 XMLStatementBuilder,并注册到 Configuration
逐层分解

  • 首先检查资源是否已加载(configuration.isResourceLoaded(resource)),避免重复解析。
  • 调用 configurationElement 解析根节点下的子元素。
  • namespace 用于绑定对应的 Mapper 接口,最终存储在 Configuration.mapperRegistry 中。
  • resultMapElement 解析 <resultMap>,构建 ResultMapping 列表并封装为 ResultMap 对象,存入 Configuration.resultMaps。若存在继承或关联,可能在后续的 parsePendingResultMaps 中完成引用绑定。
  • 遍历 select|insert|update|delete 节点,每个节点创建一个 XMLStatementBuilder 来生成 MappedStatement
  • 解析完成后,会处理缓存引用、未完成的语句等挂起资源。
    设计原理映射
  • 建造者模式XMLMapperBuilder 组装 ResultMapMappedStatement 等产品,但它又将语句节点的构建责任委托给更细粒度的 XMLStatementBuilder
  • 责任链/组合 :动态 SQL 的解析过程会在 XMLStatementBuilder 中构建 SqlNode 组合树,体现组合模式。
    工程联系与关键结论
    MappedStatement 的唯一 id 由 namespace + "." + statementId 组成,这解释了为什么在调用 sqlSession.selectOne("com.xx.UserMapper.getUser") 时需要完整限定名。Spring Boot 中 Mapper 接口代理能自动拼接 namespace,也正是基于这一规则。详细内容见系列第 1、2 篇。

4.1 XMLStatementBuilder 构建 MappedStatement

XMLStatementBuilder 继承自 BaseBuilder,直接负责从单个 <select> 等节点构建 MappedStatement

源码片段:XMLStatementBuilder.parseStatementNode() 核心逻辑

java 复制代码
public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
    // ...读取各种属性(fetchSize, timeout, parameterMap, resultType, resultMap, useCache等)
    
    // 1. 创建 SqlSource(动态SQL或静态SQL)
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    
    // 2. 使用 Builder 模式构造 MappedStatement
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .parameterMap(parameterMap)
        .resultMaps(resultMaps)
        .cache(cache)
        .useCache(isUseCache)
        // ... 更多属性
        .databaseId(databaseId);
    
    MappedStatement statement = statementBuilder.build();
    // 3. 注册到 Configuration
    configuration.addMappedStatement(statement);
}

解读

  • LanguageDriver 负责将 XML 节点转化为 SqlSource。若使用默认 XMLLanguageDriver,它会创建 DynamicSqlSource(含动态标签)或 RawSqlSource(纯文本),这是前文所述动态 SQL 引擎的源头。
  • MappedStatement.Builder 是典型的建造者模式,拥有链式调用,最终调用 build() 产出不可变的 MappedStatement 实例。
  • configuration.addMappedStatement(statement) 将其存入内部的 mappedStatements Map(键为 id)。该方法还会校验 id 唯一性,若重复会抛出 IllegalArgumentException,确保每个语句定义的唯一标识。

与前文的关联 :在运行时,Executor 根据 statementId 从 Configuration 中取出 MappedStatement,获取 SqlSourceResultMap,再生成 BoundSql 执行。这一流程的基础便是在此初始化阶段奠定的。


5. SQL 片段与 Include:XMLIncludeTransformer 的递归替换

<sql><include> 标签提供 SQL 复用能力,其解析由 XMLIncludeTransformer 完成,在 XMLStatementBuilder 构建 SqlSource 之前应用。

sequenceDiagram participant XS as XMLStatementBuilder participant XIT as XMLIncludeTransformer participant Node as DOM Node Tree participant CF as Configuration (sqlFragments) XS->>XIT: applyIncludes(sourceNode, variablesContext) loop 遍历节点树 XIT->>Node: 获取当前节点 alt 节点是 XIT->>CF: findSqlFragment(refid) 从 sqlFragments 获取 节点 XIT->>XIT: 深拷贝片段节点 XIT->>XIT: 递归 applyIncludes(拷贝的节点) 处理嵌套 include XIT->>Node: replaceChild(片段节点, originalIncludeNode) 替换 else 普通元素节点 XIT->>Node: 递归遍历子节点 applyIncludes end end XS-->>XS: 得到展开后的纯 SQL DOM

图表主旨概括 :展示 XMLIncludeTransformer.applyIncludes 如何深度遍历 XML 节点树,遇到 <include> 标签时查找并替换为对应的 <sql> 片段,并支持多层嵌套。
逐层分解

  • 方法入口时的 source 参数通常是 <select> 等语句节点或其子节点。
  • 对于每个子节点,若其名称为 include,则读取 refid 属性,从 Configuration.sqlFragments 中获取对应的 <sql> 节点(形式为 DOM Node)。
  • 执行深拷贝,并递归调用 applyIncludes 处理拷贝节点内部可能再次出现的 <include>,实现多层嵌套替换。
  • 替换完成后,原始的 <include> 节点被删除,其位置被展开后的 SQL 文本节点替代。
  • 针对普通元素节点(如 <if><where> 等),继续递归其子节点列表,保证所有嵌套的 include 均被展开。
    设计原理映射
  • 组合模式 :XML 节点树天然是组合结构,applyIncludes 利用递归遍历处理这种层次结构。
  • 装饰/转换 :原始带 include 的节点被"转换"为纯 SQL,后续 LanguageDriver 再将其解析为 SqlSource
    工程联系与关键结论
    <include> 的替换发生在 DOM 层面,而非字符串层面,因此能正确处理动态 SQL 与文本的混合。注意 <include refid="columns"/> 中的 refid 必须能在当前命名空间及共享空间中找到,否则会抛出 BuilderException。同时,由于替换用深拷贝,片段内的属性占位符和动态元素会在替换后继续参与后续解析。

5.1 源码示例:递归核心

java 复制代码
public class XMLIncludeTransformer {
    public void applyIncludes(Node source, final Properties variablesContext) {
        if (source.getNodeName().equals("include")) {
            // 找到对应的 sql 片段
            String refid = getStringAttribute(source, "refid");
            Node toInclude = findSqlFragment(refid, variablesContext);
            // 深拷贝片段并递归展开内部 include
            toInclude = toInclude.cloneNode(true);
            applyIncludes(toInclude, variablesContext);
            // 替换
            source.getParentNode().replaceChild(toInclude, source);
            // 继续处理替换后的节点(如果有未处理的属性)
            applyIncludes(toInclude, variablesContext);
        } else if (source.getNodeType() == Node.ELEMENT_NODE) {
            NodeList children = source.getChildNodes();
            for (int i = 0; i < children.getLength(); i++) {
                applyIncludes(children.item(i), variablesContext);
            }
        }
    }
}

注意 findSqlFragment 会检查当前映射文件的内部片段,并支持跨命名空间引用(如 refid="com.xx.OtherMapper.columns")。


6. 注解映射的解析:MapperAnnotationBuilder

当 Mapper 接口未对应 XML 映射文件,或需要混合使用注解时,MyBatis 会调用 MapperAnnotationBuilder 解析接口上的 @Select@Insert@ResultMap 等注解,并生成对应的 MappedStatement

源码片段:MapperAnnotationBuilder.parse()

java 复制代码
public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
        // 1. 查找同名的 XML 映射文件(如果存在,先解析以保证 XML 优先级)
        loadXmlResource();
        configuration.addLoadedResource(resource);
        
        // 2. 解析 @CacheNamespace 等类级别注解
        // ...
        
        for (Method method : type.getMethods()) {
            // 排除 Object 等基类方法
            if (!method.isBridge()) {
                parseStatement(method);
            }
        }
    }
    parsePendingMethods();
}

private void parseStatement(Method method) {
    // 获取 SQL 命令类型(由 @Select/@Insert 等决定)
    SqlCommandType sqlCommandType = getSqlCommandType(method);
    // 获取各类选项注解
    Options options = method.getAnnotation(Options.class);
    // 构建 SqlSource:从注解的 value 属性读取 SQL,并包上动态解析能力(若有 <script> 或动态 SQL 注解)
    LanguageDriver languageDriver = getLanguageDriver();
    SqlSource sqlSource = buildSqlSourceFromAnnotations(method, languageDriver);
    
    // 使用 MappedStatement.Builder 构建
    String statementId = type.getName() + "." + method.getName();
    MappedStatement.Builder builder = new MappedStatement.Builder(configuration, statementId, sqlSource, sqlCommandType);
    // 设置各种属性(结果映射、二级缓存等)
    // ...
    configuration.addMappedStatement(builder.build());
}

解读

  • loadXmlResource() 会尝试从类路径加载与 Mapper 接口同路径的 XML 文件。若存在,XML 中的 <select> 等定义会优先注册(XML 的 statementId 与注解产生的 id 相同,由于 configuration.addMappedStatement 会检查重复,先注册者胜出------这里 MyBatis 设计成优先加载 XML,注解作为补充)。因此,当 XML 和注解同时定义相同 id 的语句时,XML 会覆盖注解
  • 遍历 Method,通过反射读取 @Select 等注解的 SQL 文本,利用 LanguageDriver 创建 SqlSource。如果 SQL 中包含 <script> 标签或动态 SQL 元素,MyBatis 会将其解析为 DynamicSqlSource,实现了注解与动态 SQL 的兼容。
  • Configuration.addMappedStatement 会在内部检查 loadCompleted,若解析已完成但仍有新注册的语句,会触发异常,因此注解解析必须在 Configuration 锁定前完成。

注解与 XML 混合的最佳实践 :通常建议简单 SQL 使用注解,复杂动态 SQL 使用 XML,两者可用在不同方法上但共享同一 Mapper 接口。由于 loadXmlResource 的存在,完全可以将通用结果映射定义在 XML 中,增删改查用注解,保持清晰。


7. SqlSessionFactory 的最终构建与 Configuration 校验

XMLConfigBuilder.parse() 执行完毕,所有的全局配置、映射文件、注解均已被解析并写入 Configuration。紧接着 SqlSessionFactoryBuilder.build(Configuration) 创建 DefaultSqlSessionFactory

java 复制代码
public class DefaultSqlSessionFactory implements SqlSessionFactory {
    private final Configuration configuration;

    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }

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

Configuration 对象在此时已处于"只读"状态吗?严格说 MyBatis 并没有显式冻结 Configuration,但最佳实践是初始化完成后不应对其再修改,因为 DefaultSqlSessionFactory 会被多线程共享,修改配置可能导致线程安全问题。SqlSessionFactoryBuilder 完成任务后即可被丢弃,符合其一次性使用的语义。

Configuration 校验XMLConfigBuilder 在解析过程中会进行部分校验,如 databaseIdProvider 匹配、语句 id 唯一性等,但并未进行全量完整性检查。缺失的配置可能在运行时才暴露(如未配置 environment,打开 SqlSession 时会抛出异常)。因此生产环境建议在启动后立即执行一次 sqlSessionFactory.openSession().getConnection() 进行连接性验证,提前暴露环境配置错误。


8. 设计模式总结:建造者流水线的精妙编排

MyBatis 初始化流程集中体现了多种经典设计模式:

  • 建造者模式XMLConfigBuilderXMLMapperBuilderXMLStatementBuilder 都是针对特定产品的建造者。XMLConfigBuilder 建造 Configuration 全局设置;XMLMapperBuilder 建造一个命名空间下的多个 MappedStatementResultMapXMLStatementBuilder 建造单个 MappedStatement。它们通过层层委托完成从粗粒度到细粒度的装配。
  • 工厂模式SqlSessionFactoryBuilder.build() 最终产出 SqlSessionFactory,后者又是创建 SqlSession 的工厂。TransactionFactoryDataSourceFactory 等也遵循工厂方法。
  • 组合模式 :动态 SQL 节点的解析产生 SqlNode 树(如 MixedSqlNode 包含多个子 SqlNode),XMLIncludeTransformer 对 XML 节点树递归替换同样体现组合模式。
  • 策略模式LanguageDriver 允许替换不同的 SQL 解析策略,TransactionFactory 支持 Jdbc/Managed 两种事务管理策略。
  • 模板方法BaseBuilder 定义了建造者的基本骨架(持有 ConfigurationTypeAliasRegistry 等),各子类实现具体建造逻辑。

工程价值:理解这些模式有助于在阅读源码时快速定位职责边界。当需要扩展 MyBatis(如自定义节点解析),可参照现有 Builder 的结构进行扩展,而不是推翻重来。


9. 生产事故排查专题

9.1 事故一:mapper-locations 通配符失效,Mapper 全部 404

现象

Spring Boot 项目启动正常,但调用任何 Mapper 方法均抛出 org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.xxx.UserMapper.findById。查看日志,mybatis.mapper-locations=classpath:mapper/*.xml 配置无误。

排查过程

  1. 检查启动日志中 SqlSessionFactoryBean 打印的加载信息,发现并没有加载预期的 UserMapper.xml
  2. 查看 target/classes/mapper 目录,发现只有 .class 文件,没有 .xml 文件。
  3. 确认源代码 src/main/java/com/xxx/mapper/ 下放置了 XML 文件,但 Maven 默认只将 src/main/resources 中的文件复制到输出目录。

以下时序图重现了该问题:

sequenceDiagram participant Dev as 开发者 participant SB as Spring Boot participant SFB as SqlSessionFactoryBean participant Resolver as ResourcePatternResolver participant FS as 文件系统(target/classes) Dev->>SB: 配置 mapper-locations=classpath:mapper/*.xml SB->>SFB: afterPropertiesSet() SFB->>Resolver: getResources("classpath:mapper/*.xml") Resolver->>FS: 扫描 target/classes/mapper/*.xml FS-->>Resolver: 返回[] (无匹配) Resolver-->>SFB: 空 Resource 数组 SFB->>SFB: mapperLocations 为空,跳过解析 SB-->>Dev: 启动成功,但 Mapper 无语句绑定 Dev->>Dev: 调用 Mapper 方法 -> BindingException

根因

Maven 构建过程未将 src/main/java 下的非 Java 文件复制到输出目录。MyBatis 的 mapper-locations 通配符只能在类路径下搜索映射文件,而此类文件根本未进入类路径。

解决

pom.xml 中添加资源过滤配置:

xml 复制代码
<build>
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
        </resource>
        <resource>
            <directory>src/main/resources</directory>
        </resource>
    </resources>
</build>

或者直接将 XML 文件移动到 src/main/resources/mapper/ 目录下,保持路径与 mapper-locations 匹配。

最佳实践

  • 统一将 Mapper XML 置于 resources/mapper,避免混淆。
  • 在 CI/CD 流程中加入启动后调用一个"健康检查 Mapper"的方法,确保所有核心 SQL 可用。

9.2 事故二:type-aliases-package 配置错误,resultType 简写找不到类

现象

XML 中使用 resultType="User" 时抛出 org.apache.ibatis.type.TypeException: Could not resolve type alias 'User'。而 User 实体类位于包 com.example.entity,且在 application.yml 中配置了 mybatis.type-aliases-package: com.example.entity

排查

  1. 打印 Configuration 对象中的 TypeAliasRegistry,发现已注册别名包实际为 com.example.entities(多了一个 s)。
  2. 对比配置项,发现 YAML 中写错:type-aliases-package: com.example.entities
  3. 别名注册使用 ResolverUtil 扫描指定包,包名错误导致未找到任何类,因此 User 别名未被注册。

时序图:

sequenceDiagram participant Yml as application.yml participant SFB as SqlSessionFactoryBean participant CF as Configuration participant TAR as TypeAliasRegistry participant Resolver as ResolverUtil Yml->>SFB: mybatis.type-aliases-package = com.example.entities (错误) SFB->>CF: setTypeAliasesPackage("com.example.entities") CF->>TAR: registerAliases("com.example.entities") TAR->>Resolver: find(com.example.entities, Object.class) Resolver-->>TAR: 空集合(因包名错误) SFB-->>SFB: 初始化完成,但 User 别名未注册 Dev->>CF: 执行 XML 解析,resultType="User" CF->>TAR: resolveAlias("User") TAR-->>CF: 抛出 TypeException

根因

配置书写错误,导致别名注册失效,运行时无法根据简单类名映射到实体类。

解决

修正 YAML 中的拼写错误,并可使用 @Alias 注解显式指定别名增强健壮性。必要时通过 Configuration.getTypeAliasRegistry().getTypeAliases() 遍历调试。

最佳实践

  • 使用全限定类名作为 resultType 可完全规避别名问题,代价是冗长。
  • 在项目中约定实体包路径后,通过集成测试验证别名加载(例如扫描 TypeAliasRegistry 断言关键实体存在)。
  • Spring Boot 配置元数据可提供提示,利用 IDE 的自动补全减少手误。

10. 面试高频专题

Q1:从读入 mybatis-config.xml 到创建 SqlSessionFactory,整个过程调用了哪些关键类与方法?请画出调用链条并说明每个环节的职责。

答:

调用链条如下:
SqlSessionFactoryBuilder.build(InputStream)

new XMLConfigBuilder(inputStream, environment, properties)

XMLConfigBuilder.parse()

parseConfiguration(parser.evalNode("/configuration"))

propertiesElement(root.evalNode("properties"))

settingsAsProperties(root.evalNode("settings"))

typeAliasesElement(root.evalNode("typeAliases"))

pluginElement(root.evalNode("plugins"))

environmentsElement(root.evalNode("environments"))

mapperElement(root.evalNode("mappers"))

→ 依 <mappers> 子元素类型创建 XMLMapperBuilder 或调用 configuration.addMapper()

→ 返回 Configuration 实例

new DefaultSqlSessionFactory(configuration)

  • SqlSessionFactoryBuilder:入口,只负责委派和最终工厂构建,本身无解析逻辑。
  • XMLConfigBuilder:解析全局配置,填充 Configuration 的 settings、别名、插件、环境,触发映射文件加载。
  • XMLMapperBuilder:解析单个 mapper.xml,注册 ResultMap 并逐个创建 MappedStatement
  • XMLStatementBuilder:从 XML 节点构建单个 MappedStatement,内部调用 LanguageDriver 创建 SqlSource
  • XMLIncludeTransformer:在构建 SqlSource 前递归展开 <include> 标签。
  • MapperAnnotationBuilder:解析接口注解,生成 MappedStatement

追问:parseConfiguration 为何必须按固定顺序调用各个解析方法?

答:因为存在依赖关系。properties 最先加载,以便后续节点可用 ${} 占位符;typeAliases 必须在 mappers 之前,否则 resultType="User" 无法解析别名;mappers 最后执行,因为它依赖已完成的别名注册、类型处理器和插件链。


Q2:XMLConfigBuilder 解析 <settings> 配置时,如何将 XML 中的驼峰设置映射到 Configuration 对象?源码如何保证设置安全?

答:
XMLConfigBuilder.settingsAsProperties(XNode) 会遍历 <settings> 下每一个子元素,将其 namevalue 放入一个 Properties 对象。接着调用 settingsElement(Properties props),该方法通过 MetaObjectConfiguration 进行代理:

java 复制代码
MetaObject metaConf = SystemMetaObject.forObject(configuration);
for (String key : props.stringPropertyNames()) {
    if (!metaConf.hasSetter(key)) {
        throw new BuilderException("The setting " + key + " is not known. ...");
    }
    // 转换值类型,例如将 "true"/"false" 转为 boolean
    Class<?> type = metaConf.getSetterType(key);
    Object value = convertValue(props, type, key); // 使用 TypeHandler 或自定义转换
    metaConf.setValue(key, value);
}

安全机制

  1. metaConf.hasSetter(key) 会检查 Configuration 中是否存在该属性的 setter,若未知属性直接抛异常,防止误拼写(如 lazyLoadingEnabled 写错会立刻报错)。
  2. convertValue 负责将字符串 "true"、"false"、"PARTIAL" 等正确转换为其目标类型,还会尝试使用 TypeHandler 完成复杂类型转换。这样 XML 中的字符串能安全注入到 Configuration 中。

追问:Settings 里 mapUnderscoreToCamelCase 开启后,在哪一步影响 ResultMap 构建?

答:该属性作用于 Configuration.isMapUnderscoreToCamelCase(),在 ResultMapBuilderDefaultResultSetHandler 中构建自动结果的 ResultMapping 时,如果未显式配置 <result>,会调用 MetaObject.findProperty() 将数据库下划线字段转为驼峰属性进行匹配。


Q3:<mappers> 节点下四种引入方式(resource、url、class、package)的解析流程有何不同?它们的根本区别是什么?

答:

核心源码位于 XMLConfigBuilder.mapperElement()

  • resource :读取类路径下的 XML 文件,Resources.getResourceAsStream(resource) 打开流,然后创建 XMLMapperBuilder 解析。
  • url :通过 URL 对象获取流,适用于绝对路径或远程文件。
  • class :直接指定 Mapper 接口 Class,调用 configuration.addMapper(mapperClass),触发 MapperAnnotationBuilder 解析注解,并尝试加载同路径 XML。
  • package :扫描整个包,找到所有接口后循环调用 configuration.addMapper(),效果等同于逐个指定 class

根本区别

  • resource 和 url 加载的是 XML 映射文件,必须存在对应的物理文件。
  • class 和 package 以 Mapper 接口为驱动,会自动查找同路径 XML 并解析注解。
  • 从注册 MappedStatement 的角度看,两者最终都会注入 Configuration.mappedStatements,只是触发入口不同。

工程注意点 :Spring Boot 的 mybatis.mapper-locations 会转换为 Resource 数组,底层仍然是调用 XMLMapperBuilder,相当于多条 resource。如果同时使用 @MapperScan 扫描接口和 XML 配置,需确保两者不会重复注册同一个 statement,否则会报错。


Q4:XMLMapperBuilder 在解析 namespace 时做了什么?为什么说 namespace 是绑定 Mapper 接口的关键?

答:
XMLMapperBuilder.configurationElement() 读取 <mapper namespace="com.xx.UserMapper">,然后调用 configuration.addMapper(Class.forName(namespace)) 或使用 MapperRegistry 完成接口绑定。核心逻辑位于 MapperRegistry.addMapper(type)

java 复制代码
public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
        if (hasMapper(type)) throw new BindingException("...");
        boolean loadCompleted = false;
        try {
            knownMappers.put(type, new MapperProxyFactory<>(type));
            // 触发 MapperAnnotationBuilder 解析接口注解(如果尚未解析)
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            parser.parse();
            loadCompleted = true;
        } finally {
            if (!loadCompleted) knownMappers.remove(type);
        }
    }
}
  • knownMappers 以接口 Class 为键,存放 MapperProxyFactory,供后续创建 Mapper 代理时使用。
  • namespace 必须与某个接口的全限定名一致,否则运行时通过接口名查找 MapperProxyFactory 将失败,导致 BindingException
  • 同时,XMLMapperBuilder 还利用 namespace 构建语句 ID:String statementId = namespace + "." + sqlId;,这保证了每个 MappedStatement 的唯一性。

结论:namespace 是 XML 与 Java 接口的粘合剂,它完成了"接口方法→语句 ID"的映射基础。


Q5:XMLStatementBuilder 在构建 MappedStatement 时,如何决定使用 DynamicSqlSource 还是 RawSqlSource?动态与非动态的判定依据是什么?

答:
XMLStatementBuilder 调用 languageDriver.createSqlSource(configuration, context, parameterTypeClass)。以默认 XMLLanguageDriver 为例:

java 复制代码
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
}

XMLScriptBuilder.parseScriptNode() 会解析 <select> 等节点的文本内容和子节点:

  • 如果 XNode 的子节点中包含任何动态元素 (如 <if><where><foreach><choose> 等),XMLScriptBuilder 会将这些节点转化为 SqlNode 节点树,最终外层为 MixedSqlNodeisDynamic 标记为 true,返回 DynamicSqlSource
  • 如果只有纯文本,且没有 ${} 字符串替换符,isDynamicfalse,返回 RawSqlSource
  • 特殊情况:文本中包含 ${} 也会被判定为动态,因为 ${} 需要在运行时通过 TextSqlNode 替换,无法在初始化时完全解析。

设计意义DynamicSqlSource 每次调用都会重新构建 BoundSql,以便根据参数动态改变 SQL 结构;RawSqlSource 则在初始化阶段就已确定完整的 SQL 和参数映射,执行时可直接复用,性能更优。


Q6:XMLIncludeTransformer.applyIncludes() 如何确保多层嵌套的 <include> 被完全展开?请结合源码描述其递归策略。

答:

源码片段:

java 复制代码
public void applyIncludes(Node source, final Properties variablesContext) {
    if (source.getNodeName().equals("include")) {
        // 1. 查找片段
        Node toInclude = findSqlFragment(getStringAttribute(source, "refid"));
        // 2. 深拷贝
        toInclude = toInclude.cloneNode(true);
        // 3. 递归展开拷贝节点内部可能存在的 include
        applyIncludes(toInclude, variablesContext);
        // 4. 用展开后的节点替换原 include 节点
        source.getParentNode().replaceChild(toInclude, source);
        // 5. 再处理一次替换后的节点,以防属性遗留
        applyIncludes(toInclude, variablesContext);
    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
        // 遍历子节点递归
        NodeList children = source.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            applyIncludes(children.item(i), variablesContext);
        }
    }
}

递归机制

  • 第一个 if 块直接处理 include 节点:递归调用 applyIncludes(toInclude, ...),这将深入被引用片段内部,如果片段中又含有 include,继续进入同一分支,形成递归下降。
  • 递归的终止条件:当片段树中不再存在任何 include 节点时,遍历结束,方法仅仅遍历普通元素的子节点而无匹配。
  • 深拷贝 cloneNode(true) 确保替换不会污染原始 <sql> 片段,使得同片段在不同地方引用时互不影响。

工程易错点 :如果出现循环引用(A 引用 B,B 引用 A),findSqlFragment 会再次获取到已展开的片段,但应用深拷贝和递归可能会陷入无限循环,导致栈溢出。因此实际开发中应避免循环 <include>


Q7:当 Mapper 接口同时存在 XML 映射文件和方法注解时,如果它们定义了相同方法对应的 SQL 语句,谁会生效?源码层次是如何保证的?

答:

生效的是 XML。源码执行顺序如下:

  1. XMLMapperBuilder.parse() 运行到 bindMapperForNamespace() 或通过 class 方式触发 configuration.addMapper() 时,会调用 MapperAnnotationBuilder.parse()

  2. MapperAnnotationBuilder.parse() 首先执行 loadXmlResource()

    java 复制代码
    private void loadXmlResource() {
        String resource = type.getName().replace('.', '/') + ".xml";
        InputStream inputStream = type.getResourceAsStream(resource);
        if (inputStream != null) {
            XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            xmlParser.parse();
        }
    }

    ------此步会提前解析同路径的 XML 并注册所有 MappedStatement

  3. 随后,MapperAnnotationBuilder.parse() 遍历接口方法,为每个带有 SQL 注解的方法生成 MappedStatement,并调用 configuration.addMappedStatement(statement)

  4. Configuration.addMappedStatement 内部,如果 mappedStatements Map 已经包含相同的 statementId,且 !isNotEmpty(current.id()) 判断(实际上对于已经存在的会抛出异常;但 MyBatis 的实现是:如果已经存在则 throw new IllegalArgumentException,不会覆盖)。但在 MapperAnnotationBuilder 中,会捕获异常并忽略吗?源码中并未吞异常,而是直接让重复注册抛出错误。那么 XML 和注解同时存在时在标准 MyBatis 中是直接启动失败的。

更正:在最新 3.5.x 版本中,MapperAnnotationBuilder 解析注解产生的 statementId 可能与 XML 相同,导致 addMappedStatement 抛出异常。因此,实际上 MyBatis 不允许 XML 和注解定义同一个 statementId,一旦重复会启动失败。真正的优先级体现在:如果仅有注解且无 XML,注解生效;若只有 XML 无注解,XML 生效;两者都有但定义不同方法不会冲突;相同方法 ID 会导致重复 Key 异常。

所以严格来说,不存在"覆盖",而是互斥。面试时可突出这一点,并说明更早版本可能忽略重复,但主流版本会严格校验。


Q8:Configuration 对象在初始化完成后,MyBatis 是否提供了只读保护?如果要在运行时动态添加 MappedStatement,是否安全?

答:

MyBatis 没有为 Configuration 提供内置的冻结机制(如 Collections.unmodifiableMap)。但从设计协议上,期望 SqlSessionFactory 一旦构建完毕,Configuration 就不再被更改。若确实需要在运行时动态追加 MappedStatement

  • 可以通过 configuration.addMappedStatement(statement) 直接添加,因为 mappedStatements 是普通的 Map<String, MappedStatement>,但必须考虑线程安全,因为 Configuration 会被多个 SqlSession 共享,动态添加可能导致 ConcurrentModificationException 或可见性问题。
  • 更好的实践是扩展 Configuration,在添加时加锁,并确保添加操作在应用初始化阶段完成,避免极致性能场景下的并发危险。
  • 在 Spring Boot 中,通常通过编程式注册 SqlSessionFactoryBeanmapperLocations 属性来完成,而不是运行时动态加。

面试点睛 :可以补充对比 Hibernate 的 Configuration -> SessionFactoryBuilderSessionFactory 的不可变性设计,凸显 MyBatis 的灵活性但也暴露的风险。


Q9:解析过程中出现的 "pending result maps" 和 "pending statements" 是什么?它们解决了什么初始化顺序问题?

答:

XMLMapperBuilder 解析 <resultMap> 时,如果某个 <association><collection> 引用了其他尚未解析的 resultMap(可能定义在另一个文件且尚未加载),此时直接解析会找不到结果映射。为解决这种跨文件引用声明顺序问题,MyBatis 引入 "pending" 机制:

  • XMLMapperBuilder 维护 configuration 级别的 incompleteResultMapsincompleteStatementsincompleteCacheRefs 等集合。
  • 当解析引用失败时,ResultMapResolver 会将当前配置片段封装并放入待处理队列,然后继续向后解析。
  • 在所有映射文件解析完成后,MyBatis 会依次调用 parsePendingResultMaps() 等方法重试解析。此时所有被引用的对象均已注册,能够成功建立引用。

源码体现

java 复制代码
// 处理 resultMap 时
private ResultMap resultMapElement(XNode resultMapNode, ...) {
    // ...解析中若引用其他未完成的resultMap
    if (found == null) {
        configuration.addIncompleteResultMap(new ResultMapResolver(resultMapNode, ...));
    }
}

parsePendingResultMaps 遍历这些 resolver 再次调用 resolve() 完成绑定。


Q10:MyBatis 初始化时,如何验证 SQL 语句的语法正确性?是否存在工具层面的支持?

答:

默认初始化过程中,MyBatis 不会 校验 SQL 语法,它只负责解析 XML 结构和占位符,生成 SqlSource。真实的 SQL 语法校验延迟到数据库执行时,由 JDBC 驱动抛出 SQLSyntaxErrorException

要在启动时校验,可采取以下手段:

  1. 调用 configuration.getMappedStatement(id).getBoundSql(null),对无需参数的简单 SQL 检查构建是否报错。
  2. 使用 DatabaseMetaData 或连接池检测 ,例如启动后执行一条 SELECT 1 验证数据源连通性。
  3. 扩展 LanguageDriver ,在 createSqlSource 方法内拼接验证逻辑,如用 java.sql.Connection.prepareStatement 预编译(但需数据库连接,可能侵入性较强)。
  4. 集成 Liquibase/Flyway 等数据库迁移工具,将表结构检查提前,间接保证 SQL 正确性。

面试可答:MyBatis 侧重于对象-结果映射,语法责任下放给 JDBC 驱动;工业上推荐结合 CI 的数据库环境运行集成测试,捕捉语法错误。


Q11:(系统设计题)如果一个核心系统的 Mapper XML 数量超过 500 个,启动时解析缓慢,如何在不改变 MyBatis 源码的前提下优化初始化性能?

答:

分析瓶颈:每个 XML 的解析都需要 DOM 构建、XPath 查找、动态 SQL 节点树生成,大量文件累积会造成 CPU 密集型延迟。

优化方案:

  1. 减少不必要扫描 :精确配置 mapper-locations,避免使用宽泛通配符加载无关 XML。
  2. 启用 MyBatis 的 XML 文件缓存 :解析后的 MappedStatement 序列化到磁盘(如使用 mybatis-mapper-cache 扩展),下次启动直接反序列化加载,跳过 DOM 解析。
  3. 合并小 XML:将多个 Mapper 的 SQL 合并到一个文件中,减少 I/O 次数和解析开销(代价是管理复杂度上升)。
  4. 切换为注解模式:对于简单 SQL,用注解替代 XML,省却 XML 解析步骤。
  5. 异步预加载 :应用分层启动,先启动核心功能,非核心 Mapper 延迟加载(需自定义 ConfigurationMapperRegistry,控制 addMappedStatement 时机)。
  6. 升级硬件和 JDK :高并发解析时利用多核 CPU 进行并行加载(自行管理线程安全),MyBatis 默认是单线程解析,可自定义 SqlSessionFactoryBuilder,分批次并发调用 XMLMapperBuilder,最后合并 Configuration(需注意对象引用的并发安全)。

最佳实践:实测 500 个 XML 文件在主流 JVM 下启动时间在 1~3 秒,一般可接受;如果仍然不足,首推 XML 文件预编译缓存方案(反序列化几乎是零解析成本)。


Q12:(系统设计题延续)如果现在要设计一个多租户系统,每个租户拥有独立的数据库但使用同一套业务代码,如何让 MyBatis 初始化出一个"基础 Configuration"并针对每个租户复制调整,实现租户数据源路由?

答:

采用 Configuration 模板复制 + 动态 Environment 的方案:

  1. 首先构建一个"模板 Configuration":解析基础 mybatis-config.xml 和所有 Mapper 接口,但不指定特定环境,而是留下一个占位的 Environment(如 ID 为 "template")。此时 Configuration.getMappedStatements() 已完整。
  2. 当租户首次接入时,执行深拷贝或重新调用 SqlSessionFactoryBuilder.build(InputStream) 传入租户特有数据源配置。为了避免重复解析 Mapper,可利用 MyBatis 的 Configuration 拷贝构造函数(3.5.x 中 Configuration 提供拷贝构造,但某些内部 Map 需要手动复制)。
  3. 通过工厂模式创建多个 DefaultSqlSessionFactory,每个绑定一个租户的数据源。
  4. 在运行时,通过 ThreadLocal 或请求上下文中的租户 ID 选择对应的 SqlSessionFactory 获取 SqlSession,或通过 AbstractRoutingDataSource 动态路由数据源,此时只需一个 SqlSessionFactory 但需要配置 MyBatis 的环境引用该路由数据源。
  5. 若采用一个工厂 + 路由器方式,需要注意一级缓存和二级缓存的租户隔离(可结合插件对 CacheKey 增加租户前缀)。

面试时强调:将租户隔离下推到数据源层,能保持业务代码零侵入,同时复用 MyBatis 的所有优化。需要处理好事务管理器的绑定,确保每个租户的连接不交叉。


文末速查表:MyBatis 初始化关键 Builder 一览

Builder 职责 输入 输出 使用场景
SqlSessionFactoryBuilder 整体构建入口 XML 流或 Configuration SqlSessionFactory 应用启动,一次性
XMLConfigBuilder 解析全局配置 mybatis-config.xml Configuration(全局属性填充) 顶层,解析 settings/environments 等
XMLMapperBuilder 解析单个映射文件 mapper.xml InputStream Configuration(注册 ResultMap 和 MappedStatement) 每个 mapper.xml 一次
XMLStatementBuilder 构建单个 MappedStatement <select> 等 XML 节点 MappedStatement 在 MapperBuilder 内循环使用
XMLIncludeTransformer 展开 <include> 标签 带 include 的 XML 节点 展开后的纯 SQL 节点 在 StatementBuilder 构建 SqlSource 之前
MapperAnnotationBuilder 解析 Mapper 接口注解 Class<?> (接口) Configuration(注册 MappedStatement) 接口 + 注解模式
MappedStatement.Builder 构建 MappedStatement 对象 各属性值 MappedStatement 在 StatementBuilder 和 AnnotationBuilder 内部

关键结论:MyBatis 初始化将静态配置转化为动态能力,是整个框架的基石。彻底掌握这条建造者流水线,不仅能快速定位配置错误,也为自定义扩展(如接入自研数据源、改造 SQL 方言解析)提供了清晰的切入点。


延伸阅读

  • MyBatis 官方文档:XML 配置、Mapper XML 映射
  • 源码分析推荐:《MyBatis 3 源码深度解析》相关章节
  • 本系列后续文章将继续深入执行阶段、插件开发与调优实践,敬请关注。
相关推荐
2301_771717212 小时前
Spring Boot 自动配置核心注解
java·spring boot·mybatis
MegaDataFlowers3 小时前
使用MyBatisX快速生成CRUD
mybatis
敖正炀4 小时前
插件开发与拦截链——分页、脱敏、多租户实战
mybatis
敖正炀4 小时前
MyBatis 架构全解:SqlSession、Executor 与 StatementHandler
mybatis
敖正炀4 小时前
一级/二级缓存深度:生命周期、脏读与生产最佳实践
mybatis
空中海7 小时前
MyBatis 基础认知、配置体系与核心映射
mybatis
空中海7 小时前
05 MyBatis 架构设计、渐进式综合项目与专家题库
mybatis
空中海10 小时前
03 MyBatis Spring Boot 集成、事务、测试与工程化体系
spring boot·后端·mybatis
Nicander2 天前
理解 mybatis 源码:vibe-coding一个mini-mybatis
后端·mybatis