Apache Commons Weaver 2.0 快速入门与集成示例

以 privilizer 织入提供者为例给出 Apache Commons Weaver 2.0 快速入门:Maven 集成三依赖一配置、构建即织入、javap 织入前后字节码逐行对比、蓝图调用方与自定义提供者注入演示,以及把 Weaver 接入既有工程与 CI 的集成清单,全部示例实测通过。

Apache Commons WeaverApache Commons Weaver2026/10/15

Apache Commons Weaver 2.0 快速入门与集成示例

目标:用最小的工程理解 Commons Weaver 2.0 的织入全流程——声明注解 → Maven 构建期织入 → javap 对比织入前后字节码 → 运行验证,并掌握把它接入既有工程的关键配置;本文全部代码与回显均已在 OpenJDK 1.8.0_504 + Maven 3.9.9 环境实测通过。

Commons Weaver 解决的问题很具体:Java 里想以「特权方式」执行一段代码(读受限系统属性、读受保护文件),规范写法要把方法体手工包进匿名 PrivilegedAction 实现类、再套上 AccessController.doPrivileged 调用,样板代码多、嵌套丑、审计难。Weaver 的做法是把这件事推迟到构建期:源代码里只写业务逻辑并标注 @Privileged,Maven 插件在 process-classes 阶段扫描编译产物、按注解改写字节码,产物里才出现真正的特权块。本文使用官方 privilizer 织入提供者完成全部演示。

一、环境信息

本节固定实验基线,所有回显均来自该环境。

  • 操作系统:Linux x86_64(Ubuntu 22.04 服务器环境)
  • 架构:x86_64
  • JDK:OpenJDK 1.8.0_504(织入器实测只能运行在 JDK 8;JDK 17 执行织入报 Unsupported class file major version 61,详见第五节)
  • 构建工具:Apache Maven 3.9.9
  • 组件版本:Apache Commons Weaver 2.0(2018-09-07 发布,官方最新版)
  • Maven 坐标:org.apache.commons:commons-weaver-processor:2.0、org.apache.commons:commons-weaver-privilizer:2.0、org.apache.commons:commons-weaver-privilizer-api:2.0、org.apache.commons:commons-weaver-maven-plugin:2.0
  • 演示工程工作目录:/work/privilizer-demo
  • 演示工程坐标:demo:privilizer-demo:1.0.0

💡 技术拓展:把「增强已有类」放进构建流水线时,会碰到三个同类名词,注意区分——

名词含义与本文的关系
构建期织入(compile-time weaving)编译后、打包前由工具改写 class 文件本文方案,Weaver 只支持这种
类加载期织入(load-time weaving)JVM 装载 class 时由 -javaagent 改写Weaver 2.0 无 agent,不支持
运行期代理(dynamic proxy)运行时生成包装对象,不改 class语义完全不同,产物是代理对象而非改写后的类

二、10 分钟跑通第一个织入

本节从零创建工程,构建一次即完成织入,再用 javap 与运行输出双重验证。

2.1 概念速览:处理器 + 提供者

Commons Weaver 由两层组成,理解了这对分工,pom 里每个依赖的用途就都清楚了:

层构件职责
织入处理器(processor)commons-weaver-processor扫描 target/classes、按注解定位织入点、通过 SPI 调度提供者
织入提供者(weaver)commons-weaver-privilizer(本文)、commons-weaver-normalizer实现具体改写逻辑;privilizer 负责 @Privileged 特权块重写
注解 APIcommons-weaver-privilizer-api提供 @Privileged、@Privilizing 两个注解,工程编译期引用
触发入口commons-weaver-maven-pluginweave goal 绑定 process-classes 阶段,mvn package 自动织入

2.2 创建工程与依赖

/work/privilizer-demo/pom.xml(完整内容):

<?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>demo</groupId>
  <artifactId>privilizer-demo</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-weaver-privilizer-api</artifactId>
      <version>2.0</version>
    </dependency>
    <dependency>
      <groupId>com.example</groupId>
      <artifactId>version-weaver</artifactId>
      <version>1.0.0</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.11.0</version>
      </plugin>
      <plugin>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-weaver-maven-plugin</artifactId>
        <version>2.0</version>
        <dependencies>
          <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-weaver-privilizer</artifactId>
            <version>2.0</version>
          </dependency>
          <dependency>
            <groupId>com.example</groupId>
            <artifactId>version-weaver</artifactId>
            <version>1.0.0</version>
          </dependency>
        </dependencies>
        <configuration>
          <weaverConfig>
            <privilizer.policy>ALWAYS</privilizer.policy>
            <privilizer.accessLevel>public</privilizer.accessLevel>
          </weaverConfig>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>weave</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

