Apache Commons Weaver 2.0 常用 API 与织入提供者详解

逐类讲解 Apache Commons Weaver 2.0 的织入 API:WeaveProcessor 入口与 Weaver/Cleaner SPI、ScanRequest/WeaveInterest 扫描模型、WeaveEnvironment 环境 API,privilizer 的 @Privileged/@Privilizing 注解与 Policy/AccessLevel 配置键语义,并用 javap 剖析织入产物方法形态,签名与回显均实测核对。

Apache Commons WeaverApache Commons Weaver2026/10/15

Apache Commons Weaver 2.0 常用 API 与织入提供者详解

目标:把 Commons Weaver 2.0 的公开 API 讲透——处理器内核(扫描、SPI、环境)、privilizer 注解(@Privileged/@Privilizing)、配置键(privilizer.policy/privilizer.accessLevel 等),并用 javap 剖析织入产物的真实字节码形态;本文全部接口签名经 javap 实测核对,字节码回显来自织入后的真实产物。

Commons Weaver 的 API 面很克制:使用者只需要两个注解加一个 Maven 插件;扩展者(写自定义织入提供者)才接触处理器内核的十余个接口。本文按这两类读者组织:第二、三章面向扩展者讲内核 API(全部为 javap 实测签名),第四、五章面向使用者讲 privilizer 注解与配置,第六章用真实字节码回显把「注解 → 产物」的映射讲清楚。

一、环境信息

本节固定实验基线,全部签名与回显来自该环境实测。

  • 操作系统:Linux x86_64(Ubuntu 22.04 服务器环境)
  • 架构:x86_64
  • JDK:OpenJDK 1.8.0_504(织入器实测只能运行在 JDK 8)
  • 构建工具: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-normalizer:2.0、org.apache.commons:commons-weaver-maven-plugin:2.0、org.apache.commons:commons-weaver-antlib:2.0
  • 验证用工作目录:/work/privilizer-demo(织入演示工程)

API 签名的核对方式(实测回显,下文所有签名同源):

javap -cp commons-weaver-processor-2.0.jar org.apache.commons.weaver.spi.Weaver
Compiled from "Weaver.java"
public interface org.apache.commons.weaver.spi.Weaver extends org.apache.commons.weaver.spi.WeaveLifecycleProvider<org.apache.commons.weaver.spi.WeaveLifecycle$Weave> {
  public abstract boolean process(org.apache.commons.weaver.model.WeaveEnvironment, org.apache.commons.weaver.model.Scanner);
}

💡 技术拓展:「提供者」在 Weaver 语境里指实现 SPI 并经 ServiceLoader 注册的织入实现,与常说的「服务提供者接口(SPI)」同构——

概念Weaver 中的对应物注册方式
SPI 接口org.apache.commons.weaver.spi.Weaver(织入)/ Cleaner(清理)实现接口
提供者实现PrivilizerWeaver、NormalizerWeaver 或自定义类classpath 放置实现类
注册文件META-INF/services/org.apache.commons.weaver.spi.Weaver文件内容为实现类全名

本文所有「织入提供者」均指此机制下的实现,与 OSGi Service、Spring Bean 等其他「提供者」概念无关。

二、API 总览:模块、包与类地图

本节给出 2.0 的完整模块地图与依赖关系,帮助定位每个 API 属于哪个 jar。

2.1 模块与依赖关系

模块体积(实测)角色被谁依赖
commons-weaver-processor93 KB处理器内核:WeaveProcessor、扫描模型、SPI 接口插件/命令行/所有提供者
commons-weaver-privilizer-api10 KBprivilizer 注解(纯注解,无逻辑)业务工程(编译期)
commons-weaver-privilizer508 KBprivilizer 织入提供者(内嵌 shaded ASM 6.2.1)Maven 插件类路径
commons-weaver-normalizer357 KBnormalizer 织入提供者(内嵌 shaded ASM)Maven 插件类路径
commons-weaver-maven-plugin32 KBMaven 插件入口pom.xml 构建插件
commons-weaver-antlib463 KBAnt 任务包装Ant 构建脚本

依赖方向一句话:业务工程只依赖 *-api 注解模块;提供者模块只出现在构建插件的类路径上;processor 是二者共享的内核。

2.2 核心包地图

