前言
最近看dubbo的时候,看到了讲spi的部分,其中提到了@Adaptive注解生成了代理类
我后面去debug了一下,打了断点,发现在运行时,只是显示了class的名称,由于RegistryFactory$Adaptive 是动态生成的,所以没有class文件结构可以看,而文章里贴了class文件。
所以比较好奇,如何拿到class文件。
经过一番搜索验证之后,有了如下的文章
Java生成代理类的方式
首先呢, java生成代理类有如下几种方式。
在了解了动态代理的生成方式之后,可以分情况讨论如何拿到代理类的class文件
JDK动态代理
这个是java自身支持的代理方式,自带的,无需引入额外组件,但是只能代理接口。
因为生成的代理类需要继承Proxy类,java是单继承的,所以只能代理接口。
CGLIB动态代理
cglib可以为class创建代理类,类不必是接口,采用的是继承委托类的方式,因此不能代理final类,只能代理所有非final的public/protected类型的方法定义。
cglib是采用的asm字节码拼接技术,继承一个类,子类对方法进行拦截,织入横切逻辑。
Javassist
使用javassist可以直接操纵字节码,生成代理类。
dubbo的代理类就是使用javassist拼接出来的
ASM
asm相比javassist,更加的细粒度,且实现更加的复杂。
Dubbo相关代理类的class文件
具体到dubbo的话,@Adaptive会生成相关的代理类,Dubbo的代理类是用javaasist生成的。
再延伸到调用过程,consumer调provider,生成的代理类是jdk动态代理
provider端调用过程中,会有一个warpper代理类,生成的代理类是使用javassist生成的,然后通过Warpper0.invokeMethod()根据具体的调用信息调用到具体的实现类。
本次使用的dubbo版本是3.2.0
去年写了一篇关于 dubbo spi的文章,感兴趣的可以看一下,介绍了spi的相关实现方式,以及如何使用
具体到Adaptive代理类,使用如下的测试代码(完整的过程可以参考上面的文章地址)
public class DubboSpi {
public static void main(String[] args) {
ExtensionLoader<SimpleExt> extensionLoader = ExtensionLoader.getExtensionLoader(SimpleExt.class);
// SimpleExt defaultExtension = extensionLoader.getDefaultExtension();
// SimpleExt dog = extensionLoader.getExtension("dog");
// SimpleExt dog2 = extensionLoader.getExtension("dog", true);
// SimpleExt cat = extensionLoader.getExtension("cat");
// dog.yell(null, "dogdog");
// SimpleExt dog1 = extensionLoader.getExtension("dog");
// dog1.yell(null, "dog1");
SimpleExt adaptiveExtension = extensionLoader.getAdaptiveExtension();
URLAddress urlAddress = new URLAddress("127.0.0.1", 20880);
URLParam parse = URLParam.parse(new HashMap<>());
adaptiveExtension.echo(new URL(urlAddress, parse), "dogs");
adaptiveExtension.bang(new URL(urlAddress, parse),0);
}
}
定位到如下生成代理类的方法,代码在org.apache.dubbo.common.extension.ExtensionLoader#createAdaptiveExtensionClass
private Class<?> createAdaptiveExtensionClass() {
// Adaptive Classes' ClassLoader should be the same with Real SPI interface classes' ClassLoader
ClassLoader classLoader = type.getClassLoader();
try {
if (NativeUtils.isNative()) {
return classLoader.loadClass(type.getName() + "$Adaptive");
}
} catch (Throwable ignore) {
}
//这里生成class文件
String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
org.apache.dubbo.common.compiler.Compiler compiler = extensionDirector.getExtensionLoader(
org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
return compiler.compile(type, code, classLoader);
}
我们debug看一下生成的code, 看类名,是SimpleExt$Adaptive, 通过手动拼接的方式,生成了adaptive代理类。
调用AdaptiveCompiler,最终compileer的类型为JavassistCompiler
,即通过javassist生成的adaptive代理类
那么回归到我一开始的问题,如何查看动态生成的class的内容,在String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
这一行通过debug就可以拿到具体的class内容了
生成的代理类如下所示, 对接口的方法进行了拦截
package com.fc.rpc.dubbo;
import org.apache.dubbo.rpc.model.ScopeModel;
import org.apache.dubbo.rpc.model.ScopeModelUtil;
public class SimpleExt$Adaptive implements com.fc.rpc.dubbo.SimpleExt {
public java.lang.String yell(org.apache.dubbo.common.URL arg0, java.lang.String arg1) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg0;
String extName = url.getParameter("key1", url.getParameter("key2", "dog"));
if (extName == null) throw new IllegalStateException("Failed to get extension (com.fc.rpc.dubbo.SimpleExt) name from url (" + url.toString() + ") use keys([key1, key2])");
ScopeModel scopeModel = ScopeModelUtil.getOrDefault(url.getScopeModel(), com.fc.rpc.dubbo.SimpleExt.class);
com.fc.rpc.dubbo.SimpleExt extension = (com.fc.rpc.dubbo.SimpleExt) scopeModel.getExtensionLoader(com.fc.rpc.dubbo.SimpleExt.class).getExtension(extName);
return extension.yell(arg0, arg1);
}
public java.lang.String echo(org.apache.dubbo.common.URL arg0, java.lang.String arg1) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg0;
String extName = url.getParameter("simple.ext", "dog");
if (extName == null) throw new IllegalStateException("Failed to get extension (com.fc.rpc.dubbo.SimpleExt) name from url (" + url.toString() + ") use keys([simple.ext])");
ScopeModel scopeModel = ScopeModelUtil.getOrDefault(url.getScopeModel(), com.fc.rpc.dubbo.SimpleExt.class);
com.fc.rpc.dubbo.SimpleExt extension = (com.fc.rpc.dubbo.SimpleExt) scopeModel.getExtensionLoader(com.fc.rpc.dubbo.SimpleExt.class).getExtension(extName);
return extension.echo(arg0, arg1);
}
public java.lang.String bang(org.apache.dubbo.common.URL arg0, int arg1) {
throw new UnsupportedOperationException("The method public abstract java.lang.String com.fc.rpc.dubbo.SimpleExt.bang(org.apache.dubbo.common.URL,int) of interface com.fc.rpc.dubbo.SimpleExt is not adaptive method!");
}
}
另外debug级别的话,生成的class会输出到日志
JDK动态代理的class文件
回归到jdk的动态代理,如何查看jdk动态代理生成的class文件
这里主要讲一下如何查看动态代理的class文件,动态代理的原理啥的不深入。
设置参数
在调用之前,添加System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
这个只适用于jdk动态代理
准备一下测试类
这是调用入口
public class DynamicProxyMain {
public static void main(String[] args) {
//动态代理时生成class文件
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
DynamicProxy dynamicProxy = new DynamicProxy(new HumenImpl());
Humen humenProxy = dynamicProxy.getProxy();
humenProxy.eat("rice");
}
}
DynamicProxy实现了InvocationHandler,用于生成代理类
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class DynamicProxy implements InvocationHandler {
private Object target;
public DynamicProxy(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
this.before();
Object result = method.invoke(target, args);
this.after();
return result;
}
private void before() {
System.out.println("吃东西之前先热身");
}
private void after() {
System.out.println("吃完饭休息一下");
}
public <T> T getProxy() {
return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
}
}
测试用的接口
/**
* 人类迷惑行为大赏
**/
public interface Humen {
/**
* 吃东西
*
* @param food 食物
*/
void eat(String food);
}
测试接口的实现类
/**
* 人类迷惑行为大赏具体案例
**/
public class HumenImpl implements Humen {
@Override
public void eat(String food) {
System.out.println("人类开始行为艺术表演:吃食物 "+food);
}
}
那么主要看一下这个配置System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
这个配置是用于保存生成的class的。
缺点是这个参数只适用于jdk动态代理
加上这个参数运行一下,会发现代理类保存到了项目路径(classpath)下
看一下$Proxy0的详细内容, 代理类继承了Proxy, 所以jdk动态代理只能代理接口,因为Java是单继承的
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.sun.proxy;
import com.fc.se.proxy.Humen;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy0 extends Proxy implements Humen {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final void eat(String var1) throws {
try {
super.h.invoke(this, m3, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("com.fc.se.proxy.Humen").getMethod("eat", Class.forName("java.lang.String"));
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
构造函数传入了InvocationHandler,赋值给了成员变量h,所以方法的调用都是走的InvocationHandler。
本次示例中为DynamicProxy
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
在静态代码块中生成了对应的方法,类通用的hashCode()、toString()、equals(), 以及需要代理的方法eat()。
$Proxy0调用eat方法,然后执行到了com.fc.se.proxy.DynamicProxy#invoke
,
method.invoke(target, args)
调用具体的实现类执行方法。
输出到文件
上一步中我们知道了生成的代理类的类名是$Proxy0, debug的时候也能看到类名,我们可以调用方法将这个类打印到文件里。
更新后的测试类入口如下
import sun.misc.ProxyGenerator;
import java.io.FileOutputStream;
import java.io.IOException;
public class DynamicProxyMain {
public static void main(String[] args) {
//动态代理时生成class文件
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
DynamicProxy dynamicProxy = new DynamicProxy(new HumenImpl());
Humen humenProxy = dynamicProxy.getProxy();
humenProxy.eat("rice");
//保存$Proxy0到文件
saveProxyFile();
}
private static void saveProxyFile() {
FileOutputStream out = null;
try {
byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0", HumenImpl.class.getInterfaces());
out = new FileOutputStream("/Users/since/git/vamei/out/class/" + "$Proxy0.class");
out.write(classFile);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (out != null) {
out.flush();
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
执行之后可以发现已经生成了$Proxy0.class
所以知道类名,就可以以这种方式输出到文件里
hsdb
另一种是hsdb
我这边使用hsdb失败了,具体是命令行执行的, attach process失败了,搜了一下,感觉执行起来挺麻烦的,就没深究。感兴趣的可以自行试下
打开hsdb有2种方式
一种是执行$JAVA_HOME/lib/sa-jdi.jar,命令行执行java -classpath sa-jdi.jar "sun.jvm.hotspot.HSDB"
另一种是使用jhsdb hsdb命令打开ui, 有的jdk版本没有sa-jdi.jar,只能使用$JAVA_HOME/bin/jhsdb命令
CGLIB代理的class文件
另一种常用的动态生成代理类的方式是使用cglib库,底层是基于ASM的
cglib代理输出class文件,需要指定参数
// 指定 CGLIB 将动态生成的代理类保存至指定的磁盘路径下
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/since/git/vamei/out/cglib");
我们准备一个测试方法来测试一下,cglib依赖如下
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
一个main函数用于执行,生成代理类,Enhancer对象的superClass设置为CglibBaseBean, CglibBaseBean为要代理的目标对象,然后使用enhancer.create()
创建代理对象
public class CglibProxy {
public static void main(String[] args) {
// 指定 CGLIB 将动态生成的代理类保存至指定的磁盘路径下
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/since/git/vamei/out/cglib");
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(CglibBaseBean.class);
enhancer.setCallback(new CglibCustomizedMethodInterceptor());
CglibBaseBean baseBean = (CglibBaseBean) enhancer.create();
baseBean.say();
}
}
需要一个实现了MethodInterceptor的类,这个和InvocationHandler
比较相似。
在MethodInterceptor里添加拦截的逻辑
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* cglib CustomizedMethodInterceptor
*
* @author since
* @date 2024-09-19 13:25
*/
public class CglibCustomizedMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("invoke " + method.getName() + " before ! ");
Object result = methodProxy.invokeSuper(o, objects);
System.out.println("invoke " + method.getName() + " after ! ");
return result;
}
}
代理类持有了CglibCustomizedMethodInterceptor,真正执行的时候是通过CglibCustomizedMethodInterceptor的intercept方法执行到了被代理的目标类的。
然后看一下class文件输出结果,生成了3个代理类
CglibBaseBean$$EnhancerByCGLIB$$bf21c123.class是生成的代理类,其他2个是索引类
代理类class具体内容如下
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.fc.se.proxy.cglib;
import java.lang.reflect.Method;
import net.sf.cglib.core.ReflectUtils;
import net.sf.cglib.core.Signature;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.Factory;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class CglibBaseBean$$EnhancerByCGLIB$$bf21c123 extends CglibBaseBean implements Factory {
private boolean CGLIB$BOUND;
public static Object CGLIB$FACTORY_DATA;
private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
private static final Callback[] CGLIB$STATIC_CALLBACKS;
private MethodInterceptor CGLIB$CALLBACK_0;
private static Object CGLIB$CALLBACK_FILTER;
private static final Method CGLIB$say$0$Method;
private static final MethodProxy CGLIB$say$0$Proxy;
private static final Object[] CGLIB$emptyArgs;
private static final Method CGLIB$equals$1$Method;
private static final MethodProxy CGLIB$equals$1$Proxy;
private static final Method CGLIB$toString$2$Method;
private static final MethodProxy CGLIB$toString$2$Proxy;
private static final Method CGLIB$hashCode$3$Method;
private static final MethodProxy CGLIB$hashCode$3$Proxy;
private static final Method CGLIB$clone$4$Method;
private static final MethodProxy CGLIB$clone$4$Proxy;
static void CGLIB$STATICHOOK1() {
CGLIB$THREAD_CALLBACKS = new ThreadLocal();
CGLIB$emptyArgs = new Object[0];
Class var0 = Class.forName("com.fc.se.proxy.cglib.CglibBaseBean$$EnhancerByCGLIB$$bf21c123");
Class var1;
Method[] var10000 = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods());
CGLIB$equals$1$Method = var10000[0];
CGLIB$equals$1$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$1");
CGLIB$toString$2$Method = var10000[1];
CGLIB$toString$2$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/String;", "toString", "CGLIB$toString$2");
CGLIB$hashCode$3$Method = var10000[2];
CGLIB$hashCode$3$Proxy = MethodProxy.create(var1, var0, "()I", "hashCode", "CGLIB$hashCode$3");
CGLIB$clone$4$Method = var10000[3];
CGLIB$clone$4$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/Object;", "clone", "CGLIB$clone$4");
CGLIB$say$0$Method = ReflectUtils.findMethods(new String[]{"say", "()V"}, (var1 = Class.forName("com.fc.se.proxy.cglib.CglibBaseBean")).getDeclaredMethods())[0];
CGLIB$say$0$Proxy = MethodProxy.create(var1, var0, "()V", "say", "CGLIB$say$0");
}
final void CGLIB$say$0() {
super.say();
}
public final void say() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) {
var10000.intercept(this, CGLIB$say$0$Method, CGLIB$emptyArgs, CGLIB$say$0$Proxy);
} else {
super.say();
}
}
final boolean CGLIB$equals$1(Object var1) {
return super.equals(var1);
}
public final boolean equals(Object var1) {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) {
Object var2 = var10000.intercept(this, CGLIB$equals$1$Method, new Object[]{var1}, CGLIB$equals$1$Proxy);
return var2 == null ? false : (Boolean)var2;
} else {
return super.equals(var1);
}
}
final String CGLIB$toString$2() {
return super.toString();
}
public final String toString() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
return var10000 != null ? (String)var10000.intercept(this, CGLIB$toString$2$Method, CGLIB$emptyArgs, CGLIB$toString$2$Proxy) : super.toString();
}
final int CGLIB$hashCode$3() {
return super.hashCode();
}
public final int hashCode() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) {
Object var1 = var10000.intercept(this, CGLIB$hashCode$3$Method, CGLIB$emptyArgs, CGLIB$hashCode$3$Proxy);
return var1 == null ? 0 : ((Number)var1).intValue();
} else {
return super.hashCode();
}
}
final Object CGLIB$clone$4() throws CloneNotSupportedException {
return super.clone();
}
protected final Object clone() throws CloneNotSupportedException {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
return var10000 != null ? var10000.intercept(this, CGLIB$clone$4$Method, CGLIB$emptyArgs, CGLIB$clone$4$Proxy) : super.clone();
}
public static MethodProxy CGLIB$findMethodProxy(Signature var0) {
String var10000 = var0.toString();
switch (var10000.hashCode()) {
case -909388886:
if (var10000.equals("say()V")) {
return CGLIB$say$0$Proxy;
}
break;
case -508378822:
if (var10000.equals("clone()Ljava/lang/Object;")) {
return CGLIB$clone$4$Proxy;
}
break;
case 1826985398:
if (var10000.equals("equals(Ljava/lang/Object;)Z")) {
return CGLIB$equals$1$Proxy;
}
break;
case 1913648695:
if (var10000.equals("toString()Ljava/lang/String;")) {
return CGLIB$toString$2$Proxy;
}
break;
case 1984935277:
if (var10000.equals("hashCode()I")) {
return CGLIB$hashCode$3$Proxy;
}
}
return null;
}
public CglibBaseBean$$EnhancerByCGLIB$$bf21c123() {
CGLIB$BIND_CALLBACKS(this);
}
public static void CGLIB$SET_THREAD_CALLBACKS(Callback[] var0) {
CGLIB$THREAD_CALLBACKS.set(var0);
}
public static void CGLIB$SET_STATIC_CALLBACKS(Callback[] var0) {
CGLIB$STATIC_CALLBACKS = var0;
}
private static final void CGLIB$BIND_CALLBACKS(Object var0) {
CglibBaseBean$$EnhancerByCGLIB$$bf21c123 var1 = (CglibBaseBean$$EnhancerByCGLIB$$bf21c123)var0;
if (!var1.CGLIB$BOUND) {
var1.CGLIB$BOUND = true;
Object var10000 = CGLIB$THREAD_CALLBACKS.get();
if (var10000 == null) {
var10000 = CGLIB$STATIC_CALLBACKS;
if (var10000 == null) {
return;
}
}
var1.CGLIB$CALLBACK_0 = (MethodInterceptor)((Callback[])var10000)[0];
}
}
public Object newInstance(Callback[] var1) {
CGLIB$SET_THREAD_CALLBACKS(var1);
CglibBaseBean$$EnhancerByCGLIB$$bf21c123 var10000 = new CglibBaseBean$$EnhancerByCGLIB$$bf21c123();
CGLIB$SET_THREAD_CALLBACKS((Callback[])null);
return var10000;
}
public Object newInstance(Callback var1) {
CGLIB$SET_THREAD_CALLBACKS(new Callback[]{var1});
CglibBaseBean$$EnhancerByCGLIB$$bf21c123 var10000 = new CglibBaseBean$$EnhancerByCGLIB$$bf21c123();
CGLIB$SET_THREAD_CALLBACKS((Callback[])null);
return var10000;
}
public Object newInstance(Class[] var1, Object[] var2, Callback[] var3) {
CGLIB$SET_THREAD_CALLBACKS(var3);
CglibBaseBean$$EnhancerByCGLIB$$bf21c123 var10000 = new CglibBaseBean$$EnhancerByCGLIB$$bf21c123;
switch (var1.length) {
case 0:
var10000.<init>();
CGLIB$SET_THREAD_CALLBACKS((Callback[])null);
return var10000;
default:
throw new IllegalArgumentException("Constructor not found");
}
}
public Callback getCallback(int var1) {
CGLIB$BIND_CALLBACKS(this);
MethodInterceptor var10000;
switch (var1) {
case 0:
var10000 = this.CGLIB$CALLBACK_0;
break;
default:
var10000 = null;
}
return var10000;
}
public void setCallback(int var1, Callback var2) {
switch (var1) {
case 0:
this.CGLIB$CALLBACK_0 = (MethodInterceptor)var2;
default:
}
}
public Callback[] getCallbacks() {
CGLIB$BIND_CALLBACKS(this);
return new Callback[]{this.CGLIB$CALLBACK_0};
}
public void setCallbacks(Callback[] var1) {
this.CGLIB$CALLBACK_0 = (MethodInterceptor)var1[0];
}
static {
CGLIB$STATICHOOK1();
}
}
从上面的class文件可以看出,生成的代理类继承了CglibBaseBean,所以cglib相比于jdk动态代理,被代理类可以不用非得是接口
使用Agent输出class文件
再一种方式就是使用Instrument, 这是jdk5提供的能力。
关于instrument机制的介绍,可以看一下这一篇文章https://cnkirito.moe/instrument/
如何搭建一个demo进行测试,可以参考上面的文章,测试的代码可以在 git仓库查看
git clone之后,执行mvn clean package生成agent.jar, 在目标进程添加jvm参数-javaagent:/Users/since/git/agent/target/agent.jar=-d=/Users/since/test/;-f=com/alibaba/dubbo/registry
即可输出动态代理类的内容
-javaagent:/Users/since/git/agent/target/agent.jar
这个是agent的文件路径-d=/Users/since/test/
是结果的输出路径-f=com/alibaba/dubbo/registry
是要打印的类的前缀包路径
下面来简单说一下如何编写agent代码,实现class dump
agent项目结构如下
首先是要在META-INF下添加一个MANIFEST.MF
,里面指定版本号,主类,是否可以重建类等属性
指定的premain-class是com.fc.agent.GreetingAgent
,所以agent的入口就是GreetingAgent了。
来看一下GreetingAgent
类里有一个premain方法,和main方法比较像,options是传入的参数,以上面为例,运行时options为-d=/Users/since/test/;-f=com/alibaba/dubbo/registry
。
Instrumentation 便是 JAVA5 的 Instrument 机制的核心,它负责为类添加 ClassFileTransformer 的实现,从而对类进行装配增强修改
注意 premain 和它的两个参数不能随意修改, 规定是这样,不要修改。
我们为ins添加了一个ClazzDumpCustomTransformer, 对options进行解析,解析出文件输出路径,以及要输出的包的路径。
这里我想观察dubbo的adaptive代理,所以-f传的是dubbo的包路径
import java.lang.instrument.Instrumentation;
/**
*
*
* @author since
* @date 2024-08-21 13:43
*/
public class GreetingAgent {
public static void premain(String options, Instrumentation ins) {
if (options != null) {
System.out.printf("I've been called with options: \"%s\"\n", options);
} else {
System.out.println("I've been called with no options.");
}
ClazzDumpCustomTransformer transformer = getTransformer(options);
//更换不同的transformer
ins.addTransformer(transformer);
}
public static ClazzDumpCustomTransformer getTransformer(String options) {
String exportDir = null;
String filterStr = null;
boolean recursiveDir = false;
if (options != null) {
if (options.contains(";")) {
String[] args = options.split(";");
for (String param1 : args) {
String[] kv = param1.split("=");
if ("-d".equalsIgnoreCase(kv[0])) {
exportDir = kv[1];
} else if ("-f".equalsIgnoreCase(kv[0])) {
filterStr = kv[1];
} else if ("-r".equalsIgnoreCase(kv[0])) {
recursiveDir = true;
}
}
} else {
filterStr = options;
}
}
System.out.println("getTransformer, exportDir: " + exportDir + ", filterStr: " + filterStr + ", recursiveDir: " + recursiveDir);
return new ClazzDumpCustomTransformer(exportDir, filterStr, recursiveDir);
}
}
ClazzDumpCustomTransformer负责dump class,对符合条件的class,获取文件内容,输出到指定目录下
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.Arrays;
/**
* class dump
*
* @author since
* @date 2024-09-10 13:36
*/
public class ClazzDumpCustomTransformer implements ClassFileTransformer {
/**
* 导出过滤表达式,此处为类名前缀, 以 -f 参数指定
*/
private String filterStr;
/**
* 导出文件目录根目录, 以 -d 参数指定
*/
private String exportBaseDir = "/tmp/";
/**
* 是否创建多级目录, 以 -r 参数指定
*/
private boolean packageRecursive;
public ClazzDumpCustomTransformer(String exportBaseDir, String filterStr) {
this(exportBaseDir, filterStr, false);
}
public ClazzDumpCustomTransformer(String exportBaseDir, String filterStr, boolean packageRecursive) {
if (exportBaseDir != null) {
this.exportBaseDir = exportBaseDir;
}
this.packageRecursive = packageRecursive;
this.filterStr = filterStr;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (needExportClass(className)) {
System.out.println("needExportClass: " + className);
int lastSeparatorIndex = className.lastIndexOf("/") + 1;
String fileName = className.substring(lastSeparatorIndex) + ".class";
String exportDir = exportBaseDir;
if (packageRecursive) {
exportDir += className.substring(0, lastSeparatorIndex);
}
exportClazzToFile(exportDir, fileName, classfileBuffer); //"D:/server-tool/tmp/bytecode/exported/"
System.out.println(className + " --> EXPORTED");
}
return classfileBuffer;
}
/**
* 检测是否需要进行文件导出
*
* @param className class名,如 com.xx.abc.AooMock
* @return y/n
*/
private boolean needExportClass(String className) {
if (filterStr != null) {
return className.startsWith(filterStr);
}
return !className.startsWith("java") && !className.startsWith("sun");
}
/**
* 执行文件导出写入
*
* @param dirPath 导出目录
* @param fileName 导出文件名
* @param data 字节流
*/
private void exportClazzToFile(String dirPath, String fileName, byte[] data) {
try {
File dir = new File(dirPath);
if (!dir.isDirectory()) {
dir.mkdirs();
}
File file = new File(dirPath + fileName);
if (!file.exists()) {
System.out.println(dirPath + fileName + " is not exist, creating...");
file.createNewFile();
} else {
// String os = System.getProperty("os.name"); // 主要针对windows文件不区分大小写问题
// if(os.toLowerCase().startsWith("win")){
// // it's win
// }
try {
int maxLoop = 9999;
int renameSuffixId = 2;
String[] cc = fileName.split("\\.");
do {
long fileLen = file.length();
byte[] fileContent = new byte[(int) fileLen];
FileInputStream in = new FileInputStream(file);
in.read(fileContent);
in.close();
if (!Arrays.equals(fileContent, data)) {
fileName = cc[0] + "_" + renameSuffixId + "." + cc[1];
file = new File(dirPath + fileName);
if (!file.exists()) {
System.out.println("new create file: " + dirPath + fileName);
file.createNewFile();
break;
}
} else {
break;
}
renameSuffixId++;
maxLoop--;
} while (maxLoop > 0);
} catch (Exception e) {
System.err.println("exception in read class file..., path: " + dirPath + fileName);
e.printStackTrace();
}
}
FileOutputStream fos = new FileOutputStream(file);
fos.write(data);
fos.close();
} catch (Exception e) {
System.err.println("exception occur while export class.");
e.printStackTrace();
}
}
}
执行mvn clean package打包之后,添加到目标进程,添加启动参数,查看执行结果,在控制台输出了要打印的class
进入输出路径,查看发现都生成了对应的class
将class文件拖进idea可以查看具体内容
从url里取相应的协议,注册默认是dubbo协议
使用arthas输出class文件
提到agent,那就不得不说到arthas了,arthas在运行时排障非常的好用,点个赞。
动态代理
当使用 arthas-boot
启动 Arthas 时,它会使用 Java 的 Attach API 来将自身动态注入到目标 JVM 进程中。通过附加 agentmain
方法,Arthas 可以对 JVM 进程进行监控和修改
那么我们就拿arthas来看一下,如何打印运行时的class文件,主要是使用jad命令
arthas的安装使用相关内容参考官方文档: https://arthas.aliyun.com/doc/
修改一下动态代理的测试类
public class DynamicProxyMain {
public static void main(String[] args) throws InterruptedException {
//动态代理时生成class文件
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
DynamicProxy dynamicProxy = new DynamicProxy(new HumenImpl());
Humen humenProxy = dynamicProxy.getProxy();
humenProxy.eat("rice");
Thread.sleep(100*24*60*60*1000L);
//保存$Proxy0到文件
// saveProxyFile();
}
让进程挂在这,然后启动arthas,选择这个进程进行attach
使用sc命令搜索动态代理生成的类: sc *Proxy0*
, 得到了代理类的全路径,然后使用jad命令反编译class,得到动态生成的代理类的文件内容
再来看一下dubbo的示例,看看javassist生成的代理类如何
public class DubboSpi {
public static void main(String[] args) throws InterruptedException {
ExtensionLoader<SimpleExt> extensionLoader = ExtensionLoader.getExtensionLoader(SimpleExt.class);
// SimpleExt defaultExtension = extensionLoader.getDefaultExtension();
// SimpleExt dog = extensionLoader.getExtension("dog");
// SimpleExt dog2 = extensionLoader.getExtension("dog", true);
// SimpleExt cat = extensionLoader.getExtension("cat");
// dog.yell(null, "dogdog");
// SimpleExt dog1 = extensionLoader.getExtension("dog");
// dog1.yell(null, "dog1");
SimpleExt adaptiveExtension = extensionLoader.getAdaptiveExtension();
Thread.sleep(100*24*60*60*1000L);
URLAddress urlAddress = new URLAddress("127.0.0.1", 20880);
URLParam parse = URLParam.parse(new HashMap<>());
adaptiveExtension.echo(new URL(urlAddress, parse), "dogs");
adaptiveExtension.bang(new URL(urlAddress, parse),0);
}
}
javassist
仍然让进程挂在那,重新启动arthas进行attach
使用jad反编译class,正常
cglib
让进程空跑,使用arthas进行attach
import com.alibaba.excel.support.cglib.core.DebuggingClassWriter;
import net.sf.cglib.proxy.Enhancer;
/**
* cglib proxy
*
* @author since
* @date 2024-09-19 13:24
*/
public class CglibProxy {
public static void main(String[] args) throws InterruptedException {
// 指定 CGLIB 将动态生成的代理类保存至指定的磁盘路径下
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/since/git/vamei/out/cglib");
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(CglibBaseBean.class);
enhancer.setCallback(new CglibCustomizedMethodInterceptor());
CglibBaseBean baseBean = (CglibBaseBean) enhancer.create();
baseBean.say();
Thread.sleep(100*24*60*60*1000L);
}
}
使用sc搜索相关的代理类,然后使用jad反编译, 也正常输出了
总结
本文介绍了一下java生成动态代理类的方式,像jdk动态代理,cglib动态代理,javassist在项目中都有广泛的应用。
对于查看动态代理class的内容,介绍了相关的方法进行查看,但是不够通用。
通过写agent,了解了agent的一些知识,如何使用instrument机制在启动时和运行时增加class
但是相比于自己写agent,还是arthas更加的方便通用一点,不用打agent包,也不用添加启动参数去重启进程。
arthas直接attach到目标进程上,可以模糊查询类,然后使用jad进行反编译,非常的方便。
arthas作为一个生产排障工具,还有其他更多强大的功能,值得去了解学习