bytebuddy简单入门

2021年1月17日 45点热度 0条评论 来源: 舞动的痞老板

–> go to 总目录

bytebuddy介绍

介绍完了JVM TI和Attach和instrument ,是不是觉得自己可以动手对用户程序魔改了。其实还是很困难,bytebuddy可以帮助你做字节码修改。

作者的文章 了解作者写这个意图
官网入门教程看完后会似懂非懂的模仿用

github源码
byte buddy 源码分析 带你读懂源码

不错的使用指南

这个介绍文档不侧重讲如何用,官网文档有了,而是通过分析bytebuddy的源码,和官网文档给出一个轮廓,让使用者更好的理解。更多的细节也有一篇源码分析。后面细讲吧。
需要了解更多的细节,可以看我的源码解析,我会尽量完善的。

一、描述

  • 基于ASM的代码修改和生成工具

  • runtime期动态生成和修改

  • easy use无需理解字节码,简洁的代码风格

作者写的描述可以看出,最初的目的还是为写agent.jar,但是目标更大,目的是提供一个可以使用简单api来生成和修改类的工具。

二、总览

这张图就是bytebuddy的总体框架

  1. wave-framework : 多种代理框架或者修改字节码框架的对比
  2. classLoader: bytebudd使用classLoader的加载策略,wrap ,child-first, injection
  3. class-wave: 构造新类的方式,redefine,rebase, subclass后面会介绍
  4. matcher: 运行时判断一个类是不是目标类,这个就是matcher的作用。bytebuddy提供一堆内置的matcher,比如匹配方法异常的MethodExceptionTypeMatcher。
  5. implementation: 实现,目的是描述字节码如何转化。比如我想为类新加一个方法,使用methodCall,定义一个实现。

三、细节

描述如何使用bytebuddy的api去构造类

3.1 glance 一撇

这是简单使用bytebuddy的一个范式,很好的按照的人的思维去构造一个类

  1. 我想为Object.class 构造一个子类,那好就使用subclass
  2. 子类叫什么呢,使用name(example.Type)
  3. 子类想要覆盖Object.class的toString方法,我需要做什么呢
    a. 定义匹配的规则—matcher,named("toString")就是生成一个matcher
    b. 修改成什么呢—implementation, FixedValue.value生成一个 FixedValue.value
    c. intercept 拼接
  4. 这个定义好的类,该被什么classloader加载呢,这里传入的是当前类的加载器。
  5. 后续就是生成示例测试了。

3.2 各类类增强工具对比

这个信息来自于bytebuddy自己的说明

性能对比
类的生成策略面临一个权衡,Byte Buddy的主要重点在于以最少的运行时间生成代码。

这是面对类生成,接口实现等行为的不同增强工具的性能对比,总体看起来bytebuddy的性能没有特殊的地方,没有恶化。

到底怎么一个权衡策略,可以看官网,这不是关注的重点

java proxy

Java类库附带一个代理工具包,该工具包允许创建实现一组给定接口的类。这个内置的代理使用很方便,但功能非常有限。比如代理只能面对一个已经存在的接口,

但是对类进行扩展的时候,proxy办不到

cglib

太早了,没人维护了。

该代码生成库是在最初几年的Java实现,它也不幸的是没有与Java平台的发展跟上。尽管如此,cglib仍然是一个功能非常强大的库,但是它的积极开发却变得相当模糊。因此,它的许多用户都离开了cglib。

javassist

自带了一个相比javac弱小编译器,而且动态生成字节码时容易出错。

该库带有一个编译器,该编译器采用包含Java源代码的字符串,这些字符串在应用程序运行时会转换为Java字节代码。因为Java源代码显然是描述Java类的一种很好的方法,所以这是非常雄心勃勃的,并且从原则上讲是个好主意。

但是,Javassist编译器在功能上无法与javac编译器相提并论,并且在动态组成字符串以实现更复杂的逻辑时,容易出错。此外,Javassist附带了一个代理库,该代理库与JCL的代理实用程序相似,但是允许扩展类,并且不限于接口。

但是,Javassist代理工具的范围在API和功能方面同样受到限制。

3.3 命名策略

比如给Object.class创造一个子类,

这里没有指定任何名称,那么这个新生成的类会名为

example.Object$$ByteBuddy$$1376491271

