Apache Commons Weaver 2.0 快速入门与集成示例
以 privilizer 织入提供者为例给出 Apache Commons Weaver 2.0 快速入门:Maven 集成三依赖一配置、构建即织入、javap 织入前后字节码逐行对比、蓝图调用方与自定义提供者注入演示,以及把 Weaver 接入既有工程与 CI 的集成清单,全部示例实测通过。
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 特权块重写 |
| 注解 API | commons-weaver-privilizer-api | 提供 @Privileged、@Privilizing 两个注解,工程编译期引用 |
| 触发入口 | commons-weaver-maven-plugin | weave 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>
三条配置要点(均为实测结论,缺一不可):
- 织入提供者放插件级
<dependencies>:插件通过 ServiceLoader 从插件自身的类加载空间发现提供者;写成普通工程依赖时weavegoal 会静默空转(实测复现过,BUILD SUCCESS 但字节码纹丝不动)。 version-weaver是本文第四节要实现的自定义提供者;只体验 privilizer 时可先删掉它的两处声明与AppInfo.java,不影响其余示例。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(看调用链)。三者有其一即织入成功。
六、参考链接
- Apache Commons Weaver 官方主页:https://commons.apache.org/proper/commons-weaver/
- privilizer 模块源码(语义权威依据):https://github.com/apache/commons-weaver/tree/master/commons-weaver-modules/commons-weaver-privilizer
- Weaver Maven 插件 goal 文档:https://commons.apache.org/proper/commons-weaver/commons-weaver-maven-plugin/
- Maven 中央仓库坐标:https://repo1.maven.org/maven2/org/apache/commons/commons-weaver-privilizer-api/2.0/
- 本文配套安装指南:《Ubuntu 22.04 安装 Apache Commons Weaver 指南》《CentOS 8 安装 Apache Commons Weaver 指南》