Apache CycloneDX Ant Library 0.2 常用 API 与任务详解
逐属性实测 Apache CycloneDX Ant Library 0.2 的任务与类型:componentbom 的 format、specVersion、serialNumber 等任务属性,component 的许可证、依赖图、外部引用等组件属性,以及 archivebom 宏、sbomLink 与 ivyModule 解析器的用法、默认值与报错原文,命令与回显均实测通过。
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>
总览如下:
| 名称 | 注册方式 | 作用 |
|---|---|---|
componentbom | taskdef(任务) | 生成 CycloneDX SBOM,核心入口 |
component | typedef(类型) | 声明一个组件,可嵌套在任务或其他组件里 |
organization | typedef(类型) | 组织信息,供 supplier/manufacturer 使用 |
license | typedef(类型) | 许可证,三种写法见 4.3 节 |
externalreferenceset | typedef(类型) | 外部引用集合 |
propertyset | typedef(类型) | 属性集合 |
fsonlymappedresources | typedef(类型) | 只映射文件系统资源的资源集合,供文件场景过滤 |
archivebom | macrodef(宏) | 面向「发布归档 + 内容清单」场景的封装 |
sbomLink | componentdef(解析器) | 引用另一份既有 SBOM 文件 |
ivyModule | componentdef(解析器) | 从 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 支持三种写法,实测如下:
- 省略(默认):每次生成随机 UUID。本文各产物文件头部的 serialNumber 互不相同(如
ac62f81f-…、afa28d69-…),即为默认随机行为的直接证据。 - 显式
random:等价于默认。serial-randomtarget 生成urn:uuid:a99997cf-4838-4220-aca2-ff6855c448d9,与同工程其他产物均不同。 - 固定值:
serial-fixedtarget 写死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 实测三个关键点:
- 被链接 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"}
- 主组件与 lib-util 的依赖边成立:
DEP: {"ref": "pkg:maven/com.example/sample-app@3.0.0", "dependsOn": ["pkg:maven/com.example/lib-util@2.1.0"]}
- 被链接 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 默认或 json | x.json |
bomName="x",format=xml | x.xml |
bomName="x",format=all | x.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 加显式依赖消除驱逐。完整复现与「零驱逐配方」见进阶篇第二章。
八、参考链接
- 官方手册(任务、类型、宏的权威说明):https://ant.apache.org/antlibs/cyclonedx/manual/
- Antlib 项目页与下载:https://ant.apache.org/antlibs/cyclonedx/
- 源码仓库:https://gitbox.apache.org/repos/asf/ant-antlibs-cyclonedx.git
- CycloneDX 规范主页:https://cyclonedx.org/
- Package URL 规范:https://github.com/package-url/purl-spec
- Apache Ivy 文档:https://ant.apache.org/ivy/
- 专栏内前置阅读:《Ubuntu 22.04 安装 Apache CycloneDX Ant Library 指南》《CentOS 8 安装 Apache CycloneDX Ant Library 指南》《Apache CycloneDX Ant Library 0.2 快速入门与集成示例》
- 专栏内进阶阅读:《Apache CycloneDX Ant Library 0.2 进阶实战详解》