XML解析——实体解析器

152 阅读8分钟

EntityResolver

实体是xml文档中声明的命名引用,用于替代内容和标记。避免重复代码,自然想到作为模板、默认属性。实体分为内部Entity,外部Entity

如果SAX应用程序需要实现对外部实体的自定义处理,则必须实现此接口,并使用setEntityResolver方法向SAX驱动器注册一个实例。这样,应用程序就可以将外部实体的引用映射到另一个XML文档(例如,从缓存或本地文件加载),或者通过完全重定向到一个不同的URI来修改实体的引用。

解析器将在打开任何外部实体(除了顶级文档实体)之前调用应用程序的resolveEntity方法。

  • 这样的实体包括外部DTD子集和外部参数实体引用的DTD(无论是否出现在文档内部子集中)。
  • 此外,对于外部常规实体(如文档中通过元素引用的实体),解析器也会调用resolveEntity方法,前提是应用程序已经注册了一个此类方法。 许多应用程序不需要实现此接口,但对于那些使用DTD进行特殊处理的应用程序,或者使用URI类型(如URN)不同于URL的应用程序,它将非常有用。

内部Entity

内部实体定义在当前xml中的

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE catalog [ <!ENTITY product_name "SuperWidget 3000"> <!-- 内部实体 --> ]> 
<catalog> 
    <product>&product_name;</product> <!-- 输出:SuperWidget 3000 --> </catalog>

外部实体

定义在外部,如:xml,DTD,XSD文件。这些资源不在当前xml中定义。需要本地或网路进行资源加载。

<!DOCTYPE report [ <!ENTITY financials SYSTEM "file:///confidential/sales.xml"> <!-- 外部实体 --> ]> 
<report>&financials;</report> <!-- 尝试加载外部文件 -->

这也是我们很常见的方式,比如定义模板、默认属性等。Spring、MyBatis的xml文件开头都很常用,例如bean的xml中xsi:schemaLocation,解析器在验证xml文档时,就需要获取这个xsd文件,广义上讲这个xsd文件就是外部实体。但xsd没有DOCTYPE声明,不存在DTD的xxe,外部实体攻击漏洞。

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

例子

经典的例子,用本地xsd替换网络xsd

public class MyResolver implements EntityResolver {
 *     public InputSource resolveEntity(String publicId, String systemId) {
 *         if (systemId.equals("http://myhost.com/today.xsd")) {
 *             return new InputSource("com/myorg/resolvers/local.xsd");
 *         } else {
 *             // 使用默认行为
 *             return null;
 *         }
 *     }
 * }

自动探测编码格式

final byte[] b4 = new byte[4];
int count = 0;
for (; count<4; count++ ) {
    b4[count] = (byte)rewindableStream.readAndBuffer();
}

XMLEntityManager的getEncodingInfo通过读取前四个字节流,探测文件编码格式:

  1. 前四个字节实际读取小于两个字节,肯定是utf-8编码
  2. 前两个字节转16进制,FEFF,UTF_16_BIG_ENDIAN_WITH_BOM,FFFE就是小端序
  3. 实际字节数小于3,仍然是utf-8
  4. 此时前三个字节16进制,EF BB BF,它是UTF_8_WITH_BOM
  5. 此时如果字节数小于4,就又是utf-8
  6. 此时如果四个字节是00 00 00 3C,它就是UCS_4_BIG_ENDIAN,3C 00 00 00,就是小端序。00 00 3C 00或者00 3C 00 00就是UCS_4_UNUSUAL_BYTE_ORDER
  7. 到了这00 3C 00 3F,UTF_16_BIG_ENDIAN;3C 00 3F 00就是UTF_16_LITTLE_ENDIAN
  8. 4C 6F A7 94 就是EBCDIC
  9. 到了这默认utf-8

