说说我读这块源码最直观的感受XMLConfigBuilder就是 MyBatis 启动时那个最忙碌的组装工人——所有写在全局配置文件mybatis-config.xml里的节点最终都要经它之手被拆解、转换、灌进Configuration这个“中央登记处”。你写的每个settings、typeAliases、environments在框架眼里都只是一串字符串而XMLConfigBuilder负责把这些字符串变成真正能用的 Java 对象。这篇文章适合两类人一是已经用 MyBatis 写过一些项目、想搞明白“配置到底是怎么生效的”的开发者二是正准备啃源码、想找一条不那么陡峭的切入路径的 Java 工程师。读完你会发现MyBatis 的设计其实相当朴素XML 解析用 XPath标签映射靠反射节点处理拆成一个个方法。整体结构清晰得令人舒适。1. 先搞清 XMLConfigBuilder 在整个启动流程里的位置1.1 从 SqlSessionFactoryBuilder 到 XMLConfigBuilder 的调用链很多人第一次接触 MyBatis都写过这样一行代码SqlSessionFactory factory new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream(mybatis-config.xml));然后就得到了一个能用的SqlSessionFactory。但很少有人追问这一行背后到底发生了什么顺着源码走一遍调用链大致是这样new SqlSessionFactoryBuilder() - build(InputStream) - new XMLConfigBuilder(inputStream, environment, properties) - configBuilder.parse() - Configuration configuration - build(Configuration config) - 返回 DefaultSqlSessionFactory你会发现真正干活的核心是XMLConfigBuilder.parse()它返回一个组装好的Configuration对象而SqlSessionFactoryBuilder只是把它包了一层变成DefaultSqlSessionFactory。也就是说整个全局配置文件解析的“最终产物”就是那个Configuration对象。我在阅读时最强烈的感受是Configuration就是 MyBatis 的“内存数据库”——里面存了所有 Mapper、别名、类型处理器、拦截器、环境信息、全局开关。后续所有 SQL 执行、二级缓存、插件代理都要从它身上取配置。整个 MyBatis 框架的初始化本质上就是把 XML 配置映射到这个巨大的状态对象上。1.2 类成员拆解XPathParser、BaseBuilder 与 Configuration先看XMLConfigBuilder的继承关系和关键字段。public class XMLConfigBuilder extends BaseBuilder { private boolean parsed; private XPathParser parser; private String environment; private final ReflectionFactory localReflectorFactory new DefaultReflectorFactory(); // 实际上新版源码还有 safeMode、safeModeBypass 等字段不同版本略有差异 }而它的父类BaseBuilder是 MyBatis 里一类很典型的“基础设施”基类定义了几个所有 Builder 共享的成员成员类型作用configurationConfiguration最终要填充的目标对象typeAliasRegistryTypeAliasRegistry类型别名注册表负责把jdbc转成JdbcTransactionFactorytypeHandlerRegistryTypeHandlerRegistry类型处理器注册表负责 Java 类型与 JDBC 类型的转换resolveClass()方法通过别名或全限定类名加载Class对象这里有个非常优雅的设计所有 Builder 共享同一套别名解析机制。当我们在settings里写logImpl: SLF4J时resolveClass(SLF4J)会先去别名表里查查到Log4jImpl、Slf4jImpl对应的类查不到就直接按全限定类名加载。这种“先查别名、再按全名”的策略写配置的人不用记全限定类名框架内部也不会因为字符串写错而崩溃得莫名其妙。XMLConfigBuilder自己持有的XPathParser才是真正的解析器。它内部封装了 Java 标准库里的javax.xml.xpath外面再包一层 EntityResolver 和变量替换逻辑。这一点很关键MyBatis 并没有从零写一个 XML 解析器而是基于 JAXP 的 XPath API 做了薄封装——毕竟 XML 解析本身不是业务重点能少造轮子就少造。构造函数里还有几个容易被忽略的细节private XMLConfigBuilder(XPathParser parser, String environment, Properties props) { super(new Configuration()); this.configuration.setVariables(props); this.parsed false; this.environment environment; this.parser parser; }注意parsed标志位。parse()方法开头就会检查它如果已经被解析过直接抛异常。这是一种轻量级的“一次性对象”保护机制——XMLConfigBuilder不是线程安全的也不打算被复用parsed就是为了防止你拿同一个实例调用两次parse()导致Configuration被重复注册。2. 配置文件的节点到底长什么样要理解解析逻辑先得知道我们写的 XML 里有哪些节点。一个典型的mybatis-config.xml长这样?xml version1.0 encodingUTF-8 ? !DOCTYPE configuration PUBLIC -//mybatis.org//DTD Config 3.0//EN http://mybatis.org/dtd/mybatis-3-config.dtd configuration properties resourcedb.properties/ settings setting namecacheEnabled valuetrue/ setting namelazyLoadingEnabled valuefalse/ setting namemapUnderscoreToCamelCase valuetrue/ /settings typeAliases package namecom.example.domain/ /typeAliases typeHandlers typeHandler handlercom.example.EnumTypeHandler/ /typeHandlers objectFactory typecom.example.MyObjectFactory/ plugins plugin interceptorcom.example.MyInterceptor/ /plugins environments defaultdev environment iddev transactionManager typeJDBC/ dataSource typePOOLED property namedriver value${driver}/ property nameurl value${url}/ /dataSource /environment /environments databaseIdProvider typeDB_VENDOR property nameMySQL valuemysql/ /databaseIdProvider mappers mapper resourcemapper/UserMapper.xml/ /mappers /configurationparseConfiguration()方法里就是按这个节点的顺序依次处理。整体对应关系可以列成一张表XML 节点处理方法最终写入 Configuration 的字段propertiespropertiesElement()variables、XPathParser.variablessettingssettingsElement()反射调用 Configuration 各 settertypeAliasestypeAliasesElement()TypeAliasRegistrypluginspluginElement()InterceptorChainobjectFactoryobjectFactoryElement()objectFactoryobjectWrapperFactoryobjectWrapperFactoryElement()objectWrapperFactoryreflectorFactoryreflectorFactoryElement()reflectorFactoryenvironmentsenvironmentsElement()environmentdatabaseIdProviderdatabaseIdProviderElement()databaseIdtypeHandlerstypeHandlersElement()TypeHandlerRegistrymappersmapperElement()MapperRegistry 及 BoundSql 映射为什么是这个顺序因为存在依赖关系。properties必须最先加载——后面的dataSource里可能用${driver}占位符解析器解析到属性值时需要已经拿到变量表。typeAliases也要赶在settings、typeHandlers、mappers之前——因为settings里如果用别名指定logImplresolveClass()需要能先查到别名typeHandlers里如果用别名指定处理器类同样依赖别名注册完毕。environments必须在databaseIdProvider之前——后者要拿前者的 DataSource 去探测数据库产品名。这其实暴露了XMLConfigBuilder设计上的一个重要思路解析顺序不是随便排的它是配置文件内部依赖关系的拓扑排序。3. 主解析流程parse 方法如何一步步展开3.1 parse 与 parseConfiguration 的分工parse()方法是入口但真正的重活全在parseConfiguration()里。源码如下3.5.x 版本略有删减public Configuration parse() { if (this.parsed) { throw new BuilderException(Each XMLConfigBuilder can only be used once.); } this.parsed true; parseConfiguration(this.parser.evalNode(/configuration)); return this.configuration; } private void parseConfiguration(XNode root) { try { // 顺序敏感properties 第一然后才是其它 propertiesElement(root.evalNode(properties)); typeAliasesElement(root.evalNode(typeAliases)); pluginElement(root.evalNode(plugins)); objectFactoryElement(root.evalNode(objectFactory)); objectWrapperFactoryElement(root.evalNode(objectWrapperFactory)); reflectorFactoryElement(root.evalNode(reflectorFactory)); settingsElement(root.evalNode(settings)); environmentsElement(root.evalNode(environments)); databaseIdProviderElement(root.evalNode(databaseIdProvider)); typeHandlersElement(root.evalNode(typeHandlers)); mapperElement(root.evalNode(mappers)); } catch (Exception e) { throw new BuilderException(Error parsing SQL Mapper Configuration. Cause: e, e); } }这里有个值得注意的点evalNode(/configuration)返回的是一个XNode——这是 MyBatis 对标准 DOM 节点的一层包装。之后每个子节点解析方法都接收一个XNode参数如果节点不存在就传null方法内部先判空。我最初读这段时有个疑惑为什么每个方法都要判空而不是在调用前判断后来发现这是 MyBatis 一贯的风格——方法的健壮性不依赖调用方的输入假设。哪怕configuration下面什么都没写各个xxxElement方法也要能安全返回只是什么都不注册而已。3.2 XNode 与 XPathParser 是怎么配合的要理解evalNode先看XPathParser里最核心的两个方法public XNode evalNode(String expression) { return evalNode(root, expression); } public XNode evalNode(Object node, String expression) { try { Object result xpath.evaluate(expression, node, XPathConstants.NODE); return new XNode(this, (Node) result, variables); } catch (XPathExpressionException e) { throw new BuilderException(Error evaluating XPath ..., e); } }方法实现其实非常朴素调用 JDK 自带的XPath.evaluate()把XPathConstants.NODE当作期望返回类型然后把结果包成XNode。XNode里缓存了哪些信息看构造函数就能明白个大概class XNode { private final XPathParser xpathParser; private final Node node; private final String name; private final String body; private final Properties attributes; private final Properties variables; private final ListXNode children; ... }注意两个地方attributes被解析成了Properties所以拿到一个 XNode 后可以直接getStringAttribute(resource)不用再写 DOM 遍历代码。body是节点文本内容。比如setting namex valuey/这种自闭合节点body 为空但property namedriver value${driver}/的 body 也是空真正取属性都是用getStringAttribute。XNode内部还有一个很实用的方法叫getChildrenAsProperties()把某个节点下所有property name... value.../子节点读成一个Properties。这个方法在propertiesElement、environmentsElement里会被反复用到。3.3 各节点解析的调用顺序与依赖关系总结一下为什么要按这个顺序调用这一块其实很值得展开因为它能帮你避免很多死记硬背properties 先读propertiesElement除了把properties里的键值对存入configuration.variables还会调用parser.setVariables(defaults)把所有变量注入XPathParser。这样后续在解析dataSource里的${driver}时XNode.getStringAttribute内部会先做变量替换。typeAliases 第二后面settings里的logImpl、typeHandlers里的handler、mappers里的class都可能用别名而不是全限定类名。别名注册得晚后面所有resolveClass()都会找不到类。plugins、objectFactory、objectWrapperFactory、reflectorFactory这四个节点互不依赖位置比较自由。它们都是给Configuration注入“工厂/拦截器”类只有一个共性都是通过反射实例化。settings 之后是 environments为什么 settings 非得在 environments 前面因为environments中的transactionManager typeJDBC/实际走的是createTransactionFactory而事务工厂类名JDBC是别名——但settings中defaultExecutorType之类的配置要在解析环境之前就绪。准确说从源码看并不存在强依赖但从语义上讲全局开关应该先被设置好环境随后再装配。databaseIdProvider 依赖 environments 的 DataSource 就绪它需要调用configuration.getEnvironment().getDataSource()去拿连接、读DatabaseMetaData.getDatabaseProductName()所以必须排在后面。mappers 最后Mapper 的解析需要完整的Configuration包括类型处理器注册表、别名注册表、全局设置所以放在最后。这张顺序表不止是“面试背诵材料”真要排查配置问题时也很有用。比如你发现一个自定义拦截器没生效先检查是不是plugins节点解析时类加载失败了发现某个别名在 Mapper XML 里报错先检查typeAliases是不是写在了mappers后面——虽然这种错误几乎不可能通过 DTD 校验但实际踩过坑的人都知道IDE 不报运行时才炸。4. 重点节点的解析细节4.1 properties占位符替换的第一步propertiesElement的源码很短信息量却很大private void propertiesElement(XNode context) throws Exception { if (context ! null) { Properties defaults context.getChildrenAsProperties(); String resource context.getStringAttribute(resource); String url context.getStringAttribute(url); if (resource ! null url ! null) { throw new BuilderException(...); } if (resource ! null) { defaults.putAll(Resources.getResourceAsProperties(resource)); } else if (url ! null) { defaults.putAll(Resources.getUrlAsProperties(url)); } Properties vars configuration.getVariables(); if (vars ! null) { defaults.putAll(vars); } parser.setVariables(defaults); configuration.setVariables(defaults); } }这里有几个非常隐蔽的点。第一properties标签内部可以直接写property name... value.../也可以通过resource或url引入外部文件。MyBatis 允许“内部定义 外部文件”同时出现但不允许 resource 和 url 同时存在。第二加载顺序很关键。代码逻辑是“内部定义先作为 defaults外部文件的键值再 putAll 覆盖进去最后把构造器传入的props再 putAll 覆盖”。这意味着外部文件的优先级高于内部property构造器传入的 Properties 优先级最高。这个顺序设计是有讲究的SqlSessionFactoryBuilder.build()允许你额外传参这些参数应该能覆盖配置文件里的同名键以便做环境隔离。第三parser.setVariables(defaults)这一步经常被忽略。它把变量表注入了解析器之后凡是XNode.getStringAttribute()解析到${xxx}占位符都会自动替换。关于替换逻辑看XNode.getStringAttribute的底层实现就明白了public String getStringAttribute(String name) { String value attributes.getProperty(name); if (value null) return null; return valueParser.parse(value); // 内部做 ${} 替换如果找不到变量会抛异常 }注意MyBatis 从 3.5.x 开始对占位符处理做了多次调整。早期版本如果占位符找不到变量会原样保留${xxx}不报错后期版本直接抛BuilderException。遇到版本升级后老配置文件突然报“找不到属性”多半就是这个原因。4.2 settings一整波反射注入settings节点可能是全局配置里最常写的节点它的解析逻辑也相当有意思——本质上就是遍历 properties反射调用 Configuration 对应字段的 setter。简化后的逻辑大致如下private void settingsElement(XNode context) throws Exception { if (context ! null) { Properties props context.getChildrenAsProperties(); Configuration config configuration; Class? configClass config.getClass(); for (Map.EntryObject, Object entry : props.entrySet()) { String name (String) entry.getKey(); String value (String) entry.getValue(); if (configClass.getMethod(set capitalize(name)) null) { throw new BuilderException(The setting name is not known. ...); } // 反射调用 setter转换具体类型 configClass.getMethod(set capitalize(name), String.class).invoke(config, value); } } }这段逻辑很容易让人产生几个疑问为什么用 String 类型就能调用所有 setterBoolean 类型怎么被转换的实际上MyBatis 在反射调用前会判断 setter 参数类型如果是boolean就用Boolean.valueOf()转换如果是Integer就Integer.valueOf()如果是Long就Long.valueOf()。以mapUnderscoreToCamelCase为例它会找到Configuration.setMapUnderscoreToCamelCase(boolean)传入Boolean.parseBoolean(true)。到这里你已经能明白为什么settings里只能写 Configuration 上存在 setter 的属性。随便写一个新的setting namemyCustom valueabc/反射找不到对应 setter启动直接抛BuilderException: The setting xxx is not known。常见的 settings 项我整理了一批方便对照检查配置项setter 方法默认值说明cacheEnabledsetCacheEnabled(boolean)true二级缓存总开关lazyLoadingEnabledsetLazyLoadingEnabled(boolean)false延迟加载总开关aggressiveLazyLoadingsetAggressiveLazyLoading(boolean)false激进延迟加载multipleResultSetsEnabledsetMultipleResultSetsEnabled(boolean)true允许结果集拆分useColumnLabelsetUseColumnLabel(boolean)true用列标签而非列名useGeneratedKeyssetUseGeneratedKeys(boolean)false允许 JDBC 生成主键autoMappingBehaviorsetAutoMappingBehavior(AutoMappingBehavior)PARTIAL自动映射级别defaultExecutorTypesetDefaultExecutorType(ExecutorType)SIMPLE默认执行器类型mapUnderscoreToCamelCasesetMapUnderscoreToCamelCase(boolean)false下划线转驼峰logImplsetLogImpl(Class)未设置日志实现logPrefixsetLogPrefix(String)未设置日志前缀这里给一个排查经验配置logImpl时如果你的项目里同时存在 log4j 和 logback 依赖MyBatis 会按类路径扫描顺序选择不一定是你想要的。显式写logImpl别名能避免这种不确定性。4.3 typeAliases 与 typeHandlers注册表是如何填充的typeAliasesElement处理两种子节点package和typeAlias。源码思路如下private void typeAliasesElement(XNode parent) { if (parent ! null) { for (XNode child : parent.getChildren()) { if (package.equals(child.getName())) { String typeAliasPackage child.getStringAttribute(name); configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage); } else { String alias child.getStringAttribute(alias); String type child.getStringAttribute(type); configuration.getTypeAliasRegistry().registerAlias(alias, type); } } } }如果你用package方式会走进TypeAliasRegistry.registerAliases(String packageName)它内部用ResolverUtil扫描包下所有类过滤掉匿名类、内部类、接口然后以简单类名首字母小写作为默认别名注册进一个MapString, Class?。这里有几个细节值得注意带了Alias(xxx)注解的类会优先使用注解值。package扫描到的类如果类名首字母是大写默认别名是首字母小写比如mybatis_test/inner/User.java注册为user。如果同一个别名被注册两次会直接抛TypeException: The alias xxx is already mapped to the value ...。多人协作开发时两个实体类都叫User在不同包下就很容易踩这个坑。typeHandlersElement的逻辑几乎一样。它把typeHandler handlerxxx/或package namexxx/注册进TypeHandlerRegistry。注册完成之后MyBatis 在写 Mapper 时才能把#{param}的 Java 类型处理成 JDBC 类型。实操心得当你自定义了TypeHandler建议直接注册到全局配置里并在 Mapper XML 中显式指定resultMap columnxxx propertyxxx typeHandlercom.example.MyTypeHandler/比依赖全局扫描更可靠。全局注册会被多个列共用容易产生副作用。4.4 environments 与 databaseIdProvider环境切换与方言识别environmentsElement的解析逻辑比较绕但核心就这几步如果构造器传入的environment为 null就用根节点上的default属性值。遍历所有environment子节点找到与目标环境 id 一致的那一个。根据type属性创建TransactionFactoryJDBC对应JdbcTransactionFactoryMANAGED对应ManagedTransactionFactory。根据dataSource的type属性创建DataSourceFactoryUNPOOLED、POOLED、JNDI三个内建实现对应不同工厂。把 DataSource 交给工厂的getDataSource()方法拿到真正的连接池对象。通过Configuration 内部的 Environment.Builder构建Environment最后configuration.setEnvironment(environment)。这里有个很典型的 MyBatis 模式type 属性写的是一个字符串框架内部先在TypeAliasRegistry里找到对应类再通过反射实例化。POOLED这个字符串能变成PooledDataSourceFactory靠的正是内建别名表。databaseIdProviderElement的作用是识别数据库厂商用于多数据库兼容。typeDB_VENDOR会被替换成VendorDatabaseIdProvider它的核心逻辑是拿DataSource连接后读取DatabaseMetaData.getDatabaseProductName()再与配置文件中定义的property nameMySQL valuemysql/匹配String productName getDatabaseProductName(dataSource); if (productName null) return null; // 查找精确匹配找不到则尝试 other最终Configuration.databaseId会被设置成匹配的 value。之后 Mapper XML 里就可以写select databaseIdmysql实现不同数据库使用不同 SQL 的切换。常见误区databaseIdProvider与 Mapper 里的databaseId配合使用很少有人注意到它们的优先级规则。如果有两个select标签同 id一个带databaseId一个不带那么带匹配databaseId的优先都不匹配才用不带的那条。4.5 mappers把 SQL 映射文件交给配置中心的最后一棒mapperElement是parseConfiguration里最后处理的节点也是牵扯最多东西的节点。它支持四种配置方式方式写法处理逻辑包扫描package namecom.example.mapper/调用configuration.addMappers(packageName)扫描所有 Mapper 接口并绑定资源路径mapper resourcemapper/UserMapper.xml/读取 XML 映射文件交给XMLMapperBuilderURLmapper urlfile:///.../同上但来源是 URL接口类mapper classcom.example.UserMapper/注册接口对应 parser 并解析注解 SQL实现细节如下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) { throw new BuilderException(...); } if (resource ! null) { // 以 resource 为例 try (InputStream inputStream Resources.getResourceAsStream(resource)) { XMLMapperBuilder mapperParser new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse(); } } ... } } } }很多人以为mappers只是“注册接口”但实际上它同时把 XML 映射文件里的sql、resultMap、Statement 配置全部解析并注册到Configuration里。这块逻辑其实不属于XMLConfigBuilder的职责范围了——它创建了一个新的XMLMapperBuilder去处理具体 Mapper 文件。至此parseConfiguration的所有环节连同 XML 全局配置里的所有信息都转移到了Configuration对象中。XMLConfigBuilder的生命周期也就此结束。5. 实际使用中容易踩的坑5.1 DTD 解析失败或本地离线环境无法联网全局配置文件开头通常写着!DOCTYPE configuration PUBLIC -//mybatis.org//DTD Config 3.0//EN http://mybatis.org/dtd/mybatis-3-config.dtd。如果你在无外网的环境下启动应用早期版本的 MyBatis 默认会去联网下载 DTD导致启动卡住或失败。XMLConfigBuilder构造时其实接收了一个EntityResolver——XMLMapperEntityResolver它会把http://mybatis.org/dtd/...映射到本地 classpath 下的 DTD 文件避免联网。但如果你的构建产物里把org/apache/ibatis/builder/xml/下的.dtd文件过滤掉了就会出问题。排查思路很简单看构建配置里有没有对.dtd、.xsd后缀做 exclude。我遇到过一次原因就是打包时只保留了.xml文件MDTD 文件丢了。5.2 Configuration 默认值到底是什么时候生效的很多人会困惑settings 里没写mapUnderscoreToCamelCase为什么有时候查询结果里下划线字段能映射到驼峰属性答案在于Configuration的成员变量声明时就给了默认值protected boolean mapUnderscoreToCamelCase false; protected boolean cacheEnabled true; protected boolean lazyLoadingEnabled false;XMLConfigBuilder只是在默认值基础上做“增量修改”。如果你继承Configuration并覆盖 getter再通过settings配置就需要确保反射 setter 能正确调用。这里有个实际踩过的小坑在Configuration子类中重写newExecutor()方法后settings 里配置defaultExecutorTypeREUSE可能不生效因为自定义子类可能直接返回了SimpleExecutor。排查时不是先怀疑XMLConfigBuilder而是先检查子类逻辑。5.3 属性覆盖顺序和我踩过的一个真实问题一次在给别人做代码评审时发现对方的 job 里通过SqlSessionFactoryBuilder.build(reader, environment, props)传入了一个driverClassName但配置文件里的dataSource用的却是property namedriver value${driverClassName}/。当时觉得没问题运行却一直报“无法加载 Driver 类”。查了很久才发现构造器传入的props确实会覆盖但因为db.properties外部文件里也有driverClassName而外部文件的优先级实际高于properties子节点最终生效的还是外部文件里的旧值。这个例子的教训是Properties 的覆盖方向是“内部property 外部文件 构造器入参”。如果你希望构造器入参绝对生效就不要在外部配置文件里定义同名键否则覆盖逻辑永远不会生效。理解了propertiesElement的 putAll 顺序这个问题一眼就能看穿。5.4 想要动态修改配置怎么办Configuration对象创建完成之后XMLConfigBuilder就“功成身退”了后续运行时想改配置有人会尝试直接改Configuration的字段。比如线上想临时打开mapUnderscoreToCamelCase直接调configuration.setMapUnderscoreToCamelCase(true)当然可行但要注意已经解析完的 MappedStatement、ResultMap 不会重新解析字段映射规则是否变化取决于执行阶段是否读取这个开关。有些开关是执行时实时读取的有些则已经固化为解析结果。我个人的建议是在项目启动阶段把能确定的配置全部写进 XML 或通过建厂参数传入不要在运行期用反射去改Configuration。真要动态切换环境优先用多个SqlSessionFactory每个对应一套独立配置。有一个细节值得记住XMLConfigBuilder虽然叫“Builder”但它的构建过程不是链式的而是“命令式”的一步步解析。它把XML -- Configuration这个看似庞大的过程拆成了十一个职责单一的方法每个方法干一件事方法之间靠共享的Configuration对象联系。读懂这份拆解思路比背下所有源码都有价值。之后如果再碰到解析问题至少你能判断出毛病出在 factory 装配、别名解析还是属性占位符替换上——这个定位能力比跑通多少篇教程都实在。
阅读完成 · 觉得有帮助?