可以看出在Object内生成了一个ByteBuddyde内部类,然后有生成了一个随机数命名的内部类。

也可以给新生成的类加固定的前缀

从这个两个类子可以看出命名策略就是给新生成的类设置名称生成规则。感兴趣可以通过入门文档和源码更细致的了解。

3.4 classLoader策略

bytebudyy提供了四种有关classLoader的行为 working with Unloaded,(Wrapper,child-first,injection)
Unloaded只是说明,bytebuddy可以使用没有加载进内存里面的类。
Wrapper,child-first,injection是说明将一个定义好的类加载进内存的策略

Unloaded 未加载的类的处理

首先bytebuddy中定义了一个TypeDescription对每一个类进行封装。

还设计了一个TypePool 可以来存储这些TypeDescription,相当于一个池子。

了解过instrument的原理就不陌生,每有一个类加载时会触发一个事件,调用回调函数里transformers对类进行加工。

由于bytebuddy自己可以读取类的定义,或者提供修改后的定义— 一个TypeDescription ,放在TypePool池子里。

所以bytebuddy可以显示的加载类(自己去加载类),从而不触发加载事件,阻止jvm内置的load函数去加载类。

总结一下

bytebuddy可以主动加载未加载进内存的类,提供类定义,放在TypePool中,提供给JVM使用。

但是更细节的原理,可以参考源码去了解。我这里也没有做深入

ClassReloadingStrategy

在加载新定义的类是可以指定加载策略

namespace 隔离
有一个问题,不同的jar有相同class名称时,jvm寻找时是如何避免冲突的。答案是比较两个类是否相同时,还要看加载他们的classloader是否相同。
每个classloader加载jar包的时候,相当于打上了一个namespace标签。不同namspace之间的类即使同名,也被视为两个类。
双亲委派
当前类中new或者其他触发寻找类的行为时,当前的类的classloader优先会委托自己的父classloader去寻找并加载,找不到自己才去寻找。

wrapper

默认的加载策略。新建当前classLoader的子classLoader。使用子classLoader来加载新定义的类。

好处是被加载的类可以看见所有父classloader的类。

缺点新的 ClassLoader,意味着新的namespace。这意味着可以加载两个具有相同名称的类,只要这些类由两个不同的类加载器加载即可。这样,即使两个类都代表相同的类实现,Java虚拟机也不会将这两个类视为相等。

这意味着,如果两个类都未使用同一类加载器加载,则该类example.Foo将无法访问另一个类的程序包私有方法example.Bar

同样,如果example.Bar扩展了example.Foo,任何覆盖的包级别私有方法将不起作用,但将委派给原始实现。

child-first

和warpper类似,也是新建classLoader。wrapper的缺点是,类的寻找往往是委派给父classLoader,如果父classloader由同名类。那么定义的类永远不被被加载。

所有child-first就是直接去加载。破坏了双亲委派。

inject

利用反射,利用反射调用classloader的加载器去直接加载构造好的类型,而不用经过find & load过程。

wrpper和child-first的好处是,我们加载的类都会记录在manifest文件里,这个文件就是生成的jar包中,包含的class信息。使用inject不会在manifest生成信息。

有时加载想要获取已经被加载类的字节码,记录在manifest中的类可以通过ClassLoader::getResourceAsStream读取。但是inject是用反射注入的,就没办法取到。

这里的细节或者疑问,没有深入。有兴趣的话可以去看看。

3.5 subclass & redefined & rebase

subclass

接受目标类,返回继承目标类的子类

redefine

修改目标类的区域,覆盖式,原先的类的区域会丢失

rebase

同redefine的作用。但是他会先将原先的父方法进行名称的修改。新的方法替换掉原先的方法,在内部对原先的方法进行一次调用。如下

3.6 ElementMatcher

ElemetMatcher接口就只有一个方法 boolean matches(T target) 作用如同所讲的,就是用来锁定需要匹配的目标方法。

我们到底需要对那个类,它的什么方法,什么field,做出该表。ElementMatcher matches 可以判断是否是目标方法。

使用者的开发往往侧重在这里,需要定义自己实现,定义匹配规则。

Junction 接口

就是and 和 or方法,用来处理多Matcher接口

内置的实现

bytebuddy库内置了很多自定义的matcher,使用者可以自己使用。