注意以上判断不是if...else,而是直接通过return跳出,所以顺序不能乱,也不能统一所有的utf-8。这种检测方式就是"先有蛋还是先有鸡的问题",没有确定编码方式就无从解码,更谈不上读出encoding="UTF-8"。"ZERO WIDTH NO-BREAK SPACE"这个BOM在字节流开头,才能让我们知道是什么编码方式

spring中应用

PluggableSchemaResolver就是EntityResolver的实现。bean定义加载的时候loadBeanDefinitions中设置实体解析

beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));

最终代理实体解析器构造中,定义了schema的解析器是PluggableSchemaResolver

public DelegatingEntityResolver(@Nullable ClassLoader classLoader) {
    this.dtdResolver = new BeansDtdResolver();
    this.schemaResolver = new PluggableSchemaResolver(classLoader);
}
public PluggableSchemaResolver(@Nullable ClassLoader classLoader) {
    this.classLoader = classLoader;
    //自定义schema
    this.schemaMappingsLocation = DEFAULT_SCHEMA_MAPPINGS_LOCATION;
}
getSchemaMappings() {
...加载所有MATA-INF/spring.schemas映射文件,生成xml命名空间和xsd文件的映射
Properties mappings =
       PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader);
}

调用链如下:

loadBeanDefinitions->doLoadBeanDefinitions->Document doc = doLoadDocument(inputSource, resource)-> loadDocument->parse->fCurrentScanner.scanDocument(complete)-> scanStartElement->handleStartElement->findSchemaGrammar->XMLSchemaLoader.resolveDocument->entityResolver.resolveEntity(desc) 这里的desc是fXSDDescription,也就是xsd描述符->resolveEntity

//xsd中publicId是空的,DTD中`publicId`是`-//SPRING//DTD BEAN//EN`
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException {
    String resourceLocation = getSchemaMappings().get(systemId);
}
//懒加载,第一次调用getSchemaMappings方法加载类路径下所有META-INF/spring.schemas,形成一个schema map。idea在调试时内部eveluation会调用
//toString(),PluggableSchemaResolver的toString就调用了它。打断点
//是看不到的,他会直接跳过,所以需要手动置为null
private Map<String, String> getSchemaMappings() {
    Map<String, String> schemaMappings = this.schemaMappings;
    
}
//核心是通过PropertiesLoaderUtils加载的,this.schemaMappingsLocation
//就是字符串“META-INF/spring.schemas”
PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader)

其实每一个jar包的META-INF/spring.schemas文件就是把网络xsd资源路径对应本地xsd路径。目前6.27版本有184个键值对。实际是多个版本对应同一个值。

https://www.springframework.org/schema/beans/spring-beans-4.3.xsd=org/springframework/beans/factory/xml/spring-beans.xsd
https://www.springframework.org/schema/beans/spring-beans.xsd=org/springframework/beans/factory/xml/spring-beans.xsd

schema解析

解析我们的bean定义xml时,首先解析到beans标签,它是我们xml的起始元素

Augmentations modifiedAugs = handleStartElement(element, attributes, augs);
 | 查找schema语法,这里走完通过xsd文件对应的标签,获取到默认属性。比如beans标签的3个默认全局属性:default-lazy-init、default-merge、default-autowire。然后创建当前元素的doc节点。开始下一个子元素bean标签,它有自己的属性,外加父节点的3个全局 属性,默认属性会被子类显示声明覆盖掉。
\|/
SchemaGrammar sGrammar =
    findSchemaGrammar(
        XSDDescription.CONTEXT_ELEMENT,
        element.uri,
        null,
        element,
        attributes);
  |fGrammarPool中缓存的有需要加载的grammar,就直接取出,没有就加载schema(xsd文件)
 \|/
SchemaGrammar grammar = fSchemaHandler.parseSchema(source, desc, locationPairs)
  |parseSchema,获取schema的dom树,即它的根节点
 \|/
schemaRoot = getSchemaDocument(schemaNamespace, is,
  referType == XSDDescription.CONTEXT_PREPARSE,
  referType, null);
  |解析schema字节流,解析完会走constructTrees构建树,再遍历schemas->traverseGlobal,下面会单列讲
 \|/ 
