ByteBuddy 介绍
首先需要了解ByteBuddy是什么,ByteBuddy是一款java字节码增强框架,可以动态的生成java字节码文件,比起我们自己进行字节码文件的生成,它屏蔽了底层细节,提供一套统一易上手的Api,简化了字节码增强的学习难度。
为什么需要字节码增强技术?ByteBuddy官方文档已经给出了答案
The Java language comes with a comparatively strict type system. Java requires all variables and objects to be of a specific type and any attempt to assign incompatible types always causes an error. These errors are usually emitted by the Java compiler or at the very least by the Java runtime when casting a type illegally. Such strict typing is often desirable, for example when writing business applications. Business domains can usually be described in such an explicit manner where any domain item represents its own type. This way, we can use Java to build very readable and robust applications where mistakes are caught close to their source. Among other things, it is Java’s type system that is responsible for Java’s popularity in enterprise programming.
=
However, by enforcing its strict type system, Java imposes limitations that restrict the language’s scope in other domains. For example, when writing a general-purpose library that is to be used by other Java applications, we are normally not able to reference any type that is defined in the user’s application because these types are unknown to us when our library is compiled. In order to call methods or to access fields of the user’s unknown code, the Java Class Library comes with a reflection API. Using the reflection API, we are able to introspect unknown types and to call methods or access fields. Unfortunately, the use of the reflection API has two significant downsides:
Using the reflection API is slower than a hard-coded method invocation: First, one needs to perform a rather expensive method lookup to get hold of an object that describes a specific method. And when a method is invoked, this requires the JVM to run native code which requires long run time compared to a direct invocation. However, modern JVMs know a concept called inflation where the JNI-based method invocation is replaced by generated byte code that is injected into a dynamically created class. (Even the JVM itself uses code generation!) After all, Java’s inflation system remains with the drawback of generating very general code that for example only works with boxed primitive types such that the performance drawback is not entirely settled.
The reflection API defeats type-safety: Even though the JVM is capable of invoking code by reflection, the reflection API is itself not type-safe. When writing a library, this is not a problem as long as we do not need to expose the reflection API to the library’s user. After all, we do not know the user code during compilation and could not validate our library code against its types. Sometimes, it is however required to expose the reflection API to a user by for example letting a library invoke one of our own methods for us. This is where using the reflection API becomes problematic as the Java compiler would have all the information to validate our program’s type safety. For example, when implementing a library for method-level security, a user of this library would want the library to invoke a method only after enforcing a security constraint. For this, the library would need to reflectively call a method after the user handed over the required arguments for this method. Doing so, there is however no longer a compile-time type check if these method arguments match with the method’s reflective invocation. The method call is still validated but the check is delayed until runtime. Doing so, we voided a great feature of the Java programming language.
This is where runtime code generation can help us out. It allows us to emulate some features that are normally only accessible when programming in a dynamic languages without discarding Java’s static type checks. This way, we can get the best of both worlds and additionally improve runtime performance. To get a better understanding of this problem, let us look at the example of implementing the mentioned method-level security library.
简单总结就是java的反射存在诸多限制,java开发者需要一种手段模拟一些动态语言才具有的特性,而且不失去自己安全类型的特性,相比cglib,javasist等相同功能的工具,bytebuddy 更容易上手且具有更高的性能。
本篇博客将根据官方文档,介绍bytebuddy的一些功能和特性的使用,也作为笔者自己的一个学习记录。
本篇博客所有的代码示例都基于bytebuddy的1.8.0版本
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.8.0</version>
</dependency>
从生成一个类开始
既然是字节码增强框架,那么作为入门的HelloWorld程序生成一个类用来演示是再好不过的了,ByteBuddy的Api设计相当优秀,我们只需要简单几行代码就可以生成一个自定义的类:
@Test
public void test() throws Exception {
Object helloWorld = new ByteBuddy()
.subclass(Object.class)
.name("com.tinysakura.HelloWorld")
.make()
.load(ClassLoader.getSystemClassLoader())
.getLoaded()
.newInstance();
}
我们也可以将生成的字节码输出到指定的文件观察
@Test
public void test() throws Exception {
new ByteBuddy()
.subclass(Object.class)
.name("com.tinysakura.HelloWorld")
.make()
.saveIn(new File("/Users/chenfeihao/Desktop"));
}
我们可以在这个路径下找到我们生成的class文件
/Users/chenfeihao/Desktop/com/tinysakura
反编译一下看看长什么样
package com.tinysakura;
public class HelloWorld {
public HelloWorld() {
}
}
除了构造方法光秃秃的什么都没有,接下来会一步步去充实生成的字节码文件。
我们在subClass中指定了父类为Object,而Object是所有java的父类所以在反编译的文件中没有显式的展现出来,那我们可以指定生成其它类型的子类吗?当然可以
@Test
public void test() throws Exception {
Object helloWorld = new ByteBuddy()
.subclass(Moo.class)
.name("com.tinysakura.HelloWorld")
.make()
.saveIn(new File("/Users/chenfeihao/Desktop/com/tinysakura"));
}
反编译的结果:
package com.tinysakura;
import com.tinysakura.bytebuddylearn.method.Moo;
public class HelloWorld extends Moo {
public HelloWorld() {
}
}
可以看到这次生成的class文件继承了我们指定的Moo类型,那么所有类型的类都可以被继承吗?
尝试去继承String类型:
Object helloWorld = new ByteBuddy()
.subclass(String.class)
直接抛出了一个异常:
java.lang.IllegalArgumentException: Cannot subclass primitive, array or final types: class java.lang.String
可以看到原基本类型(int,double,char…),数组类型和final类型的类不允许被继承,看来ByteBuddy并没有打破java的规范。
以上几个示例里我都指定了生成类的类名,其实我们也可以不指定,ByteBuddy提供了一套命名策略,我们也可以提供自定义的命名策略(比如生成的子类如果是父类的实现类,我们可以指定生成类的类名为父类名+impl),下面这个例子来自bytebuddy官方文档:
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
.with(new NamingStrategy.AbstractBase() {
@Override
public String subclass(TypeDescription superClass) {
return "i.love.ByteBuddy." + superClass.getSimpleName();
}
})
.subclass(Object.class)
.make();
提供了一个NamingStrategy的实现,在自定义的i.love.ByteBuddy包下生成了与父类类名相同的子类。
可以看到bytebuddy生成类的代码相当简单,比起自己动手生成不知道简单了多少,而且十分灵活,看到这里是不是更想要了解
它了呢
重新定义已经存在的类
比起生成一个全新的类,工作中更多的需求是对已有的类做修改或增强(大部分是第三方中间件甚至基础java类),bytebuddy自然对这部分能力做了支持
现在我们已经定义了一个HelloWorld类:
package com.tinysakura.bytebuddylearn.clazz;
public class HelloWorld {
String helloWorld() {
return "hello world";
}
}
简单的对其redefine一下:
import com.tinysakura.bytebuddylearn.clazz.HelloWorld;
public void test() throws Exception {
new ByteBuddy().redefine(HelloWorld.class)
.make()
.saveIn(new File("/Users/chenfeihao/Desktop/"));
}
最终我们会在如下路径找到重新生成的class文件
/Users/chenfeihao/Desktop/com/tinysakura/bytebuddylearn/clazz
反编译一下:
package com.tinysakura.bytebuddylearn.clazz;
public class HelloWorld {
public HelloWorld() {
}
String helloWorld() {
return "hello world";
}
}
好像和redefine之前的类没有什么区别?因为我们根本还什么都没做呀,后面的内容会介绍如何修改已存在的类,bytebuddy的redifine和rebase只能重定义没有被jvm加载的类,如果试图redifine已经加载的类,会抛出异常
java.lang.IllegalStateException: Cannot inject already loaded type: class java.lang.Object
bytebuddy提供了一个类型池的概念,我们不仅可以redifine classpath路径下的类,也可以从外部文件,jar包,网络redefine,我们需要一个ClassFileLocator去指导bytebuddy从那找到我们需要redefine的字节码文件:
ClassFileLocator fileLocator = ClassFileLocator.ForJarFile