3.7 Implementation

给定义好的动态类型,提供字节码转化的方法。就是说implementation包含的接口方法,会被用来生成字节码。

比如是重置一个类内的String变量,可以使用实现了implementationFixedValue方法,这个方法最终会被用来生成字节码。

至于如何被用来生成字节码,有兴趣可以去研究源码,使用者更感兴趣的是有哪些与怎么用。

3.7.1 fiexdValue

为method返回一个指定的值。实现了Implementation接口可以被转化为字节码。

fiexdValue返回一个构造好的对象,一个对象被class文件记住,只有两种方式。

  1. 被写进classs文件的常量池。常量池会保留一堆无状态变量。这堆无状态变量就是有方法的name,变量name等信息。除了这些之外还可以存储String变量,和基础变量(未定义,数字,布尔值等)。除了前面说的,还可以保留引用。
  2. 被保存为类的静态变量。这样的话,这个值就会被一次性的加载进jvm里,这个会引发一次类的初始化加载。这个初始化加载可以通过TypeInitializer源码细致了解。我这里并没有深入。

使用FixedValue#value(Object)f,bytebuddy会分析它的参数,优先存在常量池里,其次是静态方法,这里没有深入分析,又想去可以看源码。

字节码 class文件 是一个table结构,常量池是class文件里,保存变量名称,方法名的地方。如下是一个class文件的部分截取

不难看出常量池的内容。这部分被加载时会被一同加载进方法区

3.7.2 method call

3.7.2.1 Delegationg method call

委托方法的调用:可以将不相关的类的方法替换进目标类。

bytebuddy搜索target的hello方法。生成implemetion替换进Source.class的子类中。

当tagret存在多个hello方法,命中策略怎么设置。可以参考官方文档和

关于如何使用注解来设置命中策略

3.7.2.2 SuperMethod

对父方法的调用,参考官方文档。可以指明调用父方法。

3.7.2.3 Default Method

jdk8以后支持接口设置default方法。这里是设置default方法的范式。

3.7.2.4 Specific method

一些特殊的使用。一些特殊使用中现有方法不满足是,可以直接使用MethodCall方法

3.7.2.5 AccessingField

就是属性的的set和get方法,为类构造这两个方法

3.7.3 注解

  1. 为类添加或删除注解
  2. 修改注解的属性。

四. java agent

前面讲的都是bytebuddy的如何构造一个类,有现成的范式。然后讲解了范式各个组件的功能,如果想更深入了解原理,可以从源码和入门文档出发。

4.1 agent builder

将修改放进Instrumention。这个就和instrument的api就对应上了。

transformer()中放置转换逻辑。细节不说了,可以研究文档和源码,会有更详细的说明。

4.2 bytebuddy agent

后面的源码分析中有细致的讲解

运行时加载,底层还是调用前面说的,vm attach接口。

4.3 AOP

byebyte 可以实现aop的功能。在前面的讲解中,没有提到过ASM,这部分也是前面那些api的底层。

其中源码中Adivce中封装的注解可以用来实现AOP

示例

4.3.1 @Advice.OnMethodEnter

被标注此注解的方法会被织入到被bytebudy增强的方法前调用

  1. Advice.Argument
    用来获取 被执行对象的field或者参数
  2. Advice.This
    是当前advice对象的载体,标注之后,方法内部可以获取目前advice对象。

4.3.2 @ Advice.OnMethodExit

  1. Advice.Argument
    用来获取 被执行对象的field或者参数
  2. Advice.This
    是当前advice对象的载体,标注之后,方法内部可以获取目前advice对象。
  3. Advice.Return
    给一个参数标识Advice.Return 后可以接收返回结果。
  4. Advice.Thrown
    给一个参数标识Advice.Thrown 后可以接收抛出的异常。如果方法引发异常,则该方法将提前终止。
    如果由Advice.OnMethodEnter注释的方法引发异常,则不会调用由Advice.OnMethodExit方法注释的方法。如果检测的方法引发异常,则仅当Advice.OnMethodExit.onThrowable()属性设置为true(默认值)时,才调用由Advice.OnMethodExit注释的方法。
    原文作者:舞动的痞老板
    原文地址: https://blog.csdn.net/wanxiaoderen/article/details/106544773
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。