Apache Whisker 0.2-SNAPSHOT License 审计与生成器实战详解

深度实测 Apache Whisker 0.2-SNAPSHOT 的元数据模型、CLI 三命令与 Maven 插件,含 JDK 17 兼容缺陷的字节码级根因分析、NOTICE 生成行为差异、真实报错汇编与维护状态评估,全部结论以实测回显为证。

Apache WhiskerApache Whisker2026/10/15

Apache Whisker 0.2-SNAPSHOT License 审计与生成器实战详解

目标:对 Apache Creadur 家族的许可证合规审计工具 Apache Whisker 做一次完整实测拆解——元数据模型、--generate/--audit/--skeleton 三个命令、Maven 插件集成、JDK 17 兼容缺陷的根因与修复、真实报错行为与维护状态评估;实测版本为 0.2-SNAPSHOT(cli 快照编号 20261014.173522-582),全部命令与回显均在 Ubuntu 22.04 实测通过。

Apache Whisker 解决一个特殊问题:当应用由多个来源的组件组装而成时,发行版随附的 LICENSE 与 NOTICE 文档如何与"实际打进来的资源"保持一致。它的思路不是从源码注释反推(那是同门 Apache Rat 的工作),而是让维护者用一份 XML 元数据描述符声明「每个资源由谁、以什么许可证提供」,再由工具从描述符生成对外法律文档,或反向审计发行目录与描述符的偏差。本文按「版本事实 → 元数据模型 → 命令行 → 三大命令实战 → 错误行为 → 插件 → 兼容性根因 → 维护状态」的顺序展开,每一步都给出实测回显。

一、环境信息

本节固定实测基线,下文所有回显均产生于此环境。

  • 操作系统:Ubuntu 22.04.5 LTS(x86_64)
  • JDK:OpenJDK 17.0.20.1(apt openjdk-17-jdk-headless)与 OpenJDK 1.8.0_504(Temurin eclipse-temurin:8-jdk 级别的发行版,用于兼容性对照)
  • Maven:3.6.3(apt)与 3.9.9(用于插件实测对照)
  • 组件版本:org.apache.creadur.whisker:apache-whisker-cli:0.2-SNAPSHOT(快照编号 20261014.173522-582,sha1 a107b429d7a995c68da71de45f69b61b35a17a5e,4816276 字节)
  • 测试目录:/root/whisker(jar 与描述符)、/root/whisker/app(演示发行目录)、/root/plugin-demo(插件工程)

演示发行目录结构(与文中描述符对应):

/root/whisker/app
├── readme.txt
├── run.sh
└── alpha
    ├── five.js
    └── six.rb

💡 说明:Whisker 无常驻进程、无监听端口、无系统配置,"环境"仅指 JDK 与 jar 两个文件,验证成本极低。

二、版本与维护状态实测

本节回答「实测版本从哪来」——这是 Whisker 与常见组件最不一样的地方。

Maven Central 查询为零。 双渠道核实(搜索接口与目录路径),2026-10-15 实测:

curl -4 -s --max-time 30 "https://search.maven.org/solrsearch/select?q=g:%22org.apache.creadur.whisker%22&rows=40&wt=json" | grep -o '"numFound":[0-9]*'
curl -4 -s -o /dev/null -w "%{http_code}\n" --max-time 30 https://repo1.maven.org/maven2/org/apache/creadur/whisker/
"numFound":0
404

即任何 org.apache.creadur.whisker:whisker-* 或 apache-whisker-app 的中央仓库坐标都不存在,引用此类坐标的配置一律无法构建。

Apache 快照仓库是唯一 Maven 渠道。 org.apache.creadur.whisker 组下有 apache-whisker、apache-whisker-app、apache-whisker-cli、apache-whisker-maven-plugin、apache-whisker-model、apache-whisker-plugin4maven、apache-whisker-scan、apache-whisker-velocity、apache-whisker-xml 九个构件,全部只有 0.2-SNAPSHOT 一个版本线(app 模块元数据实时回显):

<?xml version="1.0" encoding="UTF-8"?>
<metadata>
  <groupId>org.apache.creadur.whisker</groupId>
  <artifactId>apache-whisker-app</artifactId>
  <versioning>
    <latest>0.2-SNAPSHOT</latest>
    <release></release>
    <versions>
      <version>0.2-SNAPSHOT</version>
    </versions>
    <lastUpdated>20261014173522</lastUpdated>
  </versioning>
</metadata>

快照由持续集成每日重建,<lastUpdated> 与最新快照编号一致(cli 当日最新为 20261014.173522-582,app 当日最新为 20261014.173522-2441)。

