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通过读取前四个字节流,探测文件编码格式:
- 前四个字节实际读取小于两个字节,肯定是utf-8编码
- 前两个字节转16进制,FEFF,
UTF_16_BIG_ENDIAN_WITH_BOM,FFFE就是小端序 - 实际字节数小于3,仍然是utf-8
- 此时前三个字节16进制,EF BB BF,它是
UTF_8_WITH_BOM - 此时如果字节数小于4,就又是utf-8
- 此时如果四个字节是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 - 到了这00 3C 00 3F,
UTF_16_BIG_ENDIAN;3C 00 3F 00就是UTF_16_LITTLE_ENDIAN - 4C 6F A7 94 就是
EBCDIC - 到了这默认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
属性遍历
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个全局属性
构建元素节点
//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比任何文字都有用。理解完了,再回过头指正一下我的错误。