包关键类/接口一句话职责
org.apache.commons.weaverWeaveProcessor织入总入口,编排扫描与提供者
org.apache.commons.weaver.spiWeaver、Cleaner、WeaveLifecycleProvider提供者 SPI 契约
org.apache.commons.weaver.modelScanner、ScanRequest、WeaveInterest、ScanResult、Weavable*、WeaveEnvironment扫描与织入环境模型
org.apache.commons.weaver.utilsProviders、AnnotationInflaterServiceLoader 发现、CLASS retention 注解膨胀
org.apache.commons.weaver.privilizer(privilizer 模块)PrivilizerWeaver、Privilizer、Policy、AccessLevel特权块织入实现与配置
org.apache.commons.weaver.privilizer(privilizer-api 模块)@Privileged、@Privilizing、@Privilizing.CallTo业务侧注解
org.apache.commons.weaver.normalizer(normalizer 模块)NormalizerWeaver、Normalizer匿名类收敛实现

三、织入处理器内核 API(commons-weaver-processor)

本节面向扩展者讲内核 API;签名均为 javap 实测输出。

3.1 入口类 WeaveProcessor

WeaveProcessor 是唯一需要手工构造的内核类(Maven 插件内部同样调它),实测签名:

public class org.apache.commons.weaver.WeaveProcessor {
  public org.apache.commons.weaver.WeaveProcessor(java.lang.Iterable<java.lang.String>, java.io.File, java.util.Properties);
  public org.apache.commons.weaver.WeaveProcessor(java.lang.Iterable<java.lang.String>, java.io.File[], java.util.Properties);
  public void weave();
  public void clean();
}

参数含义:第一个参数是织入器 classpath(被扫描字节码的定位来源,目录或 jar 路径列表);第二个参数是织入目标目录(Maven 场景即 target/classes);第三个参数是织入配置(Properties,键值见第五章)。weave() 执行织入,clean() 执行逆向清理。命令行直调示例(实测通过):

new WeaveProcessor(classpath, new File("/work/privilizer-demo/target/classes"), config).weave();

3.2 SPI 接口:Weaver 与 Cleaner

提供者只需实现一个方法。实测签名:

public interface org.apache.commons.weaver.spi.Cleaner
    extends org.apache.commons.weaver.spi.WeaveLifecycleProvider<org.apache.commons.weaver.lifecycle.WeaveLifecycleToken$Clean> {
  public abstract boolean clean(org.apache.commons.weaver.model.WeaveEnvironment, org.apache.commons.weaver.model.Scanner);
}

public interface org.apache.commons.weaver.spi.Weaver
    extends org.apache.commons.weaver.spi.WeaveLifecycleProvider<org.apache.commons.weaver.lifecycle.WeaveLifecycleToken$Weave> {
  public abstract boolean process(org.apache.commons.weaver.model.WeaveEnvironment, org.apache.commons.weaver.model.Scanner);
}

要点:Weaver.process(env, scanner) 与 Cleaner.clean(env, scanner) 签名对称,均接收织入环境与扫描器;WeaveLifecycleProvider 本身是空标记接口,泛型参数 org.apache.commons.weaver.lifecycle.WeaveLifecycleToken$Weave/$Clean 用于提供者排序时的生命周期配对;privilizer 的注册文件里两类都注册了(PrivilizerWeaver 与 PrivilizerCleaner),clean 负责把织入产物还原为未织入状态(插件 prepare goal 走的就是这条路)。

3.3 扫描模型:ScanRequest / WeaveInterest / ScanResult / Weavable*

提供者不自己翻目录找类,而是向 Scanner 提交一个 ScanRequest。实测签名链:

public interface org.apache.commons.weaver.model.Scanner {
  public abstract org.apache.commons.weaver.model.ScanResult scan(org.apache.commons.weaver.model.ScanRequest);
}

public interface org.apache.commons.weaver.model.ScanRequest {
  public abstract org.apache.commons.weaver.model.ScanRequest add(org.apache.commons.weaver.model.WeaveInterest<?>);
}

public interface org.apache.commons.weaver.model.WeaveInterest<T> {
  public static <T> org.apache.commons.weaver.model.WeaveInterest of(java.lang.Class<? extends T>, java.lang.annotation.ElementType);
  public abstract java.lang.Class<? extends T> getAnnotationType();
  public abstract java.lang.annotation.ElementType getElementType();
}

public interface org.apache.commons.weaver.model.ScanResult {
  public abstract org.apache.commons.weaver.model.WeavableClass$Builder<? extends java.lang.Object> getClasses();
  public abstract org.apache.commons.weaver.model.WeavableMethod$Builder<? extends java.lang.Object> getMethods();
  public abstract org.apache.commons.weaver.model.WeavableConstructor$Builder<? extends java.lang.Object> getConstructors();
  public abstract org.apache.commons.weaver.model.WeavableField$Builder<? extends java.lang.Object> getFields();
  public abstract org.apache.commons.weaver.model.WeavableAnnotation<?> getAnnotations();
}