维护状态画像:源码仓库 apache/creadur-whisker 仅有一个 tag(apache-whisker-0.1,唯一历史正式版);官网页头 Version: 0.2-SNAPSHOT | Last Published: 2026-07-22;最近提交为 2026-10-02 的依赖版本机器人合并。结论:项目处于小众低频维护状态,无正式发布渠道,依赖每日快照,升级节奏与稳定性均以官方 CI 构建为准——这是采用该工具时必须如实告知团队的背景。

💡 技术拓展:为什么会有"从未发正式版却每日快照"的形态——Apache Creadur 家族中 Rat 是活跃主流(中央仓库全量发布),Tentacles 与 Whisker 长期处于"有功能、少社区推动"状态;Whisker 0.2 的候选功能在主干已稳定多年,但收尾的发布流程一直未走完。对使用者而言,等价于"0.2 常态可用、0.1 过旧不可用",直接消费快照是社区事实上的用法(官网文档与站点本身就是 0.2-SNAPSHOT 驱动的)。

三、元数据模型详解

本节拆解 Whisker 唯一的配置面——XML 元数据描述符。理解本节,三个命令都只是消费方式不同。

3.1 三段式结构

描述符根元素为 <manifest>,按职责分三段(DTD 定义为 <!ELEMENT manifest (licenses, organisations, primary-license, primary-notice?, within*)>):

段元素职责
定义段licenses/license、notices、organisations/organisation把许可证全文、第三方声明、组织实体定义一次并赋予 id
主归属段primary-license、primary-notice、primary-organisation声明发行版整体的许可证、版权通知与主体组织
资源归属段within → with-license → by-organisation → resource按目录穷举每个资源的许可证 id 与提供组织 id

关键规则:

  • id 命名与引用:license id 与 organisation id 用 ID 类型声明,with-license id 与 by-organisation id 用 IDREF 引用;引用不存在的 id 在解析阶段即失败(实测见 6.1 节)。
  • primary-notice 可选:定义后 NOTICE 文档才会生成(插件路径实测见第七章);primary-notice 文本中的 ${year} 变量在解析时替换为当前年份。
  • resource 可带 sha1:为资源附加内容指纹,用于发行审计时核对文件是否被意外替换。
  • 许可证家族模板:像 BSD 这类"各分发的文本结构相同、仅版权行不同"的许可证家族,可在 license 下用 template 元素参数化,资源侧用 license-parameters 传值,避免重复粘贴全文。

3.2 最小可工作描述符

MIT 单许可证样例(/root/whisker/main.xml,完整内容):

<?xml version="1.0" encoding="UTF-8"?>
<manifest>
  <licenses>
    <license name='MIT License'
             id='osi:mit'
             url='https://www.opensource.org/licenses/mit-license.php'>
      <text>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
      </text>
    </license>
  </licenses>
  <notices/>
  <organisations>
    <organisation id='example.org'
                  name='Example Technology Ltd.'
                  url='https://www.example.org'/>
  </organisations>
  <primary-license id='osi:mit'>
    <copyright-notice>Copyright (c) 2026 Example Technology Ltd.</copyright-notice>
  </primary-license>
  <primary-organisation id='example.org'/>
  <within dir='.'>
    <with-license id='osi:mit'>
      <by-organisation id='example.org'>
        <resource name='readme.txt'/>
        <resource name='run.sh'/>
      </by-organisation>
    </with-license>
  </within>
  <within dir='alpha'>
    <with-license id='osi:mit'>
      <by-organisation id='example.org'>
        <resource name='five.js'/>
        <resource name='six.rb'/>
      </by-organisation>
    </with-license>
  </within>
</manifest>

MIT 不要求 NOTICE 文档,因此本描述符没有 primary-notice;换成 Apache License 2.0 等带通知保留条款的许可证时应补上(第七章 AL2 样例)。资源归属的粒度是目录级(within dir)加资源级(resource name),public-domain 元素可替代 with-license 用于声明公有领域内容。

3.3 DTD 与校验

描述符自带完整内嵌 DTD(--skeleton 输出可见全文),不写 DOCTYPE 也能解析——引擎按元素名直接读取,DTD 主要服务于编辑器的辅助校验。实测两个对齐要点:within 的目录名是相对 -s 发行目录的路径;within dir='.' 表示发行根目录本身。

四、命令行全解

4.1 入口与参数表

可执行入口为 cli 模块 jar(内嵌全部依赖),--help 全量回显:

