Apache XMLSchema 2.3.3 配置参数详解
逐项实测 Apache XMLSchema 2.3.3 的全部配置项——导入深度与解析次数上限、网络与本地解析开关、只读集合保护、序列化输出选项与自定义 URIResolver,附安全基线 JVM 参数模板,全部在 Ubuntu 22.04 实测通过。
Apache XMLSchema 2.3.3 配置参数详解
目标:把 Apache XMLSchema(xmlschema-core)2.3.3 的全部配置项——系统属性、API 选项、序列化输出选项——逐项讲清并逐项实测,给出解析不可信 XSD 的安全基线参数模板。全部命令与回显在 Ubuntu 22.04.5 LTS + OpenJDK 17 + Maven 3.6.3 环境实测通过。
一、环境信息
本章列出实测环境。操作系统与工具链版本:
grep PRETTY_NAME /etc/os-release
实测输出:
PRETTY_NAME="Ubuntu 22.04.5 LTS"
java -version 2>&1 | head -1 && mvn -version | head -1
实测输出:
openjdk version "17.0.20.1" 2026-08-18
Apache Maven 3.6.3
实测构件:
ls -l /root/lib/xmlschema-core-2.3.3.jar | awk '{print $5, $9}' && sha256sum /root/lib/xmlschema-core-2.3.3.jar
实测输出:
193402 /root/lib/xmlschema-core-2.3.3.jar
9d9b17f0d383e38c2ce041675211572992d6a1012926cc77f72d89be201614e3 /root/lib/xmlschema-core-2.3.3.jar
版本 2.3.3 为 Maven Central 当前 <release>(上架时间 2026-09-29,实测自 maven-metadata.xml)。会话统一使用 UTF-8 locale(原因见第六章常见问题):
export LANG=C.UTF-8 LC_ALL=C.UTF-8
该命令无回显,属正常。
二、前置条件
xmlschema-core 是纯 Java 库,配置项全部通过 JVM 系统属性或 API 传入,无需安装服务端。前置条件:JDK 17 与 Maven 已安装;一个能编译运行的演示工程。工程 POM(/root/demo/pom.xml,依赖引入细节见《Apache XMLSchema 在 Ubuntu 22.04 集成使用指南》):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>xmlschema-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.ws.xmlschema</groupId>
<artifactId>xmlschema-core</artifactId>
<version>2.3.3</version>
</dependency>
</dependencies>
</project>
编译验证:
cd /root/demo && mvn -q compile; echo COMPILE_EXIT=$?
实测输出:
COMPILE_EXIT=0
2.1 演示工程源码
本章所有实测复用以下演示类。src/main/java/com/example/OptionsDemo.java(六个演示合在一个类里):
package com.example;
import org.apache.ws.commons.schema.XmlSchema;
import org.apache.ws.commons.schema.XmlSchemaCollection;
import org.apache.ws.commons.schema.XmlSchemaException;
import org.apache.ws.commons.schema.XmlSchemaType;
import org.apache.ws.commons.schema.resolver.DefaultURIResolver;
import org.xml.sax.InputSource;
import javax.xml.namespace.QName;
import javax.xml.transform.stream.StreamSource;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
/**
* C1 主示例:XmlSchemaCollection 关键选项与行为逐项实测。
* 演示 1 自定义 URIResolver 重定向;2 序列化对照;3 本地文件解析开关;
* 4 网络解析开关;5 导入深度上限;6 重复全局元素读取行为。
*/
public class OptionsDemo {
public static void main(String[] args) throws Exception {
Files.createDirectories(Paths.get("out"));
System.out.println("########## 演示 1:自定义 URIResolver,远程 schemaLocation 重定向本地 ##########");
remapResolverDemo();
System.out.println();
System.out.println("########## 演示 2:序列化对照(read 进来 write 出去)##########");
serializerDemo();
System.out.println();
System.out.println("########## 演示 3:local.allowFileSystem=false 拒绝文件解析 ##########");
fileSystemOffDemo();
System.out.println();
System.out.println("########## 演示 4:remote.allowNetwork=false 拒绝网络解析 ##########");
networkOffDemo();
System.out.println();
System.out.println("########## 演示 5:maxImportDepth 导入深度上限 ##########");
depthLimitDemo();
System.out.println();
System.out.println("########## 演示 6:重复全局元素读取行为 ##########");
duplicateElementDemo();
}
/** 演示 1:library.xsd 里 import 的 http://schemas.example.com/common.xsd 重定向到本地文件。 */
static void remapResolverDemo() throws Exception {
XmlSchemaCollection collection = new XmlSchemaCollection();
collection.setSchemaResolver(new DefaultURIResolver() {
@Override
public InputSource resolveEntity(String namespace, String schemaLocation, String baseUri) {
if ("http://schemas.example.com/common.xsd".equals(schemaLocation)) {
System.out.println("[resolver] 重定向 " + schemaLocation
+ " -> file:schemas/common.xsd(本地文件)");
return new InputSource(new File("schemas/common.xsd").toURI().toString());
}
return super.resolveEntity(namespace, schemaLocation, baseUri);
}
});
File xsd = new File("schemas/library.xsd");
StreamSource source = new StreamSource(xsd);
source.setSystemId(xsd.toURI().toString());
XmlSchema schema = collection.read(source);
System.out.println("主 schema targetNamespace = " + schema.getTargetNamespace());
System.out.println("collection.getXmlSchemas().length = " + collection.getXmlSchemas().length
+ "(主 schema + import 解析进来的公共 schema)");
for (XmlSchema s : collection.getXmlSchemas()) {
System.out.println(" - 集合内 schema:" + s.getTargetNamespace());
}
XmlSchemaType t = collection.getTypeByQName(new QName("http://example.com/common", "AuthorType"));
System.out.println("跨 schema 类型查找 getTypeByQName(common:AuthorType) = "
+ t.getClass().getSimpleName());
}
/** 演示 2:读入 company.xsd 后 write 序列化,输出到 out/company-serialized.xsd 供 diff 对照。 */
static void serializerDemo() throws Exception {
XmlSchemaCollection collection = new XmlSchemaCollection();
File xsd = new File("schemas/company.xsd");
StreamSource source = new StreamSource(xsd);
source.setSystemId(xsd.toURI().toString());
XmlSchema schema = collection.read(source);
try (FileOutputStream fos = new FileOutputStream("out/company-serialized.xsd")) {
schema.write(fos);
}
System.out.println("已序列化输出 out/company-serialized.xsd(与输入 schemas/company.xsd 对照见 diff)");
org.w3c.dom.Document[] docs = schema.getAllSchemas();
System.out.println("schema.getAllSchemas() 返回 " + docs.length
+ " 个 DOM 文档(本 schema 的主文档;含 include/import 时会更多)");
}
/** 演示 3:local.allowFileSystem=false 后,xs:include 的相对文件路径被拒绝。 */
static void fileSystemOffDemo() throws Exception {
System.setProperty("org.apache.ws.commons.schema.local.allowFileSystem", "false");
try {
XmlSchemaCollection collection = new XmlSchemaCollection();
File xsd = new File("schemas/include-main.xsd");
StreamSource source = new StreamSource(xsd);
source.setSystemId(xsd.toURI().toString());
try {
collection.read(source);
System.out.println("意外读取成功(不应发生)");
} catch (XmlSchemaException e) {
System.out.println("读取被拒绝,异常类型:" + e.getClass().getName());
System.out.println("异常消息:" + e.getMessage());
}
} finally {
System.clearProperty("org.apache.ws.commons.schema.local.allowFileSystem");
}
}
/** 演示 4:remote.allowNetwork=false 后,https 远程 import 在发起网络请求前即被拒绝。 */
static void networkOffDemo() throws Exception {
System.setProperty("org.apache.ws.commons.schema.remote.allowNetwork", "false");
try {
XmlSchemaCollection collection = new XmlSchemaCollection();
File xsd = new File("schemas/import-remote.xsd");
StreamSource source = new StreamSource(xsd);
source.setSystemId(xsd.toURI().toString());
try {
collection.read(source);
System.out.println("意外读取成功(不应发生)");
} catch (XmlSchemaException e) {
System.out.println("读取被拒绝,异常类型:" + e.getClass().getName());
System.out.println("异常消息:" + e.getMessage());
}
} finally {
System.clearProperty("org.apache.ws.commons.schema.remote.allowNetwork");
}
}
/** 演示 5:生成 70 层 xs:include 链,超出默认深度 64 触发上限异常。 */
static void depthLimitDemo() throws Exception {
int n = 70;
for (int i = 0; i < n; i++) {
String self = String.format("depth-%02d.xsd", i);
String next = String.format("depth-%02d.xsd", i + 1);
String content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">\n"
+ " <xs:include schemaLocation=\"" + next + "\"/>\n"
+ "</xs:schema>\n";
Files.write(Paths.get("out/" + self), content.getBytes(StandardCharsets.UTF_8));
}
XmlSchemaCollection collection = new XmlSchemaCollection();
File xsd = new File("out/depth-00.xsd");
StreamSource source = new StreamSource(xsd);
source.setSystemId(xsd.toURI().toString());
try {
collection.read(source);
System.out.println("意外读取成功(不应发生)");
} catch (XmlSchemaException e) {
System.out.println("异常类型:" + e.getClass().getName());
System.out.println("异常消息:" + e.getMessage());
}
}
/** 演示 6:同名全局元素重复声明,观察读取行为。 */
static void duplicateElementDemo() throws Exception {
XmlSchemaCollection collection = new XmlSchemaCollection();
File xsd = new File("schemas/broken-duplicate.xsd");
StreamSource source = new StreamSource(xsd);
source.setSystemId(xsd.toURI().toString());
try {
collection.read(source);
System.out.println("读取未抛异常(重复全局组件在建模层被容忍,对照见 5.7)");
} catch (XmlSchemaException e) {
System.out.println("异常类型:" + e.getClass().getName());
System.out.println("异常消息:" + e.getMessage());
}
}
}
2.2 演示工程 XSD 素材
/root/demo/schemas/company.xsd(序列化对照用):
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:co="http://example.com/company"
targetNamespace="http://example.com/company"
elementFormDefault="qualified">
<xs:simpleType name="EmployeeIdType">
<xs:restriction base="xs:string">
<xs:pattern value="EMP-[0-9]{4}"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="EmployeeType">
<xs:sequence>
<xs:element name="name" type="xs:string"/>
<xs:element name="id" type="co:EmployeeIdType"/>
<xs:element name="salary" type="xs:decimal" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="grade" type="xs:int" use="required"/>
</xs:complexType>
<xs:element name="employee" type="co:EmployeeType"/>
<xs:element name="company">
<xs:complexType>
<xs:sequence>
<xs:element ref="co:employee" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
/root/demo/schemas/library.xsd(import 的 schemaLocation 故意写成不可直连的远程地址,供重定向演示用):
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:lib="http://example.com/library"
xmlns:cmn="http://example.com/common"
targetNamespace="http://example.com/library"
elementFormDefault="qualified">
<!-- schemaLocation 故意写成一个不可直连的远程地址,用于演示自定义 URIResolver 重定向 -->
<xs:import namespace="http://example.com/common"
schemaLocation="http://schemas.example.com/common.xsd"/>
<xs:complexType name="BookType">
<xs:sequence>
<xs:element name="title" type="xs:string"/>
<xs:element name="author" type="cmn:AuthorType"/>
</xs:sequence>
</xs:complexType>
<xs:element name="library">
<xs:complexType>
<xs:sequence>
<xs:element name="book" type="lib:BookType" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
/root/demo/schemas/common.xsd(被重定向的公共类型):
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/common"
elementFormDefault="qualified">
<!-- 被远程 schemaLocation 引用的公共类型,将由自定义 URIResolver 重定向到本地 -->
<xs:complexType name="AuthorType">
<xs:sequence>
<xs:element name="first" type="xs:string"/>
<xs:element name="last" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:schema>
剩余三个素材全文如下。/root/demo/schemas/include-main.xsd:
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:in="http://example.com/includer"
targetNamespace="http://example.com/includer"
elementFormDefault="qualified">
<!-- 同目标命名空间的 include,相对路径将由默认解析器按 file: 方案读取 -->
<xs:include schemaLocation="include-common.xsd"/>
<xs:element name="doc" type="in:NoteType"/>
</xs:schema>
/root/demo/schemas/include-common.xsd:
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/includer"
elementFormDefault="qualified">
<xs:simpleType name="NoteType">
<xs:restriction base="xs:string">
<xs:minLength value="1"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>
/root/demo/schemas/import-remote.xsd:
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/remoteimport">
<!-- 仅声明一个 https 远程导入,配合 remote.allowNetwork=false 演示网络解析被拒绝 -->
<xs:import namespace="http://www.w3.org/XML/1998/namespace"
schemaLocation="https://www.w3.org/2001/xml.xsd"/>
</xs:schema>
/root/demo/schemas/broken-duplicate.xsd:
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/dup"
elementFormDefault="qualified">
<!-- 同名全局元素重复声明,读取时将抛出 XmlSchemaException -->
<xs:element name="dup" type="xs:string"/>
<xs:element name="dup" type="xs:string"/>
</xs:schema>
三、参数查看与修改方式
xmlschema-core 没有配置文件,参数只有两种形态,查看与修改方式各不相同。
形态一:JVM 系统属性(-D 启动参数或 System.setProperty)。这类参数在类加载时读取一次,改完必须重启 JVM(或保证 System.setProperty 先于首次使用对应类)。全部属性名以 sources jar 里的常量定义为准,先下载源码包再 grep:
cd /root/lib && curl -sO https://repo1.maven.org/maven2/org/apache/ws/xmlschema/xmlschema-core/2.3.3/xmlschema-core-2.3.3-sources.jar && unzip -q -o xmlschema-core-2.3.3-sources.jar -d src
该命令无回显。查看 DefaultURIResolver 里的全部属性常量:
cd /root/lib && grep -A1 '_PROPERTY =' src/org/apache/ws/commons/schema/resolver/DefaultURIResolver.java
实测输出:
public static final String CONNECT_TIMEOUT_PROPERTY =
"org.apache.ws.commons.schema.remote.connectTimeoutMillis";
public static final String READ_TIMEOUT_PROPERTY =
"org.apache.ws.commons.schema.remote.readTimeoutMillis";
public static final String MAX_FETCH_MILLIS_PROPERTY =
"org.apache.ws.commons.schema.remote.maxFetchMillis";
public static final String MAX_BYTES_PROPERTY =
"org.apache.ws.commons.schema.remote.maxBytes";
--
public static final String MAX_REDIRECTS_PROPERTY =
"org.apache.ws.commons.schema.remote.maxRedirects";
--
public static final String CHECK_ADDRESSES_PROPERTY =
"org.apache.ws.commons.schema.remote.checkAddresses";
--
public static final String ALLOW_NETWORK_PROPERTY =
"org.apache.ws.commons.schema.remote.allowNetwork";
--
public static final String ALLOW_FILE_SYSTEM_PROPERTY =
"org.apache.ws.commons.schema.local.allowFileSystem";
查看集合层的导入限制常量:
cd /root/lib && grep -B1 -A1 'MAX_IMPORT_DEPTH =\|MAX_RESOLUTIONS_PER_READ =' src/org/apache/ws/commons/schema/XmlSchemaCollection.java
实测输出:
*/
private static final int MAX_IMPORT_DEPTH =
getIntProperty("org.apache.ws.commons.schema.maxImportDepth", 64);
private static final int MAX_RESOLUTIONS_PER_READ =
getIntProperty("org.apache.ws.commons.schema.maxSchemaResolutions", 1000);
形态二:API 编程式选项。集合解析器 setSchemaResolver(URIResolver)、序列化输出选项 write(OutputStream, Map<String,String>)、模型构造参数等,作用于单个对象实例,随对象创建即时生效。
💡 技术拓展:「JVM 系统属性」是 Java 生态常见的库级配置形态,与同类形态的对比——
形态 生效时机 典型写法 适用场景 JVM 系统属性 类加载时读取一次 java -Dkey=value -jar app.jar全局策略,如网络安全开关 API 编程式 对象创建/调用时即时 collection.setSchemaResolver(...)按实例差异化配置 环境变量 进程启动时 export KEY=value云编排环境统一注入 配置文件 库自行加载 无(本库不采用) 常规服务端组件 xmlschema-core 采用前两种组合:全局安全策略走系统属性,对象行为走 API,本文两种都有实测。
四、参数总览
2.3.3 的全部可配置项汇总如下,默认值均为 sources jar 实测(标注「源码实测」)或行为实测(标注「行为实测」):
| 参数(系统属性) | 默认值 | 作用 | 验证方式 |
|---|---|---|---|
| org.apache.ws.commons.schema.maxImportDepth | 64 | xsd:import/include 解析栈最大深度 | 行为实测(5.1) |
| org.apache.ws.commons.schema.maxSchemaResolutions | 1000 | 单次顶层 read 的最大解析次数 | 源码实测(见第三章) |
| org.apache.ws.commons.schema.remote.allowNetwork | true | 是否允许对远程 schemaLocation 发起网络请求 | 行为实测(5.2) |
| org.apache.ws.commons.schema.remote.checkAddresses | true | 取远程 schema 前校验目标地址(拒绝链路本地/多播等地址类) | 源码实测 |
| org.apache.ws.commons.schema.remote.maxRedirects | (有限值,重定向跳数上限,0 表示拒绝重定向) | 单次 fetch 允许跟随的重定向数 | 源码实测 |
| org.apache.ws.commons.schema.remote.connectTimeoutMillis | (未设置则无限制) | 单次网络 fetch 的连接超时(毫秒) | 源码实测 |
| org.apache.ws.commons.schema.remote.readTimeoutMillis | (未设置则无限制) | 单次网络 fetch 的读超时(毫秒) | 源码实测 |
| org.apache.ws.commons.schema.remote.maxFetchMillis | (未设置则无限制) | 单次网络 fetch 总时长上限(毫秒) | 源码实测 |
| org.apache.ws.commons.schema.remote.maxBytes | (未设置则无限制) | 单次网络 fetch 的响应体大小上限(字节) | 源码实测 |
| org.apache.ws.commons.schema.local.allowFileSystem | true | 是否允许从文件系统读取 schemaLocation | 行为实测(5.3) |
| org.apache.ws.commons.schema.protectReadOnlyCollections | false | getElements() 等返回的集合是否运行时只读化 | 行为实测(5.4) |
序列化输出默认选项(API 形态,源码实测 loadDefaultOptions):
| 输出属性(javax.xml.transform.OutputKeys) | 默认值 | 作用 |
|---|---|---|
| OMIT_XML_DECLARATION | yes | 输出不带 XML 声明 |
| INDENT | yes | 四空格缩进 |
💡 说明:网络限额类参数(connectTimeout/readTimeout/maxFetchMillis/maxBytes)在「未设置时无限制」——源码注释明确说这是为了让单个恶意 schemaLocation 无法无限占用解析线程或堆内存而提供的上限开关,是否设置取决于 schema 来源可信度,建议见第六章生产模板。
五、核心参数分类详解
5.1 导入深度与解析次数上限(maxImportDepth / maxSchemaResolutions)
这两个参数防的是「import 图爆炸」:恶意或失控的 XSD 用变化的 schemaLocation 互相引用,绕过循环检测,把解析线程拖到栈溢出。默认值分别为 64 和 1000(源码见第三章)。实测:生成 70 层 xs:include 链(每层文件不同,不构成精确键循环),从第 0 层读入:
cd /root/demo && mvn -q exec:java -Dexec.mainClass=com.example.OptionsDemo 2>&1 | sed -n '/演示 5/,/^$/p'
实测输出:
########## 演示 5:maxImportDepth 导入深度上限 ##########
异常类型:org.apache.ws.commons.schema.XmlSchemaException
异常消息:Maximum schema import/include depth of 64 exceeded while resolving file:/root/demo/out/depth-65.xsd; the import graph is too deep or does not terminate. The limit may be changed with the org.apache.ws.commons.schema.maxImportDepth system property.
第 65 层(depth-65.xsd)被拒绝,异常消息直接给出属性名。调整方式是在 JVM 启动参数加 -Dorg.apache.ws.commons.schema.maxImportDepth=128。验证结果:超限立即失败且报错可读,行为符合预期。
5.2 网络解析开关(remote.allowNetwork 与 remote.* 家族)
remote.allowNetwork=false 让「schemaLocation 指向 http/https」的 import 在发起网络请求前直接被拒绝,适合 schema 全部本地的部署。实测:
cd /root/demo && mvn -q exec:java -Dexec.mainClass=com.example.OptionsDemo 2>&1 | sed -n '/演示 4/,/^$/p'
实测输出:
########## 演示 4:remote.allowNetwork=false 拒绝网络解析 ##########
读取被拒绝,异常类型:org.apache.ws.commons.schema.XmlSchemaException
异常消息:The schema location "https://www.w3.org/2001/xml.xsd" would be fetched over the network, which org.apache.ws.commons.schema.remote.allowNetwork has turned off.
异常消息里点名了被拒绝的 URL 与属性名。同一家族的其余参数(checkAddresses/maxRedirects/超时/字节数上限)作用于「允许网络时的单次 fetch」:checkAddresses 默认 true,在 fetch 前拒绝链路本地地址(如 169.254.169.254 云元数据端点)、多播、通配地址与内嵌这些地址的 IPv6 形式,但放行回环与 RFC1918 私网地址——它是「必然非法地址」的黑名单,不是主机白名单;需要白名单粒度时应自实现 resolver(见 5.6)。验证结果:网络开关立即生效于读路径,无需重启以外的任何操作(属性在解析前设置即可)。
5.3 本地文件解析开关(local.allowFileSystem)
local.allowFileSystem=false 拒绝从文件系统读取 schemaLocation(file:、jar:file: 及相对路径),适合「schema 文档必须自包含」的部署。实测:
cd /root/demo && mvn -q exec:java -Dexec.mainClass=com.example.OptionsDemo 2>&1 | sed -n '/演示 3/,/^$/p'
实测输出:
########## 演示 3:local.allowFileSystem=false 拒绝文件解析 ##########
读取被拒绝,异常类型:org.apache.ws.commons.schema.XmlSchemaException
异常消息:The schema location "include-common.xsd" would be read from the filesystem, which org.apache.ws.commons.schema.local.allowFileSystem has turned off.
注意细节:被拒的是 include 目标 include-common.xsd,而顶层文档本身是调用方直接交给 read() 的,不经过 resolver,所以顶层文件仍可读。验证结果:开关只约束「解析过程中对 schemaLocation 的文件读取」,语义精确。
5.4 只读集合保护(protectReadOnlyCollections)
默认情况下 getElements()/getSchemaTypes() 等返回的是内部可变集合的直通视图,外部误改会污染模型。系统属性置 true 后,返回集合带运行时校验。实测分两次独立 JVM 运行对照(演示类 ProtectDemo:未开启时对返回 Map 执行 remove,开启后再执行):src/main/java/com/example/ProtectDemo.java:
package com.example;
import org.apache.ws.commons.schema.XmlSchema;
import org.apache.ws.commons.schema.XmlSchemaCollection;
import javax.xml.namespace.QName;
import javax.xml.transform.stream.StreamSource;
import java.io.File;
/**
* C1 补充:protectReadOnlyCollections 系统属性对照实验(两次独立 JVM 运行)。
*/
public class ProtectDemo {
public static void main(String[] args) throws Exception {
boolean protect = args.length > 0 && "protect".equals(args[0]);
if (protect) {
System.setProperty("org.apache.ws.commons.schema.protectReadOnlyCollections", "true");
}
System.out.println(protect
? "== 启动参数:protectReadOnlyCollections=true =="
: "== 启动参数:未设置(默认)==");
XmlSchemaCollection collection = new XmlSchemaCollection();
File xsd = new File("schemas/company.xsd");
StreamSource source = new StreamSource(xsd);
source.setSystemId(xsd.toURI().toString());
XmlSchema schema = collection.read(source);
QName probe = new QName("http://example.com/company", "employee");
try {
schema.getElements().remove(probe);
System.out.println("对 getElements() 返回的 Map 执行 remove:成功,size=" + schema.getElements().size());
System.out.println("结论:默认返回的是可变 Map,外部代码可修改模型内部集合");
} catch (UnsupportedOperationException e) {
System.out.println("对 getElements() 返回的 Map 执行 remove:被拒绝");
System.out.println(" 异常类型:" + e.getClass().getName());
System.out.println("结论:开启保护后返回运行时校验的只读 Map");
}
}
}
第一次运行(默认):
cd /root/demo && mvn -q exec:java -Dexec.mainClass=com.example.ProtectDemo
实测输出:
== 启动参数:未设置(默认)==
对 getElements() 返回的 Map 执行 remove:成功,size=1
结论:默认返回的是可变 Map,外部代码可修改模型内部集合
第二次运行(开启保护):
cd /root/demo && mvn -q exec:java -Dexec.mainClass=com.example.ProtectDemo -Dexec.args='protect'
实测输出:
== 启动参数:protectReadOnlyCollections=true ==
对 getElements() 返回的 Map 执行 remove:被拒绝
异常类型:java.lang.UnsupportedOperationException
结论:开启保护后返回运行时校验的只读 Map
对照结论:默认是「裸 Map」,开启后对 getElements() 的任何写操作抛 UnsupportedOperationException。该属性也是 CXF 的测试基线(CXF 父 POM 中的 cxf.protect-xmlschema-collections 属性最终注入同名系统属性,实测见其 POM),长期运行的服务端建议开启。
5.5 序列化输出选项(write 的 options 与默认行为)
write(OutputStream) 的默认输出行为由 loadDefaultOptions 决定:OMIT_XML_DECLARATION=yes、INDENT=yes(源码见第四章表格)。实测对照:读入 company.xsd 后 write 输出,再与输入文件 diff:
cd /root/demo && mvn -q exec:java -Dexec.mainClass=com.example.OptionsDemo 2>&1 | sed -n '/演示 2/,/^$/p'
实测输出:
########## 演示 2:序列化对照(read 进来 write 出去)##########
已序列化输出 out/company-serialized.xsd(与输入 schemas/company.xsd 对照见 diff)
schema.getAllSchemas() 返回 1 个 DOM 文档(本 schema 的主文档;含 include/import 时会更多)
cd /root/demo && diff -u schemas/company.xsd out/company-serialized.xsd; echo DIFF_EXIT=$?
实测输出(完整):
--- schemas/company.xsd 2026-10-16 02:39:21.000000000 +0000
+++ out/company-serialized.xsd 2026-10-16 02:48:46.032307655 +0000
@@ -1,36 +1,23 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
- xmlns:co="http://example.com/company"
- targetNamespace="http://example.com/company"
- elementFormDefault="qualified">
-
- <!-- 1. 全局简单类型:员工编号,用 pattern 限定格式 -->
- <xs:simpleType name="EmployeeIdType">
- <xs:restriction base="xs:string">
- <xs:pattern value="EMP-[0-9]{4}"/>
- </xs:restriction>
- </xs:simpleType>
-
- <!-- 2. 全局复杂类型:员工 -->
- <xs:complexType name="EmployeeType">
- <xs:sequence>
- <xs:element name="name" type="xs:string"/>
- <xs:element name="id" type="co:EmployeeIdType"/>
- <xs:element name="salary" type="xs:decimal" minOccurs="0"/>
- </xs:sequence>
- <xs:attribute name="grade" type="xs:int" use="required"/>
- </xs:complexType>
-
- <!-- 3. 全局元素:employee 引用全局类型 -->
- <xs:element name="employee" type="co:EmployeeType"/>
-
- <!-- 4. 全局元素:company,匿名复杂类型 + ref 引用 employee -->
- <xs:element name="company">
- <xs:complexType>
- <xs:sequence>
- <xs:element ref="co:employee" maxOccurs="unbounded"/>
- </xs:sequence>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:co="http://example.com/company" attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="http://example.com/company">
+ <xs:simpleType name="EmployeeIdType">
+ <xs:restriction base="xs:string">
+ <xs:pattern value="EMP-[0-9]{4}"/>
+ </xs:restriction>
+ </xs:simpleType>
+ <xs:complexType name="EmployeeType">
+ <xs:sequence>
+ <xs:element name="name" type="xs:string"/>
+ <xs:element name="id" type="co:EmployeeIdType"/>
+ <xs:element minOccurs="0" name="salary" type="xs:decimal"/>
+ </xs:sequence>
+ <xs:attribute name="grade" type="xs:int" use="required"/>
</xs:complexType>
- </xs:element>
-
+ <xs:element name="employee" type="co:EmployeeType"/>
+ <xs:element name="company">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element maxOccurs="unbounded" ref="co:employee"/>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
</xs:schema>
DIFF_EXIT=1
cd /root/demo && wc -l schemas/company.xsd out/company-serialized.xsd
实测输出:
36 schemas/company.xsd
23 out/company-serialized.xsd
59 total
diff 揭示了序列化器的五条规范化行为:① 不写 XML 声明(默认 OMIT_XML_DECLARATION=yes);② 注释全部丢失(模型不保留注释);③ 属性按字母序重排(maxOccurs 跑到 ref 前);④ 默认值物化(attributeFormDefault="unqualified" 补齐);⑤ 命名空间声明统一提升到根元素。因此 write 的输出不能当原文备份用,适合做规范化归一。需要 XML 声明时用带 options 的重载,实测 src/main/java/com/example/SerializeOptionsDemo.java:
package com.example;
import org.apache.ws.commons.schema.XmlSchema;
import org.apache.ws.commons.schema.XmlSchemaCollection;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.stream.StreamSource;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
/**
* C1 补充:write(out, options) 覆盖默认输出属性——默认不写 XML 声明,可显式要求输出。
*/
public class SerializeOptionsDemo {
public static void main(String[] args) throws Exception {
XmlSchemaCollection collection = new XmlSchemaCollection();
File xsd = new File("schemas/company.xsd");
StreamSource source = new StreamSource(xsd);
source.setSystemId(xsd.toURI().toString());
XmlSchema schema = collection.read(source);
Map<String, String> options = new HashMap<>();
options.put(OutputKeys.OMIT_XML_DECLARATION, "no");
options.put(OutputKeys.ENCODING, "UTF-8");
options.put(OutputKeys.INDENT, "yes");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
schema.write(bos, options);
String out = new String(bos.toByteArray(), "UTF-8");
System.out.println("开启 XML 声明后的输出前 3 行:");
String[] lines = out.split("\n");
for (int i = 0; i < 3 && i < lines.length; i++) {
System.out.println(lines[i]);
}
}
}
cd /root/demo && mvn -q exec:java -Dexec.mainClass=com.example.SerializeOptionsDemo
实测输出:
开启 XML 声明后的输出前 3 行:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:co="http://example.com/company" attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="http://example.com/company">
<xs:simpleType name="EmployeeIdType">
验证结果:options 实质是透传给 JAXP Transformer 的 OutputKeys 属性表,OMIT_XML_DECLARATION=no 后 XML 声明恢复输出。
5.6 自定义解析器(setSchemaResolver)
setSchemaResolver(URIResolver) 替换 import/include 的解析策略,2.3.3 默认解析器是 DefaultURIResolver(支持 http/https/file/jar 四种协议并内置 5.2/5.3 的防护)。子类化按 URL 重映射是内网部署的标准做法,实测:
cd /root/demo && mvn -q exec:java -Dexec.mainClass=com.example.OptionsDemo 2>&1 | sed -n '/演示 1/,/^$/p'
实测输出:
########## 演示 1:自定义 URIResolver,远程 schemaLocation 重定向本地 ##########
[resolver] 重定向 http://schemas.example.com/common.xsd -> file:schemas/common.xsd(本地文件)
主 schema targetNamespace = http://example.com/library
collection.getXmlSchemas().length = 3(主 schema + import 解析进来的公共 schema)
- 集合内 schema:http://www.w3.org/2001/XMLSchema
- 集合内 schema:http://example.com/library
- 集合内 schema:http://example.com/common
跨 schema 类型查找 getTypeByQName(common:AuthorType) = XmlSchemaComplexType
验证结果:http://schemas.example.com/common.xsd 未发起任何网络请求就被映射到本地 schemas/common.xsd,import 进来的公共命名空间进了集合,跨 schema 类型查找命中。同样的重定向逻辑也可封装为独立的 RemapDemo 类运行(《Apache XMLSchema 在 Ubuntu 22.04 集成使用指南》5.5 节),本文用 OptionsDemo 演示 1 验证。
5.7 读取容错行为(重复全局组件)
xmlschema-core 的读取层对「同一命名空间下两个同名全局元素」不报错(后声明覆盖前声明),而 JAXP SchemaFactory 编译同一文件直接失败。对照实测,src/main/java/com/example/DuplicateCompare.java:
package com.example;
import org.apache.ws.commons.schema.XmlSchema;
import org.apache.ws.commons.schema.XmlSchemaCollection;
import org.xml.sax.SAXException;
import javax.xml.XMLConstants;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.SchemaFactory;
import java.io.File;
import java.util.Map;
/**
* C1 补充:重复全局元素——xmlschema-core 读取行为 vs JAXP SchemaFactory 编译行为。
*/
public class DuplicateCompare {
public static void main(String[] args) throws Exception {
XmlSchemaCollection collection = new XmlSchemaCollection();
File xsd = new File("schemas/broken-duplicate.xsd");
StreamSource source = new StreamSource(xsd);
source.setSystemId(xsd.toURI().toString());
XmlSchema schema = collection.read(source);
System.out.println("xmlschema-core 读取:未抛异常,schema.getElements().size() = "
+ schema.getElements().size());
for (Map.Entry<javax.xml.namespace.QName, org.apache.ws.commons.schema.XmlSchemaElement> e
: schema.getElements().entrySet()) {
System.out.println(" 注册的全局元素:" + e.getKey() + " -> " + e.getValue().getSchemaTypeName());
}
System.out.println();
try {
SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
sf.newSchema(xsd);
System.out.println("JAXP SchemaFactory.newSchema:编译通过");
} catch (SAXException e) {
System.out.println("JAXP SchemaFactory.newSchema:编译失败");
System.out.println(" " + e.getMessage());
}
}
}
cd /root/demo && mvn -q exec:java -Dexec.mainClass=com.example.DuplicateCompare
实测输出:
xmlschema-core 读取:未抛异常,schema.getElements().size() = 1
注册的全局元素:{http://example.com/dup}dup -> {http://www.w3.org/2001/XMLSchema}string
JAXP SchemaFactory.newSchema:编译失败
sch-props-correct.2: A schema cannot contain two global components with the same name; this schema contains two occurrences of 'http://example.com/dup,dup'.
结论:建模层按 Map 语义去重(size=1,后值生效),规范校验交给 JAXP 才会发生(sch-props-correct.2)。依赖 xmlschema-core 做 schema 治理时,不能把「读取成功」当作「schema 合法」。
六、生产配置模板
6.1 解析不可信 XSD 的安全基线(推荐)
适用场景:服务端接收外部提交的 WSDL/XSD(企业服务总线、API 网关的 schema 导入功能等),内存 4GB 以上、普通磁盘即可。JVM 启动参数模板:
java -Dorg.apache.ws.commons.schema.remote.allowNetwork=false \
-Dorg.apache.ws.commons.schema.protectReadOnlyCollections=true \
-Dorg.apache.ws.commons.schema.maxImportDepth=32 \
-jar your-app.jar
逐项说明:remote.allowNetwork=false 一刀切禁止远程解析(外部 schema 应先入库再由自定义 resolver 从库加载);protectReadOnlyCollections=true 防止误改共享模型;maxImportDepth=32 收紧默认 64 的深度上限。若业务确需在线解析白名单域名的 schema,则不关网络,改设资源上限并保持地址校验:
java -Dorg.apache.ws.commons.schema.remote.connectTimeoutMillis=3000 \
-Dorg.apache.ws.commons.schema.remote.readTimeoutMillis=5000 \
-Dorg.apache.ws.commons.schema.remote.maxFetchMillis=10000 \
-Dorg.apache.ws.commons.schema.remote.maxBytes=2097152 \
-Dorg.apache.ws.commons.schema.remote.checkAddresses=true \
-jar your-app.jar
逐项说明:连接 3 秒、读 5 秒、单次 fetch 总长 10 秒、响应体上限 2MB(2097152 字节)、保持默认的地址黑名单校验;再配合自实现 resolver 做域名白名单(拒绝逻辑写在自己的 resolveEntity 里),形成「限额 + 校验 + 白名单」三层防护。checkAddresses 保持 true(默认值,勿关)。
6.2 CXF/Axis2 集成场景
与 CXF 同工程时,xmlschema-core 版本以 dependencyManagement 统一到 2.3.3;如需对齐 CXF 测试基线行为,设置 -Dorg.apache.ws.commons.schema.protectReadOnlyCollections=true(CXF 父 POM 的 cxf.protect-xmlschema-collections 属性即注入该系统属性,实测取自 CXF 4.1.4 POM)。Axis2 1.8.2 默认锁定 2.3.0,升级 Axis2 时同步核对该属性变量名。
七、常见问题
Q1:读取含相对 include 的 XSD 报找不到文件?
先确认 StreamSource 设置了 systemId(如 source.setSystemId(xsd.toURI().toString())),相对路径以 systemId 为基准解析;未设置时相对 include 无法定位。若仍失败,检查 local.allowFileSystem 是否被置为 false(报错消息会点名该属性,实测回显见 5.3)。
Q2:程序输出中文变成问号?
会话 locale 非 UTF-8 所致(实测复现:输出 == 1. ?????? ==)。执行 export LANG=C.UTF-8 LC_ALL=C.UTF-8 后重跑;systemd 托管的应用在单元文件的 Environment=LANG=C.UTF-8 注入(每行只写一个 Environment 项)。
Q3:重复的全局元素为什么读取不报错,JAXP 却报错?
职责不同:xmlschema-core 是建模库,全局组件存于 Map,同名键后值覆盖前值(实测 size=1);JAXP SchemaFactory 是规范校验器,直接报 sch-props-correct.2。需要严格校验 schema 本身时,两个工具都跑一遍,或只信 JAXP 的编译结果。
Q4:maxImportDepth 超限报错后怎么处理?
先确认 import 图是否真的需要那么深——正常业务 schema 链深个位数,超限通常意味着循环或恶意构造;确有合法深链时用 -Dorg.apache.ws.commons.schema.maxImportDepth=128 提高(同时留意 maxSchemaResolutions=1000 的总量上限),报错消息本身会提示属性名(实测回显见 5.1)。
Q5:write 出来的 XSD 与输入不一致是 bug 吗? 不是。序列化按模型重建文档:注释丢失、属性按字母序、默认值物化、命名空间声明提升(diff 实测见 5.5)。它是规范化输出,不是字节级回写;需要保真请另存原始文件。
八、参考链接
- javadoc(2.3.3):https://ws.apache.org/commons/xmlschema20/apidocs/
- 源码镜像:https://github.com/apache/ws-xmlschema
- Maven Central 构件页:https://repo1.maven.org/maven2/org/apache/ws/xmlschema/xmlschema-core/
- JAXP OutputKeys 常量:https://docs.oracle.com/javase/8/docs/api/javax/xml/transform/OutputKeys.html
- 相关文档:《Apache XMLSchema 介绍与选型指南》《Apache XMLSchema 在 Ubuntu 22.04 集成使用指南》