语义逐条解读:WeaveInterest.of(注解类型, ElementType.TYPE/METHOD/FIELD/CONSTRUCTOR) 声明「我要找标注了某注解的某类程序元素」;ScanRequest 可累加多个 interest;ScanResult 按元素类别返回 Weavable* 流水线。过滤经 AnnotatedElements 的 with() 完成,实测签名:

public interface org.apache.commons.weaver.model.AnnotatedElements<T extends java.lang.reflect.AnnotatedElement>
    extends java.lang.Iterable<T> {
  public abstract org.apache.commons.weaver.model.AnnotatedElements<T> with(java.lang.Class<? extends java.lang.annotation.Annotation>);
}

Weavable 家族把「反射元素 + 字节码注解」绑在一起,基类是抽象类(非接口),实测签名:

public abstract class org.apache.commons.weaver.model.Weavable<SELF extends org.apache.commons.weaver.model.Weavable<SELF, TARGET>, TARGET>
    implements java.lang.Comparable<SELF>, java.lang.reflect.AnnotatedElement {
  public TARGET getTarget();
  public final boolean addAnnotations(java.lang.annotation.Annotation...);
  public final synchronized java.lang.annotation.Annotation[] getAnnotations();
  public synchronized <T extends java.lang.annotation.Annotation> T getAnnotation(java.lang.Class<T>);
  public final java.lang.annotation.Annotation[] getDeclaredAnnotations();
  public boolean isAnnotationPresent(java.lang.Class<? extends java.lang.annotation.Annotation>);
}

getTarget() 返回反射 Class/Method/Field;AnnotatedElement 侧的 getAnnotation() 读的是经 AnnotationInflater 从字节码膨胀出来的注解实例——这是 Weaver 支持 CLASS retention 注解的关键(普通反射在运行期看不到 CLASS retention 注解,Weaver 直接读 class 文件,因此织入注解可以不进运行时)。实测注意点:提供者代码里引用注解类时,getAnnotation(注解.class) 依赖注解类的 Class 对象唯一性;跨类加载器场景(自定义提供者同时出现在插件类路径与工程类路径)更稳妥的写法是遍历 getAnnotations() 按 annotationType().getName() 字符串匹配(第五章示例三按此实现并通过实测)。

3.4 织入环境 WeaveEnvironment 与 Resource

WeaveEnvironment 是提供者读写产物与打日志的唯一通道,实测签名:

public interface org.apache.commons.weaver.model.WeaveEnvironment {
  public abstract java.lang.ClassLoader getClassLoader();
  public abstract java.util.Properties getConfig();
  public abstract org.apache.commons.weaver.model.WeaveEnvironment$Resource getClassfile(java.lang.Class<?>);
  public abstract void debug(java.lang.String, java.lang.Object...);
  public abstract void info(java.lang.String, java.lang.Object...);
  public abstract void warn(java.lang.String, java.lang.Object...);
  public abstract void error(java.lang.String, java.lang.Object...);
}

public interface org.apache.commons.weaver.model.WeaveEnvironment$Resource {
  public abstract java.io.InputStream getInputStream() throws java.io.IOException;
  public abstract java.io.OutputStream getOutputStream() throws java.io.IOException;
  public abstract java.lang.ClassLoader getClassLoader();
}

典型用法(第五章示例三实测):env.getClassfile(target).getInputStream() 读原始字节码 → ASM 改写 → env.getClassfile(target).getOutputStream() 写回。getConfig() 返回的即 Maven weaverConfig/命令行 Properties 中的键值。日志方法经 JavaLoggingToMojoLoggingRedirector 重定向到 Maven 构建日志(Maven 下以 env.warn 输出的行在普通 -B 构建即可见)。

3.5 提供者发现与排序

内核经 org.apache.commons.weaver.utils.Providers 做 ServiceLoader 发现:以 Weaver.class 的定义类加载器调用 ServiceLoader.load(Weaver.class, typeLoader)。这决定了实操铁律——提供者 jar 必须与 processor 出现在同一个类加载空间。Maven 插件场景即「插件级 <dependencies>」;实测把提供者写成普通工程依赖时,插件发现不到任何提供者,weave goal 静默空转。发现后的实例经 Providers.sort() 按生命周期拓扑排序(@Consumes/@Produces 标记)后依次执行。

四、privilizer 注解 API(commons-weaver-privilizer-api)

本节讲业务侧仅有的两个注解;源码位于 privilizer-api 模块,注解语义以模块源码为权威依据。

4.1 @Privileged