usage: apache-whisker-cli -a | -g | -t  [-h] -l <file> [-s <dir>]

Pass the descriptor containing license meta-data and the act to be done.
 -a,--audit                       report audit details
 -g,--generate                    generate license and notice
 -h,--help                        print help then exit, ignoring other
                                  options.
 -l,--license-descriptor <file>   use given license descriptor
 -s,--source <dir>                application source
 -t,--skeleton                    generates skeleton meta-data

Apache Whisker assists assembled applications maintain correct legal
documentation. Whisker is part of the Apache Creadur suite of tools for
auditing and comprehending software distributions, and is open source
community developed software. Get involved at http://www.apache.org.
参数必填语义与实测行为
-a, --audit三选一对照描述符审计发行目录,输出偏差报告
-g, --generate三选一从描述符生成法律文档,写标准输出
-t, --skeleton三选一扫描发行目录生成骨架描述符,写标准输出
-l, --license-descriptor必填元数据描述符路径;缺失报 Missing required option: l
-s, --sourceaudit/skeleton 必填,generate 可省发行目录;目录不存在报 Expected '...' to exist,skeleton 缺省报 -s application source
-h, --help否打印用法后退出,忽略其他参数

4.2 输出设计与退出码

两个实测要点:

  • generate/skeleton 固定写标准输出。源码中命令行方式固定使用 WriteResultsToSystemOutFactory(每个生成文档一个输出 writer),落盘靠重定向。设计动机是让文档生成可被管道消费(如与其他模板拼接);需要直接写文件时用 Maven 插件(第七章)。
  • 退出码语义:--help 与成功的动作返回 0;描述符解析失败(如 id 引用缺失)返回 1;audit 的"发现缺失"是报告不是失败,列出缺失项时退出码仍为 0——接入流水线需解析输出而非只看退出码。

4.3 内嵌依赖构成

对 shade jar 内 META-INF/maven 清单的实测枚举:

依赖版本说明
apache-whisker-{app,cli,model,scan,xml,velocity}0.2-SNAPSHOT项目自身六模块
org.apache.velocity:velocity-engine-core2.4.1文档模板引擎
org.apache.logging.log4j:{log4j-api,log4j-core}2.26.1日志实现(JDK 17 兼容问题源头,见第八章)
commons-cli / commons-io / commons-lang3 / slf4j-api——参数解析与工具类

五、generate 实战:生成 LICENSE 文档

5.1 MIT 最小样例

cd /root/whisker && java -jar apache-whisker-cli-0.2-SNAPSHOT.jar --generate --license-descriptor main.xml --source app

实测回显(头部与尾部,正文为 MIT 全文):

Copyright (c) 2026 Example Technology Ltd.


Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
(输出节选:MIT 许可证正文已折叠)

WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

输出结构:首行是 primary-license/copyright-notice 原文,随后空行分隔许可证全文(license/text 原样输出)。落盘:

java -jar apache-whisker-cli-0.2-SNAPSHOT.jar --generate --license-descriptor main.xml > LICENSE
wc -l LICENSE
23 LICENSE

5.2 Apache License 2.0 样例与主通知

将主许可证换成 AL2(license/text 放入官方全文,id='osi:AL2')并添加主通知段:

  <primary-license id='osi:AL2'>
    <copyright-notice>Copyright (c) 2026 Example Technology Ltd.</copyright-notice>
  </primary-license>
  <primary-notice>Apache Whisker Demo
Copyright 2026 Example Technology Ltd.

