Apache CycloneDX Ant Library 0.2 常用 API 与任务详解

逐属性实测 Apache CycloneDX Ant Library 0.2 的任务与类型:componentbom 的 format、specVersion、serialNumber 等任务属性,component 的许可证、依赖图、外部引用等组件属性,以及 archivebom 宏、sbomLink 与 ivyModule 解析器的用法、默认值与报错原文,命令与回显均实测通过。

Apache CycloneDX Ant LibraryApache CycloneDX Ant Library2026/10/15

Apache CycloneDX Ant Library 0.2 常用 API 与任务详解

目标:把 Apache CycloneDX Ant Library 0.2 提供的全部任务、类型与解析器逐个跑一遍,固定每个属性的默认值、取值范围、真实输出与报错原文,形成一份可直接对照使用的 API 速查手册;本文全部命令与回显均在 Ubuntu 22.04 实测通过。

本文是专栏的第三篇。安装步骤见《Ubuntu 22.04 安装 Apache CycloneDX Ant Library 指南》与《CentOS 8 安装 Apache CycloneDX Ant Library 指南》,首次生成 SBOM 的最小流程见《Apache CycloneDX Ant Library 0.2 快速入门与集成示例》;本文在其之上系统展开任务库本身的用法。Ivy 依赖解析的深度实战、可复现构建与 schema 校验门禁见《Apache CycloneDX Ant Library 0.2 进阶实战详解》。

一、环境信息

本节固定可复现的基线环境,后续所有命令均在此环境执行。

  • 操作系统:Ubuntu 22.04.5 LTS,x86_64
  • JDK:OpenJDK 17.0.20.1
  • 构建工具:Apache Ant 1.10.12
  • 组件版本:ant-cyclonedx 0.2(Maven 坐标 org.apache.ant:ant-cyclonedx:0.2)
  • Antlib 依赖目录:/opt/cyclonedx-antlib/lib(17 个 jar,装配方式见安装指南)
  • 实验工程目录:/root/cdx-lab(一个演示库 sample-lib 与一组演示 target,本文逐节引用)

系统与环境确认(实时回显):

head -3 /etc/os-release && uname -m
PRETTY_NAME="Ubuntu 22.04.5 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
x86_64
java -version 2>&1 | head -1
ant -version
openjdk version "17.0.20.1" 2026-08-18
Apache Ant(TM) version 1.10.12 compiled on January 17 1970

实验工程结构:src/com/example/demo/Sample.java 是一个仅含 greet() 方法的演示类,prepare target 负责编译并打成 dist/sample-lib-1.0.0.jar,各演示 target 依赖它:

package com.example.demo;

public class Sample {
    public static String greet() {
        return "sample-lib " + "OK";
    }
}

💡 说明:componentbom 任务运行时会在控制台打印一行 [cdx:componentbom] creating CycloneDX SBOM <输出文件路径>,本文大量利用这行日志来证明「默认值到底是什么」——比翻手册更直接。

二、任务与类型总览

本节先看 jar 里到底注册了哪些东西。antlib 的清单文件可以直接从安装目录的 jar 中提取查看(jar 是 JDK 自带工具):

mkdir -p /tmp/jx && cd /tmp/jx
jar xf /opt/cyclonedx-antlib/lib/ant-cyclonedx-0.2.jar org/apache/ant/cyclonedx/antlib.xml

提取出的 antlib.xml(节选自实测文件,保留核心结构):

<antlib xmlns:cdx="ant:current" xmlns:unless="ant:unless">
  <taskdef name="componentbom"
    classname="org.apache.ant.cyclonedx.ComponentBomTask"/>
  <typedef name="component"
    classname="org.apache.ant.cyclonedx.Component"/>
  <typedef name="organization"
    classname="org.apache.ant.cyclonedx.Organization"/>
  <typedef name="license"
    classname="org.apache.ant.cyclonedx.License"/>
  <typedef name="externalreferenceset"
    classname="org.apache.ant.cyclonedx.ExternalReferenceSet"/>
  <typedef name="propertyset"
    classname="org.apache.ant.cyclonedx.PropertySet"/>
  <typedef name="fsonlymappedresources"
    classname="org.apache.ant.cyclonedx.FilesystemOnlyMappedResourceCollection"/>

  <macrodef name="archivebom" backtrace="false">
    <!-- 属性与展开体见第五章 5.1 节 -->
  </macrodef>

  <!-- ComponentResolvers -->
  <componentdef
      name="sbomLink"
      classname="org.apache.ant.cyclonedx.SbomLinkComponentResolver"/>
  <componentdef
      name="ivyModule"
      classname="org.apache.ant.cyclonedx.IvyModuleComponentResolver"/>

</antlib>

总览如下:

名称注册方式作用
componentbomtaskdef(任务)生成 CycloneDX SBOM,核心入口
componenttypedef(类型)声明一个组件,可嵌套在任务或其他组件里
organizationtypedef(类型)组织信息,供 supplier/manufacturer 使用
licensetypedef(类型)许可证,三种写法见 4.3 节
externalreferencesettypedef(类型)外部引用集合
propertysettypedef(类型)属性集合
fsonlymappedresourcestypedef(类型)只映射文件系统资源的资源集合,供文件场景过滤
archivebommacrodef(宏)面向「发布归档 + 内容清单」场景的封装
sbomLinkcomponentdef(解析器)引用另一份既有 SBOM 文件
ivyModulecomponentdef(解析器)从 Ivy 依赖解析结果生成组件

属性取值范围可以用一段小代码从 jar 里直接枚举出来。把 DumpEnums4.java 放进 /root/cdx-lab 编译运行:

import org.apache.ant.cyclonedx.SpecVersion;
import org.apache.ant.cyclonedx.OutputFormat;
import org.apache.ant.cyclonedx.ComponentScope;
import org.apache.ant.cyclonedx.CompositionAggregate;