实测源码定义(逐字保留,去除许可证头与 javadoc):

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Privileged {
}
  • 标注对象:方法。被标注方法的方法体在织入后被搬入合成方法,并包进 AccessController.doPrivileged 特权块。
  • CLASS retention 是有意设计:注解只服务于构建期织入。实测 javap -v 查看织入产物,@Privileged 会以 RuntimeInvisibleAnnotations 形式保留在字节码中,但因为不是 RUNTIME retention,运行期反射(isAnnotationPresent 等)不可见,不会影响运行时行为。这也意味着织入必须在构建期一次性完成,无法运行期补织。
  • 默认只允许标注 private 方法(见 5.2 节 AccessLevel)。

4.2 @Privilizing 与 @CallTo

实测源码定义(逐字保留,去除许可证头与 javadoc):

package org.apache.commons.weaver.privilizer;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
public @interface Privilizing {
    @interface CallTo {
        Class<?> value();
        String[] methods() default { };
    }

    CallTo[] value();
}
  • 标注对象:类。语义是「本类会调用 value 指定类中标注了 @Privileged 的方法,请织入器重写本类的这些调用点」。
  • CallTo.value 指向特权方法提供者类;methods 可选,限定方法名(缺省为该类全部 @Privileged 方法)。
  • 未标注 @Retention:按 Java 规范默认 RetentionPolicy.CLASS,与 @Privileged 一致,只存在于字节码、运行期反射不可见。
  • 织入效果(实测产物见第六章 6.2):调用点改为调用复制进本类的蓝图副本 提供者类名简化$$方法名,特权块随之落在调用方的 codeSource 里。这与「直接调用」的语义差异及安全影响见第七章。

两个注解的分工记忆法:@Privileged 管「方法体本身要不要特权执行」,@Privilizing 管「调用方的调用点怎么重写」。只标前者即可获得特权块(调用方零改动);后者是构建期优化与显式声明。

五、privilizer 织入提供者与配置键

本节讲提供者侧的配置语义,全部键经实测验证。

5.1 Policy 四种策略

org.apache.commons.weaver.privilizer.Policy(privilizer 模块内枚举)实测取值与语义:

取值织入产物行为
NEVER不做任何特权块织入
ON_INIT织入为「初始化时检查一次 SecurityManager,存在则走特权块」的形态
DYNAMIC(默认)每次 @Privileged 方法执行时检查 System.getSecurityManager(),存在才经 doPrivileged
ALWAYS无条件织入为 doPrivileged 调用(本文示例所用,产物最简、javap 最直观)

Maven 配置写法:<weaverConfig><privilizer.policy>ALWAYS</privilizer.policy></weaverConfig>。

5.2 AccessLevel 访问级别

org.apache.commons.weaver.privilizer.AccessLevel(privilizer 模块内枚举)控制 @Privileged 允许标注的最大方法访问级别,默认 PRIVATE。实测报错形态(public 方法未配置时):

Method demo/SecretStore#readToken()Ljava/lang/String; must have maximum access level 'private' but is defined wider ('public')

配置 privilizer.accessLevel=public 后织入通过,但 privilizer 会输出安全审计警告(实测回显):

[WARNING] Possible security leak: granting privileges to public method demo/SecretStore.readToken()Ljava/lang/String;

工程语义:特权方法越公开,能「搭便车」获得特权的代码面越大。生产代码优先把特权方法收窄为 private/package 级,用包内门面暴露结果。

5.3 weaverConfig 配置键总表

配置键取值(实测有效)归属提供者作用
privilizer.policyNEVER/ON_INIT/DYNAMIC/ALWAYSprivilizer特权块织入策略(见 5.1)
privilizer.accessLevelpublic/protected/package/privateprivilizer允许 @Privileged 的最大访问级别(见 5.2)
privilizer.verifytrue/falseprivilizer织入后用 ASM CheckClassAdapter 校验产物(取自模块源码,未单独实测开关效果)
normalizer.targetPackage合法包名normalizer合格匿名类重映射的目标包(取自模块源码)

Maven 插件自身的三个参数(取自插件 plugin.xml,实测):target(织入目标目录,默认 ${project.build.outputDirectory})、verbose(织入详细日志,默认 false)、weaverConfig(上表键值对)。goal 清单:prepare(绑定 initialize,清理织入痕迹)、weave(绑定 process-classes)、test-prepare、test-weave、help。

六、织入产物形态剖析(javap 逐方法解读)

本节把「注解 → 字节码」的映射用实测产物讲清楚,全部回显来自 /work/privilizer-demo 织入后的 target/classes(privilizer.policy=ALWAYS)。

6.1 特权方法产物(SecretStore)