This product includes software developed by Example Technology Ltd.
(https://www.example.org).</primary-notice>
  <primary-organisation id='example.org'/>

实测 generate 输出 206 行,内容为版权声明与 AL2 全文。实测发现:本快照的命令行 generate 在配置了 primary-notice 的描述符下,标准输出中只有 LICENSE 文档,NOTICE 文本未出现——经源码与模板(notice.vm 首行 #if ( ! $work.isNoticeRequired() ) #stop #end)比对,引擎层"是否生成 NOTICE"的开关与描述符解析结果均正常(独立渲染该模板可完整输出主通知文本),问题定位在命令行 shade 集成路径。需要 NOTICE 时使用 Maven 插件,实测可正常产出(第七章),该差异建议向项目邮件列表反馈。

六、audit 实战:审计发行目录

6.1 干净目录与未声明文件

干净目录(实测全量回显):

No issues found.

No missing licenses.

No duplicate definitions.

No resources are missing required source links.

No extra licenses

在发行目录放入描述符未声明的文件后重跑(实测全量回显):

Resources missing licenses:
    extra-undeclared.txt in .

No duplicate definitions.

No resources are missing required source links.

No extra licenses

五个报告段逐项对应:缺失许可证的资源、重复定义、缺必需源链接的资源、多余许可证。审计的价值在于把「目录现状」与「描述符声明」的双向偏差固定成清单:目录里多出的文件(未声明)与描述符里声明但目录缺失的资源都会暴露。

6.2 目录缺失

-s 指向不存在的目录时(实测回显):

Exception in thread "main" java.lang.IllegalArgumentException: Expected '/root/app' to exist

6.3 描述符非法

with-license id 引用未定义的许可证 id(实测回显):

Exception in thread "main" org.apache.creadur.whisker.fromxml.MissingIDException: Missing ID 'osi:gpl3' for element 'license' linked from element 'with-license'

该错误发生在解析阶段,generate 与 audit 同样触发,退出码 1——把描述符校验前置到了任何动作之前,是接流水线时的第一道闸。

七、Maven 插件深度集成

7.1 工程配置

插件坐标 org.apache.creadur.whisker:apache-whisker-maven-plugin:0.2-SNAPSHOT,仅存在于 Apache 快照仓库。完整 pom 关键段:

  <pluginRepositories>
    <pluginRepository>
      <id>apache.snapshots</id>
      <url>https://repository.apache.org/content/repositories/snapshots/</url>
      <releases><enabled>false</enabled></releases>
      <snapshots><enabled>true</enabled></snapshots>
    </pluginRepository>
  </pluginRepositories>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.creadur.whisker</groupId>
        <artifactId>apache-whisker-maven-plugin</artifactId>
        <version>0.2-SNAPSHOT</version>
        <configuration>
          <descriptor>${project.basedir}/main.xml</descriptor>
        </configuration>
        <executions>
          <execution>
            <phase>generate-resources</phase>
            <goals>
              <goal>generate</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

generate 目标三个参数:descriptor(File,必填,用户属性 apacheWhiskerDescriptor)、outputDirectory(默认 ${project.build.directory})、outputEncoding(默认 ${project.build.sourceEncoding})。

7.2 运行与产物

cd /root/plugin-demo && mvn -B generate-resources 2>&1 | tail -6
[INFO] Reading descriptor from /root/plugin-demo/main.xml
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  47.433 s
[INFO] Finished at:  2026-10-15T13:41:33Z

首次运行从快照与中央仓库解析插件及依赖耗时约 47 秒,二次运行 1 秒内完成。MIT 描述符产物为 target/LICENSE(版权声明加全文)。换成 5.2 节的 AL2 加 primary-notice 描述符后,实测同时产出两份文档:

-rw-r--r-- 1 root root 11399 Oct 15 13:41 target/LICENSE
-rw-r--r-- 1 root root   171 Oct 15 13:41 target/NOTICE

target/NOTICE 实测全量(缩进 2 空格):


  Apache Whisker Demo
  Copyright 2026 Example Technology Ltd.

  This product includes software developed by Example Technology Ltd.
  (https://www.example.org).

7.3 locale 字符集问题

默认 POSIX/C locale 下插件构建实测报错:

[ERROR] ... Whisker failed to generate materials: Mismatched Charset(UTF-8) and CharsetEncoder(US-ASCII) -> [Help 1]

定位:模板引擎按平台默认字符集初始化,插件又按 UTF-8 编码写文件,二者不一致。实测修复:执行前 export LANG=C.UTF-8。含中文的描述符同样依赖该 locale,建议写进构建环境基线而非只挂在命令行上。

7.4 CLI 与插件的行为差异结论

能力CLI(shade jar)Maven 插件
LICENSE 生成实测正常(标准输出)实测正常(写 target/)
NOTICE 生成实测未出现(快照缺陷)实测正常生成
描述符校验同引擎,行为一致同引擎,行为一致
JDK 17 兼容需 Multi-Release 修复(第八章)实测无需修复

生产建议:文档产出以插件为准(NOTICE 完整、随构建自动更新、无 JDK 17 特殊处理);CLI 适合临时审计与脚本化检查。

八、JDK 17 兼容缺陷根因分析

本节是对实测崩溃的字节码级定位,也是 Whisker 环境适配中最有复用价值的结论。

8.1 现象与边界

JDK 17 上 --help 正常,--generate/--audit/--skeleton 一律崩溃(实测头部):

Exception in thread "main" java.lang.ExceptionInInitializerError
        at org.apache.creadur.whisker.cli.Main.configure(Main.java:131)
        at org.apache.creadur.whisker.cli.Main.configure(Main.java:119)
        at org.apache.creadur.whisker.cli.Main.run(Main.java:176)
        at org.apache.creadur.whisker.cli.Main.main(Main.java:70)
Caused by: java.lang.UnsupportedOperationException: No class provided, and an appropriate one cannot be found.
        at org.apache.logging.log4j.LogManager.callerClass(LogManager.java:531)
        at org.apache.logging.log4j.LogManager.getLogger(LogManager.java:556)
        at org.apache.logging.log4j.LogManager.getLogger(LogManager.java:543)
        at org.apache.creadur.whisker.out.velocity.LoggingVelocityEngine.<clinit>(LoggingVelocityEngine.java:37)

崩溃点在模板引擎类的静态初始化(第一次调用 LogManager.getLogger),因此只要动作进入生成引擎就触发,与具体命令无关。

8.2 根因:shaded jar 缺 Multi-Release 声明

三条实测证据链:

  1. jar 内存在多版本目录 META-INF/versions/9/org/apache/logging/log4j/util/StackLocator.class(基于 StackWalker 的 JDK 9+ 实现)。
  2. MANIFEST 实测无 Multi-Release: true 属性,于是 JDK 17 只加载基线版 StackLocator,其调用者定位逻辑在新版 JVM 上抛出上述 UnsupportedOperationException。
  3. 同一 jar 在 OpenJDK 8 上未修复即全部正常——基线 StackLocator 在 JDK 8 是完整可用的。

字节码基线为 major 52(Java 8),javap 确认主类 org.apache.creadur.whisker.cli.Main,与三条证据互洽。

8.3 单行修复与验证矩阵

cd /root/whisker && printf 'Multi-Release: true\n' > mr.mf
jar ufm apache-whisker-cli-0.2-SNAPSHOT.jar mr.mf
jar xf apache-whisker-cli-0.2-SNAPSHOT.jar META-INF/MANIFEST.MF
grep -E 'Multi-Release|Main-Class' META-INF/MANIFEST.MF
Main-Class: org.apache.creadur.whisker.cli.Main
Multi-Release: true

修复后 JDK 17 三命令实测全部通过。兼容矩阵(均为实测结果):

运行时未修复 jar修复后 jarMaven 插件
OpenJDK 8全部正常全部正常正常
OpenJDK 17动作类命令崩溃全部正常正常

💡 技术拓展:该缺陷是「shade 打包多版本 jar」的经典陷阱——jar uf 重组包时不会自动补写 Multi-Release: true;自建 shaded 交付物时,检查 META-INF/versions 存在即必须同步声明该属性。对 Whisker 而言,上游修复会在下一次快照重建时自然生效,届时可移除本节补丁步骤。

九、维护状态评估与采用建议

综合第二、七、八章的实测证据,给出采用决策参考。

  • 版本策略:固定快照编号(如本文 20261014.173522-582)而非漂移的 0.2-SNAPSHOT 引用,配合 sha1 校验;升级 = 换时间戳 + 重跑验证清单,成本可控。
  • 上游风险:无正式发布、低频人工维护(近期提交以依赖机器人为主),遇缺陷(如 NOTICE 命令行缺失、清单属性缺失)应以邮件列表反馈加本地 workaround 为主,等待上游周期与 Rat 不可同日而语。
  • 功能边界:适合"组装发行、以声明式元数据为准"的团队;若诉求是"扫描源码头注释、CI 门禁",选 Apache Rat 更合适;批量核查仓库构件选 Apache Tentacles。
  • 工程化落点:Maven 插件挂 generate-resources 阶段,把 LICENSE/NOTICE 生成纳入 mvn package;CLI audit 挂流水线时需解析输出(audit 退出码恒为 0)。

十、参考链接

  1. Apache Whisker 官网(含 In 5 Minutes 快速上手):https://creadur.apache.org/whisker/
  2. 元数据模型参考(含 DTD 说明):https://creadur.apache.org/whisker/meta-data.html
  3. 命令行模块官方说明:https://creadur.apache.org/whisker/apache-whisker-cli/index.html
  4. Maven 插件 generate 目标参数:https://creadur.apache.org/whisker/apache-whisker-maven-plugin/generate-mojo.html
  5. Apache 快照仓库 whisker 目录:https://repository.apache.org/content/repositories/snapshots/org/apache/creadur/whisker/
  6. 源码仓库(含 apache-whisker-0.1 标签):https://github.com/apache/creadur-whisker
  7. Apache Creadur 项目主页(Rat/Tentacles/Whisker):https://creadur.apache.org/