public class DumpEnums4 {
    public static void main(String[] args) {
        System.out.println("specVersion          = " + String.join(", ", new SpecVersion().getValues()));
        System.out.println("format               = " + String.join(", ", new OutputFormat().getValues()));
        System.out.println("scope                = " + String.join(", ", new ComponentScope().getValues()));
        System.out.println("compositionAggregate = " + String.join(", ", new CompositionAggregate().getValues()));
    }
}
cd /root/cdx-lab && javac -cp "/opt/cyclonedx-antlib/lib/*" DumpEnums4.java
java -cp "/opt/cyclonedx-antlib/lib/*:." DumpEnums4
specVersion          = VERSION_10, 1.0, VERSION_11, 1.1, VERSION_12, 1.2, VERSION_13, 1.3, VERSION_14, 1.4, VERSION_15, 1.5, VERSION_16, 1.6, VERSION_17, 1.7
format               = XML, xml, JSON, json, ALL, all
scope                = REQUIRED, required, OPTIONAL, optional, EXCLUDED, excluded
compositionAggregate = COMPLETE, complete, INCOMPLETE, incomplete, INCOMPLETE_FIRST_PARTY_ONLY, incomplete_first_party_only, INCOMPLETE_FIRST_PARTY_PROPRIETARY_ONLY, incomplete_first_party_proprietary_only, INCOMPLETE_THIRD_PARTY_ONLY, incomplete_third_party_only, INCOMPLETE_THIRD_PARTY_PROPRIETARY_ONLY, incomplete_third_party_proprietary_only, INCOMPLETE_THIRD_PARTY_OPENSOURCE_ONLY, incomplete_third_party_opensource_only, UNKNOWN, unknown, NOT_SPECIFIED, not_specified

💡 技术拓展:SpecVersion 这类枚举实现了 getValues() 接口,同时给出枚举名(VERSION_10)与用户写法(1.0)。在 build.xml 里写的是 1.0 这种用户形式;format 与 scope 同理,实测大小写均可(build.xml 中统一用小写)。

三、componentbom 任务属性详解

本章以 /root/cdx-lab/build.xml 中的演示 target 为准,逐个属性实测。任务级共 7 个属性:format、specVersion、serialNumber、bomName、outputDirectory、useComponentManufacturer、useComponentSupplier。

3.1 format:输出格式与文件命名

三个演示 target 分别不写 format(默认)、写 xml、写 all:

<target name="format-default" depends="prepare">
  <cdx:componentbom bomName="fmt-default" outputDirectory="bom">
    <cdx:component name="sample-lib" group="com.example" version="${v}"
                   purl="pkg:maven/com.example/sample-lib@${v}"/>
  </cdx:componentbom>
</target>

<target name="format-xml" depends="prepare">
  <cdx:componentbom format="xml" bomName="fmt-xml" outputDirectory="bom">
    <cdx:component name="sample-lib" group="com.example" version="${v}"
                   purl="pkg:maven/com.example/sample-lib@${v}"/>
  </cdx:componentbom>
</target>

<target name="format-all" depends="prepare">
  <cdx:componentbom format="all" bomName="fmt-all" outputDirectory="bom">
    <cdx:component name="sample-lib" group="com.example" version="${v}"
                   purl="pkg:maven/com.example/sample-lib@${v}"/>
  </cdx:componentbom>
</target>

format="all" 的运行回显(注意 XML 先写、JSON 后写,共两行日志):

ant format-all 2>&1 | grep creating
[cdx:componentbom] creating CycloneDX SBOM /root/cdx-lab/bom/fmt-all.xml
[cdx:componentbom] creating CycloneDX SBOM /root/cdx-lab/bom/fmt-all.json

format="xml" 只产出 fmt-xml.xml。产物目录核对:

ls -la bom/fmt-*
-rw-r--r-- 1 root root 5591 Oct 15 07:37 bom/fmt-all.json
-rw-r--r-- 1 root root 4284 Oct 15 07:37 bom/fmt-all.xml
-rw-r--r-- 1 root root 5591 Oct 15 07:37 bom/fmt-default.json
-rw-r--r-- 1 root root 4284 Oct 15 07:37 bom/fmt-xml.xml

实测结论:

  • format 取值 xml / json / all,默认 json(不带 format 的 target 产出 .json)。
  • 文件命名为 <outputDirectory>/<bomName>.json 与 <outputDirectory>/<bomName>.xml。
  • all 等价于 XML + JSON 各生成一份,XML 在前。

3.2 specVersion:规范版本与版本差异

默认值用产物直接验证——fmt-default.json 头部(实测):