源码中 readToken 只有一行 System.getProperty;织入后 javap -p -c 实测产物:

  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() 保存原始方法体(逐字节与织入前一致);__privileged_access$0() 是 ACTION 类回调的桥接方法(包级私有,供 SecretStore$readToken_ACTION.run() 调用);SecretStore$readToken_ACTION 是织入生成的 PrivilegedAction 实现类(ls target/classes/demo/ 可见 SecretStore$readToken_ACTION.class);readToken() 本体被改写为「构造 ACTION → doPrivileged → 强转返回」。@Privileged 注解并未被删除——javap -v 可见它仍以 RuntimeInvisibleAnnotations 保留在产物中,只是运行期反射不可见。

6.2 蓝图调用方产物(BlueprintClient)

源码标注 @Privilizing(@CallTo(value = SecretStore.class, methods = "readToken")),织入后 javap -p 实测方法清单:

Compiled from "BlueprintClient.java"
public final class demo.BlueprintClient {
  private demo.BlueprintClient();
  public static java.lang.String token();
  private static java.lang.String demo_SecretStore$$readToken();
  static java.lang.String __privileged_access$0();
  private static java.lang.String __privileged_demo_SecretStore$$readToken();
}

token() 的调用点(实测):

  public static java.lang.String token();
    Code:
       0: invokestatic  #42                 // Method demo_SecretStore$$readToken:()Ljava/lang/String;
       3: areturn

命名规律即语义:demo_SecretStore$$readToken 是从提供者类复制进本类的蓝图方法(类名简化$$方法名),其内部自带 doPrivileged 包装(配套生成 BlueprintClient$demo_SecretStore$$readToken_ACTION 内部类,ls 实测可见);__privileged_demo_SecretStore$$readToken 保存复制的原始方法体。调用点从 invokestatic SecretStore.readToken 变为 invokestatic 本类蓝图副本。

6.3 normalizer 提供者(对照参考)

normalizer 的织入对象是匿名类:从字节码中识别合格的匿名类(源码定义的三类不合格原因 IneligibilityReason.NOT_ANONYMOUS/TOO_MANY_CONSTRUCTORS/IMPLEMENTS_METHODS——多个构造器或实现了额外方法的匿名类不动),将其复制重映射到 normalizer.targetPackage 包下以内容摘要命名的规范位置($normalized<摘要> 类名),原位置类保留、逻辑重定向。设计目的:多个构建单元重复生成同一形态匿名类时,运行期收敛为唯一定义,避免重复类定义膨胀。本文未织入 normalizer 样例工程,语义以上述模块源码事实为准。

七、运行时行为:织入产物与 SecurityManager

本节给出织入产物的运行时语义结论,完整对照实验(policy 文件、分离 codeSource、三类回显)见《Apache Commons Weaver 2.0 织入示例与最佳实践详解》第三章。

7.1 两种调用方的语义差异

调用方形态特权块落点受限调用方调用结果(实测)
直接调用 SecretStore.readToken()(ConfigClient)提供者方法内部(SecretStore 的 codeSource)成功:特权块截断于提供者帧,提供者持有权限即可
@Privilizing 蓝图调用(BlueprintClient)调用方类内部(BlueprintClient 的 codeSource)失败:AccessControlException,特权块所在 codeSource 必须自身持权

7.2 关键栈回显

蓝图调用方在受限 codeSource 下运行的真实栈(实测节选):

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)

读栈要点:doPrivileged(Native Method) 之下全是 BlueprintClient 的帧——特权块确实在调用方侧执行;AccessControlContext.checkPermission 对特权截断点之前所有帧逐一遍历,本例全部属于无权限的调用方 codeSource,故拒绝。结论写进工程规范:@Privilizing 蓝图适合调用方自身已持权的场景(减少特权帧层数);让无权调用方获得结果的正确姿势是不加 @Privilizing,让提供者方法的 @Privileged 去做提权。

八、参考链接

  1. Apache Commons Weaver 官方主页与 javadoc:https://commons.apache.org/proper/commons-weaver/
  2. processor 模块源码(API 语义权威依据):https://github.com/apache/commons-weaver/tree/master/commons-weaver-processor
  3. privilizer 模块源码:https://github.com/apache/commons-weaver/tree/master/commons-weaver-modules/commons-weaver-privilizer
  4. Maven 中央仓库坐标:https://repo1.maven.org/maven2/org/apache/commons/commons-weaver-processor/2.0/
  5. 本文配套实操文档:《Apache Commons Weaver 2.0 快速入门与集成示例》《Apache Commons Weaver 2.0 织入示例与最佳实践详解》