三条配置要点(均为实测结论,缺一不可):

  1. 织入提供者放插件级 <dependencies>:插件通过 ServiceLoader 从插件自身的类加载空间发现提供者;写成普通工程依赖时 weave goal 会静默空转(实测复现过,BUILD SUCCESS 但字节码纹丝不动)。
  2. version-weaver 是本文第四节要实现的自定义提供者;只体验 privilizer 时可先删掉它的两处声明与 AppInfo.java,不影响其余示例。
  3. privilizer.accessLevel=public:@Privileged 默认只允许标注 private 方法,本文示例方法是 public 的,必须放宽。

2.3 三个源文件

/work/privilizer-demo/src/main/java/demo/SecretStore.java——特权方法提供者,方法体只写业务逻辑:

package demo;

import org.apache.commons.weaver.privilizer.Privileged;

/**
 * 特权方法提供者:readToken 的方法体需要以特权方式读取系统属性。
 */
public final class SecretStore {

    private SecretStore() {
    }

    @Privileged
    public static String readToken() {
        return System.getProperty("demo.token", "unset");
    }
}

/work/privilizer-demo/src/main/java/demo/ConfigClient.java——普通调用方,直接调用特权方法:

package demo;

/**
 * 普通调用方:直接调用 SecretStore.readToken(),不做任何声明。
 */
public final class ConfigClient {

    private ConfigClient() {
    }

    public static String token() {
        return SecretStore.readToken();
    }
}

/work/privilizer-demo/src/main/java/demo/BlueprintClient.java——声明式调用方,多一个 @Privilizing 注解,让织入器把调用点重写为蓝图(blueprint)形式:

package demo;

import org.apache.commons.weaver.privilizer.Privilizing;
import org.apache.commons.weaver.privilizer.Privilizing.CallTo;

/**
 * 声明式调用方:标注 @Privilizing 后,调用点由织入器重写。
 */
@Privilizing(@CallTo(value = SecretStore.class, methods = "readToken"))
public final class BlueprintClient {

    private BlueprintClient() {
    }

    public static String token() {
        return SecretStore.readToken();
    }
}

2.4 构建即织入

cd /work/privilizer-demo && mvn -B clean package 2>&1 | grep -E 'weave \(default\)|security leak|BUILD|Total time'
[INFO] --- commons-weaver:2.0:weave (default) @ privilizer-demo ---
[WARNING] Possible security leak: granting privileges to private method demo/BlueprintClient.demo_SecretStore$$readToken()Ljava/lang/String;
[WARNING] Possible security leak: granting privileges to private method demo/BlueprintClient.demo_SecretStore$$readToken()Ljava/lang/String;
[WARNING] Possible security leak: granting privileges to public method demo/SecretStore.readToken()Ljava/lang/String;
[INFO] BUILD SUCCESS
[INFO] Total time:  1.634 s

回显解读:weave (default) 执行于 process-classes 阶段(先于 jar 打包,所以产物 jar 里就是织入后的字节码);三条 Possible security leak 是 privilizer 的访问级别审计——它对每个被授予特权的(蓝图)方法逐一提醒,属预期警告。织入前后对 target/classes 的 javap 检查证明改写真实发生,见第三节。

2.5 运行验证

java -Ddemo.token=secret-123 -cp target/classes demo.Main
ConfigClient.token()    = secret-123
BlueprintClient.token() = secret-123
AppInfo.WOVEN_VERSION   = 2.0-demo
SecurityManager active  = false

(Main.java 依次打印两个调用方的结果、自定义织入注入的字段值与 SecurityManager 状态;无 SecurityManager 时两种调用方式行为一致。)

验证点:织入后的程序运行正常,且运行期 classpath 只有 target/classes,没有携带任何 Weaver jar——织入只发生在构建期,产物自包含。

三、织入前后字节码对比

本节用 javap 逐行对比,看清 Weaver 到底改了什么。对比基线是织入前的编译产物快照(mvn clean compile 后立即复制 target/classes 保存)。

3.1 未织入的 readToken

