本人阅读了 Skywalking 的大部分核心代码,也了解了相关的文献,对此深有感悟,特此借助巨人的思想自己手动用JAVA语言实现了一个 “调用链监控APM” 系统。本书采用边讲解实现原理边编写代码的方式,看本书时一定要跟着敲代码。
作者已经将过程写成一部书籍,奈何没有钱发表,如果您知道渠道可以联系本人。一定重谢。
本书涉及到的核心技术与思想
JavaAgent , ByteBuddy,SPI服务,类加载器的命名空间,增强JDK类,kafka,插件思想,切面,链路栈等等。实际上远不止这么多,差不多贯通了整个java体系。
适用人群
自己公司要实现自己的调用链的;写架构的;深入java编程的;阅读Skywalking源码的;
版权
本书是作者呕心沥血亲自编写的代码,不经同意切勿拿出去商用,否则会追究其责任。
原版PDF+源码请见:
本章涉及到的工具类也在这里面:
PDF书籍《手写调用链监控APM系统-Java版》第1章 开篇介绍-CSDN博客
第4章 插桩插件与bytebuddy字节码增强
本章是链路监控的核心,也是最难讲清的部分。我们先来了解下什么是插桩插件。
插桩插件是针对每个要增强的组件设计的一个辅组工具,里面的信息包含:“要增强的组件具体的类”,“增强类的具体方法”,以及“增强方法的执行前,执行后,执行出异常的逻辑实现”。
方法执行前,后,异常这种设计类似与切面,我们叫方法环绕拦截器MethodsAroundInterceptor。
比如一个Http接口请求容器是tomcat, 如果我们要采样接口,就需要找到接口请求必定要执行tomcat的哪个类的哪个方法,然后对这个方法进行字节码修改(增强)。针对此,我们新开一个tomcat插件模块, 插件有一个描述定义文件,指定一个Instrumentation类, 这个类里面描述了要增强的tomcat的类以及方法,然后方法里面还配置了要执行的方法环绕拦截器。
这样通过加载这个Instrumentation,我们就得到了当前插桩插件的用途,还可以在方法环绕拦截器里面编写出采集上报数据,完成我们调用链系统。
执行tomcat方法时,同时执行方法环绕拦截器这部分逻辑就属于bytebuddy的字节码部分。
所以这里涉及到两方面的知识:插桩插件体系 + bytebuddy字节码修改。
4.1 插件的初探
插件的重要作用就是要描述清楚:对哪个类的哪个方法进行增强,以及增强逻辑。
对此,我们可以定义一个抽象类,用来代表插件定义。在apm-commoms项目里面,新建类:
com.hadluo.apm.commons.plugins.AbstractClassEnhancePluginDefine
public abstract class AbstractClassEnhancePluginDefine {/**** 要增强的目标类名称* @return*/public abstract String enhanceClass();/*** 獲取实例方法的拦截点** @return*/public MethodsInterceptPoint[] configMethodsInterceptPoint() {return null;}/*** 獲取静态方法的拦截点** @return*/public MethodsInterceptPoint[] configStaticMethodsInterceptPoint() {return null;}/*** 是否拦截的是jdk的类** @return*/public boolean isBootstrapInstrumentation() {return false;}}
MethodsInterceptPoint其实就是描述的方法信息,我们叫方法拦截点,后续我就直接说这个名字了。因为拦截方法需要指定拦截目标方法的名称,拦截后的方法环绕拦截器(拦截的执行逻辑)等,所以用MethodsInterceptPoint这个类来指定。
然后方法又分为静态方法和实例方法,由于两者字节码修改的逻辑不一样,于是分成了两个配置。
isBootstrapInstrumentation是指明当前拦截的类是否是引导类加载器加载的类(例如:rt.jar)。修改bootstrap加载器加载的类比较复杂,不同于修改app classloader加载的类,所以也通过此配置来声明。
方法拦截点的代码,在apm-commoms项目里面,新建类:
com.hadluo.apm.commons.plugins.MethodsInterceptPoint
public interface MethodsInterceptPoint {/**** 方法的匹配器* @return*/ElementMatcher<MethodDescription> getMethodsMatcher();/**** 指定方法环绕拦截器 类路径* @return*/String getMethodsInterceptor();/*** 是否覆盖修改原方法的参数* @return*/boolean isOverrideArgs();
}
MethodsMatcher为指明拦截方法的规则,比如是按名称匹配还是按注解匹配。ElementMatcher是原生bytebuddy类。
isOverrideArgs指明要不要覆盖原生方法的参数,bytebuddy对覆盖参数的情况需要额外操作,后面会说到。
插件定义建好之后,我们下面来尝试先新建一个tomcat插桩插件,在顶级pom工程hadluo-smart-apm上新建一个聚合pom工程apm-agent-plugins(注意不是jar包),然后在apm-agent-plugins上新建jar项目tomcat-plugin。结构如下图:
以后apm-agent-plugins专门放插桩插件模块。
4.2 插件的设计与使用
项目建立好之后,我们新建一个插件定义类,在tomcat-plugin项目下新建类:
com.hadluo.apm.plugin.tomcat.TomcatInstrumentation
public class TomcatInstrumentation extends AbstractClassEnhancePluginDefine {@Overridepublic String enhanceClass() {// 要增强的 tomcat 类return "org.apache.catalina.core.StandardHostValve";}@Overridepublic MethodsInterceptPoint[] configMethodsInterceptPoint() {return new MethodsInterceptPoint[]{new MethodsInterceptPoint() {@Overridepublic ElementMatcher<MethodDescription> getMethodsMatcher() {// 拦截名称为 invoke 方法return ElementMatchers.named("invoke");}@Overridepublic String getMethodsInterceptor() {// 指定方法环绕拦截器 (拦截的逻辑实现)return "com.hadluo.apm.plugin.tomcat.TomcatInvokeInterceptor";}@Overridepublic boolean isOverrideArgs() {return false;}}};}
}
通过上面的插件定义,我们得知了拦截增强了org.apache.catalina.core.StandardHostValve.invoke实例方法。执行逻辑交给了TomcatInvokeInterceptor方法环绕拦截器。
上述描述没任何问题,但是TomcatInstrumentation 这个插件定义的发现需要通过一个途径来告诉系统,一个好的办法就是通过配置文件声明。
在tomcat-plugin模块的resource目录下新建hadluo-apm-plugin.def插件定义配置,内容如下:
##tomcat为插件的别名
tomcat=com.hadluo.apm.plugin.tomcat.TomcatInstrumentation
其实每个插件模块都必须要有hadluo-apm-plugin.def这个配置文件。用来告诉系统有哪些插件定义。
上面还漏掉了一个方法环绕拦截器 部分。这个就是拦截方法的执行逻辑,类似与切面的设计。由于拦截的方法有静态方法和实例方法的区分,所以分为两个方法环绕拦截器。
在apm-commoms项目里面,新建描述实例方法的方法环绕拦截器:
com.hadluo.apm.commons.plugins.InstanceMethodsAroundInterceptor
public interface InstanceMethodsAroundInterceptor {/*** 增强方法执行之前执行*/void beforeMethod(Object objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes) throws Throwable;/*** 增强方法执行之后 执行** @return the method's actual return value.*/Object afterMethod(Object objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes , Object ret) throws Throwable;/*** 增强方法异常 执行** @param t the exception occur.*/void handleMethodException(Object objInst, Method method, Object[] allArguments,Class<?>[] argumentsTypes, Throwable t);
}
在apm-commoms项目里面,新建描述静态方法的方法环绕拦截器:
com.hadluo.apm.commons.plugins.StaticMethodsAroundInterceptor
public interface StaticMethodsAroundInterceptor {/*** 增强方法执行之前执行*/void beforeMethod(Class<?> clazz, Method method, Object[] allArguments, Class<?>[] argumentsTypes) throws Throwable;/*** 增强方法执行之后 执行** @return the method's actual return value.*/Object afterMethod(Class<?> clazz, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Object ret) throws Throwable;/*** 增强方法异常 执行** @param t the exception occur.*/void handleMethodException(Class<?> clazz, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Throwable t);
}
静态与实例的区别就在于第一个参数是Class类型和Object类型。
然后回到我们的tomcat插件,在插件定义中声明TomcatInvokeInterceptor ,我们现在可以在tomcat-plugin项目下新建类:
com.hadluo.apm.plugin.tomcat.TomcatInvokeInterceptor
public class TomcatInvokeInterceptor implements InstanceMethodsAroundInterceptor {@Overridepublic void beforeMethod(Object objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes) throws Throwable {System.out.println("TomcatInvokeInterceptor.beforeMethod");}@Overridepublic Object afterMethod(Object objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Object ret) throws Throwable {System.out.println("TomcatInvokeInterceptor.afterMethod");return ret;}@Overridepublic void handleMethodException(Object objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Throwable t) {System.out.println("TomcatInvokeInterceptor.handleMethodException");}
}
经过上面的努力,我们就实现了一个简单的tomcat插桩插件,但是现在还无法测试,因为还没有查找这些插件进行处理与修改字节码等逻辑。
4.3 插桩插件的查找与归档
上一节中,我们在def文件中声明了插件定义,我们首要任务就是要寻找到这些插件定义。
在 apm-agent-core项目下新建类:
com.hadluo.apm.agentcore.plugins.PluginBootstrap
public class PluginBootstrap {// def文件的解析器private final PluginDefResolver pluginDefResolver = new PluginDefResolver();// 加载环境下所有的def文件并解析反射实例化出插件定义public List<AbstractClassEnhancePluginDefine> loadPlugins() throws IOException {//解析 插件定义hadluo-apm-plugin.def文件Map<String, Set<String>> defMap = pluginDefResolver.resolvePluginDefList();List<AbstractClassEnhancePluginDefine> plugins = new ArrayList<>();//加载实例化插件for (String key : defMap.keySet()) {Set<String> pluginClassList = defMap.get(key);for (String pluginClass : pluginClassList) {try {// 反射实例化 插件定义Object plugin = Class.forName(pluginClass).newInstance();plugins.add((AbstractClassEnhancePluginDefine)plugin);Logs.debug(getClass(), "查找到插件:" + plugin.getClass());} catch (Exception e) {Logs.err(PluginBootstrap.class, "加载插件失败 : " + pluginClass, e);}}}return plugins;}
}
PluginBootstrap 就是插件引导类,通过loadPlugins方法来加载环境下的所有def文件,然后通过反射实例化出def配置的插件定义类。
PluginDefResolver工具代码如下:
com.hadluo.apm.agentcore.plugins.PluginDefResolver
public class PluginDefResolver {/**** 解析 插件定义hadluo-apm-plugin.def文件* @return* @throws IOException*/public Map<String, Set<String>> resolvePluginDefList() throws IOException {Map<String, Set<String>> defs = new HashMap<>();List<File> defFileList = ResourceFinder.findFile("hadluo-apm-plugin.def");for (File defFile : defFileList) {// 一行一行读FileReader fr = null;BufferedReader bf = null;try {fr = new FileReader(defFile);bf = new BufferedReader(fr);String str;// 按行读取字符串while ((str = bf.readLine()) != null) {if (str.trim().startsWith("#")) {// 注释continue;}if(!str.contains("=")){continue;}String k = str.split("=")[0];Set<String> val = defs.get(k);if(val == null){val = new HashSet<>();defs.put(k, val);}val.add(str.trim().split("=")[1].trim());}} finally {if (bf != null) {bf.close();}if (fr != null) {fr.close();}if(defFile.exists()){boolean delete = defFile.delete();if(!delete){Logs.err(getClass(),"删除临时文件失败,请手动删除,否则下次启动可能会报错! path:"+defFile.getAbsolutePath());}}}}return defs ;}
}
通过ResourceFinder工具,拿到def配置文件,这里需要注意一点,agent-core是一个jar,这个def配置文件是在plugin模块下的,也是在plugin的jar里面,所以ResourceFinder工具理论上需要从jar里面解析然后在解析jar,然后才能拿到def文件内容,还好我们配置了一个打包插件:maven-shade-plugin ,前面已经提到过了,我们反编译下agent-core.jar 会发现,def文件就在根目录
而且内容是多个def文件合并后的内容,这样就不会丢失def。
为什么后面会有一个文件的删除逻辑,就是ResourceFinder工具是从agent-core.jar里面复制hadluo-apm-plugin.def出来一个临时文件返回的,所以读取内容完成后,需要删除这个临时文件。
前面的PluginBootstrap 是通过反射来加载插件定义类的,而具体的插件定义类是写在plugin模块下的,也就是说在agent-core模块下是找不到plugin的,所以反射实例化时,会提示ClassNotFoundException。
为解决这个问题,就必须在agent-core的POM下面增加插件的依赖:
<dependency><groupId>com.hadluo.apm</groupId><artifactId>tomcat-plugin</artifactId><version>1.0</version>
</dependency>
也就是说以后每定义一个新的插件,都必须要在POM中添加对此插件的依赖。
拿到插件定义集合后,我们需要放到一个容器里面,于是需要设计一个插件查找器的类,在apm-agent-core项目下新建类:
com.hadluo.apm.agentcore.plugins.PluginFinder
public class PluginFinder {// 按名称匹配的插件 , 可以有多个插件拦截一个 目标类private final Map<String, List<AbstractClassEnhancePluginDefine>> nameMatchDefine = new HashMap<>();// 增强jdk类的 插件定义private final Set<AbstractClassEnhancePluginDefine> bootstrapDefine = new HashSet<>();public PluginFinder(List<AbstractClassEnhancePluginDefine> defines){for(AbstractClassEnhancePluginDefine define : defines){
// 获取要拦截的类名String enhance = define.enhanceClass();List<AbstractClassEnhancePluginDefine> list = nameMatchDefine.get(enhance);if(list == null){list = new ArrayList<>();nameMatchDefine.put(enhance , list) ;}list.add(define) ;if(define.isBootstrapInstrumentation()){// 拦截jdk的插件定义bootstrapDefine.add(define) ;}}}// 通过类名称来找 拦截此类的 所有插件定义public List<AbstractClassEnhancePluginDefine> find(String className){List<AbstractClassEnhancePluginDefine> plugins = nameMatchDefine.get(className);if(plugins == null){return Collections.emptyList();}return plugins;}// 返回增强引导类类加载器的插件定义public Set<AbstractClassEnhancePluginDefine> findBootstrap(){return bootstrapDefine;}/**** 按照名称查找* @return*/public ElementMatcher<? super TypeDescription> buildMatch() {return new ElementMatcher.Junction.AbstractBase<NamedElement>() {@Overridepublic boolean matches(NamedElement target) {return nameMatchDefine.containsKey(target.getActualName());}};}
}
PluginFinder类将要插桩的类名称和插件定义,归档到了一个map里面,方便后续操作。同时将增强jdk类的插件定义放到了一个Set集合里面。
buildMatch方法,构造了bytebuddy拦截按名称匹配规则的junction条件,也就是说,只要系统加载的class类名称只要是与nameMatchDefine map集合的key里面的类名匹配,就进行增强这个类。
到此我们插件查找与归档就编写完成了,在premain里面进行调用:
//3. 插件的查找与归档
PluginFinder pluginFinder;
try {pluginFinder = new PluginFinder(new PluginBootstrap().loadPlugins());
} catch (Exception e) {Logs.err(AgentMain.class, "AgentMain启动失败,插件初始化失败", e);return;
}
4.4 插件与bytebuddy结合
这节讲解需要有bytebuddy的知识,但是不需要很深,只要知道是干什么的就差不多了。
Byte Buddy
是一个代码生成和操作库,用于在Java应用程序运行时创建和修改Java类,而无需编译器的帮助。除了Java类库附带的代码生成实用程序外,Byte Buddy还允许创建任意类,并且不限于实现用于创建运行时代理的接口。此外,Byte Buddy提供了一种方便的API,可以使用Java代理或在构建过程中手动更改类。
bytebuddy 这块的代码属定型的工具代码,只要知道意思就可以了,在premain里面继续编写初始化bytebuddy代码:
// 4. 初始化bytebuddy
final ByteBuddy byteBuddy = new ByteBuddy();
AgentBuilder agentBuilder = new AgentBuilder.Default(byteBuddy).ignore(// 忽略哪些类肯定不被增强ElementMatchers.nameStartsWith("net.bytebuddy.").or(ElementMatchers.nameContains("javassist")).or(ElementMatchers.nameContains(".asm.")).or(ElementMatchers.nameContains(".reflectasm.")).or(ElementMatchers.nameStartsWith("sun.reflect")).or(ElementMatchers.isSynthetic()));// 注入一些类到bootstrap加载器中 ,用来增强 jdk类(引导类加载器加载的类)
try {agentBuilder = BootstrapInstrumentBoost.inject(pluginFinder.findBootstrap(),instrumentation,agentBuilder) ;
} catch (Exception e) {throw new RuntimeException("AgentMain启动失败,BootstrapInstrumentBoost inject失败" , e);
}
agentBuilder.type(pluginFinder.buildMatch())// 增强类逻辑处理.transform(new Transformer(pluginFinder)).with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION).with(new RedefinitionListener()).with(new Listener()).installOn(instrumentation);
ByteBuddy 通过AgentBuilder的installOn 就可以和java agent打通关联,也就是使字节码修改生效。所以核心要关注AgentBuilder的一些设置的属性。
通过ignore方法,设置了过滤拦截的一些类,增加了启动效率,BootstrapInstrumentBoost是为增强JDK类的一个辅助工具,后面会新增一个小节来讲。
type方法就是设置拦截的匹配条件junction ,前面我们已经实现了pluginFinder里面的buildMatch了。
Transformer就是当类加载时,如果类名匹配到了上面junction条件,又不在ignore过滤里面,则会执行Transformer的逻辑,这里面就可以对加载的类进行字节码修改。
RedefinitionListener,Listener为字节码修改的一些生命周期回调。两个类直接定义成AgentMain的内部类即可,代码如下:
private static class RedefinitionListener implements AgentBuilder.RedefinitionStrategy.Listener {@Overridepublic Iterable<? extends List<Class<?>>> onError(int index, List<Class<?>> batch, Throwable throwable, List<Class<?>> types) {Logs.err(RedefinitionListener.class,"Redefinition错误",throwable);return Collections.emptyList();}
}
private static class Listener implements AgentBuilder.Listener {@Overridepublic void onError(final String typeName,final ClassLoader classLoader,final JavaModule module,final boolean loaded,final Throwable throwable) {Logs.err(Listener.class,"Enhance class " + typeName + " error.",throwable);}
}
只实现了错误的回调,打了日志堆栈,其余的空方法读者自行实现就行。
实接下来就是核心的Transformer类, 这个类也可以时AgentMain的内部类,代码如下:
private static class Transformer implements AgentBuilder.Transformer {private final PluginFinder pluginFinder;Transformer(PluginFinder pluginFinder) {this.pluginFinder = pluginFinder;}@Overridepublic DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule) {// 获取到 加载的class的名称String typeName = typeDescription.getTypeName();// 通过插件查找器找出 拦截这个类的 所有插件定义List<AbstractClassEnhancePluginDefine> matchPlugins = pluginFinder.find(typeName);if (matchPlugins == null || matchPlugins.isEmpty()) {return builder;}for (AbstractClassEnhancePluginDefine pluginDefine : matchPlugins) {// 增强这个类builder = pluginDefine.define(builder, typeDescription, classLoader);}Logs.debug(AgentMain.class, "类: " + typeName +" 增强字节码描述完成");return builder;}}
能回调到transform方法的类,都是已经匹配上的,且正在加载的类,TypeDescription 就是这个类的一些描述信息。
然后通过我们前面编写的插件查找器,匹配出增强这个类的所有插件定义。接着遍历插件定义,调用插件定义的define方法修改字节码。
于是在AbstractClassEnhancePluginDefine类中新增方法:
/**** 增强核心逻辑* @param builder* @param typeDescription* @param classLoader* @return*/
public DynamicType.Builder<?> define(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader) {// 增强实例方法builder = enhance(builder, classLoader, configMethodsInterceptPoint());// 增强静态方法return enhanceStatic(builder, classLoader, configStaticMethodsInterceptPoint());
}
通过插件定义的configMethodsInterceptPoint拿到了要增强的实例方法的方法拦截点(方法信息)。同理还有静态方法。
enhance代码如下:
private DynamicType.Builder<?> enhance(DynamicType.Builder<?> builder, ClassLoader classLoader, MethodsInterceptPoint[] methodsInterceptPoints) {if (methodsInterceptPoints == null) {return builder;}for (MethodsInterceptPoint methodsInterceptPoint : methodsInterceptPoints) {// 获取到 配置 的 增强方法 具体的执行逻辑点String interceptor = methodsInterceptPoint.getMethodsInterceptor();// 获取到 配置 的方法匹配器ElementMatcher.Junction<MethodDescription> junction = not(isStatic()).and(methodsInterceptPoint.getMethodsMatcher());boolean isOverrideArgs = methodsInterceptPoint.isOverrideArgs();// 覆盖参数的 需要加 Morph.Binder.install(OverrideCallable.class)if (isOverrideArgs && isBootstrapInstrumentation()) {// 覆盖参数 && 增强jdk类builder = builder.method(junction).intercept(MethodDelegation.withDefaultConfiguration().withBinders(Morph.Binder.install(OverrideCallable.class)).to(BootstrapInstrumentBoost.getRedefineClass(interceptor)));}if (isOverrideArgs && !isBootstrapInstrumentation()) {// 覆盖参数 && 增强非jdk类builder = builder.method(junction).intercept(MethodDelegation.withDefaultConfiguration().withBinders(Morph.Binder.install(OverrideCallable.class)).to(new InstMethodsProxyWithOverrideArgs(interceptor, classLoader)));}if (!isOverrideArgs && isBootstrapInstrumentation()) {// 不覆盖参数 && 增强jdk类builder = builder.method(junction).intercept(MethodDelegation.withDefaultConfiguration().to(BootstrapInstrumentBoost.getRedefineClass(interceptor)));}if (!isOverrideArgs && !isBootstrapInstrumentation()) {// 不覆盖参数 && 增强非jdk类builder = builder.method(junction).intercept(MethodDelegation.withDefaultConfiguration().to(new InstMethodsProxy(interceptor, classLoader)));}}return builder;
}
not(isStatic()).and(methodsInterceptPoint.getMethodsMatcher()); 这段代码很关键,
也是构造一个条件junction,用来匹配拦截的方法,条件为非静态方法,且满足方法拦截点设置的junction。
builder.method就是指定方法满足条件junction 。
覆盖参数时,会多出withBinders(Morph.Binder.install(OverrideCallable.class)) 设置,这个是bytebuddy的写法,无需关注。
在apm-commons项目下新建类:
com.hadluo.apm.commons.plugins.OverrideCallable
public interface OverrideCallable {Object call(Object[] args);
}
重点关注的是to的逻辑,to 指明了拦截的逻辑具体处理。先抛开增强JDK类的,对于覆盖参数的分为了两种:
InstMethodsProxy : 实例方法不覆盖参数的逻辑处理。
InstMethodsProxyWithOverrideArgs:实例方法且覆盖参数的逻辑处理。
在apm-commons项目下新建类:
com.hadluo.apm.commons.plugins.InstMethodsProxy
public class InstMethodsProxy {// 具体的插桩插件定义的方法环绕拦截器private final InstanceMethodsAroundInterceptor real;public InstMethodsProxy(String interceptor, ClassLoader classLoader) {try {this.real = (InstanceMethodsAroundInterceptor) Class.forName(interceptor,true,classLoader).newInstance();} catch (Exception e) {throw new RuntimeException(e);}}/**** @param obj 注入被拦截的目标对象。* @param allArguments 注入目标方法的全部参数* @param zuper 用来调用原方法* @param method 注入被拦截的源方法* @return* @throws Throwable*/@RuntimeTypepublic Object intercept(@This Object obj, @AllArguments Object[] allArguments, @SuperCall Callable<?> zuper,@Origin Method method) throws Throwable {// 方法执行之前real.beforeMethod(obj,method,allArguments,method.getParameterTypes());Object ret = null;try{// 调用原方法ret = zuper.call();}catch (Throwable t){// 方法执行异常real.handleMethodException(obj,method,allArguments,method.getParameterTypes(),t);throw t;}finally {// 方法执行完成ret = real.afterMethod(obj,method,allArguments,method.getParameterTypes() , ret);}return ret ;}
}
通过上述代码,我们很明显的把bytebuddy的拦截处理逻辑优雅的桥接到了我们插桩插件定义的方法环绕拦截器上。
@This , @AllArguments , @SuperCall ,@Origin 注解就是bytebuddy为了找到执行逻辑方法编写的。
同理我们得出覆盖参数的,在apm-commons项目下新建类:
com.hadluo.apm.commons.plugins.InstMethodsProxyWithOverrideArgs
public class InstMethodsProxyWithOverrideArgs {private final InstanceMethodsAroundInterceptor real;public InstMethodsProxyWithOverrideArgs(String interceptor, ClassLoader classLoader) {try {this.real = (InstanceMethodsAroundInterceptor) Class.forName(interceptor,true,classLoader).newInstance();} catch (Exception e) {throw new RuntimeException(e);}}@RuntimeTypepublic Object intercept(@This Object obj, @AllArguments Object[] allArguments, @Morph OverrideCallable zuper,@Origin Method method) throws Throwable {// 方法执行之前real.beforeMethod(obj,method,allArguments,method.getParameterTypes());Object ret = null;try{// 调用原方法 , 这里可以在 beforeMethod 里面对参数进行调整修改ret = zuper.call(allArguments);}catch (Throwable t){// 方法执行异常real.handleMethodException(obj,method,allArguments,method.getParameterTypes(),t);throw t;}finally {// 方法执行完成ret = real.afterMethod(obj,method,allArguments,method.getParameterTypes() ,ret);}return ret ;}
}
增强实例方法讲解完了,下面来实现下增强静态方法的逻辑,
enhanceStatic代码如下:
private DynamicType.Builder<?> enhanceStatic(DynamicType.Builder<?> builder, ClassLoader classLoader, MethodsInterceptPoint[] staticsInterceptPoints) {if (staticsInterceptPoints == null) {return builder;}for (MethodsInterceptPoint methodsInterceptPoint : staticsInterceptPoints) {// 获取到 配置 的 增强方法 具体的执行逻辑点String interceptor = methodsInterceptPoint.getMethodsInterceptor();// 获取到 配置 的方法匹配器ElementMatcher.Junction<MethodDescription> junction = isStatic().and(methodsInterceptPoint.getMethodsMatcher());boolean isOverrideArgs = methodsInterceptPoint.isOverrideArgs();// 覆盖参数的 需要加 Morph.Binder.install(OverrideCallable.class)if (isOverrideArgs && isBootstrapInstrumentation()) {// 覆盖参数 && 增强jdk类builder = builder.method(junction).intercept(MethodDelegation.withDefaultConfiguration().withBinders(Morph.Binder.install(OverrideCallable.class)).to(BootstrapInstrumentBoost.getRedefineClass(interceptor)));}if (isOverrideArgs && !isBootstrapInstrumentation()) {// 覆盖参数 && 增强非jdk类builder = builder.method(junction).intercept(MethodDelegation.withDefaultConfiguration().withBinders(Morph.Binder.install(OverrideCallable.class)).to(new InstStaticMethodsProxyWithOverrideArgs(interceptor)));}if (!isOverrideArgs && isBootstrapInstrumentation()) {// 不覆盖参数 && 增强jdk类builder = builder.method(junction).intercept(MethodDelegation.withDefaultConfiguration().to(BootstrapInstrumentBoost.getRedefineClass(interceptor)));}if (!isOverrideArgs && !isBootstrapInstrumentation()) {// 不覆盖参数 && 增强非jdk类builder = builder.method(junction).intercept(MethodDelegation.withDefaultConfiguration().to(new InstStaticMethodsProxy(interceptor)));}}return builder;
}
同实例方法类似,不在赘述逻辑。
com.hadluo.apm.commons.plugins.InstStaticMethodsProxy代码如下:
public class InstStaticMethodsProxy {private static StaticMethodsAroundInterceptor real;public InstStaticMethodsProxy(String interceptor){try {real = (StaticMethodsAroundInterceptor) Class.forName(interceptor).newInstance();} catch (Exception e) {throw new RuntimeException(e);}}@RuntimeTypepublic static Object intercept(@Origin Class<?> clazz, @AllArguments Object[] allArguments, @Origin Method method,@SuperCall Callable<?> zuper) throws Throwable {// 方法执行之前real.beforeMethod(clazz,method,allArguments,method.getParameterTypes());Object ret = null;try{// 调用原方法ret = zuper.call();}catch (Throwable t){// 方法执行异常real.handleMethodException(clazz,method,allArguments,method.getParameterTypes(),t);throw t ;}finally {// 方法执行完成ret = real.afterMethod(clazz,method,allArguments,method.getParameterTypes() , ret);}return ret ;}
}
com.hadluo.apm.commons.plugins.InstStaticMethodsProxyWithOverrideArgs代码如下:
public class InstStaticMethodsProxyWithOverrideArgs {private static StaticMethodsAroundInterceptor real;public InstStaticMethodsProxyWithOverrideArgs(String interceptor){try {real = (StaticMethodsAroundInterceptor) Class.forName(interceptor).newInstance();} catch (Exception e) {throw new RuntimeException(e);}}@RuntimeTypepublic static Object intercept(@This Class<?> clazz, @AllArguments Object[] allArguments, @Morph OverrideCallable zuper,@Origin Method method) throws Throwable {// 方法执行之前real.beforeMethod(clazz,method,allArguments,method.getParameterTypes());Object ret = null;try{// 调用原方法 , 这里可以在 beforeMethod 里面对参数进行调整修改ret = zuper.call(allArguments);}catch (Throwable t){// 方法执行异常real.handleMethodException(clazz,method,allArguments,method.getParameterTypes(),t);throw t;}finally {// 方法执行完成ret = real.afterMethod(clazz,method,allArguments,method.getParameterTypes() ,ret);}return ret ;}
}
到此我们已经将插桩插件与bytebuddy修改字节码进行了逻辑关联,架子已经搭建好了,但是还有一个重点,就是我们对JDK的类增强只字未提,因为它真的很复杂,作者本人我也是研究了很久。下节我们仔细讲解。
4.5 对JDK类的增强实现
在这里我们思考一个问题,为什么我们前面的代码不能实现对JDK类的增强?答案就是类加载器的命名空间的问题。
不同的类加载器A和B,只要是没有父子关系,从环境中加载的同一份字节码得到的Class对象都是不同的。 也就是A加载器加载的类是无法访问到B加载器加载的类的。
也就是说每个类加载器都有自己的命名空间。命名空间由该加载器和所有父加载器所加载的类组成。
如果是有父子关系。子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。
例如App ClassLoader 加载的类能看到Bootstrap ClassLoader加载的java.lang.String ,这也就是你写的类为什么能调用到String类的原因。
反之,由父类加载器加载的类不能看见子加载器加载的类。这也就解释了如果我们对java.lang.String 进行字节码增强,调用我们自己的代码,我们自己的代码肯定是App ClassLoader 加载的, 在父类加载器加载的类中是无法访问到子加载器加载的类的。
要想解决这个问题, 就是要把我们自己定义的类注入到Bootstrap ClassLoader类加载器中去加载。
好在bytebuddy通过unsafe类解决了这个问题,让我们有途径去定义类并且将类注入到Bootstrap ClassLoader 中。
在前面的premain方法中,我们还没有实现BootstrapInstrumentBoost.inject这个方法,在apm-commons项目下新建类:
com.hadluo.apm.commons.plugins.BootstrapInstrumentBoost
由于代码篇幅过长, 请到第一章的pdf和源码获取
bootstrapClassesTypeMap 就是存放我们要注入到Bootstrap类加载器加载类, 方法最后通过ClassInjector.UsingUnsafe这一段代码就进行了注入。
baseClass就是将一些必须类放到map中, 代码如下:
private static void baseClass(Map<String, byte[]> classesTypeMap) {String[] CLASSES = {"net.bytebuddy.implementation.bind.annotation.RuntimeType","net.bytebuddy.implementation.bind.annotation.This","net.bytebuddy.implementation.bind.annotation.AllArguments","net.bytebuddy.implementation.bind.annotation.AllArguments$Assignment","net.bytebuddy.implementation.bind.annotation.SuperCall","net.bytebuddy.implementation.bind.annotation.Origin","net.bytebuddy.implementation.bind.annotation.Morph","com.hadluo.apm.commons.plugins.OverrideCallable"
};for (String classname : CLASSES) {classesTypeMap.put(classname, getClassBytes(classname));}
}private static byte[] getClassBytes(String className) {byte[] enhancedInstanceClassFile;try {String classResourceName = className.replaceAll("\\.", "/") + ".class";InputStream resourceAsStream = BootstrapInstrumentBoost.class.getClassLoader().getResourceAsStream(classResourceName);ByteArrayOutputStream os = new ByteArrayOutputStream();byte[] buffer = new byte[1024];int len;while ((len = resourceAsStream.read(buffer)) != -1) {os.write(buffer, 0, len);}enhancedInstanceClassFile = os.toByteArray();} catch (IOException e) {throw new RuntimeException(e.getMessage(), e);}return enhancedInstanceClassFile;
}
baseClass注入的类都是@This ,@RuntimeType等注解 , 这些注解你应该还有印象,之前我们讲到bytebuddy的拦截逻辑出处理类中就有这些注解。
为什么要注入这些注解?
我们知道要增强JDK的类, 增强的执行逻辑类是不是肯定也要注入到引导类加载器中, 不然就会有前面提到的命名空间的问题。那么这个执行逻辑类引用的任何class都必须注入到引导类加载器中。所以我们先将上面注解进行注入。
pluginClass 方法里面我们为每一个插件定义的方法环绕拦截器都按照模板动态生成一个class,代码如下:
由于代码篇幅过长, 请到第一章的pdf和源码获取
首先根据静态,非静态,覆盖参数,不覆盖参数等四种情况的四种模板类生成TypeDescription , 通过这个TypeDescription 就可以动态生成一个内容为模板类代码的类。
动态生成的类的名称策略是nameStrategy方法,代码如下:
private static String nameStrategy(String methodsInterceptor) {return methodsInterceptor + "_internal";
}
methodsInterceptor就是具体的插桩插件配置的方法环绕拦截器类名。在后加了_internal。
然后还为动态生成的类的String字段interceptor 设置了值, 值就是方法环绕拦截器类名。 也就是说,当前这个动态生成的xxx_internal就是bytebuddy对JDK类增强的执行逻辑类。由于赋值了interceptor 字段,就知道是要执行哪个方法环绕拦截器了。
下面就是4种模板类,这些模板类根本都没有加载到JVM中,只是为了动态生成类提供了内容。这些模板类都在apm-commons项目中新建。
静态,覆盖参数的模板类代码:
com.hadluo.apm.commons.plugins.BootstrapStaticMethodsWithOverrideArgsTemplate
public class BootstrapStaticMethodsWithOverrideArgsTemplate {private static Object real;private static String interceptor;private static Method beforeMethod ;private static Method handleExceptionMethod ;private static Method afterMethod ;private static void createInterceptor (){if (real == null) {try {real = Class.forName(interceptor, true, Thread.currentThread().getContextClassLoader()).newInstance();beforeMethod = real.getClass().getDeclaredMethod("beforeMethod",new Class[]{Class.class, Method.class, Object[].class, Class[].class});beforeMethod.setAccessible(true);afterMethod = real.getClass().getDeclaredMethod("afterMethod",new Class[]{Class.class, Method.class, Object[].class, Class[].class, Object.class});afterMethod.setAccessible(true);handleExceptionMethod = real.getClass().getDeclaredMethod("handleMethodException",new Class[]{Class.class, Method.class, Object[].class, Class[].class, Throwable.class});handleExceptionMethod.setAccessible(true);} catch (Exception e) {throw new RuntimeException(e);}}}@RuntimeTypepublic static Object intercept(@This Class<?> clazz, @AllArguments Object[] allArguments, @Morph OverrideCallable zuper,@Origin Method method) throws Throwable {createInterceptor();// 方法执行之前beforeMethod.invoke(real, clazz, method, allArguments, method.getParameterTypes());Object ret = null;try{// 调用原方法 , 这里可以在 beforeMethod 里面对参数进行调整修改ret = zuper.call(allArguments);}catch (Throwable t){// 方法执行异常handleExceptionMethod.invoke(real, clazz, method, allArguments, method.getParameterTypes(), t);throw t;}finally {// 方法执行完成afterMethod.invoke(real, clazz, method, allArguments, method.getParameterTypes(), ret);}return ret ;}
}
createInterceptor 里面必须通过反射来获取Method然后进行beforeMethod,handleExceptionMethod,afterMethod的调用。而且不能将Object替换成InstanceMethodsAroundInterceptor, 因为这个类没有被注入到引导类加载器,是找不到的。
同理可得,静态,不覆盖参数的模板类代码:
com.hadluo.apm.commons.plugins.BootstrapStaticMethodsTemplate
public class BootstrapStaticMethodsTemplate {private static Object real;private static String interceptor;private static Method beforeMethod ;private static Method handleExceptionMethod ;private static Method afterMethod ;private static void createInterceptor (){if (real == null) {try {real = Class.forName(interceptor, true, Thread.currentThread().getContextClassLoader()).newInstance();beforeMethod = real.getClass().getDeclaredMethod("beforeMethod",new Class[]{Class.class, Method.class, Object[].class, Class[].class});beforeMethod.setAccessible(true);afterMethod = real.getClass().getDeclaredMethod("afterMethod",new Class[]{Class.class, Method.class, Object[].class, Class[].class, Object.class});afterMethod.setAccessible(true);handleExceptionMethod = real.getClass().getDeclaredMethod("handleMethodException",new Class[]{Class.class, Method.class, Object[].class, Class[].class, Throwable.class});handleExceptionMethod.setAccessible(true);} catch (Exception e) {throw new RuntimeException(e);}}}@RuntimeTypepublic static Object intercept(@Origin Class<?> clazz, @AllArguments Object[] allArguments, @Origin Method method,@SuperCall Callable<?> zuper) throws Throwable {createInterceptor();// 方法执行之前beforeMethod.invoke(real, clazz, method, allArguments, method.getParameterTypes());Object ret = null;try {// 调用原方法ret = zuper.call();} catch (Throwable t) {// 方法执行异常handleExceptionMethod.invoke(real, clazz, method, allArguments, method.getParameterTypes(), t);throw t;} finally {// 方法执行完成afterMethod.invoke(real, clazz, method, allArguments, method.getParameterTypes(), ret);}return ret;}
}
同理可得,实例,覆盖参数的模板类代码:
com.hadluo.apm.commons.plugins.BootstrapMethodsWithOverrideArgsTemplate
public class BootstrapMethodsWithOverrideArgsTemplate {private static Object real;private static String interceptor;private static Method beforeMethod;private static Method handleExceptionMethod;private static Method afterMethod;private static void createInterceptor() {if (real == null) {try {real = Class.forName(interceptor, true, Thread.currentThread().getContextClassLoader()).newInstance();beforeMethod = real.getClass().getDeclaredMethod("beforeMethod",new Class[]{Object.class, Method.class, Object[].class, Class[].class});beforeMethod.setAccessible(true);afterMethod = real.getClass().getDeclaredMethod("afterMethod",new Class[]{Object.class, Method.class, Object[].class, Class[].class, Object.class});afterMethod.setAccessible(true);handleExceptionMethod = real.getClass().getDeclaredMethod("handleMethodException",new Class[]{Object.class, Method.class, Object[].class, Class[].class, Throwable.class});handleExceptionMethod.setAccessible(true);} catch (Exception e) {throw new RuntimeException(e);}}}@RuntimeTypepublic static Object intercept(@This Object obj, @AllArguments Object[] allArguments, @Morph OverrideCallable zuper, @Origin Method method) throws Throwable {createInterceptor();// 方法执行之前beforeMethod.invoke(real, obj, method, allArguments, method.getParameterTypes());Object ret = null;try {// 调用原方法 , 这里可以在 beforeMethod 里面对参数进行调整修改ret = zuper.call(allArguments);} catch (Throwable t) {// 方法执行异常handleExceptionMethod.invoke(real, obj, method, allArguments, method.getParameterTypes(), t);throw t;} finally {// 方法执行完成afterMethod.invoke(real, obj, method, allArguments, method.getParameterTypes(), ret);}return ret;}
}
同理可得,实例,不覆盖参数的模板类代码:
com.hadluo.apm.commons.plugins.BootstrapMethodsTemplate
public class BootstrapMethodsTemplate {private static Object real;private static String interceptor;private static Method beforeMethod ;private static Method handleExceptionMethod ;private static Method afterMethod ;private static void createInterceptor (){if (real == null) {try {real = Class.forName(interceptor, true, Thread.currentThread().getContextClassLoader()).newInstance();beforeMethod = real.getClass().getDeclaredMethod("beforeMethod",new Class[]{Object.class, Method.class, Object[].class, Class[].class});beforeMethod.setAccessible(true);afterMethod = real.getClass().getDeclaredMethod("afterMethod",new Class[]{Object.class, Method.class, Object[].class, Class[].class, Object.class});afterMethod.setAccessible(true);handleExceptionMethod = real.getClass().getDeclaredMethod("handleMethodException",new Class[]{Object.class, Method.class, Object[].class, Class[].class, Throwable.class});handleExceptionMethod.setAccessible(true);} catch (Exception e) {throw new RuntimeException(e);}}}@RuntimeTypepublic static Object intercept(@This Object obj, @AllArguments Object[] allArguments, @SuperCall Callable<?> zuper,@Origin Method method) throws Throwable {createInterceptor();// 方法执行之前beforeMethod.invoke(real, obj, method, allArguments, method.getParameterTypes());Object ret = null;try {// 调用原方法ret = zuper.call();} catch (Throwable t) {// 方法执行异常handleExceptionMethod.invoke(real, obj, method, allArguments, method.getParameterTypes(), t);throw t;} finally {// 方法执行完成afterMethod.invoke(real, obj, method, allArguments, method.getParameterTypes(), ret);}return ret;}
}
到此,我们终于把JDK的增强逻辑处理类的生成给写完了。回到AbstractClassEnhancePluginDefine的enhanceStatic和enhance方法里面,我们需要指定名称找到动态生成的类,然后通过to交给ByteBuddy。
上面的代码已经写了,只是没有实现getRedefineClass ,代码如下:
public static Class getRedefineClass(String methodsInterceptor) {try {return Class.forName(nameStrategy(methodsInterceptor));} catch (ClassNotFoundException e) {throw new RuntimeException(e);}
}
getRedefineClass 方法就是获取动态生成的类,Class.forName加载时,如果是已经存在类了,就不会被再次加载,所以能获取到注入到Bootstrap类加载器的动态生成的类。
到此,关于对JDK增强的代码就已经编写完成了。
其实对JDK类增强有些类可能会增强失败, 在执行bytebuddy修改字节码之前加载的JDK类,也就是说在premain之前加载的JDK类就无法增强。具体看下节的测试。
4.6 增强JDK类库(增强线程池)测试和出现问题的思考
本次测试主要就对JDK类增强进行测试。 对JDK类库的增强一定要小心,否则会出现不可预料的错误。
增强线程池ThreadPoolExecutor#submit
我们之前已经创建好了tomcat-plugin这个项目,暂时就在这个里面进行测试, 修改TomcatInstrumentation代码 ,代码如下:
public class TomcatInstrumentation extends AbstractClassEnhancePluginDefine {@Overridepublic String enhanceClass() {// 要增强的 线程池 类return "java.util.concurrent.ThreadPoolExecutor";}@Overridepublic boolean isBootstrapInstrumentation() {// 标志是 jdk类return true;}@Overridepublic MethodsInterceptPoint[] configMethodsInterceptPoint() {return new MethodsInterceptPoint[]{new MethodsInterceptPoint() {@Overridepublic ElementMatcher<MethodDescription> getMethodsMatcher() {// 拦截 submit 方法return ElementMatchers.named("submit");}@Overridepublic String getMethodsInterceptor() {// 拦截逻辑交给 TomcatInvokeInterceptorreturn "com.hadluo.apm.plugin.tomcat.TomcatInvokeInterceptor";}@Overridepublic boolean isOverrideArgs() {return false;}}};}
}
TomcatInvokeInterceptor 代码如下:
public class TomcatInvokeInterceptor implements InstanceMethodsAroundInterceptor {@Overridepublic void beforeMethod(Object objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes) throws Throwable {System.out.println("ThreadPoolExecutor.submit 执行前");}@Overridepublic Object afterMethod(Object objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Object ret) throws Throwable {System.out.println("ThreadPoolExecutor.submit 执行后");return ret;}@Overridepublic void handleMethodException(Object objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Throwable t) {}
}
当线程池提交任务时,就执行TomcatInvokeInterceptor ,打印出日志。
在之前的SpringBoot的测试项目中, 写一个接口,代码如下:
重新打包,启动测试, 发现启动报错:
这个错误意思大概就是对已经加载类进行redefinition时,改变了结构(修改实现接口,增加字段等)。 在第一章提到Agent时,说了动态绑定方案的限制就是无法修改结构。所以这里报错大概就是Agent的限制: “ 通过对已加载的类进行字节码修改(Instrumentation#retransformClasses)时,无法增加,修改字段,接口等”
于是我们添加JVM启动参数 -XX:+TraceClassLoading , 来观察下类加载的顺序。
这里我直接说结果了,我发现之前定义的ThreadFactory线程名字就会间接加载线程池,下面是相关加载类的顺序。
而线程名称是在SPI服务初始化加载的时候加载的,然后确定premain代码,确实是先初始化服务,然后在初始化bytebuddy,如下图:
于是,我们将初始化服务的代码放在初始化bytebuddy之后,启动测试:
发现日志打印成功,说明我们的线程池字节码增强成功了,至于线程id在执行后打印出来,这是因为线程id是在线程里面打印的。
增强java.lang.String#valueOf
这个类到目前为止还没有办法增强,因为在premain执行之前就已经被加载了。是启动JVM必须要先加载的类。
4.7 本章小结
本章介绍了插件的设计思想, 对要增强的类,方法抽象成一个一个的插件进行单独管理,还将字节码增强逻辑封装成类似切面的设计,后续,我们会在切面方法里面进行数据采集,上报。
本章还重点讲解了bytebuddy的字节码增强系统,指明了Bootstrap类加载器加载的类和App ClassLoader加载器加载的类的不同实现方案。在介绍Bootstrap的实现方案时,还引出了类加载器的命名空间问题。
在测试增强JDK类库时,还指出了目前无法增强String等类的原因,就是这些类是JVM启动加载的必须类,在premian挂载之前就已经被加载了,无法在被重定义修改,这也是Agent的动态绑定限制特性。