head -4 bom/fmt-default.json
{
  "bomFormat" : "CycloneDX",
  "specVersion" : "1.7",
  "serialNumber" : "urn:uuid:ac62f81f-6d13-47f6-a1d6-a0747151bcab",

即 默认 specVersion 是 1.7(0.2 版本的主线规范)。合法取值为 1.0 到 1.7 共 8 档(见第二章枚举)。指定 1.4 与 1.6 均正常,spec-14.json 全文很短,完整收录:

{
  "bomFormat" : "CycloneDX",
  "specVersion" : "1.4",
  "serialNumber" : "urn:uuid:afa28d69-07d6-4bad-9ae3-7f621e9962e8",
  "version" : 1,
  "metadata" : {
    "timestamp" : "2026-10-15T06:42:44Z",
    "component" : {
      "type" : "library",
      "bom-ref" : "pkg:maven/com.example/sample-lib@1.0.0",
      "group" : "com.example",
      "name" : "sample-lib",
      "version" : "1.0.0",
      "purl" : "pkg:maven/com.example/sample-lib@1.0.0"
    }
  },
  "dependencies" : [
    {
      "ref" : "pkg:maven/com.example/sample-lib@1.0.0",
      "dependsOn" : [ ]
    }
  ]
}

版本差异有一处非常直观:用同一份工程分别生成 1.4 与 1.7,对比 metadata 的字段集合(实测):

spec-14 | metadata keys: ['component', 'timestamp']
spec-16 | metadata keys: ['component', 'lifecycles', 'timestamp', 'tools']
fmt-default(1.7) | metadata keys: ['component', 'lifecycles', 'timestamp', 'tools']

即 1.4 输出没有 tools 与 lifecycles,1.6/1.7 才有。工具自标识(tools)只在较新的规范版本上输出,如果下游工具链要消费工具组件信息,别降到 1.4。

两个失败边界(回显均为原文):

BUILD FAILED
/root/cdx-lab/build.xml:64: 2.0 is not a legal value for this attribute
BUILD FAILED
/root/cdx-lab/build-spec11json.xml:43: java.lang.IllegalArgumentException: CycloneDX version 1.1 does not support the JSON format
  • specVersion="2.0":超出枚举范围,属性级直接拒绝。
  • 1.1 + JSON:CycloneDX 1.1 规范没有 JSON 表示,运行期抛 IllegalArgumentException。
  • 1.1 + format="all" 不会报错,只会降级为仅生成 XML;此时依赖图以 1.1 时代的扩展命名空间表达(spec-11.xml 实测):
<?xml version="1.0" encoding="UTF-8"?>
<bom serialNumber="urn:uuid:3a028a91-f90b-4ffe-b296-4d30a96efe7a" version="1" xmlns="http://cyclonedx.org/schema/bom/1.1">
  <dg:dependencies xmlns:dg="http://cyclonedx.org/schema/ext/dependency-graph/1.0">
    <dg:dependency ref="pkg:maven/com.example/sample-lib@1.0.0"/>
  </dg:dependencies>
</bom>

💡 技术拓展:选版本的经验法则——新工程保持默认 1.7;只有当下游审计平台明确声明只支持旧版本时才降级,并注意 1.4 会丢失 tools/lifecycles 信息、1.1 没有 JSON。

3.3 bomName 与 outputDirectory:默认输出路径

把两个属性全部省略(bomname-default target):

<target name="bomname-default" depends="prepare">
  <cdx:componentbom>
    <cdx:component name="sample-lib" group="com.example" version="${v}"
                   purl="pkg:maven/com.example/sample-lib@${v}"/>
  </cdx:componentbom>
</target>

运行回显:

[cdx:componentbom] creating CycloneDX SBOM /root/cdx-lab/bom.json
ls -la /root/cdx-lab/bom.json
-rw-r--r-- 1 root root 4631 Oct 15 07:59 /root/cdx-lab/bom.json

实测结论:bomName 默认 bom,outputDirectory 默认 ${basedir}(构建文件所在目录),两者组合出 bom.json。显式指定时(3.1 节的 bomName="fmt-default" outputDirectory="bom")则落到 bom/fmt-default.json。bomName 里可以带点与连字符,5.1 节会利用这一点生成 xxx.cdx.json 双后缀命名。

3.4 serialNumber:随机与固定

serialNumber 支持三种写法,实测如下:

  1. 省略(默认):每次生成随机 UUID。本文各产物文件头部的 serialNumber 互不相同(如 ac62f81f-…、afa28d69-…),即为默认随机行为的直接证据。
  2. 显式 random:等价于默认。serial-random target 生成 urn:uuid:a99997cf-4838-4220-aca2-ff6855c448d9,与同工程其他产物均不同。
  3. 固定值:serial-fixed target 写死 urn:uuid:6f6c9cc3-0f9a-4a26-a9c2-51e7b0f2b3a1,连续跑两次,序列号逐字相同:
ant serial-fixed 2>&1 | tail -3
grep -h serialNumber /tmp/sf-before.json bom/serial-fixed.json
[cdx:componentbom] creating CycloneDX SBOM /root/cdx-lab/bom/serial-fixed.json

BUILD SUCCESSFUL
Total time: 1 second
  "serialNumber" : "urn:uuid:6f6c9cc3-0f9a-4a26-a9c2-51e7b0f2b3a1",
  "serialNumber" : "urn:uuid:6f6c9cc3-0f9a-4a26-a9c2-51e7b0f2b3a1",

💡 技术拓展:固定 serialNumber 是 0.2 相对 0.1 的新能力,配合环境变量 SOURCE_DATE_EPOCH(同时固定 serialNumber 与 timestamp)可实现逐字节可复现的 SBOM,完整五连测见《Apache CycloneDX Ant Library 0.2 进阶实战详解》第三章。

3.5 useComponentManufacturer 与 useComponentSupplier

这两个布尔开关把组件上的组织信息「提升」到 BOM 级 metadata 中。先看 manufacturer(use-manufacturer target):

<cdx:componentbom format="json" useComponentManufacturer="true"
                  bomName="use-manufacturer" outputDirectory="bom">
  <cdx:component name="sample-lib" group="com.example" version="${v}"
                 purl="pkg:maven/com.example/sample-lib@${v}">
    <cdx:manufacturer name="Example Corp">
      <url url="https://example.example.com"/>
    </cdx:manufacturer>
  </cdx:component>
</cdx:componentbom>

产物实测(提取关键字段):metadata.manufacturer 与 metadata.component.manufacturer 同时出现:

"metadata.manufacturer": {"name": "Example Corp", "url": ["https://example.example.com"]}
{
 "type": "library",
 "bom-ref": "pkg:maven/com.example/sample-lib@1.0.0",
 "manufacturer": {
  "name": "Example Corp",
  "url": [
   "https://example.example.com"
  ]
 },
 "group": "com.example",
 "name": "sample-lib",
 "version": "1.0.0",
 "purl": "pkg:maven/com.example/sample-lib@1.0.0"
}

开关打开但组件上没有 manufacturer 时直接构建失败(use-manufacturer-missing target 实测):

BUILD FAILED
/root/cdx-lab/build.xml:106: useComponentManufacturer is true but component manufacturer is null

supplier 同理(use-supplier target,同时演示任务级 cdx:license):

<cdx:componentbom format="json" useComponentSupplier="true"
                  bomName="use-supplier" outputDirectory="bom">
  <cdx:component name="sample-lib" group="com.example" version="${v}"
                 purl="pkg:maven/com.example/sample-lib@${v}">
    <cdx:supplier name="Example Distribution">
      <url url="https://dist.example.com"/>
    </cdx:supplier>
    <cdx:license licenseId="Apache-2.0"/>
  </cdx:component>
  <cdx:license licenseId="Apache-2.0"/>
</cdx:componentbom>

产物实测:metadata.supplier 与 metadata.licenses 均出现,而组件自身的 licenses 不受影响:

"metadata.supplier": {"name": "Example Distribution", "url": ["https://dist.example.com"]}
"metadata.licenses": [{"license": {"id": "Apache-2.0"}}]

注意区分两层 cdx:license 的落点(实测结论):写在 componentbom 任务下的 license 进入 metadata.licenses(BOM 级许可证);写在 cdx:component 下的 license 进入该组件的 licenses 字段。

3.6 任务级嵌套:toolComponent

任务级还可以放 cdx:toolComponent,向 metadata.tools.components 追加自研工具(tool-component target):

<cdx:componentbom format="json" bomName="tool-component" outputDirectory="bom">
  <cdx:component name="sample-lib" group="com.example" version="${v}"
                 purl="pkg:maven/com.example/sample-lib@${v}"/>
  <cdx:toolComponent name="sign-tool" group="com.example.tools" version="3.2.1"
                     purl="pkg:maven/com.example.tools/sign-tool@3.2.1"
                     type="application"/>
</cdx:componentbom>

产物中 metadata.tools.components 实测为两个元素,自研工具排在 antlib 自身之前:

[
 {"type": "application",
  "bom-ref": "pkg:maven/com.example.tools/sign-tool@3.2.1",
  "group": "com.example.tools", "name": "sign-tool", "version": "3.2.1",
  "purl": "pkg:maven/com.example.tools/sign-tool@3.2.1"},
 {"type": "library",
  "supplier": {"name": "Apache Ant Project Management Committee", "url": ["https://ant.apache.org/"]},
  "manufacturer": {"name": "Apache Ant Project Management Committee", "url": ["https://ant.apache.org/"]},
  "publisher": "The Apache Software Foundation",
  "group": "org.apache.ant", "name": "ant-cyclonedx", "version": "0.2",
  "description": "Apache CycloneDX Antlib",
  "hashes": [ "…MD5/SHA-1/SHA-256/SHA-512/SHA3-256/SHA3-512/SHA-384/SHA3-384 共 8 个…" ],
  "licenses": [{"license": {"id": "Apache-2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.txt"}}],
  "purl": "pkg:maven/org.apache.ant/ant-cyclonedx@0.2?type=jar",
  "externalReferences": [ "…vcs、issue-tracker、website、mailing-list、documentation、source-distribution、distribution、license、build-system、security-contact、rfc-9116 共 11 条…" ]}
]

antlib 每次都会把自己登记进 metadata.tools.components(名称、版本、8 个哈希、11 条外部引用全部自动填充),无需任何配置。上游审计平台若按「工具指纹」统计生成器版本,读这里即可。

3.7 additionalComponent 与 pureFileComponents

additionalComponent:向 components 数组追加附加组件。不带 id 时自成一体的实测(scope-optional target,scope 属性只有附加组件支持):

<cdx:additionalComponent name="gson" group="com.google.code.gson" version="2.10.1"
                         scope="optional"
                         purl="pkg:maven/com.google.code.gson/gson@2.10.1"/>
COMP: {"type": "library", "bom-ref": "pkg:maven/com.google.code.gson/gson@2.10.1", "group": "com.google.code.gson", "name": "gson", "version": "2.10.1", "scope": "optional", "purl": "pkg:maven/com.google.code.gson/gson@2.10.1"}

并且它同时生成了自己的依赖条目 DEP: {"ref": "pkg:maven/com.google.code.gson/gson@2.10.1", "dependsOn": []}。

pureFileComponents:把一批文件作为 file 类型组件登记(pure-files target):

<cdx:componentbom format="json" bomName="pure-files" outputDirectory="bom">
  <cdx:component name="sample-lib" group="com.example" version="${v}"
                 purl="pkg:maven/com.example/sample-lib@${v}"/>
  <cdx:pureFileComponents>
    <fileset dir="dist" includes="*.jar"/>
  </cdx:pureFileComponents>
</cdx:componentbom>

产物 components 数组实测(文件组件没有 purl 与 bom-ref,只有名字与哈希):

{"type": "file", "name": "sample-lib-1.0.0.jar", "hashes": [{"alg": "MD5", "content": "e61ff2370389e350a0a1bf0f4ccaae0a"}, {"alg": "SHA-1", "content": "c422ba6cc95fa5abc07835c0a3addc7266f54cf9"}, {"alg": "SHA-256", "content": "957a625dcb61360b3e34f2c6240f6f3b4f4ce24152a5ffe2fa28d6b32056062d"}, {"alg": "SHA-512", "content": "1c50ac55cf7abb783535677ba0791354209eb6b694e34d009c9cb253b91cd39e688858f2439dc98c954d4342d02128090542490ce7e6176471ea2b766c60d6be"}, {"alg": "SHA3-256", "content": "3d913d4f2c8c1e7351197b1c0958c3fb5752b48b89c6ceff29db66d05442731d"}, {"alg": "SHA3-512", "content": "64c08bfd8117061fab524174ebf23b81f0ea7fc750acfdfac298e1b9bb784e5aafac2fe466a9bad11ba12d1f9a04bb903d0319d276da4c930f9217458a54372f"}, {"alg": "SHA-384", "content": "cc78b6c8d9f5e667d9a884aa7845304db9aac64640f492a069051623c03b232874a6e4ad96b40670eda4eba4aeb54d99"}, {"alg": "SHA3-384", "content": "d5f355aa60883aca7f6b178d2bb07940b379b207ac0959454c7d02300144416b02ba17b741d25ea68036ad8f7b045f30"}]}

四、component 类型属性详解

cdx:component 是信息密度最高的类型。本章所有产物均为 1.7 JSON,主组件默认落在 metadata.component(只有一个组件时它就是 BOM 的「主体」),以下 JSON 均提取自该字段。

4.1 基础属性与 bom-ref 生成规则

基础属性:type、name、group、version、purl。type 不写时默认 library(3.2 节 spec-14.json 的 metadata.component 里 type 值为 library,target 里并未写 type)。bom-ref 的生成规则实测三种情况:

<!-- 情况一:显式 purl -->
<cdx:component name="sample-lib" group="com.example" version="${v}"
               purl="pkg:maven/com.example/sample-lib@${v}"/>
<!-- 情况二:只给 group + version,不给 purl(implicit-purl target) -->
<cdx:component name="sample-lib" group="com.example" version="${v}"/>
<!-- 情况三:只有 name(no-purl-no-version target) -->
<cdx:component name="sample-lib"/>

三者的 metadata.component 实测输出:

{"type": "library", "bom-ref": "pkg:maven/com.example/sample-lib@1.0.0", "group": "com.example", "name": "sample-lib", "version": "1.0.0", "purl": "pkg:maven/com.example/sample-lib@1.0.0"}
{"type": "library", "bom-ref": "pkg:maven/com.example/sample-lib@1.0.0?type=jar", "group": "com.example", "name": "sample-lib", "version": "1.0.0", "purl": "pkg:maven/com.example/sample-lib@1.0.0?type=jar"}
{"type": "library", "name": "sample-lib"}

规则总结:显式 purl 原样作为 bom-ref;没有 purl 但给了 group+version 时会自动合成 purl 并追加 ?type=jar 后缀(library 类型的默认打包类型);只有 name 时既无 purl 也无 bom-ref。

💡 技术拓展:?type=jar 是 purl 规范的限定符语法,用来区分同一构件的不同打包形态。自动合成只覆盖 Maven 风格坐标;非 Maven 组件请显式给 purl(如 pkg:generic/...)。

4.2 描述性属性:publisher/description/copyright/mimeType/author

comp-attrs target 一次覆盖五个属性(注意 mimeType 是驼峰属性名):

<cdx:component name="sample-lib" group="com.example" version="${v}"
               type="library"
               purl="pkg:maven/com.example/sample-lib@${v}"
               publisher="Example Corp"
               copyright="Copyright 2026 Example Corp"
               mimeType="application/java-archive"
               description="Component attribute demo">
  <cdx:author name="Zhang San" email="zhangsan@example.com"/>
</cdx:component>

实测输出(逐字段对齐,注意 JSON 键名 mime-type 与 authors 数组):

{
 "type": "library",
 "mime-type": "application/java-archive",
 "bom-ref": "pkg:maven/com.example/sample-lib@1.0.0",
 "authors": [
  {
   "name": "Zhang San",
   "email": "zhangsan@example.com"
  }
 ],
 "publisher": "Example Corp",
 "group": "com.example",
 "name": "sample-lib",
 "version": "1.0.0",
 "description": "Component attribute demo",
 "copyright": "Copyright 2026 Example Corp",
 "purl": "pkg:maven/com.example/sample-lib@1.0.0"
}

4.3 license 的三种写法

license 类型支持表达式、名称+URL、SPDX 编号三种写法,产物分别实测:

<!-- 写法一:SPDX 表达式(expression 属性) -->
<cdx:license expression="(Apache-2.0 OR MIT)"/>
"licenses": [ { "expression": "(Apache-2.0 OR MIT)" } ]
<!-- 写法二:自定义名称 + 嵌套 url 元素 -->
<cdx:license name="Custom License">
  <url url="https://example.com/licenses/custom"/>
</cdx:license>
"licenses": [ { "license": { "name": "Custom License", "url": "https://example.com/licenses/custom" } } ]
<!-- 写法三:SPDX 编号,可写多个 -->
<cdx:license licenseId="Apache-2.0"/>
<cdx:license licenseId="MIT"/>
"licenses": [ { "license": { "id": "Apache-2.0" } }, { "license": { "id": "MIT" } } ]

三者互斥于同一个 cdx:license 元素内(一个元素只能选一种),但同一组件可挂多个 cdx:license。licenseId 必须是合法 SPDX 编号,输出时只有 id;URL 是 SPDX 官方许可证列表地址的回填仅在 Ivy 描述符来源场景出现(见 5.3 节 gson 的例子)。

4.4 tag、property 与 propertySet:排序行为

tag-property target 声明顺序故意打乱:

<cdx:tag>release</cdx:tag>
<cdx:tag>lts</cdx:tag>
<cdx:property name="build.timestamp" value="2026-10-15"/>
<cdx:propertySet>
  <property name="scm.branch" value="main"/>
  <property name="build.host" value="runner-01"/>
</cdx:propertySet>

实测输出——tags 按字典序、properties 按名称排序,propertySet 与 property 合并进同一个 properties 数组:

"properties": [
 { "name": "build.host", "value": "runner-01" },
 { "name": "build.timestamp", "value": "2026-10-15" },
 { "name": "scm.branch", "value": "main" }
],
"tags": [ "lts", "release" ]

声明顺序是 release, lts、build.timestamp 然后 propertySet 里的 scm.branch/build.host,输出全部重排。不要依赖声明顺序,需要固定输出时把名字取得有序。

4.5 externalReference 与 externalReferenceSet

ext-refs target 混用单条引用与引用集合:

<cdx:externalReference type="website" url="https://example.com/sample-lib"/>
<cdx:externalReference type="vcs" url="https://git.example.com/sample-lib.git"/>
<cdx:externalReferenceSet>
  <externalReference type="issue-tracker" url="https://bugs.example.com/sample-lib"/>
  <externalReference type="documentation" url="https://docs.example.com/sample-lib"/>
</cdx:externalReferenceSet>

实测输出——两条单引用与集合内两条合并成同一个 externalReferences 数组,且顺序被重排为 vcs → issue-tracker → website → documentation:

"externalReferences": [
 { "type": "vcs", "url": "https://git.example.com/sample-lib.git" },
 { "type": "issue-tracker", "url": "https://bugs.example.com/sample-lib" },
 { "type": "website", "url": "https://example.com/sample-lib" },
 { "type": "documentation", "url": "https://docs.example.com/sample-lib" }
]

声明顺序是 website → vcs →(集合)issue-tracker → documentation,输出既不是声明顺序也不是字典序;与 3.6 节 antlib 自身 11 条外部引用的开头次序(vcs、issue-tracker、website…)一致,可判断为库内部按固定的类型优先级排列。type 取值遵循 CycloneDX 规范枚举(website/vcs/issue-tracker/documentation 等均实测通过)。

4.6 supplier/manufacturer 组织与 supplierIsManufacturer

组织类型(organization)的 url 必须是嵌套元素而不是属性。错误写法实测报错:

BUILD FAILED
/root/cdx-lab/probe-urlattr.xml:6: cdx:supplier doesn't support the "url" attribute

正确写法与产物(orgs target,supplierIsManufacturer="true" 让 supplier 同时充当 manufacturer):

<cdx:component name="sample-lib" group="com.example" version="${v}"
               purl="pkg:maven/com.example/sample-lib@${v}"
               supplierIsManufacturer="true">
  <cdx:supplier name="Example Distribution">
    <url url="https://dist.example.com"/>
  </cdx:supplier>
  <cdx:license licenseId="Apache-2.0"/>
</cdx:component>
{
 "type": "library",
 "bom-ref": "pkg:maven/com.example/sample-lib@1.0.0",
 "supplier": {
  "name": "Example Distribution",
  "url": [
   "https://dist.example.com"
  ]
 },
 "manufacturer": {
  "name": "Example Distribution",
  "url": [
   "https://dist.example.com"
  ]
 },
 "group": "com.example",
 "name": "sample-lib",
 "version": "1.0.0",
 "licenses": [
  {
   "license": {
    "id": "Apache-2.0"
   }
  }
 ],
 "purl": "pkg:maven/com.example/sample-lib@1.0.0"
}

两个组织字段分开声明的效果见 3.5 节:supplier 与 manufacturer 各自独立输出。任务描述里推荐的分工是——supplier 表示「谁分发」,manufacturer 表示「谁制造」,发布方与制造方相同时用 supplierIsManufacturer="true" 一份声明填两处。

4.7 scope、isExternal 与 unknownDependencies

三个属性都只对附加组件有意义(unknown-deps target):

<cdx:additionalComponent name="native-runtime" group="com.example"
                         version="9.9.9" type="operating-system"
                         isExternal="true" unknownDependencies="true"
                         purl="pkg:generic/com.example/native-runtime@9.9.9"/>

实测输出:isExternal 落盘为 "isExternal": true,type="operating-system" 正常输出,scope 取值 required/optional/excluded(第二章枚举实测,scope="optional" 的落盘见 3.7 节):

{"type": "operating-system", "bom-ref": "pkg:generic/com.example/native-runtime@9.9.9", "isExternal": true, "group": "com.example", "name": "native-runtime", "version": "9.9.9", "purl": "pkg:generic/com.example/native-runtime@9.9.9"}

两个如实记录的观察:

  • unknownDependencies="true" 在 1.7 JSON 里没有对应输出字段(产物中搜不到任何标记)。
  • 带 unknownDependencies="true" 的附加组件不会生成自己的 dependencies 条目(对比 3.7 节 gson 附加组件生成了空 dependsOn 条目)。

💡 技术拓展:CycloneDX 规范中「依赖未知」在 dependencies 数组里本就没有直接表达(空 dependsOn 表达的是「已知无依赖」),antlib 的处理是不落盘、不建边,把判断留给下游。审计口径上若必须区分「无依赖」与「依赖未知」,需要靠流程约定而非这份 JSON。

4.8 dependency、composition 与嵌套组件

dependency 用 componentRef 指向带 id 的 additionalComponent(dep-graph target):

<cdx:component name="sample-lib" group="com.example" version="${v}"
               purl="pkg:maven/com.example/sample-lib@${v}"
               compositionAggregate="complete">
  <dependency componentRef="gson"/>
  <dependency componentRef="slf4j"/>
</cdx:component>
<cdx:additionalComponent id="gson" name="gson" group="com.google.code.gson"
                         version="2.10.1"
                         purl="pkg:maven/com.google.code.gson/gson@2.10.1"/>
<cdx:additionalComponent id="slf4j" name="slf4j-api" group="org.slf4j"
                         version="2.0.17"
                         purl="pkg:maven/org.slf4j/slf4j-api@2.0.17"/>

产物 dependencies 数组实测——主组件的 dependsOn 填充为两个附加组件的 purl:

DEP: {"ref": "pkg:maven/com.example/sample-lib@1.0.0", "dependsOn": ["pkg:maven/com.google.code.gson/gson@2.10.1", "pkg:maven/org.slf4j/slf4j-api@2.0.17"]}
DEP: {"ref": "pkg:maven/com.google.code.gson/gson@2.10.1", "dependsOn": []}
DEP: {"ref": "pkg:maven/org.slf4j/slf4j-api@2.0.17", "dependsOn": []}

compositionAggregate 落在 BOM 根级而不是组件上——这是最容易想错的落点,实测 compositions 数组:

"compositions": [
 {
  "aggregate": "complete",
  "dependencies": [
   "pkg:maven/com.example/sample-lib@1.0.0"
  ]
 }
]

嵌套组件(nested-component target):component 内再嵌 component,落在主组件的 components 字段:

{
 "type": "library",
 "bom-ref": "pkg:maven/com.example/sample-lib@1.0.0",
 "group": "com.example",
 "name": "sample-lib",
 "version": "1.0.0",
 "purl": "pkg:maven/com.example/sample-lib@1.0.0",
 "components": [
  {
   "type": "library",
   "bom-ref": "pkg:maven/com.example/sample-core@1.0.0",
   "group": "com.example",
   "name": "sample-core",
   "version": "1.0.0",
   "purl": "pkg:maven/com.example/sample-core@1.0.0"
  }
 ]
}

同时顶层 dependencies 数组会出现嵌套组件自己的条目(DEP: {"ref": "pkg:maven/com.example/sample-core@1.0.0", "dependsOn": []},与主组件条目按 ref 字典序排列)。

五、archivebom 宏与组件解析器

5.1 archivebom:发布归档一键生成

archivebom 是 antlib 内置的宏(macrodef),面向「一个发布归档 + 归档内文件清单」的场景。宏定义(节选自 jar 内 antlib.xml,属性默认值是重点):

<macrodef name="archivebom" backtrace="false">
  <attribute name="specVersion" default="1.7"/>
  <attribute name="format" default="json"/>
  <attribute name="serialNumber" default="random"/>
  <attribute name="bomName" default="bom"/>
  <attribute name="outputDirectory" default="${basedir}"/>
  <attribute name="useComponentManufacturer" default="true"/>
  <attribute name="useComponentSupplier" default="true"/>

  <attribute name="componentType" default="file"/>
  <attribute name="archiveVersion"/>
  <attribute name="purl"/>
  <attribute name="description"/>
  <attribute name="publisher"/>
  <attribute name="supplierIsManufacturer" default="true"/>

  <attribute name="archiveLocation" default=""/>

  <element name="archiveContent"/>
  <element name="additionalComponentChildren" optional="true"/>
  <element name="additionalBomContent" optional="true"/>
  <sequential>
    <!-- 展开为 componentbom + component + pureFileComponents,此处省略 -->
  </sequential>
</macrodef>

要点逐条说:

  • 前排 7 个属性与 componentbom 同名同义,默认值也一致(specVersion 1.7、format json、serialNumber random、bomName bom、outputDirectory ${basedir})。
  • archiveVersion、purl、description、publisher 四个属性没有默认值,必须显式给。
  • supplierIsManufacturer 默认 true,但 useComponentManufacturer 也默认 true——两者叠加意味着必须提供 supplier,否则报错(实测):
BUILD FAILED
/root/cdx-lab/probe-archive-nosupplier.xml:10: component without supplier can't use supplier as manufacturer
  • 解法是把 supplier 放进 additionalComponentChildren 元素(它会被注入主组件内部)。
  • 主组件的 name 会被宏写成 purl 的值(这是宏的固定行为),产物实测:"name": "pkg:maven/com.example/sample-lib-dist@1.0.0?type=tar.gz"。
  • archiveContent 里的文件集走 pureFileComponents,成为 file 类型组件并带 8 哈希。

完整可运行示例(build-resolvers.xml,注意 taskdef 不带 classpath,改用 ant -lib 加载,原因见第七章 Q1):

<target name="archive-macro" depends="tarball">
  <cdx:archivebom bomName="sample-lib-${v}-dist.cdx"
                  outputDirectory="bom"
                  format="all"
                  purl="pkg:maven/com.example/sample-lib-dist@${v}?type=tar.gz"
                  archiveVersion="${v}"
                  description="Sample library distribution archive"
                  publisher="Example Corp">
    <additionalComponentChildren>
      <supplier name="Example Corp"><url url="https://example.example.com"/></supplier>
      <license licenseId="Apache-2.0"/>
    </additionalComponentChildren>
    <archiveContent>
      <fileset dir="dist" includes="*.jar"/>
    </archiveContent>
    <additionalBomContent>
      <license licenseId="Apache-2.0"/>
    </additionalBomContent>
  </cdx:archivebom>
</target>

运行回显与产物(bomName 带 .cdx 后缀形成双后缀命名):

ant -lib /opt/cyclonedx-antlib/lib -f build-resolvers.xml archive-macro 2>&1 | grep -E "creating|BUILD"
[cdx:componentbom] creating CycloneDX SBOM /root/cdx-lab/bom/sample-lib-1.0.0-dist.cdx.xml
[cdx:componentbom] creating CycloneDX SBOM /root/cdx-lab/bom/sample-lib-1.0.0-dist.cdx.json
BUILD SUCCESSFUL

主组件与任务级元数据实测:type=file、supplier 与 manufacturer 均为 Example Corp(supplierIsManufacturer 生效)、metadata.licenses 为 Apache-2.0(additionalBomContent 贡献),components 数组内是 tar 包里的 jar 文件(8 哈希,与 3.7 节 pure-files 的哈希完全一致,同一文件)。

5.2 sbomLink:引用既有 SBOM

sbomLink 是组件解析器(componentdef):给组件挂上另一份 SBOM 文件,antlib 解析该文件并把其中的组件并入当前 BOM。典型两步走(先给库生成 SBOM,再在应用里引用):

<target name="lib-sbom" depends="prepare">
  <cdx:componentbom format="json" bomName="linked-lib" outputDirectory="bom">
    <cdx:component name="linked-lib" group="com.example" version="2.0.0"
                   purl="pkg:maven/com.example/linked-lib@2.0.0"
                   description="Library described by its own SBOM">
      <file file="dist/sample-lib-${v}.jar"/>
      <license licenseId="Apache-2.0"/>
      <tag>library</tag>
      <dependency componentRef="lib-util"/>
    </cdx:component>
    <cdx:additionalComponent id="lib-util" name="lib-util" group="com.example" version="2.1.0"
                             purl="pkg:maven/com.example/lib-util@2.1.0"/>
  </cdx:componentbom>
</target>

<target name="sbom-link" depends="lib-sbom">
  <cdx:componentbom format="json" bomName="sbom-link" outputDirectory="bom">
    <cdx:component name="sample-app" group="com.example" version="3.0.0"
                   purl="pkg:maven/com.example/sample-app@3.0.0"
                   description="Application reusing the library SBOM">
      <dependency bomRef="pkg:maven/com.example/lib-util@2.1.0"/>
      <cdx:sbomLink>
        <file file="bom/linked-lib.json"/>
      </cdx:sbomLink>
    </cdx:component>
  </cdx:componentbom>
</target>

产物 sbom-link.json 实测三个关键点:

  1. 被链接 SBOM 的附加组件 lib-util 被拉进了应用的 components 数组:
COMP: {"type": "library", "bom-ref": "pkg:maven/com.example/lib-util@2.1.0", "group": "com.example", "name": "lib-util", "version": "2.1.0", "purl": "pkg:maven/com.example/lib-util@2.1.0"}
  1. 主组件与 lib-util 的依赖边成立:
DEP: {"ref": "pkg:maven/com.example/sample-app@3.0.0", "dependsOn": ["pkg:maven/com.example/lib-util@2.1.0"]}
  1. 被链接 SBOM 主组件的 licenses 与 tags 联动到了应用主组件上(应用 target 里并没有声明这些):
{
 "type": "library",
 "bom-ref": "pkg:maven/com.example/sample-app@3.0.0",
 "group": "com.example",
 "name": "sample-app",
 "version": "3.0.0",
 "description": "Application reusing the library SBOM",
 "licenses": [
  {
   "license": {
    "id": "Apache-2.0"
   }
  }
 ],
 "purl": "pkg:maven/com.example/sample-app@3.0.0",
 "tags": [
  "library"
 ]
}

三个必踩的坑,报错原文均实测:

  • 解析器必须带 cdx: 前缀。componentdef 注册的类型不走普通嵌套元素查找,写成 <sbomLink> 报:
BUILD FAILED
/root/cdx-lab/probe-sbomlink-noprefix.xml:6: cdx:component doesn't support the nested "sbomLink" element.
  • dependency 指向的组件必须存在于被链接 SBOM 的 components 数组。指向不存在的 bom-ref 报:
BUILD FAILED
/root/cdx-lab/probe-sbomlink-unknowndep.xml:6: dependency 'pkg:maven/com.example/nonexistent@9.9.9' is unknown
  • 链接场景的 dependency 用 bomRef 属性,不能用 componentRef。componentRef 只认本任务内带 id 的 additionalComponent,误用报:
BUILD FAILED
/root/cdx-lab/probe-sbomlink-compref.xml:6: Reference lib-util not found.

5.3 ivyModule:从 Ivy 依赖解析生成组件

ivyModule 是另一个组件解析器:放在 cdx:component 内部,把最近一次 ivy:resolve 的结果变成该组件的依赖子组件。前置条件有两个——单独加载 Ivy 的 antlib、先执行 resolve(build-resolvers.xml 头部与 ivy-resolve target):

<taskdef uri="antlib:org.apache.ant.cyclonedx"
         resource="org/apache/ant/cyclonedx/antlib.xml"/>

<taskdef uri="antlib:org.apache.ivy.ant"
         resource="org/apache/ivy/ant/antlib.xml"/>
<target name="ivy-resolve">
  <ivy:resolve/>
  <ivy:retrieve pattern="lib/[artifact]-[revision].[ext]" conf="compile"/>
</target>

<target name="ivy-module" depends="ivy-resolve">
  <cdx:componentbom format="json" bomName="ivy-module" outputDirectory="bom">
    <cdx:component type="application" name="notes-service" group="com.example" version="1.0.0">
      <cdx:ivyModule conf="compile"/>
    </cdx:component>
  </cdx:componentbom>
</target>

用最小 Ivy 描述符(ivy-gson.xml,单依赖)实测:

<?xml version="1.0" encoding="UTF-8"?>
<ivy-module version="2.0">
  <info organisation="com.example" module="gson-only" revision="1.0.0"/>
  <configurations>
    <conf name="compile"/>
  </configurations>
  <dependencies>
    <dependency org="com.google.code.gson" name="gson" rev="2.10.1" conf="compile->default"/>
  </dependencies>
</ivy-module>

产物 ivy-module-gson.json 实测(gson 组件由 Ivy 元数据富化,信息量远超手写 additionalComponent):

{"type": "library", "bom-ref": "pkg:maven/com.google.code.gson/gson@2.10.1?type=jar", "group": "com.google.code.gson", "name": "gson", "version": "2.10.1", "hashes": [ "…MD5=df6097815738cb31fc56391553210843 等 8 个算法…" ], "licenses": [{"license": {"id": "Apache-2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.txt"}}], "purl": "pkg:maven/com.google.code.gson/gson@2.10.1?type=jar", "externalReferences": [{"type": "website", "url": ""}, {"type": "license", "url": "https://www.apache.org/licenses/LICENSE-2.0.txt"}]}

依赖边同时生成(注意主组件没有显式 purl,bom-ref 也被合成了 ?type=jar 后缀):

DEP: {"ref": "pkg:maven/com.example/notes-service@1.0.0?type=jar", "dependsOn": ["pkg:maven/com.google.code.gson/gson@2.10.1?type=jar"]}
DEP: {"ref": "pkg:maven/com.google.code.gson/gson@2.10.1?type=jar", "dependsOn": []}

三个实测细节值得记住:

  • 哈希来源:优先取 ivy:retrieve 落盘的 jar(lib/ 目录),未 retrieve 时回退 Ivy 缓存目录中的构件——所以「不 retrieve 也有哈希」,多配置与驱逐场景的深度行为见进阶篇。
  • externalReferences 会出现 "url": "" 的空 website 引用:Ivy 模块描述符没有 homepage 属性时如此,属真实行为,下游解析器要容忍空 URL。
  • conf 属性选 Ivy 配置;多配置(compile/test/optional)、optional 依赖的 scope 落盘、templateComponent 富化等内容见《Apache CycloneDX Ant Library 0.2 进阶实战详解》第二章。

六、输出行为细节

6.1 哈希:8 种算法与固定顺序

只要组件挂了 file(或走 pureFileComponents/ivyModule),输出固定 8 个哈希,算法集合为 MD5、SHA-1、SHA-256、SHA-384、SHA-512、SHA3-256、SHA3-384、SHA3-512。输出顺序并非算法名的字典序,实测 JSON 中的固定次序是:

MD5 → SHA-1 → SHA-256 → SHA-512 → SHA3-256 → SHA3-512 → SHA-384 → SHA3-384

SHA3 系列依赖 Java 9+ 的 MessageDigest 实现,这也是安装指南强制 JDK 17 的原因(JDK 8 实测报 SHA3-256 算法不可用)。

6.2 排序与去重小结

实测中发生重排的字段:tags(字典序)、properties(按 name 排序)、externalReferences(库内部固定类型优先级)、dependencies 数组(按 ref 字典序)。不重排的字段:licenses 保持声明顺序(4.3 节 Apache-2.0 在前 MIT 在后原样输出)、components 数组基本按声明顺序、metadata.tools.components 自研工具在前 antlib 在后。

6.3 文件命名汇总

场景输出
bomName="x",format 默认或 jsonx.json
bomName="x",format=xmlx.xml
bomName="x",format=allx.xml + x.json(XML 先写)
bomName 省略基于 ${basedir} 的 bom.json
bomName="x.cdx"x.cdx.json / x.cdx.xml(CycloneDX 官方推荐的双后缀风格)

七、常见问题与报错速查

Q1:两个 taskdef 各带 classpath 时 ivyModule 报 ClassCastException

taskdef 内嵌 classpath 会各建一个 AntClassLoader,Ivy 的 ResolveReport 类被加载两份,跨加载器赋值即失败。实测原文:

/root/cdx-lab/probe-cce.xml:18: java.lang.ClassCastException: class org.apache.ivy.core.report.ResolveReport cannot be cast to class org.apache.ivy.core.report.ResolveReport (org.apache.ivy.core.report.ResolveReport is in unnamed module of loader org.apache.tools.ant.AntClassLoader @7e0e6aa2; org.apache.ivy.core.report.ResolveReport is in unnamed module of loader org.apache.tools.ant.AntClassLoader @1d16f93d)

解法:所有 taskdef 一律不写 classpath,改用 ant -lib /opt/cyclonedx-antlib/lib 在同一加载器里共享全部 jar。本文所有 build-resolvers.xml 场景都按此方式运行。

Q2:sbomLink/ivyModule 报「不支持的嵌套元素」

componentdef 注册的解析器必须以 cdx: 前缀使用(<cdx:sbomLink>、<cdx:ivyModule>),报错原文见 5.2 节。Ivy 的 antlib 还需要单独的 taskdef(antlib:org.apache.ivy.ant),它不随 cyclonedx antlib 自动加载。

Q3:useComponentManufacturer/useComponentSupplier 打开后构建失败

开关为 true 时对应组件必须已声明 manufacturer/supplier,缺失即失败(原文见 3.5 节)。解法:补组织声明,或关闭开关。

Q4:specVersion 与 format 的非法组合

超范围版本报 2.0 is not a legal value for this attribute;1.1 配 JSON 抛 CycloneDX version 1.1 does not support the JSON format;1.1 配 all 静默降级为仅 XML。原文见 3.2 节。

Q5:archivebom 报 supplier 相关错误

component without supplier can't use supplier as manufacturer(原文见 5.1 节)。宏默认要求 supplier 存在,在 additionalComponentChildren 里补 <supplier> 即可。

Q6:ivyModule 运行期空指针

当 Ivy 解析报告里存在被仲裁驱逐(evicted)的模块时,ivyModule 内部会触发空指针,工程上的解法是用 exclude 加显式依赖消除驱逐。完整复现与「零驱逐配方」见进阶篇第二章。

八、参考链接