javap -p -c /work/snapshot-unwoven/demo/SecretStore.class
  public static java.lang.String readToken();
    Code:
       0: ldc           #2                  // String demo.token
       2: ldc           #3                  // String unset
       4: invokestatic  #4                  // Method java/lang/System.getProperty:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
       7: areturn
}

与源码一一对应:两个字符串常量入栈,调 System.getProperty,返回。

3.2 织入后的 SecretStore

javap -p -c target/classes/demo/SecretStore.class
Compiled from "SecretStore.java"
public final class demo.SecretStore {
  private demo.SecretStore();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static java.lang.String readToken();
    Code:
       0: new           #34                 // class demo/SecretStore$readToken_ACTION
       3: dup
       4: invokespecial #36                 // Method demo/SecretStore$readToken_ACTION."<init>":()V
       7: invokestatic  #42                 // Method java/security/AccessController.doPrivileged:(Ljava/security/PrivilegedAction;)Ljava/lang/Object;
      10: checkcast     #44                 // class java/lang/String
      13: areturn

  static java.lang.String __privileged_access$0();
    Code:
       0: invokestatic  #48                 // Method __privileged_readToken:()Ljava/lang/String;
       3: areturn

  private static java.lang.String __privileged_readToken();
    Code:
       0: ldc           #2                  // String demo.token
       2: ldc           #3                  // String unset
       4: invokestatic  #4                  // Method java/lang/System.getProperty:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
       7: areturn
}

织入器做了四件事:原方法体原封不动搬进合成方法 __privileged_readToken();生成 SecretStore$readToken_ACTION 内部类(实现 PrivilegedAction,其 run() 调回 __privileged_access$0());readToken() 被改写为「构造 ACTION → AccessController.doPrivileged → 强转返回」;@Privileged 注解(CLASS retention)虽未被删除,但只以 RuntimeInvisibleAnnotations 形式留在字节码中,运行期反射不可见。织入后 target/classes/demo/ 的文件清单印证了新增内部类:

ls target/classes/demo/
AppInfo.class
BlueprintClient$demo_SecretStore$$readToken_ACTION.class
BlueprintClient.class
ConfigClient.class
Main.class
SecretStore$readToken_ACTION.class
SecretStore.class

3.3 声明式调用方被重写为蓝图

@Privilizing(@CallTo(SecretStore.class, methods="readToken")) 让织入器把 BlueprintClient 的调用点从「远程调 SecretStore.readToken()」改写为「调本地蓝图副本」:

javap -p -c target/classes/demo/BlueprintClient.class
Compiled from "BlueprintClient.java"
public final class demo.BlueprintClient {
  private demo.BlueprintClient();
  public static java.lang.String token();
    Code:
       0: invokestatic  #42                 // Method demo_SecretStore$$readToken:()Ljava/lang/String;
       3: areturn

  private static java.lang.String demo_SecretStore$$readToken();
  static java.lang.String __privileged_access$0();
  private static java.lang.String __privileged_demo_SecretStore$$readToken();
}

方法清单说明织入器把特权方法的完整实现复制进了调用方(demo_SecretStore$$readToken 及其配套的 __privileged_* 方法与 BlueprintClient$demo_SecretStore$$readToken_ACTION 内部类),调用点改为直接调本地副本。语义差异在第五章的 SecurityManager 对照实验里会用栈回显讲清楚:特权块在调用方一侧执行,调用方的 codeSource 必须自身持有相应权限。

3.4 自定义提供者注入的字段

demo/AppInfo.java 标注了自定义注解 @Versioned("2.0-demo")(实现见第五章示例三),构建期被注入了源码中不存在的字段:

javap -p target/classes/demo/AppInfo.class | grep WOVEN
  public static final java.lang.String WOVEN_VERSION;

验证点:字段 WOVEN_VERSION 只存在于织入后的字节码,运行期通过反射读到了值 2.0-demo(见 2.5 节运行回显第三行),证明 Weaver 的提供者机制对任意自定义注解处理器开放。

四、把 Weaver 集成进既有工程

本节给出接入既有工程的最小改动清单与两种触发方式。

4.1 集成检查清单

