一、XML解析为BeanDefinition的时机
1.1 在Spring生命周期中的位置
XML解析为BeanDefinition发生在Spring容器启动阶段,具体时机如下:
Spring容器启动流程: 1. 创建BeanFactory 2. 【XML解析阶段】加载配置文件,解析XML,注册BeanDefinition ← 发生在这里 3. BeanFactoryPostProcessor处理 4. 实例化Bean 5. 依赖注入 6. 初始化Bean 7. 容器就绪1.2 详细执行流程
以ClassPathXmlApplicationContext为例:
// 1. 构造器调用publicClassPathXmlApplicationContext(StringconfigLocation){this(newString[]{configLocation},true,null);}// 2. refresh()方法 - Spring容器启动的核心方法publicvoidrefresh()throwsBeansException{synchronized(this.startupShutdownMonitor){// 准备刷新上下文prepareRefresh();// 【关键步骤】获取BeanFactory,在此过程中完成XML解析ConfigurableListableBeanFactorybeanFactory=obtainFreshBeanFactory();// 后续步骤...prepareBeanFactory(beanFactory);postProcessBeanFactory(beanFactory);invokeBeanFactoryPostProcessors(beanFactory);registerBeanPostProcessors(beanFactory);// ...}}// 3. obtainFreshBeanFactory() - 触发XML解析protectedConfigurableListableBeanFactoryobtainFreshBeanFactory(){refreshBeanFactory();// 在这里进行XML解析returngetBeanFactory();}// 4. refreshBeanFactory() - AbstractRefreshableApplicationContextprotectedfinalvoidrefreshBeanFactory(){// 创建BeanFactoryDefaultListableBeanFactorybeanFactory=createBeanFactory();// 【核心】加载BeanDefinitionloadBeanDefinitions(beanFactory);}// 5. loadBeanDefinitions() - AbstractXmlApplicationContextprotectedvoidloadBeanDefinitions(DefaultListableBeanFactorybeanFactory){// 创建XML的BeanDefinition读取器XmlBeanDefinitionReaderbeanDefinitionReader=newXmlBeanDefinitionReader(beanFactory);// 使用Reader读取XML并注册BeanDefinitionloadBeanDefinitions(beanDefinitionReader);}1.3 时机总结
| 阶段 | 说明 |
|---|---|
| 发生时机 | 容器refresh()方法的obtainFreshBeanFactory()阶段 |
| 在Bean实例化之前 | 此时只是注册元数据,Bean还未创建 |
| 在BeanFactoryPostProcessor之前 | BeanFactoryPostProcessor可以修改BeanDefinition |
| 可修改性 | 此阶段注册的BeanDefinition可被后续处理器修改 |
二、XML解析方式详解
Spring中主要有两种XML解析方式:DOM解析和SAX解析。Spring默认使用DOM解析。
2.1 DOM(Document Object Model)解析
2.1.1 基本原理
工作流程: XML文件 → 完整读入内存 → 构建DOM树 → 遍历节点 → 提取信息2.1.2 Spring中的使用
Spring通过DocumentLoader加载XML为DOM Document:
// DefaultDocumentLoader.javapublicDocumentloadDocument(InputSourceinputSource,EntityResolverentityResolver,ErrorHandlererrorHandler,intvalidationMode,booleannamespaceAware){// 创建DocumentBuilderFactoryDocumentBuilderFactoryfactory=createDocumentBuilderFactory(validationMode,namespaceAware);// 创建DocumentBuilderDocumentBuilderbuilder=createDocumentBuilder(factory,entityResolver,errorHandler);// 解析为Document对象(DOM树)returnbuilder.parse(inputSource);}2.1.3 解析过程
// 1. 加载DocumentDocumentdoc=documentLoader.loadDocument(...);// 2. 获取根元素Elementroot=doc.getDocumentElement();// 3. 遍历解析(以bean标签为例)NodeListnodeList=root.getElementsByTagName("bean");for(inti=0;i<nodeList.getLength();i++){ElementbeanElement=(Element)nodeList.item(i);// 4. 提取属性Stringid=beanElement.getAttribute("id");StringclassName=beanElement.getAttribute("class");Stringscope=beanElement.getAttribute("scope");// 5. 创建BeanDefinitionBeanDefinitionbd=newGenericBeanDefinition();bd.setBeanClassName(className);bd.setScope(scope);// 6. 解析子元素(property、constructor-arg等)parsePropertyElements(beanElement,bd);// 7. 注册到BeanFactoryregistry.registerBeanDefinition(id,bd);}2.1.4 优势
| 优势 | 说明 |
|---|---|
| 随机访问 | 可以随意访问DOM树的任意节点 |
| 双向遍历 | 可以从父节点到子节点,也可以从子节点到父节点 |
| 易于操作 | API简单直观,便于增删改查 |
| 支持修改 | 可以修改DOM树的结构和内容 |
| 支持XPath | 可以使用XPath快速定位节点 |
2.1.5 劣势
| 劣势 | 说明 |
|---|---|
| 内存占用大 | 需要将整个XML加载到内存构建DOM树 |
| 解析速度慢 | 大文件解析时需要较长时间 |
| 不适合大文件 | 大型XML文件可能导致内存溢出 |
| 解析前必须完整 | 必须等待整个文档加载完成 |
2.1.6 适用场景
- ✅ Spring配置文件(通常较小,需要随机访问)
- ✅ 需要频繁访问不同节点
- ✅ 需要修改XML内容
- ❌ 超大型XML文件(几百MB以上)
2.2 SAX(Simple API for XML)解析
2.2.1 基本原理
工作流程: XML文件 → 边读边解析 → 触发事件 → 事件处理器处理 → 不保留完整树结构SAX采用事件驱动模型:
// SAX事件处理器publicclassMyHandlerextendsDefaultHandler{// 1. 文档开始事件publicvoidstartDocument(){System.out.println("开始解析文档");}// 2. 元素开始事件publicvoidstartElement(Stringuri,StringlocalName,StringqName,Attributesattributes){if("bean".equals(qName)){Stringid=attributes.getValue("id");StringclassName=attributes.getValue("class");// 处理bean定义...}}// 3. 元素内容事件publicvoidcharacters(char[]ch,intstart,intlength){Stringcontent=newString(ch,start,length);// 处理文本内容...}// 4. 元素结束事件publicvoidendElement(Stringuri,StringlocalName,StringqName){if("bean".equals(qName)){// bean标签结束,注册BeanDefinition}}// 5. 文档结束事件publicvoidendDocument(){System.out.println("文档解析完成");}}2.2.2 解析过程
// 1. 创建SAX解析器工厂SAXParserFactoryfactory=SAXParserFactory.newInstance();// 2. 创建解析器SAXParserparser=factory.newSAXParser();// 3. 创建事件处理器MyHandlerhandler=newMyHandler();// 4. 开始解析(触发事件)parser.parse(newFile("beans.xml"),handler);2.2.3 事件触发顺序示例
对于以下XML:
<?xml version="1.0" encoding="UTF-8"?><beans><beanid="userService"class="com.example.UserService"><propertyname="name"value="test"/></bean></beans>SAX解析触发的事件序列:
1. startDocument() 2. startElement("beans") 3. startElement("bean", {id="userService", class="com.example.UserService"}) 4. startElement("property", {name="name", value="test"}) 5. endElement("property") 6. endElement("bean") 7. endElement("beans") 8. endDocument()2.2.4 优势
| 优势 | 说明 |
|---|---|
| 内存占用小 | 不需要加载整个文档到内存 |
| 解析速度快 | 边读边解析,无需等待完整加载 |
| 适合大文件 | 可以处理超大型XML文件 |
| 流式处理 | 支持流式数据处理 |
2.2.5 劣势
| 劣势 | 说明 |
|---|---|
| 单向解析 | 只能从前往后解析,不能回退 |
| 无法随机访问 | 不能直接访问特定节点 |
| 编程复杂 | 需要手动维护状态,代码较复杂 |
| 不能修改 | 无法修改XML内容 |
| 需要手动管理状态 | 需要在事件处理器中维护上下文信息 |
2.2.6 适用场景
- ✅ 超大型XML文件解析
- ✅ 流式数据处理
- ✅ 内存受限环境
- ❌ 需要随机访问节点
- ❌ 需要修改XML
2.3 其他解析方式
2.3.1 StAX(Streaming API for XML)
StAX是拉式解析(Pull Parsing),介于DOM和SAX之间:
XMLInputFactoryfactory=XMLInputFactory.newInstance();XMLStreamReaderreader=factory.createXMLStreamReader(newFileInputStream("beans.xml"));while(reader.hasNext()){intevent=reader.next();// 主动拉取事件if(event==XMLStreamConstants.START_ELEMENT){StringtagName=reader.getLocalName();if("bean".equals(tagName)){Stringid=reader.getAttributeValue(null,"id");StringclassName=reader.getAttributeValue(null,"class");// 处理...}}}特点对比:
| 特性 | SAX | StAX |
|---|---|---|
| 模型 | 推式(事件驱动) | 拉式(迭代器模式) |
| 控制 | 解析器控制流程 | 应用程序控制流程 |
| 易用性 | 需要回调处理 | 更直观,类似迭代器 |
| 性能 | 略快 | 略慢 |
2.3.2 JDOM 和 DOM4J
这些是第三方XML解析库,不是Spring默认使用的方式:
// DOM4J示例SAXReaderreader=newSAXReader();Documentdocument=reader.read(newFile("beans.xml"));Elementroot=document.getRootElement();for(Elementbean:root.elements("bean")){Stringid=bean.attributeValue("id");StringclassName=bean.attributeValue("class");// 处理...}三、Spring为什么选择DOM解析
3.1 Spring的选择理由
| 理由 | 说明 |
|---|---|
| 配置文件通常较小 | Spring配置文件一般不超过几MB,DOM的内存开销可接受 |
| 需要随机访问 | 解析bean依赖关系需要随机访问不同节点 |
| 需要支持命名空间 | Spring支持自定义命名空间,DOM更容易处理 |
| 易于扩展 | DOM API更容易实现自定义标签解析 |
| 向后兼容 | 历史原因,保持API稳定性 |
3.2 Spring的优化措施
虽然使用DOM,但Spring做了很多优化:
// 1. 延迟加载BeanDefinition// 只在第一次获取Bean时才解析// 2. 缓存解析结果// BeanDefinition注册后会缓存,避免重复解析// 3. 支持@Lazy注解// 延迟Bean的实例化// 4. 支持条件注册// @Conditional可以跳过不必要的Bean注册四、DOM vs SAX 深度对比
4.1 技术对比表
| 对比维度 | DOM | SAX |
|---|---|---|
| 解析方式 | 树形结构,全部加载 | 事件驱动,边读边解析 |
| 内存占用 | 高(与文件大小成正比) | 低(固定大小) |
| 解析速度 | 慢(需要构建完整树) | 快(流式处理) |
| 访问方式 | 随机访问 | 顺序访问 |
| 遍历方向 | 双向 | 单向(前向) |
| 修改能力 | 支持 | 不支持 |
| API复杂度 | 简单直观 | 复杂(需要状态管理) |
| 适用场景 | 中小型文件、需要修改 | 大型文件、流式处理 |
| XPath支持 | 支持 | 不支持 |
| 线程安全 | 不安全(需要同步) | 每个解析器独立使用 |
4.2 性能对比
以10MB的XML文件为例:
DOM解析: - 内存占用:约40-50MB(4-5倍文件大小) - 解析时间:约2-3秒 - 优势:解析后访问快速 SAX解析: - 内存占用:约5-10MB(固定开销) - 解析时间:约0.5-1秒 - 优势:内存友好,适合大文件4.3 代码复杂度对比
DOM解析代码(简洁):
Documentdoc=builder.parse(xmlFile);NodeListbeans=doc.getElementsByTagName("bean");for(inti=0;i<beans.getLength();i++){Elementbean=(Element)beans.item(i);Stringid=bean.getAttribute("id");// 直接访问,逻辑清晰}SAX解析代码(复杂):
publicclassBeanHandlerextendsDefaultHandler{privateStack<String>elementStack=newStack<>();privateBeanDefinitioncurrentBean;publicvoidstartElement(Stringuri,StringlocalName,StringqName,Attributesattributes){elementStack.push(qName);if("bean".equals(qName)){currentBean=newBeanDefinition();currentBean.setId(attributes.getValue("id"));}elseif("property".equals(qName)&&isInBean()){// 需要手动维护状态}}privatebooleanisInBean(){returnelementStack.contains("bean");}// 需要更多状态管理代码...}五、Spring XML解析核心类
5.1 核心类图
XmlBeanDefinitionReader(XML读取器) ├── DocumentLoader(文档加载器) │ └── DefaultDocumentLoader(默认实现,使用DOM) │ ├── BeanDefinitionDocumentReader(文档解析器) │ └── DefaultBeanDefinitionDocumentReader │ ├── parseBeanDefinitions() - 解析beans标签 │ └── processBeanDefinition() - 解析bean标签 │ └── BeanDefinitionParserDelegate(解析委托) ├── parseBeanDefinitionElement() - 解析bean元素 ├── parsePropertyElements() - 解析property元素 └── parseConstructorArgElements() - 解析constructor-arg元素5.2 关键方法调用链
// 1. XmlBeanDefinitionReader.loadBeanDefinitions()publicintloadBeanDefinitions(Resourceresource){returnloadBeanDefinitions(newEncodedResource(resource));}// 2. doLoadBeanDefinitions()protectedintdoLoadBeanDefinitions(InputSourceinputSource,Resourceresource){// 加载Document(使用DOM)Documentdoc=doLoadDocument(inputSource,resource);// 注册BeanDefinitionreturnregisterBeanDefinitions(doc,resource);}// 3. registerBeanDefinitions()publicintregisterBeanDefinitions(Documentdoc,Resourceresource){BeanDefinitionDocumentReaderdocumentReader=createBeanDefinitionDocumentReader();// 委托给DocumentReader处理documentReader.registerBeanDefinitions(doc,createReaderContext(resource));}// 4. DefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions()protectedvoiddoRegisterBeanDefinitions(Elementroot){BeanDefinitionParserDelegateparent=this.delegate;this.delegate=createDelegate(getReaderContext(),root,parent);// 解析beans标签parseBeanDefinitions(root,this.delegate);}// 5. parseBeanDefinitions() - 遍历子元素protectedvoidparseBeanDefinitions(Elementroot,BeanDefinitionParserDelegatedelegate){NodeListnl=root.getChildNodes();for(inti=0;i<nl.getLength();i++){Nodenode=nl.item(i);if(nodeinstanceofElement){Elementele=(Element)node;if(delegate.isDefaultNamespace(ele)){// 默认命名空间(bean、import、alias等)parseDefaultElement(ele,delegate);}else{// 自定义命名空间(aop、tx、context等)delegate.parseCustomElement(ele);}}}}六、实际应用建议
6.1 Spring配置文件优化
<!-- ❌ 避免:单个超大配置文件 --><beans><!-- 上千个bean定义... --></beans><!-- ✅ 推荐:拆分为多个模块 --><beans><importresource="spring-dao.xml"/><importresource="spring-service.xml"/><importresource="spring-web.xml"/></beans>6.2 何时考虑SAX
如果需要处理超大型XML(非Spring配置场景):
// 场景:处理1GB的数据导出XMLpublicclassLargeXmlProcessorextendsDefaultHandler{privateintrecordCount=0;@OverridepublicvoidstartElement(Stringuri,StringlocalName,StringqName,Attributesattributes){if("record".equals(qName)){// 处理单条记录processRecord(attributes);recordCount++;if(recordCount%10000==0){System.out.println("Processed "+recordCount+" records");}}}privatevoidprocessRecord(Attributesattributes){// 处理并立即释放,不保留在内存}}6.3 现代Spring项目建议
// 优先使用注解配置,避免XML@Configuration@ComponentScan("com.example")publicclassAppConfig{@BeanpublicUserServiceuserService(){returnnewUserServiceImpl();}}// 或使用Spring Boot@SpringBootApplicationpublicclassApplication{publicstaticvoidmain(String[]args){SpringApplication.run(Application.class,args);}}