Apache XMLSchema 2.3.3 配置参数详解

逐项实测 Apache XMLSchema 2.3.3 的全部配置项——导入深度与解析次数上限、网络与本地解析开关、只读集合保护、序列化输出选项与自定义 URIResolver,附安全基线 JVM 参数模板,全部在 Ubuntu 22.04 实测通过。

Apache XMLSchemaApache XMLSchemaJavaXMLUbuntuApache2026/10/16

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.maxImportDepth64xsd:import/include 解析栈最大深度行为实测(5.1)
org.apache.ws.commons.schema.maxSchemaResolutions1000单次顶层 read 的最大解析次数源码实测(见第三章)
org.apache.ws.commons.schema.remote.allowNetworktrue是否允许对远程 schemaLocation 发起网络请求行为实测(5.2)
org.apache.ws.commons.schema.remote.checkAddressestrue取远程 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.allowFileSystemtrue是否允许从文件系统读取 schemaLocation行为实测(5.3)
org.apache.ws.commons.schema.protectReadOnlyCollectionsfalsegetElements() 等返回的集合是否运行时只读化行为实测(5.4)

序列化输出默认选项(API 形态,源码实测 loadDefaultOptions):

输出属性(javax.xml.transform.OutputKeys)默认值作用
OMIT_XML_DECLARATIONyes输出不带 XML 声明
INDENTyes四空格缩进

💡 说明:网络限额类参数(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)。它是规范化输出,不是字节级回写;需要保真请另存原始文件。

八、参考链接