步骤操作校验方式
确认构建 JDK织入阶段用 JDK 8(JDK 11+ 会报 major version 错误)mvn -v 看 Java version
加编译期注解依赖工程 <dependencies> 加 commons-weaver-privilizer-api:2.0源码能 import @Privileged
加织入提供者commons-weaver-maven-plugin 的插件级 <dependencies> 加 commons-weaver-privilizer:2.0构建日志出现 weave (default) goal
配置织入参数weaverConfig 设 privilizer.policy/privilizer.accessLevel警告或报错符合预期
触发织入execution 绑定 weave goal(默认 process-classes 阶段)mvn package 一次完成
验证产物javap -p -c 检查目标类出现 doPrivileged/__privileged_*与第三节回显形态一致

4.2 两种触发方式

方式一(默认):构建期自动织入。 weave goal 默认绑定 process-classes 阶段,mvn package 自动触发,是常规工程的标准用法。

方式二:命令行单步执行。 调试织入问题时,可先 mvn compile 再单独执行 goal,实时观察改写结果:

mvn compile && mvn commons-weaver:weave && javap -p -c target/classes/demo/SecretStore.class | grep doPrivileged
       7: invokestatic  #42                 // Method java/security/AccessController.doPrivileged:(Ljava/security/PrivilegedAction;)Ljava/lang/Object;

插件另有 prepare goal(绑定 initialize 阶段,把被织入过的类恢复为未织入状态)与对应的 test-weave/test-prepare(作用于测试类目录),可组合使用。

方式三(补充):脱离 Maven 直调处理器。 无构建体系场景可用 20 行 WeaveRunner 调 WeaveProcessor.weave(),classpath 需要 processor + privilizer + api 三个官方 jar 加 xbean-finder-shaded-4.9、xbean-asm6-shaded-4.9、commons-lang3-3.7 三个传递依赖 jar;完整命令与回显见《Ubuntu 22.04 安装 Apache Commons Weaver 指南》方案 B。

4.3 与打包、测试的时序配合

织入发生在 process-classes(早于 test、package),因此:mvn test 跑的测试类加载的是织入后字节码,可以直接对「特权行为」写单测;maven-shade-plugin、spring-boot-maven-plugin 等重打包插件拿到的输入已是织入产物,无需特殊适配。反向注意点:凡是「在 process-classes 之前就打包/签名」的非标准插件都会拿到未织入字节码,接入前先核对插件绑定阶段。

五、常见集成问题

问题一:weave goal 显示 BUILD SUCCESS 但类没被织入。 织入提供者被写成了普通工程依赖,插件类加载空间里没有提供者,goal 空转。解法:挪到插件级 <dependencies>。

问题二:构建报 Unsupported class file major version 61。 织入器运行在 JDK 17 上,其内嵌 ASM 6.2.1(shaded 在 privilizer jar 内,无法替换升级)读不懂 JDK 17 自身类的 major 61 字节码(织入时要读取被调用方类文件判断可访问性)。解法:构建环境换 JDK 8;实测该错误在 JDK 8 上不会出现。

问题三:报 must have maximum access level 'private' but is defined wider ('public')。 @Privileged 默认只许标注 private 方法。解法:weaverConfig 配 <privilizer.accessLevel>public</privilizer.accessLevel>,放宽后伴随 Possible security leak 警告,属预期。

问题四:把 maven.compiler.release 设为 8 在 JDK 8 上构建失败。 --release 选项是 JDK 9+ javac 特性,JDK 8 不支持(实测报 invalid flag: --release)。JDK 8 构建用 maven.compiler.source/target=1.8。

问题五:怀疑织入没生效时如何快速自证? 固定三板斧:ls target/classes/包名/(看有没有多出来的 *$_ACTION.class 文件)→ javap -p 类名(看有没有 __privileged_* 合成方法)→ javap -p -c 类名 | grep doPrivileged(看调用链)。三者有其一即织入成功。

六、参考链接

  1. Apache Commons Weaver 官方主页:https://commons.apache.org/proper/commons-weaver/
  2. privilizer 模块源码(语义权威依据):https://github.com/apache/commons-weaver/tree/master/commons-weaver-modules/commons-weaver-privilizer
  3. Weaver Maven 插件 goal 文档:https://commons.apache.org/proper/commons-weaver/commons-weaver-maven-plugin/
  4. Maven 中央仓库坐标:https://repo1.maven.org/maven2/org/apache/commons/commons-weaver-privilizer-api/2.0/
  5. 本文配套安装指南:《Ubuntu 22.04 安装 Apache Commons Weaver 指南》《CentOS 8 安装 Apache Commons Weaver 指南》