提起 JavaAgent,很多人都说几句,就像古龙武侠小说里的「孔雀翎」,威力很大,江湖上都是它的传说。但真的见识过的人并没几个。
JavaAgent 虽说没这么神秘,但也一直给人曲高和寡的感觉,除了一些中间件产品、大型的框架中使用外,在业务中一直很少出现。
原因可能有很多,一来是可能确实不需要,再者需要开发独立的 Agent Jar 文件,在 Jar 内对类的 transform 开发也并不容易。
我们知道,无论是启动时的 Java Agent,还是运行时的动态 attach 到远程JVM, 都是为了拿到 Instrument,对 class 的字节码进行修改。这么底层的东西,当然使用起来让人不太容易下手。
不过就像机器语言不方便,人们发明了汇编语言,又发现了高级语言。对于字节码的操作也类似,人们觉得直接操作字节码有难度,而且需要深入理解 JVM 规范,具体什么位置多少字节代表啥,这不是一般人喜欢的,于是 ASM 框架出现了;但还是有规范的影子,不太「高级」,于是又出现了Javassist 这一类的「高级」工具。
我们今天要说的这个工具,和 Javassist 类似,都提供了更高层的API,来操作class,对普通程序员更友好,除此之外呢?
就像今天人们购物、读书等,都更相信专业的平台、或者专家的推荐,像XX严选,XX读书会推荐。今天说的这个工具是Duke 的推荐,对,就是它, Java 的吉祥物,这个小胖子。今天的这个工具在 2015年被 Oracle 评选为「Duke's Choice award」。
除了Duke,框架也得到了众多开发者的认可,每年有七千多万次的下载。
这个工具是:Bytebuddy。
从名字你就看的出来,它立志要做字节码的好伙伴。所以在很多开源框架里也能看到它的身影。
既然已经有了不少的工具, byteBuddy能带来什么不一样呢?
除了API 上的简洁易操作,官方自己也大字强调了运行时动态的「代码生成和字节码操作」,不需要再借助 Java 编译器。
来看看官网是怎么自我介绍的,后面再附上几个代码片段,就能很快 Get 到了。
官网:https://bytebuddy.net/
Byte Buddy is a code generation and manipulation library for creating and modifying Java classes during the runtime of a Java application and without the help of a compiler. Other than the code generation utilities that ship with the Java Class Library, Byte Buddy allows the creation of arbitrary classes and is not limited to implementing interfaces for the creation of runtime proxies. Furthermore, Byte Buddy offers a convenient API for changing classes either manually, using a Java agent or during a build.
阅读理解开始。重点你一定会看到了:
- 「code generation」
- 「creating and modifying Java classes」
作者贴心的加了一段小字来描述框架的优势。选重点的说就是:
- 不需要理解字节码,也不需要理解class 文件格式
- API 非侵入,设计简洁易懂
- 高度可定制,可以任意自定义
我自己认为该把这点也加上,不写 Java Agent 也可以 Attach 到 JVM 上,把 ByteBuddy 自己当成一个Agent,运行时直接install。Cool。
不写JVM Agent 也能对类拦截和修改,我们来认识下揭开字节码修改黑魔法的家伙。
为了对 Class 进行一些操作,我们一般都离不了 JVM Agent。不管是启动时直接连接,还是运行时动态的 Attach到对应的JVM 上,都需要 Agent。也就是我们熟悉的premain 和 agentmain 的触发入口,通过它们,我们才能拿到 Instrumentation,从而进行 transform和 redeine。
但这个东西的使用,给人总是「阳春白雪」的感觉,让人觉得是黑魔法一样,一般不会轻易尝试使用。
有了ByteBuddy,就不用再羡慕一些框架的「运行时增强」,「动态织入」等等,都可以实现。
如何上手呢?
只需要下载 Jar 文件或者 Maven 添加依赖之后就能狂奔了。
比如官方的这个 HelloWorld
- Class> dynamicType = new ByteBuddy()
- .subclass(Object.class)
- .method(ElementMatchers.named("toString"))
- .intercept(FixedValue.value("Hello World!"))
- .make()
- .load(getClass().getClassLoader())
- .getLoaded();
-
- assertThat(dynamicType.newInstance().toString(), is("Hello World!"));
直接把 Object 的 toString 方法改写了。
再比如我们可以在开发 Java Agent的时候使用这个伙计
- public static void premain(String args, Instrumentation inst) {
- AgentBuilder agentBuilder = new AgentBuilder.Default();
- AgentBuilder.Transformer transformer = new AgentBuilder.Transformer() {
- public DynamicType.Builder> transform(DynamicType.Builder> builder,
- TypeDescription typeDescription,
- ClassLoader classLoader, JavaModule javaModule) {
- String className = typeDescription.getCanonicalName();
- builder = builder.method(ElementMatchers.any())//匹配任意方法
- .intercept(MethodDelegation.to(new SimplePackageInstanceMethodInterceptor()));
- return builder;
- }
- };
-
- agentBuilder = agentBuilder.type(ElementMatchers.nameStartsWith("com.example.hello.sample")).transform(transformer);
- agentBuilder.installOn(inst);
- }
在类里进行拦截匹配的时候,可以通过类名来限定,同时以不同的模式去匹配方法名等,这里的ElementMatchers可以用在类名与方法名等匹配场景中
- //ElementMatchers.named("abc") // 特定名称的方法
- //ElementMatchers.nameStartsWith("hello") // 以什么开头的
- //ElementMatchers.nameEndsWith("test") // 以什么结尾的
我们看到前面的代码中 agentBuilder.installOn(inst);
通过 JavaAgent的Instrument 进行类修改。
AgentBuilder 还提供了一个神奇的方法:
- agentBuilder.installOnByteBuddyAgent();
这样无须提供 Jar 文件也一样能实现运行时增强。不过需要注意,这样使用时,一定要先执行这行代码,这也是其实现的秘密:
- ByteBuddyAgent.install();
因为 ByteBuddy 自己做为一个 Jar 也 Attach ,然后再将其它后续的增强代码加入,像不像「特洛伊木马」 :)
另外, ByteBuddy 还支持类似于 AOP 的 Advice实现,在拦截指定方法后可以实现OnMethodEnter 和 OnMethodExit 的控制,在这其中,可以完成绕过用户代码,执行自定义内容的逻辑。
咱们在开始的时候,还提到了代码的生成。这在 ByteBuddy 看来也是易如反掌。
和上面的代码一样,先要拿到 AgentBuilder,之后在执行 tranform的时候,直接指定方法名,以及对应的参数,访问控制符等。
- DynamicType.Builder.MethodDefinition.ExceptionDefinition> hello =
- builder.defineMethod(methodName, types, Visibility.PUBLIC)
- .withParameters(m.getParameters().asTypeList());
再比如在运行时给一个方法增加注解,
- builder.method(ElementMatchers.named("methodName")).intercept(SuperMethodCall.INSTANCE)
- .annotateMethod(AnnotationDescription.Builder.ofType(TestAnnotation.class)
- .define("testValue", 123).build());
是不是功能很强大?
更多的用法,可以参考官方的Github或者官网。
本文转载自微信公众号「Tomcat那些事儿」,可以通过以下二维码关注。转载本文请联系Tomcat那些事儿公众号。