在这篇文章中描述了 Java 中Monkey修补的几种方法:类Proxy、通过 Java 代理进行检测、通过 AspectJ 进行 AOP 以及javac编译器插件。
要选择其中一种,请考虑以下标准:构建时与运行时、复杂性、本机与第三方以及安全问题。
这篇文章的完整源代码可以在Github上找到
JVM 是一个出色的monkey patching平台。
Java代理
Java 代理是一个通用装饰器,允许附加动态行为:
Proxy 提供静态方法来创建对象,这些对象的行为类似于接口实例,但允许自定义方法调用。
Spring 框架大量使用 Java 代理。如果您注释一个方法,Spring 会在运行时围绕封装类创建一个 Java 代理。当您调用它时,Spring 会调用代理。根据配置,它打开事务或加入现有事务,然后调用实际方法,最后提交(或回滚)。
我们可以编写以下处理程序:
public class RepeatingInvocationHandler implements InvocationHandler {
private final Logger logger; private final int times;
public RepeatingInvocationHandler(Logger logger, int times) { this.logger = logger; this.times = times; }
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Exception { if (method.getName().equals("log") && args.length == 1 && args[0] instanceof String) { for (int i = 0; i < times; i++) { method.invoke(logger, args[0]); } } return null; } }
|
创建代理的方法如下:
var logger = new ConsoleLogger(); var proxy = (Logger) Proxy.newProxyInstance( Main.class.getClassLoader(), new Class[]{Logger.class}, new RepeatingInvocationHandler(logger, 3)); proxy.log("Hello world!");
|
Instrumentation
检测Instrumentation是 JVM 在通过Java 代理加载字节码之前转换字节码的能力。有两种 Java 代理风格可用:
Instrumentation API是通过字节数组向用户公开低级字节码操作。直接做的话会很不方便。
因此,现实生活中的项目依赖于字节码操作库。ASM一直是这方面的传统库,但[url=https://bytebuddy.net/]Byte Buddy[/url]似乎已经取代了它。请注意,Byte Buddy 使用 ASM,但提供了更高级别的抽象。
Byte Buddy API 超出了本博客文章的范围,因此让我们直接深入了解代码:
public class Repeater {
public static void premain(String arguments, Instrumentation instrumentation) { var withRepeatAnnotation = isAnnotatedWith(named("ch.frankel.blog.instrumentation.Repeat")); new AgentBuilder.Default() //Byte Buddy 提供了一个构建器来创建 Java 代理 .type(declaresMethod(withRepeatAnnotation)) .transform((builder, typeDescription, classLoader, module, domain) -> builder .method(withRepeatAnnotation) .intercept( SuperMethodCall.INSTANCE .andThen(SuperMethodCall.INSTANCE) .andThen(SuperMethodCall.INSTANCE)) ).installOn(instrumentation); } }
|
下一步是创建 Java 代理包。Java 代理是具有特定清单属性的常规 JAR。让我们配置 Maven 来构建代理:
<plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifestEntries> <Premain-Class>ch.frankel.blog.instrumentation.Repeater</Premain-Class> </manifestEntries> </archive> </configuration> <executions> <execution> <goals> <goal>single</goal> </goals> <phase>package</phase> </execution> </executions> </plugin>
测试更加复杂,因为我们需要两种不同的代码库,一种用于代理,另一种用于带有注释的常规代码。我们首先创建代理:
mvn install 然后我们可以使用代理运行应用程序:
java -javaagent:/Users/nico/.m2/repository/ch/frankel/blog/agent/1.0-SNAPSHOT/agent-1.0-SNAPSHOT-jar-with-dependencies.jar \ -cp ./target/classes ch.frankel.blog.instrumentation.Main
|
面向方面的编程
AOP背后的想法是在不同的不相关的对象层次结构中应用一些代码 - 横切关注点。对于不允许特征的语言来说,这是一项有价值的技术,您可以将代码移植到第三方对象/类上。有趣的事实:我之前了解过 AOP Proxy。AOP 依赖于两个主要概念:切面是应用于代码的转换,而切入点则匹配切面应用的位置。
在Java中,AOP的历史实现是优秀的AspectJ库。AspectJ 提供了两种称为编织的方法:构建时编织(转换已编译的字节码)和运行时编织(依赖于上述工具)。无论哪种方式,AspectJ 对方面和切入点使用特定的格式。在 Java 5 之前,该格式看起来像 Java,但又不完全一样;例如,它使用了aspect关键字。使用 Java 5,人们可以在常规 Java 代码中使用注释来实现相同的目标。
我们需要一个 AspectJ 依赖项:
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.9.19</version> </dependency>
|
和Byte Buddy一样,AspectJ底层也使用了ASM。
这是代码:
@Aspect public class RepeatingAspect {
@Pointcut("@annotation(repeat) && call(* *(..))") //定义切入点 public void callAt(Repeat repeat) {}
@Around("callAt(repeat)") //需要调用的原始方法 public Object around(ProceedingJoinPoint pjp, Repeat repeat) throws Throwable { for (int i = 0; i < repeat.times(); i++) { pjp.proceed(); } return null; } } 此时,我们需要编织切面。让我们在构建时进行。为此,我们可以添加 AspectJ 构建插件:
pom.xml <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <executions> <execution> <goals> <goal>compile</goal> </goals> </execution> </executions> </plugin>
查看演示效果:
mvn compile exec:java -Dexec.mainClass=ch.frankel.blog.aop.Main
|
Java编译器插件
最后,可以通过 Java 编译器插件更改生成的字节码,该插件在 Java 6 中作为JSR 269引入。从鸟瞰角度来看,插件涉及挂钩 Java 编译器以分三个阶段操作AST:将源代码解析为多个 AST,进一步分析Element,并可能生成源代码。
如果您有兴趣,我相信DocLint 源代码是一个很好的起点。