fSchemaParser.parse(schemaSource)
  |parse扫描
 \|/ 
fCurrentScanner.scanDocument(complete)
  |核心就是next方法遍历,根据fScannerState,它是根元素
 \|/ 
scanRootElementHook()->scanStartElement()
  |其中scanAttribute(fAttributes)会扫描该元素所有属性
 \|/
fDocumentHandler.startElement(fElementQName, fAttributes, null) 
  |
 \|/
schemaDOM.startAnnotation(element, attributes, fNamespaceContext);
//非注释,走下面的
schemaDOM.startElement(element, attributes,
        fLocator.getLineNumber(),
        fLocator.getColumnNumber(),
        fLocator.getCharacterOffset());
 |非自闭合标签,会更新父节点可能有子内容,走startElement
 |<xsd:enumeration value="default"></xsd:enumeration>标签中间可以有子标签
\|/
public ElementImpl startElement(QName element, XMLAttributes attributes,
        int line, int column, int offset) {
    ElementImpl node = new ElementImpl(line, column, offset);
    processElement(element, attributes, node);
    // now the current node added, becomes the parent
    parent = node;
    return node;
}
//自闭和标签走emptyElement,不更新新父节点,无子内容(无嵌套内容需处理)
//优化逻辑,避免无意义的上下文切换。例如<xsd:enumeration value="default"/>自闭合。
public ElementImpl emptyElement(QName element, XMLAttributes attributes,
        int line, int column, int offset) {
    ElementImpl node = new ElementImpl(line, column, offset);
    processElement(element, attributes, node);
    return node;
}
 |装配节点,设置属性(放到节点的attrs中,除了元素之外,标签中所有都是属性)
 |填充relations数组。扫描的属性赋给节点node.attrs = attrs
\|/
processElement 

{9199359F-4388-433A-B4AC-A1885A16BBE9}.png

{AEB65B22-F9FB-422D-BB36-14EF7373E53F}.png

属性遍历

