JAVA动态字节码实现方式对比之Byte Buddy
Posted reallx
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA动态字节码实现方式对比之Byte Buddy相关的知识,希望对你有一定的参考价值。
1、背景
JAVA为什么要动态操作字节码?除Byte Buddy说的因为JAVA有严格的类型校验,在开发接口交互的过程中限制了类型编译,其实还有其他很多方面的应用。比如说通用抽象,将脚本描述动态生成JAVA代码,这种流程泛华动态生成代码的应用场景十分普遍。
在JAVA字节码操作有几种常规方式,在技术选型时,我们主要考虑如下几点:
- 适用范围
- 受欢迎程度
- 开发难度
- 维护成本
2、增强选型
- java.lang.reflect.Proxy:https://docs.oracle.com/javas...
- javassist: https://github.com/jboss-java...
- byte buddy:https://bytebuddy.net/#/tutorial
- cglib:https://github.com/cglib/cglib
- java agent:https://docs.oracle.com/javas...
- groovy://TODO
3、Byte Buddy
3.1、三种增强方式
3.1.1、subclass(创建)
通过继承已有的类,动态创建一个新类。
subclass可以自定义属性、方法,也可以重写现有方法。
subclass的一个好处是,类是新建,运行时加载不存在类冲突的问题;缺点是,对已加载的类不能增强,因为编译时没有任何类会依赖新增类。
/**
* 创建一个空类
*
*/@Runner
public class SimpleCreateRunner implements Runnable {
static String newClassName = "net.bytepuppy.subclass.HelloWorld";
@SneakyThrows
@Override public void run() {
// DynamicType.Unloaded,顾名思义,创建了字节码,但未加载到虚拟机
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
// 继承Object.class
.subclass(Object.class)
// 指定固定的名字
.name(newClassName)
// 创建字节码
.make();
// 将字节码保存到指定文件
dynamicType.saveIn(Consts.newFile(Consts.CLASS_OUTPUT_BASE_DIR));
System.out.println("save class: " + Consts.CLASS_OUTPUT_BASE_DIR + newClassName);
}
}
3.1.2、redefine(重写)
重写顾明思议就是可以对一个现有类的属性、方法进行增、删、改。
重写的前提是redefine后的类名不变,如果重命名redefine后的类,其实跟subclass效果相当。
属性、方法被redefine后,原定义(属性、方法)会丢失,好像类被重写了一样,这也是我将redefine翻译成重写的原因。
JVM runtime redefine一个类,不能被加载到JVM中,因为会报错:java.lang.IllegalStateException: Class already loaded: class xxx
JVM runtime类替换的的方法之一,是JVM热加载。byte buddy通过ByteBuddyAgent.install() + ClassReloadingStrategy.fromInstalledAgent()封装了简洁的热加载调用。
但遗憾的是,JVM 热加载不允许增减原class的schema(比如增减属性、方法),因此使用场景非常受限。修改Schema后热加载报错:UnsupportedOperationException: class redefinition failed: attempted to change the schema (add/remove fields)
/**
* 重写一个类,并在类加载前替换类,然后再实例化
*/
public class RedefineMain2 {
public static void main(String[] args) throws Exception {
DynamicType.Unloaded unloaded = createWithoutTriggerClassLoad();
unloaded.saveIn(Consts.newFile(Consts.CLASS_OUTPUT_BASE_DIR));
Object demoService = unloaded.load(ClassLoader.getSystemClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded().newInstance();
Object o = demoService.getClass()
.getMethod("report", String.class, int.class)
.invoke(demoService, "reallx", 12);
System.out.println(
o.toString());
System.out.println(demoService.getClass().getDeclaredField("qux"));
}
private static DynamicType.Unloaded createWithoutTriggerClassLoad() {
TypePool typePool = TypePool.Default.ofSystemLoader();
DynamicType.Unloaded unloaded = new ByteBuddy()
// try rebase
.redefine(typePool.describe("net.bytepuppy.redefine.delegate.DemoService").resolve(),
ClassFileLocator.ForClassLoader.ofSystemLoader())
// 如果用ClassLoadingStrategy.Default.WRAPPER,那必须为新类指定一个名字,否则在相同ClassLoader中名字冲突
// ClassLoadingStrategy.Default.CHILD_FIRST,name定义可以省略
.name("WhatEver")
.defineField("qux", String.class)
.method(ElementMatchers.named("report"))
.intercept(FixedValue.value("Hello World!"))
.make();
return unloaded;
}
}
3.1.3、rebase(增强)
rebase功能与redefine相当,也可以已有类的方法、属性自定义增删改。
rebase与redefine的区别,redefine后的原属性、原方法丢失;rebase后的原属性、原方法被拷贝 + 重命名保留在class内。
rebase可以实现一些类似java.lang.reflect.Proxy的代理功能。但rebase与redefine一样,热加载类的问题依然存在。见:https://github.com/raphw/byte-buddy/issues/104
// 将redefine示例中的语句,new ByteBuddy().redefine()替换为new ByteBuddy().rebase()即可。
3.2、加载创建类
类加载器参考:https://blog.csdn.net/briblue/article/details/54973413。自定义类加载器,一般重写findClass即可,loadClass不重写。
byte buddy增强后创建的类,如果类名是新的,都可以通过ClassLoader加载。
鉴于ClassLoader的双亲委派模式:AppClassLoader -> ExtClassLoader -> BootstrapClassLoader,新创建的类可以直接使用AppClassLoader来加载,新类在整个JVM中都是可见的。
byte buddy封装了几个常用的ClassLoader相关调用:
- ClassLoadingStrategy.BOOTSTRAP_LOADER: 在Byte Buddy中代表BootstrapClassLoader,但赋值为Null,不能直接使用。ClassLoadingStrategy.BOOTSTRAP_LOADER的作用是用于构建ByteArrayClassLoader。(BootstrapClassLoader是用C++编写,Java中没有直接类可以引用)
- ByteArrayClassLoader:byte buddy自定义类加载器,继承自ClassLoader,未重写loadClass方法,符合双亲委派模式。即用ByteArrayClassLoader加载的类,在JVM中全局可见。ChildFirst
- ByteArrayClassLoader.ChildFirst: ChildFirst继承了ByteArrayClassLoader,但是重写了loadClass方法,破坏了双亲委派模式。
ClassLoader与ByteArrayClassLoader.ChildFirst代码区别对比如下:
ClassLoader:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded // 在本地loader已加载内容中查找类 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { // 找不到,尝试从父加载器中找。找到了就返回。 if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } // 父加载器也找不到此类 if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); // 自定义Loader重写此方法,自定义findClass逻辑。 // 如果没有自定义实现,原方法抛异常:ClassNotFoundException c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
ByteArrayClassLoader.ChildFirst
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (SYNCHRONIZATION_STRATEGY.initialize().getClassLoadingLock(this, name)) { // 在本地loader已加载内容中查找类 Class<?> type = findLoadedClass(name); if (type != null) { return type; } // 找不到,跳过父类尝试,直接从本地loader的findClass中找 // 这里有一个问题:如果用这个ChildFirstClassLoader加载的类,不会尝试父类查找。换句话讲,同名类可以被父loader和子loader同时加载,但class却不相等 try { type = findClass(name); if (resolve) { resolveClass(type); } return type; } catch (ClassNotFoundException exception) { // If an unknown class is loaded, this implementation causes the findClass method of this instance // to be triggered twice. This is however of minor importance because this would result in a // ClassNotFoundException what does not alter the outcome. // 本地loader找不到,在尝试从父loader中找 return super.loadClass(name, resolve); } } }
结合Byte Buddy使用情况:
- ClassLoadingStrategy.Default.WRAPPER:构建一个ByteArrayClassLoader,符合双亲委派规则。
- ClassLoadingStrategy.Default.CHILD_FIRST:构建一个ByteArrayClassLoader.ChildFirst,rebase、redefine的类,可以被加载,但在AppClassLoader中不认识同名类。
3.3、替换加载类
byte buddy增强后创建的类,如果想类名不变,就不可以通过ClassLoader加载了,因为JVM拒绝重复加载相同类:java.lang.IllegalStateException: Class already loaded。
替换已加载的类有两种方式:
3.3.1、热加载
参考:https://developer.aliyun.com/article/65023
热加载需要借助java agent的Instrumentation.redefineClasses。
Byte Buddy提供了便捷的热加载实现:ByteBuddyAgent.install()配合ClassReloadingStrategy.fromInstalledAgent()。
热加载有极大的使用限制:不允许修改已有的属性、方法,如果class schema变化,JVM拒绝重载修改类。这意味着我们很难用热加载的方式编写切面逻辑。
// 加载类时使用热加载ClassLoader策略。
DynamicType.Unloaded<?>.load(ClassLoader.getSystemClassLoader(), ClassReloadingStrategy.fromInstalledAgent());
3.3.2、懒加载
JVM有一个特性,启动后,类要到使用的时候才加载。这意味着,如果我们不直接引用类,触发类加载,那么在此之前我们都可以自由替换增强类。
增强类的生成发生在构建时,因为构建是在另一个JVM中完成的,所以不影响运行时类加载。
为了实现懒加载,byte buddy构建了几个有用的类:
TypeDescription
类型描述对象。用此对象包装的类,不会触发类加载,但可以获得包装类的各种信息。
TypePool
TypeDescription类型池。用TypePool.Default.ofSystemLoader()语句可以获得但前ClassLoader下所有的类描述,但不会触发类加载。
typePool.describe("{your_class_string_name}").resolve()可以获得对应类的TypeDescription。
//TypePool, TypeDescription用法,参考3.1 redefine示例
3.4、java agent
参考:https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html
在项目规模庞大的时候,上面替换加载类的方式都限制太多,不适用。
java agent可以在类加载前,修改(transform)类,避免热加载,同时还能对业务逻辑进行增强。
替换增强类,主要是通过java.lang.instrument.Instrumentation,Java Agent之所以能有用,主要还是Java Agent的两个入口提供了java.lang.instrument.Instrumentation的访问入口:
- premain
JVM初始化后被调用,方法为: public static void premain(String arguments, Instrumentation instrumentation) {...}
-javaagent:jarpath[=options]方式添加JVM启动参数,permain被调用,agentmain即便实现也不会调用。 - agentmain
JVM启动后被调用,方法为:public static void agentmain(String arguments, Instrumentation instrumentation) {...}
agentmain被调用有三个条件:
1)agent jar的manifest必须显式定义属性Agent-Class;
2)Agent-Class指定类,必须定义agentmain方法;
3)agent jar在JVM启动的classpath路径中。
例如:byte buddy定义了net.bytebuddy.agent.Installer就是用agentmain的方式获取Instrumentation类。我们解压byte-buddy-agent-1.10.23-SNAPSHOT.jar,cat META-INF/MANIFEST.MF,可以找到Agent-Class定义:
../META-INF$ cat MANIFEST.MF
Manifest-Version: 1.0
Bundle-Description: The Byte Buddy agent offers convenience for attach
ing an agent to the local or a remote VM.
Bundle-License: http://www.apache.org/licenses/LICENSE-2.0.txt
Bundle-SymbolicName: net.bytebuddy.byte-buddy-agent
Built-By: liuh
**Agent-Class: net.bytebuddy.agent.Installer**
Bnd-LastModified: 1616574091622
Bundle-ManifestVersion: 2
Can-Redefine-Classes: true
Import-Package: com.sun.tools.attach;resolution:=optional,com.ibm.tool
s.attach;resolution:=optional
Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.5))"
Can-Set-Native-Method-Prefix: true
Tool: Bnd-3.5.0.201709291849
Export-Package: net.bytebuddy.agent;version="1.10.23"
Premain-Class: net.bytebuddy.agent.Installer
Bundle-Name: Byte Buddy agent
Bundle-Version: 1.10.23.SNAPSHOT
Multi-Release: true
Can-Retransform-Classes: true
Created-By: Apache Maven Bundle Plugin
Build-Jdk: 1.8.0_144
4、byte buddy增强类的两种方式
很多时候,我们修改一个类,可能不会改变原方法的逻辑,而是只会在方法调用前后做一个拦截,对业务无关的日志、安全、监控等需求织入一个切面,而不会影响原代码逻辑变更。Byte Buddy提供如下两种方式增强一个类。
这里再强调一下,这里的增强都涉及到运行时代码重载,但代码热加载不允许增、删原有类的属性、方法,使用场景有很大限制。使用时,字节码的生产,尽量安排在加载前,这样可以最大限度的自由编辑已有类库。
4.1、net.bytebuddy.implementation.MethodDelegation
参考:https://bytebuddy.net/#/tutorial #Delegating a method call#
MethodDelegation作用是将一个方法调用,重定向(代理)到另一个方法调用上,无论这个方法是否静态,也无论这些方法是否属于同一个类。
Apache SkyWalking字节码增强,就是用了MethodDelegate的方式。下面代码是SkyWalking源码的一个简洁版。
我们首先抽象拦截切面的变化点,并实现拦截操作的公共流程。
/**
* 代理拦截接口,抽象方法调用前后的两个切面
*/
public interface InstMethodAroundInterceptor {
/**
* 拦截点前
* @param inst: 被增强类实例
* @param interceptPoint:被增强方法
* @param allArguments:被增强方法入参
* @param argumentsTypes:被增强方法入参类型
* @param result:result 包装类
*/
void beforeMethod(Object inst, Method interceptPoint,
Object[] allArguments, Class<?>[] argumentsTypes,
ResultWrapper result);
Object afterMethod(Object inst, Method interceptPoint,
Object[] allArguments, Class<?>[] argumentsTypes,
Object ret);
void handleMethodException(Object inst, Method method, Object[] allArguments,
Class<?>[] argumentsTypes, Throwable t);
}
/**
* 统一代理模板
*/
public class DelegateTemplate {
private InstMethodAroundInterceptor interceptor;
public DelegateTemplate(InstMethodAroundInterceptor interceptor) {
this.interceptor = interceptor;
}
/**
* 拦截增强主方法
*
* @param inst: 被拦截对象本身
* @param allArguments:被代理方法原参数
* @param zuper:被代理方法的包装对象,zuper.call()调用原方法
* @param method:原方法对象
* @return
*/
public Object interceptor(@This Object inst, @AllArguments Object[] allArguments,
@SuperCall Callable<?> zuper, @Origin Method method) {
ResultWrapper rw = new ResultWrapper();
if (this.interceptor != null) {
try {
// 调用前拦截处理
this.interceptor.beforeMethod(inst, method,
allArguments, method.getParameterTypes(), rw);
} catch (Throwable t) {
t.printStackTrace();
}
}
if (!rw.isContinue()) {
return rw.getResult();
}
Object result = null;
try {
// 被代理方法调用
result = zuper.call();
if (this.interceptor != null) {
try {
// 调用后拦截处理
result = this.interceptor.afterMethod(inst, method,
allArguments, method.getParameterTypes(), result);
} catch (Throwable t) {
t.printStackTrace();
}
}
} catch (Exception e) {
if (this.interceptor != null) {
try {
// 调用异常拦截处理
this.interceptor.handleMethodException(inst, method,
allArguments, method.getParameterTypes(), e);
} catch (Throwable t) {
t.printStackTrace();
}
}
}
return result;
}
@Data
public class ResultWrapper {
private boolean isContinue;
private Object result;
}
}
接下来我们开始利用拦截切口 + 拦截模板来增强一个自定义的方法。
/**
* 被增强类。模拟一个业务类,有report和compute两个方法。
*/
public class DemoService {
public String report(String name, int value) {
return String.format("name: %s, value: %s", name, value);
}
public void compute(List<Integer> values) {
System.out.println("compute result:" + values.stream().mapToInt(v -> v.intValue()).sum());
}
}
/**
* DemoService增强切面,实现切面接口InstMethodAroundInterceptor
*/
public class DemoServiceInterceptor implements InstMethodAroundInterceptor {
@Override
public void beforeMethod(Object inst, Method interceptPoint, Object[] allArguments,
Class<?>[] argumentsTypes, ResultWrapper result) {
System.out.println("DemoService Interceptor in ...");
}
@Override
public Object afterMethod(Object inst, Method interceptPoint, Object[] allArguments,
Class<?>[] argumentsTypes, Object ret) {
System.out.println("DemoService Interceptor out ...");
return ret;
}
@Override
public void handleMethodException(Object inst, Method method, Object[] allArguments,
Class<?>[] argumentsTypes, Throwable t) {
System.out.println("DemoService Interceptor error handle ...");
}
}
/**
* 模拟java agent增强类
*/
public class JavaAgentMain {
public static void premain(String agentArgs, Instrumentation instrumentation) {
new AgentBuilder.Default()
// 增强类通过类名匹配
.type(ElementMatchers.named("net.bytepuppy.redefine.delegate.DemoService"))
// 自定义Transformer
.transform(new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassLoader classLoader, JavaModule module) {
// 实例化自己的拦截实例DemoServiceInterceptor
// 将拦截实例传入拦截模板,并完成实例化
// 将DemoService实例的report方法,拦截代理到DelegateTemplate的interceptor
return builder.method(ElementMatchers.named("report"))
.intercept(MethodDelegation.to(new DelegateTemplate(new DemoServiceInterceptor())));
}
})
// 增强
.installOn(instrumentation);
}
}
上面的实例,用JAVA Agent在加载前增强类,而不是在运行是用热加载的增强类,是有原因的:
redefine和rebase都是对已有类进行修改。
在正常JVM启动(这里是main函数,也就是运行时)中,DemoService已被加载
被redefine的DemoService,不能再被加载,会报错ClassAlreadyExist.
另外,JVM热加载时,禁止修改已有类的schema(方法、属性,但可以修改逻辑片段)
- redefine:重新定义一个类,被增强的方法、属性,会丢失原方法、属性
- rebase:与redefine相似,但被增强的方法、属性不会丢失,而是会已拷贝 + 重命名的方式被保留
因此:
- redefine:MethodDelegate无效。因为redefine会丢失原方法,@SuperCall调用父类方法找不着了。
- rebase:MethodDelegate无效。因为rebase拷贝、重命名原有方法,会新增方法,破坏了热加载规则,代理失效。参考{@link RedefineMain2}
问题参考: https://github.com/raphw/byte...
4.2、net.bytebuddy.asm.Advice
参考:https://blog.csdn.net/wanxiao...
https://medium.com/@lnishada/...
在MethodDelegation中有个问题,一个类被反复增强,会导致新的字节码实例方法调用堆栈变化。例如:byte buddy issue 829。
Byte Buddy在其官方文档只字未提。试用了一下,稍微复杂的逻辑增强Advise就失效,原因未名,而且Advise会导致断点失效,对于复杂业务开发并不友好。
简单示例如下:
// 模拟一个业务逻辑类,及方法
public class ComputeService {
public String compute(String name, List<Integer> values) {
try {
Thread.sleep(10);
} catch (InterruptedException e) { }
return String.format("compute name: %s, compute result: %s",
name, values.stream().mapToInt(v -> v.intValue()).sum());
}
}
// 抽象Advise 公共模板
public class AdviceTemplate {
// 引入LogInterceptor,导致@Advice.OnMethodEnter失效
// private static LogInterceptor logInterceptor;
// static {
// logInterceptor = new LogInterceptor();
// }
/**
* @Advice.OnMethodEnter 必须是静态方法
*
* @param thisObject
* @param origin
* @param detaildOrigin
* @param args
* @return
*/
@Advice.OnMethodEnter(suppress = Throwable.class)
public static long beforeMethod(@Advice.This Object thisObject,
@Advice.Origin String origin,
@Advice.Origin("#t #m") String detaildOrigin,
@Advice.AllArguments Object[] args) {
StringBuilder logBuilder = new StringBuilder();
if(args != null) {
for(int i =0 ; i < args.length ; i++) {
logBuilder.append("Argument- " + i + " is: " + args[i] + ", ");
}
logBuilder.delete(logBuilder.length() - 2, logBuilder.length());
}
// LogInterceptor.log(logBuilder.toString());
// 调用内部静态log方法,导致@Advice.OnMethodEnter失效
// log(logBuilder.toString());
long startTime = System.currentTimeMillis();
System.out.println("start time: " + startTime);
return startTime;
}
/**
* @Advice.OnMethodExit 必须是静态方法
*
* @param time
* @param ret
*/
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
public static void afterMethod(@Advice.Enter long time, @Advice.Return Object ret) {
long endTime = System.currentTimeMillis();
System.out.println("end time: " + endTime);
System.out.println("Method Execution Cost Time: " + (endTime - time) + " mills");
}
private static void log(String log) {
System.out.println("advised log:" + log);
}
}
public class AdviceRedefineMain {
public static void main(String[] args) throws Exception {
ByteBuddyAgent.install();
DynamicType.Unloaded dtu = new ByteBuddy()
.redefine(ComputeService.class)
// advise 区别于delegation的核心语句
.visit(Advice.to(AdviceTemplate.class)
.on(ElementMatchers.named("compute")))
.make();
Class<?> clazz = dtu.load(ClassLoadingStrategy.BOOTSTRAP_LOADER,
ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();
Object service = clazz.newInstance();
Object result = clazz.getMethod("compute", String.class, List.class)
.invoke(service, "AdviceDemo", Lists.newArrayList(1, 2, 4));
System.out.println(result);
// ((ComputeService) service).compute("AdviceDemo", Lists.newArrayList(1, 2, 4));
}
}
5、Byte Buddy使用限制
- JVM类热加载,不能修改类的Schema,否则报错UnsupportedOperationException
- 运行时,尽量避免使用Byte Buddy对某个类的refine和rebase,因为这两个操作都涉及到操作类的Schema。但如果是基于某个类创建新类,则没有此限制。
- Byte Buddy配合JAVA Agent最优解,将类修改放到JVM类真实加载前。(permain,顾名思义在JVM Main方法执行前执行,此时所有类还未加载)
以上是关于JAVA动态字节码实现方式对比之Byte Buddy的主要内容,如果未能解决你的问题,请参考以下文章
字节码Byte-buddy 监控方法执行耗时动态获取出入参类型 和值