探秘Java:用ByteBuddy编写一个简单的Agent

探秘Java:用ByteBuddy编写一个简单的Agent

人生苦短,不如养狗

一、从认识ByteBuddy开始

  在之前的博客当中我们了解了Java Agent的一些基本概念和如何编写一个简单的Java Agent,但是在之前的博客中所使用的Agent编写方法还是相对原始和繁琐的。在原先的逻辑中我们是使用Instrument直接进行二进制码操作和修改,这种方式要求使用者对Java class文件格式的相关知识能够了然于胸,简单来说就是需要做到人肉翻译二进制文件这样一个非人操作。为了进一步简化编写Java Agent的复杂度,这里我们要介绍下面这样一款字节码处理利器——ByteBuddy

  ByteBuddy是一个能够在Java应用程序运行时用于创建和修改Java类的代码生成和操作类库,而这种处理能力是不需要编译器参与的。从官网的介绍中可以发现,ByteBuddy是基于另一款字节码操作神器ASM创造出来的,但是相比ASM的高使用门槛(仍然需要对Java字节码有一定的了解),ByteBuddy使用起来会显得更为简单便捷。由于ByteBuddy提供了一系列完善且便捷的API,使用者可以在不需要了解Java字节码和class文件格式的情况下很方便地进行字节码操作(通过使用Java Agent或者在程序构建时完成对应的操作)。

二、编写一个简单的Java Agent——方法耗时统计

  从上面的描述中我们可以了解到,ByteBuddy的诞生并非单纯为了创建Java Agent,我们只是借助了ByteBuddy提供的API来生成更易维护的Java Agent,下面我们通过一个简单的例子来了解一下如何使用ByteBuddy来编写一个Java Agent。

  下面我们要编写的Java Agent主要是用于进行方法执行的耗时统计,参考以往使用AOP方式的思路,我们需要进行以下处理:

  • 指定需要拦截处理的对象(可以是类、方法或者被注解的元素);
  • 明确如何处理拦截的对象;

  在Java Agent当中所有关于字节码的操作都需要通过Instrumentation来进行,为了完成上面的两个操作和关于Instrumentation的操作,ByteBuddy提供了AgentBuilder类来提供所需的API接口。我们借助下面的例子看一下AgentBuilder提供的API接口。

DemoAgent(示例Agent)

public class DemoAgent {

/**
* 在主线程启动之前进行处理
*
* @param agentArgs 代理请求参数
* @param instrumentation 插桩
*/

public static void premain(String agentArgs, Instrumentation instrumentation) {
handleInstrument(instrumentation);
}

/**
* 进行插桩处理
*
* @param instrumentation 待处理桩
*/

private static void handleInstrument(Instrumentation instrumentation) {
new AgentBuilder.Default()
.type(ElementMatchers.nameEndsWith("App"))
.transform((builder, type, classLoader, module) -> builder.method(ElementMatchers.any()).intercept(MethodDelegation.to(TimeInterceptor.class)))
.installOn(instrumentation)
;
}

}

TimeInterceptor(方法执行耗时统计拦截器)

/**
* 时间统计拦截器,虚拟机维度的aop
*
* @author brucebat
* @version 1.0
* @since Created at 2021/12/30 10:31 上午
*/

public class TimeInterceptor {

/**
* 进行方法拦截, 注意这里可以对所有修饰符的修饰的方法(包含private的方法)进行拦截
*
* @param method 待处理方法
* @param callable 原方法执行
* @return 执行结果
*/

@RuntimeType
public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Exception {
long start = System.currentTimeMillis();
System.out.println("agent test: before method invoke! Method name: " + method.getName());
try {
return callable.call();
} catch (Exception e) {
// 进行异常信息上报
System.out.println("方法执行发生异常" + e.getMessage());
throw e;
} finally {
System.out.println("agent test: after method invoke! Method name: " + method.getName());
System.out.println(method + ": took " + (System.currentTimeMillis() - start) + " millisecond");
}
}
}

  在上面的示例中我们使用了AgentBuilder的默认实现类(这里不难发现创建者模式的身影,由于不是关注的重点这里暂时不提),通过AgentBuilder的默认实现类完成了以下三件事件:

  1. 通过Identified.Narrowable type(ElementMatcher<? super TypeDescription> typeMatcher);方法,指定了当前Agent需要拦截处理的对象,在本例中需要处理的对象为所有名称以App结尾的类型;
  2. 通过Extendable transform(Transformer transformer);方法明确了需要如何处理被拦截的对象,这里使用了lambda方式来简写了对于Transformer#transform方法的实现。在实现的过程中通过builder.method()进一步明确需要处理的方法,在本例中会处理符合上一个拦截条件的所有方法,接着通过intercept()方法和MethodDelegation来注入关于被拦截方法的另一种实现方法,本例中通过TimeInterceptor来完成对于方法的执行耗时统计。看到这里是否会感觉和代理模式(或者说我们常用的AOP)有些类似,尤其是TimeInterceptor当中的处理逻辑,只是在这一过程中并没有使用反射机制,这也是使用ByteBuddy的一个优势;
  3. 最后,在完成了对于拦截对象的指定和对象处理逻辑的编写后,通过ResettableClassFileTransformer installOn(Instrumentation instrumentation);完成Instrumentation装载ClassFileTransformer(即上面关于文件修改的逻辑)的逻辑;

  从上面的流程可以看出,本质上AgentBuilder帮助我们完成了关于ClassFileTransformer的实现和装载逻辑。和原先直接编写一个ClassFileTransformer然后修改其中的二进制文件数据相比,使用AgentBuilder来会让我们对于整个的处理逻辑更加明确和专注,在编写的过程我们只需要关注所需要修改的对象和修改的逻辑,其余只需要通过API就可以完成所有的操作。

三、总结

  本文更多在于介绍ByteBuddy的概要和使用ByteBuddy创建Java Agent的使用流程,对于ByteBuddy具体的原理这里不做过多的说明,在后续的篇章中会进行具体的介绍。

  最后,在新年的第一天,祝各位新年快乐、身体健康,祝疫情早日结束~~


原文始发于微信公众号(Brucebat的伪技术鱼塘):探秘Java:用ByteBuddy编写一个简单的Agent

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/90056.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!