traverseGlobal全局属性遍历,例componentType.equals(SchemaSymbols.ELT_ELEMENT
 |
\|/ 
fElementTraverser.traverseGlobal(globalComp, currSchemaDoc, currSG)
  | checkAttributes属性检查就会用到下面两张图的数据结构
 \|/
XSElementDecl element = traverseNamedElement(elmDecl, attrValues, schemaDoc, grammar, true, null);
  |
 \|/
traverseLocal
 | 符合内容处理
\|/
processComplexContent(child, mixedAtt.booleanValue(), false,
        schemaDoc, grammar);
 | 遍历元素子元素atribute标签属性,把子元素为atribute标签的所有属性放入fAttrGrp
\|/
Element node =   traverseAttrsAndAttrGrps(attrNode,fAttrGrp,schemaDoc,grammar,fComplexTypeDecl);

静态块中初始化了元素属性map

在XSAttributeChecker类中
static{
14个元素属性map的初始化
fEleAttrsMapG.put(SchemaSymbols.ELT_ATTRIBUTE, attrList);
fEleAttrsMapG.put(SchemaSymbols.ELT_ELEMENT, attrList);
fEleAttrsMapG.put(SchemaSymbols.ELT_COMPLEXTYPE, attrList);
fEleAttrsMapG.put(SchemaSymbols.ELT_NOTATION, attrList);
fEleAttrsMapG.put(SchemaSymbols.ELT_ATTRIBUTEGROUP, attrList);
fEleAttrsMapG.put(SchemaSymbols.ELT_GROUP, attrList);
fEleAttrsMapG.put(SchemaSymbols.ELT_ANNOTATION, attrList);
fEleAttrsMapG.put(SchemaSymbols.ELT_APPINFO, attrList);
fEleAttrsMapG.put(SchemaSymbols.ELT_DOCUMENTATION, attrList);
fEleAttrsMapG.put(SchemaSymbols.ELT_SIMPLETYPE, attrList);
fEleAttrsMapG.put(SchemaSymbols.ELT_SCHEMA, attrList);
fEleAttrsMapG.put(SchemaSymbols.ELT_INCLUDE, attrList);
fEleAttrsMapG.put(SchemaSymbols.ELT_REDEFINE, attrList);
fEleAttrsMapG.put(SchemaSymbols.ELT_IMPORT, attrList);
}
---------------
例如element元素的10个全局属性
// for element "element" - global
attrList = Container.getContainer(10);
// abstract = boolean : false
attrList.put(SchemaSymbols.ATT_ABSTRACT, allAttrs[ATT_ABSTRACT_D]);
// block = (#all | List of (extension | restriction | substitution))
attrList.put(SchemaSymbols.ATT_BLOCK, allAttrs[ATT_BLOCK_N]);
// default = string
attrList.put(SchemaSymbols.ATT_DEFAULT, allAttrs[ATT_DEFAULT_N]);
// final = (#all | List of (extension | restriction))
attrList.put(SchemaSymbols.ATT_FINAL, allAttrs[ATT_FINAL_N]);
// fixed = string
attrList.put(SchemaSymbols.ATT_FIXED, allAttrs[ATT_FIXED_N]);
// id = ID
attrList.put(SchemaSymbols.ATT_ID, allAttrs[ATT_ID_N]);
// name = NCName
attrList.put(SchemaSymbols.ATT_NAME, allAttrs[ATT_NAME_R]);
// nillable = boolean : false
attrList.put(SchemaSymbols.ATT_NILLABLE, allAttrs[ATT_NILLABLE_D]);
// substitutionGroup = QName
attrList.put(SchemaSymbols.ATT_SUBSTITUTIONGROUP, allAttrs[ATT_SUBSTITUTION_G_N]);
// type = QName
attrList.put(SchemaSymbols.ATT_TYPE, allAttrs[ATT_TYPE_N]);
fEleAttrsMapG.put(SchemaSymbols.ELT_ELEMENT, attrList);

element标签在map,它又具有10个全局属性 {1092240F-29A2-47D4-AC4D-293BE8AC1FD8}.png

{820B7303-C34E-437F-991D-889A23FE89CB}.png

构建元素节点

//call handlers 处理器调用
fDocumentHandler.startElement(element, attributes, modifiedAugs);
 | 创建延迟元素
\|/
int el = fDeferredDocumentImpl.createDeferredElement (fNamespaceAware ?
        element.uri : null, element.rawname);
 | 创建元素节点,ensureCapacity(chunk)对元素中各个属性结构初始化例如fNodeType
 | 、fNodeName...
 | 通过setChunkIndex(fNodeType, nodeType, chunk, index)初始化节点,节点数+1
\|/
createNode
 | 节点创建完成,将对应的元素名称放入节点
\|/
setChunkValue(fNodeName, elementName, elementChunk, elementIndex);
 | 设置节点属性
\|/
fDeferredDocumentImpl.setDeferredAttribute
 |最后将新节点拼接到dom树
\|/ 
fDeferredDocumentImpl.appendChild (fCurrentNodeIndex, el)

这样就会形成一个bean-xxx.xml解析后的dom树对象。我们就可以使用该对象进行你想做的任何事情,此时xml文档信息已经完全存在于此对象中,比如进行bean定义的注册。

后记

由于xml解析器比较复杂,JDK使用的Xerces,源代码已有20多年历史,骨灰级玩家不停的补丁更新。很多代码很长,逻辑线条复杂,不容易看懂。我只是调试了几下大概明白流程。细节没有深究,在此做一个记录。记录中很多信息缺失的(每一项都是记录很麻烦),调试中我看懂的都略过了。记录的这三处代码是复杂跳转的地方,如果有小伙伴看不懂,不要慌,想要深入研究,自己debug比任何文字都有用。理解完了,再回过头指正一下我的错误。