Apache Commons Weaver 2.0 织入示例与最佳实践详解
给出 Apache Commons Weaver 2.0 的四个实测示例——privilizer 特权方法织入全流程、SecurityManager 下织入前后行为对照(含三段真实栈回显)、实现 Weaver SPI 的自定义织入提供者(含 CLASS retention 实测坑)、脱离 Maven 的命令行织入,并沉淀构建期织入的安全与工程化最佳实践。
Apache Commons Weaver 2.0 织入示例与最佳实践详解
目标:用四个可复现的示例把 Commons Weaver 2.0 的织入能力从「能跑」推进到「敢用」——特权方法织入全流程、SecurityManager 下的织入前后行为对照、自定义织入提供者开发、命令行直调织入器,并沉淀可直接落地的最佳实践清单;本文全部命令与回显均已在 OpenJDK 1.8.0_504 + Maven 3.9.9 环境实测通过。
Commons Weaver 2.0 的官方文档偏「能编译就行」,安全语义与扩展机制都藏在模块源码里。本文以 privilizer 织入提供者为主线做四件事:把特权块织入的产物形态讲到底(示例一);用 SecurityManager 做一次受控行为实验,给出「织入前后到底差在哪」的实证答案(示例二);实现一个最小的自定义织入提供者验证 SPI 开放性(示例三);脱离 Maven 直调 WeaveProcessor(示例四)。阅读前置:@Privileged/@Privilizing 注解与织入产物形态详见《Apache Commons Weaver 2.0 常用 API 与织入提供者详解》。
一、环境信息
本节固定实验基线,四个示例共用同一环境与同一演示工程。
- 操作系统: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 发布,官方最新版;内嵌 ASM 6.2.1、xbean-finder 4.9)
- 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(示例一、二、三)、/work/cli-runner(示例四) - 自定义提供者模块:
/work/custom-weaver(com.example:version-weaver:1.0.0)
💡 技术拓展:验证「织入是否真的改变了程序行为」有两类手段,实测各有盲区——
验证手段 能证明什么 盲区 javap反编译对比字节码结构变了(新增合成方法/内部类/字段) 不证明运行时语义正确 SecurityManager 行为对照 织入产物在受限权限下真实改变了执行结果 需要 policy 与 codeSource 设计(示例二解决) 本文两者都用:示例一、三、四用
javap证明「织了」,示例二用 SecurityManager 证明「织得对」。
二、示例一:特权方法织入全流程
本节从源码到产物完整走一遍 privilizer 织入,是示例二、三的基础。
2.1 工程与源码
工程 pom.xml 的关键配置(完整 pom 见《Apache Commons Weaver 2.0 快速入门与集成示例》2.2 节):织入提供者 commons-weaver-privilizer 放在 commons-weaver-maven-plugin 的插件级 <dependencies> 中;weaverConfig 设 <privilizer.policy>ALWAYS</privilizer.policy> 与 <privilizer.accessLevel>public</privilizer.accessLevel>;weave goal 默认绑定 process-classes 阶段。
三个核心源文件如下。特权方法提供者 /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:
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.2 构建与产物验证
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
产物验证:ls target/classes/demo/ 实测出现两类织入产物——提供者侧的 SecretStore$readToken_ACTION.class 与调用方侧的 BlueprintClient$demo_SecretStore$$readToken_ACTION.class;javap -p -c target/classes/demo/SecretStore.class 中 readToken() 已是 AccessController.doPrivileged 调用,原方法体在私有合成方法 __privileged_readToken() 中(逐行回显见快速入门 3.2 节)。
2.3 运行验证
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
要点:无 SecurityManager 时两种调用方行为一致;织入产物不依赖任何 Weaver jar(运行期 classpath 只有 target/classes)。
三、示例二:SecurityManager 行为对照实验
本节回答一个关键问题:织入前后,程序在「权限受限」环境下的行为到底差在哪。实验的核心技巧是分离 codeSource——同一个 codeSource 内的代码权限相同,不分离就无法观察特权块的截断效果。
3.1 实验设计:分离 codeSource
把织入产物拆成两个 codeSource:特权方法提供者 SecretStore(连同织入生成的 ACTION 内部类)单独打成 secret-store.jar,调用方与入口留在 classes 目录;policy 文件只给 secret-store.jar 授予 AllPermission,classes 目录没有任何权限。布局命令(实测):
rm -rf /work/split && mkdir -p /work/split/classes
cp -r /work/privilizer-demo/target/classes/* /work/split/classes/
cd /work/split/classes
jar cf /work/split/secret-store.jar demo/SecretStore.class 'demo/SecretStore$readToken_ACTION.class'
rm demo/SecretStore.class 'demo/SecretStore$readToken_ACTION.class'
policy 文件 /work/split/policy.txt(完整内容):
grant codeBase "file:/work/split/secret-store.jar" {
permission java.security.AllPermission;
};
对照基线:未织入快照(mvn clean compile 后、织入前复制保存的 target/classes)做同样的拆分,得到 /work/split-unwoven/。JDK 8 上用 -Djava.security.manager 安装 SecurityManager、-Djava.security.policy 加载上述 policy 运行同一入口。
3.2 对照组:未织入产物被拒
java -Ddemo.token=secret-123 -Djava.security.manager -Djava.security.policy=/work/split/policy.txt \
-cp /work/split-unwoven/classes:/work/split-unwoven/secret-store.jar demo.Main
Exception in thread "main" java.security.AccessControlException: access denied ("java.util.PropertyPermission" "demo.token" "read")
at java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
at java.security.AccessController.checkPermission(AccessController.java:884)
at java.lang.SecurityManager.checkPermission(SecurityManager.java:549)
at java.lang.SecurityManager.checkPropertyAccess(SecurityManager.java:1294)
at java.lang.System.getProperty(System.java:716)
at demo.SecretStore.readToken(SecretStore.java)
at demo.ConfigClient.token(ConfigClient.java)
at demo.Main.main(Main.java)
栈解读:权限检查从栈顶(System.getProperty)向下逐帧遍历,第一个帧就是无权限的调用方 ConfigClient,检查在到达持有 AllPermission 的 SecretStore 帧之前就失败了——这正是 doPrivileged 要解决的问题:调用链上游无权限时,即使特权提供者自身有权限也无济于事。
3.3 实验组:织入后提供者提权成功
java -Ddemo.token=secret-123 -Djava.security.manager -Djava.security.policy=/work/split/policy.txt \
-cp /work/split/classes:/work/split/secret-store.jar demo.Main
ConfigClient.token() = secret-123
Exception in thread "main" java.security.AccessControlException: access denied ("java.util.PropertyPermission" "demo.token" "read")
at java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
at java.security.AccessController.checkPermission(AccessController.java:884)
at java.lang.SecurityManager.checkPermission(SecurityManager.java:549)
at java.lang.SecurityManager.checkPropertyAccess(SecurityManager.java:1294)
at java.lang.System.getProperty(System.java:716)
at demo.BlueprintClient.__privileged_demo_SecretStore$$readToken(BlueprintClient.java)
at demo.BlueprintClient.__privileged_access$0(BlueprintClient.java)
at demo.BlueprintClient$demo_SecretStore$$readToken_ACTION.run(BlueprintClient.java)
at java.security.AccessController.doPrivileged(Native Method)
at demo.BlueprintClient.demo_SecretStore$$readToken(BlueprintClient.java)
at demo.BlueprintClient.token(BlueprintClient.java)
at demo.Main.main(Main.java)
第一行成功的原因在织入后的字节码里:ConfigClient 调用的 SecretStore.readToken() 已被改写为「方法体包进 doPrivileged」,权限检查的遍历在 SecretStore(secret-store.jar,持有 AllPermission)的特权帧处被截断,永远走不到无权限的调用方帧。同一份调用方源码、同一份 policy,织入前后结果相反——这就是 privilizer 的价值。第二行的异常正是 3.4 节要分析的反例:BlueprintClient 经 @Privilizing 重写后,特权块落在了调用方自己的 codeSource 里。
3.4 反例:蓝图调用方被拒
BlueprintClient 的调用点经 @Privilizing 重写为本地蓝图副本(特权块随之落在调用方侧),运行即抛 3.3 节回显第二行的 AccessControlException。逐帧解读该栈:
doPrivileged(Native Method)之下的帧依次是BlueprintClient.demo_SecretStore$$readToken(蓝图方法本体)、BlueprintClient$demo_SecretStore$$readToken_ACTION.run(织入生成的 ACTION 类)、__privileged_access$0/__privileged_demo_SecretStore$$readToken(复制的原始方法体)——全部属于BlueprintClient,即特权块确实在调用方侧。- 调用方的 codeSource 是
/work/split/classes,policy 未授予任何权限;AccessControlContext.checkPermission对特权截断点之前的每一帧逐一检查,全部失败。 - 对照 3.2 节:同样是
System.getProperty调用,未织入时检查失败在调用方帧,织入蓝图后失败点移到了特权块内部的同一批帧——蓝图不会把提供者的权限「借」给调用方。
3.5 结论与适用边界
| 形态 | 特权块落点 | 受限调用方结果(实测) | 适用场景 |
|---|---|---|---|
直接调用 @Privileged 方法 | 提供者 codeSource | 成功 | 让无权调用方获得受控结果(标准用法) |
@Privilizing 蓝图调用 | 调用方 codeSource | 失败 | 调用方自身已持权时减少特权帧层数(性能优化) |
两点边界提醒:其一,SecurityManager 自 JDK 17 起已标记废弃(JEP 411),privilizer 的价值场景随 JDK 演进收窄,新项目选型时请把这一点计入;其二,privilizer.policy=DYNAMIC(默认)时织入产物会在运行期检查 SecurityManager 是否存在,无 SecurityManager 的环境里特权块等价于普通直调——本文示例统一用 ALWAYS 使产物形态确定。
四、示例三:自定义织入提供者
本节实现一个最小自定义提供者:自定义注解 @Versioned 标注的类,构建期被注入 public static final String WOVEN_VERSION 字段,证明 Weaver 的 SPI 对第三方注解处理器完全开放。
4.1 目标与设计
模块 /work/custom-weaver(com.example:version-weaver:1.0.0)包含三件东西:注解 @Versioned、织入器 VersionWeaver、SPI 注册文件。依赖仅两个(编译期需要,运行期随插件类路径走):
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-weaver-processor</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>6.2.1</version>
</dependency>
</dependencies>
4.2 注解与织入器实现
注解 /work/custom-weaver/src/main/java/com/example/woven/Versioned.java:
package com.example.woven;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Versioned {
String value();
}
织入器 /work/custom-weaver/src/main/java/com/example/woven/VersionWeaver.java(完整实现):
package com.example.woven;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.reflect.Method;
import org.apache.commons.weaver.model.ScanRequest;
import org.apache.commons.weaver.model.ScanResult;
import org.apache.commons.weaver.model.Scanner;
import org.apache.commons.weaver.model.WeaveEnvironment;
import org.apache.commons.weaver.model.WeaveInterest;
import org.apache.commons.weaver.model.WeavableClass;
import org.apache.commons.weaver.spi.Weaver;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
/**
* 最小自定义织入器:为标注了 @Versioned 的类注入 public static final String WOVEN_VERSION 字段。
*/
public class VersionWeaver implements Weaver {
@Override
public boolean process(final WeaveEnvironment env, final Scanner scanner) {
final ScanResult result = scanner.scan(
new ScanRequest().add(WeaveInterest.of(Versioned.class, ElementType.TYPE)));
boolean wove = false;
for (final WeavableClass<?> weavable : result.getClasses()) {
final String version = readVersion(weavable);
if (version == null) {
continue;
}
final Class<?> target = weavable.getTarget();
try {
final byte[] original = readAll(env.getClassfile(target).getInputStream());
final ClassReader reader = new ClassReader(original);
final ClassWriter writer = new ClassWriter(reader, 0);
reader.accept(new ClassVisitor(Opcodes.ASM6, writer) {
@Override
public void visitEnd() {
cv.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC + Opcodes.ACC_FINAL,
"WOVEN_VERSION", "Ljava/lang/String;", null, version).visitEnd();
super.visitEnd();
}
}, 0);
final OutputStream out = env.getClassfile(target).getOutputStream();
out.write(writer.toByteArray());
out.close();
env.info("WOVEN_VERSION=%s injected into %s", version, target.getName());
wove = true;
} catch (final IOException e) {
throw new IllegalStateException("weaving " + target.getName() + " failed", e);
}
}
return wove;
}
/**
* 跨类加载器安全地读取 @Versioned 的 value(避免依赖 Versioned.class 的唯一 Class 对象)。
*/
private String readVersion(final WeavableClass<?> weavable) {
for (final Annotation a : weavable.getAnnotations()) {
if ("com.example.woven.Versioned".equals(a.annotationType().getName())) {
try {
final Method value = a.annotationType().getMethod("value");
return (String) value.invoke(a);
} catch (final ReflectiveOperationException e) {
throw new IllegalStateException(e);
}
}
}
return null;
}
private static byte[] readAll(final InputStream in) throws IOException {
final ByteArrayOutputStream buf = new ByteArrayOutputStream();
final byte[] data = new byte[8192];
int n;
while ((n = in.read(data)) != -1) {
buf.write(data, 0, n);
}
in.close();
return buf.toByteArray();
}
}
实现要点与实测依据:扫描侧用 WeaveInterest.of(Versioned.class, ElementType.TYPE) 声明「找标注了 @Versioned 的类」;读取侧用 env.getClassfile(target).getInputStream() 拿原始字节码、ASM ClassWriter 在 visitEnd 里追加常量字段、getOutputStream() 写回;readVersion 按 annotationType().getName() 字符串匹配注解而非直接 getAnnotation(Versioned.class),原因是提供者与注解可能被插件类路径与工程类路径两个加载器各加载一份,Class 对象不同会导致 getAnnotation 匹配失败(实测发生过)。
4.3 SPI 注册与插件集成
注册文件 /work/custom-weaver/src/main/resources/META-INF/services/org.apache.commons.weaver.spi.Weaver(完整内容):
com.example.woven.VersionWeaver
演示工程的集成点有三处:插件级 <dependencies> 加入 com.example:version-weaver:1.0.0(进插件类路径,ServiceLoader 才能发现);工程 <dependencies> 以 provided 作用域加入同一构件(编译期可见注解,不进运行期 classpath);演示类 /work/privilizer-demo/src/main/java/demo/AppInfo.java:
package demo;
import com.example.woven.Versioned;
/**
* 自定义织入演示类:构建期被 VersionWeaver 注入 WOVEN_VERSION 字段。
*/
@Versioned("2.0-demo")
public final class AppInfo {
private AppInfo() {
}
}
4.4 织入验证
cd /work/custom-weaver && mvn -B install -q
cd /work/privilizer-demo && mvn -B clean package 2>&1 | grep -E 'BUILD'
[INFO] BUILD SUCCESS
产物验证(字段在源码中不存在,只可能来自织入):
javap -p target/classes/demo/AppInfo.class | grep WOVEN
public static final java.lang.String WOVEN_VERSION;
运行期经反射读取(Main 中 Class.forName("demo.AppInfo").getField("WOVEN_VERSION").get(null)),输出 AppInfo.WOVEN_VERSION = 2.0-demo(见示例一运行回显第三行)。命令行直调同一提供者时可见织入器自己的日志行(实测回显):
INFO: WOVEN_VERSION=2.0-demo injected into demo.AppInfo
4.5 CLASS retention 实测坑
开发过程中实测到一个隐蔽约束:@Versioned 最初声明为 @Retention(RetentionPolicy.RUNTIME) 时,Maven 插件路径下扫描结果为 0(织入器内加入诊断日志后的实测回显):
INFO: scan candidates=0
改为 @Retention(RetentionPolicy.CLASS) 后同一工程同一命令扫描即命中并完成注入。结论:Weaver 的扫描链路面向 CLASS retention 注解设计(官方 @Privileged/@Privilizing 也均为 CLASS retention),自定义注解请直接使用 CLASS retention,与官方保持一致。
五、示例四:脱离 Maven 的命令行织入
本节证明 WeaveProcessor 可以作为普通库直调,便于嵌入自研打包流水线。
5.1 WeaveRunner 实现
/work/cli-runner/WeaveRunner.java(完整内容):
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.apache.commons.weaver.WeaveProcessor;
import org.apache.commons.weaver.spi.Weaver;
public class WeaveRunner {
public static void main(String[] args) throws Exception {
// 参数 1:target/classes 路径;参数 2:冒号分隔的织入器 classpath
final String target = args[0];
final List<String> classpath = new ArrayList<>(java.util.Arrays.asList(args[1].split(":")));
final Properties config = new Properties();
config.setProperty("privilizer.policy", "ALWAYS");
config.setProperty("privilizer.accessLevel", "public");
System.out.println("== ServiceLoader 发现的 Weaver 提供者 ==");
for (final Weaver w : org.apache.commons.weaver.utils.Providers.sort(
java.util.ServiceLoader.load(Weaver.class, Weaver.class.getClassLoader()))) {
System.out.println("PROVIDER: " + w.getClass().getName());
}
new WeaveProcessor(classpath, new File(target), config).weave();
System.out.println("WEAVE_DONE");
}
}
5.2 执行与验证
织入器 classpath 需要 8 个 jar:3 个官方 Weaver 模块(processor、privilizer、privilizer-api)+ 3 个传递依赖(xbean-finder-shaded-4.9、xbean-asm6-shaded-4.9、commons-lang3-3.7,从中央仓库下载)+ 自定义提供者及其 ASM 依赖(示例三产物)。实测命令:
R=/work/commons-weaver-2.0
M2=/root/.m2/repository
CP=$R/commons-weaver-processor-2.0.jar:$M2/org/apache/xbean/xbean-finder-shaded/4.9/xbean-finder-shaded-4.9.jar:$M2/org/apache/xbean/xbean-asm6-shaded/4.9/xbean-asm6-shaded-4.9.jar:$M2/org/apache/commons/commons-lang3/3.7/commons-lang3-3.7.jar:$R/commons-weaver-privilizer-2.0.jar:$R/commons-weaver-privilizer-api-2.0.jar:$M2/com/example/version-weaver/1.0.0/version-weaver-1.0.0.jar:$M2/org/ow2/asm/asm/6.2.1/asm-6.2.1.jar
cd /work/cli-runner && javac -encoding UTF-8 -cp $CP -d classes WeaveRunner.java
cd /work/privilizer-demo && mvn -q clean compile
java -cp /work/cli-runner/classes:$CP WeaveRunner /work/privilizer-demo/target/classes $CP 2>&1 | grep -E '==|PROVIDER|INFO:|WEAVE_DONE|WARNING' | head -8
== ServiceLoader 发现的 Weaver 提供者 ==
PROVIDER: com.example.woven.VersionWeaver
PROVIDER: org.apache.commons.weaver.privilizer.PrivilizerWeaver
INFO: WOVEN_VERSION=2.0-demo injected into demo.AppInfo
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;
WEAVE_DONE
回显解读:两行 PROVIDER 证明 ServiceLoader 按 SPI 注册文件同时发现了自定义提供者与官方 privilizer 提供者;INFO 行来自 VersionWeaver 的 env.info(java.util.logging 格式输出);WARNING 三条与 Maven 插件路径完全一致;WEAVE_DONE 表示织入完成。
织入后验证与 Maven 插件产物逐字节等价(实测):
javap -p target/classes/demo/AppInfo.class | grep WOVEN
public static final java.lang.String WOVEN_VERSION;
javap -p -c target/classes/demo/SecretStore.class | grep doPrivileged
7: invokestatic #42 // Method java/security/AccessController.doPrivileged:(Ljava/security/PrivilegedAction;)Ljava/lang/Object;
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
六、最佳实践清单
本节把四个示例的实测结论收敛为可执行规范。
6.1 构建与版本实践
- 执行织入的构建环境锁定 JDK 8。JDK 11+ 实测报
Unsupported class file major version 61(内嵌 ASM 6.2.1 无法升级);产物字节码 major 52 可被更高版本 JRE 运行,因此「构建期 8、运行期 17」在字节码层面可行。 - 提供者放插件级
<dependencies>,注解放工程<dependencies>。放反了轻则编译不过,重则weavegoal 静默空转(BUILD SUCCESS 但零织入,实测复现过)。 - JDK 8 构建用
source/target=1.8,不要用maven.compiler.release。--release是 JDK 9+ 选项,JDK 8 实测报invalid flag: --release。 - 用固定三板斧自证织入生效:
ls target/classes/包名/找*$_ACTION.class→javap -p 类名找__privileged_*→javap -p -c 类名 | grep doPrivileged。CI 中建议保留第二步作为织入门禁。 - 调试织入时先跑
mvn compile再单跑mvn commons-weaver:weave,避免 clean 与织入混在一个日志里看不清;preparegoal 可把织入产物还原后再复织。
6.2 安全语义实践
- 特权方法访问级别尽量收窄。默认
private之外每放宽一级都会触发Possible security leak警告,public 特权方法等于把特权开放给所有能调用者。 - 给无权调用方提权时不要加
@Privilizing。蓝图把特权块搬到调用方 codeSource,调用方无权时照样被拒(示例二 3.4 节实测栈);标准姿势是直接调用提供者的@Privileged方法。 @Privilizing仅用于调用方自身已持权的性能优化(消除特权块截断点的多余帧),并在代码评审时对照示例二的语义表确认。- policy 与 codeSource 是特权块生效的前提。上线涉及 SecurityManager 的部署前,按示例二的布局做一次受控演练。
- 正视 SecurityManager 废弃趋势。JEP 411 已将其标记废弃(JDK 17 起),privilizer 的长期价值随之收窄;存量系统迁移评估时优先排查特权块依赖。
6.3 工程化实践
- 自定义注解一律 CLASS retention。RUNTIME retention 在 Maven 插件路径下扫描不到(示例三 4.5 节实测),且 CLASS retention 让织入注解不进运行时。
- 自定义提供者读注解值按
annotationType().getName()字符串匹配,避免跨类加载器的Class对象不一致问题(示例三实测踩坑)。 - 织入后的字节码同样要走代码评审。特权块在产物里而不在源码里,安全审计必须覆盖
javap产物核对,建议把织入报告(verbose=true日志)归档。 - 产物可重复性:织入由编译产物决定,同一输入重复织入结果一致;但不要对已织入的目录重复执行
weave,先prepare清理再织入。 - 依赖闭包检查:命令行直调时 classpath 缺失传递依赖(xbean-finder/xbean-asm6/commons-lang3)会在扫描阶段直接失败,Maven 方案则由坐标解析保证,两方案迁移时逐一核对。
七、常见问题回显速查
| 现象(实测回显) | 原因 | 解法 |
|---|---|---|
Unsupported class file major version 61 | JDK 17/11 上执行织入 | 构建环境换 JDK 8 |
weave BUILD SUCCESS 但无 *_ACTION.class 产物 | 提供者写成工程依赖,插件发现不到 | 挪到插件级 <dependencies> |
must have maximum access level 'private' but is defined wider ('public') | @Privileged 方法过宽且未放宽配置 | 配 privilizer.accessLevel=public |
invalid flag: --release | JDK 8 不支持 --release | 改用 source/target=1.8 |
unmappable character for encoding ASCII(javac) | POSIX locale 默认 ASCII 解码 | javac -encoding UTF-8 |
自定义注解扫描 scan candidates=0 | 注解用了 RUNTIME retention | 改 CLASS retention |
蓝图调用方 AccessControlException | 特权块落在调用方 codeSource,调用方无权 | 调用方持权或改用直接调用 |
八、参考链接
- 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
- processor 模块源码(SPI 与扫描模型):https://github.com/apache/commons-weaver/tree/master/commons-weaver-processor
- JEP 411:Deprecate the Security Manager for Removal:https://openjdk.org/jeps/411
- 本文配套文档:《Apache Commons Weaver 2.0 快速入门与集成示例》《Apache Commons Weaver 2.0 常用 API 与织入提供者详解》