范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

实现一个分布式调用链路追踪Java探针你可能会遇到的问题

  Java探针可以在Java应用运行时毫无感知的切入应用代码,是一种用于监听代码行为或改变代码行为的工具。
  分布式调用链路追踪的实现无非两种方式,代码侵入式和非代码侵入式,基于Java探针实现的属于非代码侵入式。
  运行在Java虚拟机上的编程语言所编写的代码,都有一种统一的中间格式:class文件格式。实现动态修改class字节码插入额外行为的代码,可实现非代码侵入式的应用调用行为收集。
  得益于Java SE 6提供的Instrumentation接口。基于Instrumentation可开发运行时修改class字节码的Java Agent应用(Java探针),可在类加载之前替换类的字节码、或在类加载之后通过重新加载类方式修改类的字节码。
  只是实现运行时修改class字节码还不足以称为"探针"。基于Instrumentation开发的Java Agent,只需要在Java应用启动命令上加上虚拟机参数"-javaagent"指定Java Agent应用jar包的位置,而不需要在工程项目中引入其jar包,即可将探针插入应用代码的各个角落。通过与应用使用不同的类加载实现环境隔离,让人有种Java Agent是吸附在应用上运行的错觉。
  Instrumentation之所以难驾驭,在于需要了解Java类加载机制以及字节码,一不小心就能遇到各种陌生的Exception。笔者在实现Java探针时就踩过不少坑,其中一类就是类加载相关的问题,也是本篇所要跟大家分享的。
  父类加载器加载的类不能引用子类加载器加载的类
  由父类加载器加载的类,不能引用子类加载器加载的类,否则会抛出NoClassDefFoundError。
  怎么理解这句话呢?这其实也是道面试题。
  JDK提供的java.*类都由启动类加载器加载。如果我们在java agent中修改java包下的类,插入调用logback打印日记的代码,结果会怎样?由于java agent包下的logback由AppClassLoader(应用类加载器,也称为系统类加载器)加载,而加载java包下的类的启动类加载器是AppClassLoader的父类加载器,在java包下的类中插入调用logback打印日记的代码,首先在加载java包下的类时,jvm会查看启动类加载器有没有加载过这个类,如果没有加载过尝试加载,但启动类加载器加载不了logback包的类,而启动类加载器不会向子类加载器去询问,任何类加载器都不会向子类加载器询问子类加载器是否能加载,即使子类加载器加载了这个类。所以就会出现NoClassDefFoundError。
  如果非要修改java包下的类,且非要在java包下的类中访问项目中我们编写的类或者第三方jar包提供的类、或者我们编写的javaagent包下的类,如何避免NoClassDefFoundError呢?
  笔者遇到这个问题网上找过很多资源,遗憾的是并未找到。于是笔者想起自己电脑上下载有Arthas的源码,不如学习下Arthas是如何解决的。
  Arthas是Alibaba开源的一款Java诊断工具,非常适合用于线上问题排查。
  参考Alibaba开源的Arthas的解决方案:
  用于接收埋点代码上报事件的类(Spy):
  public final class Spy {     public static void before(String className, String methodName, String descriptor, Object[] params) {     }      public static void complete(Object returnValueOrThrowable, String className, String methodName, String descriptor) {     } } before:方法执行之前上报;complete:方法return之前或者抛出异常之前上报,当方法抛出异常时,第一个参数为异常,否则第一个参数为返回值;
  将Spy放在一个独立的jar包下,在premain、agentmain方法中调用Instrumentation的appendToBootstrapClassLoaderSearch方法,将Spy类所在的jar包交由启动类加载器扫描加载,如下代码所示。
  // agent-spy.jar String agentSpyJar = jarPath[1]; File spyJarFile = new File(agentSpyJar); instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(spyJarFile));
  在Spy类中打印类加载器,如果打印的结果为null,则说明Spy类是由启动类加载器加载的。
  public final class Spy {     static {         System.out.println("Spy class loader is " + Spy.class.getClassLoader());     }     //....... }
  最后,给Spy注入上报方法,在Spy中通过反射调用上报方法,完整的Spy类的代码如下。
  public final class Spy {      public static Method beforMethod;     public static Method completeMethod;      public static void before(String className, String methodName, String descriptor, Object[] params) {         if (beforMethod != null) {             try {                 beforMethod.invoke(null, className, methodName, descriptor, params);             } catch (IllegalAccessException | InvocationTargetException e) {             }         }     }      public static void complete(Object returnValueOrThrowable, String className, String methodName, String descriptor) {         if (completeMethod != null) {             try {                 completeMethod.invoke(null, returnValueOrThrowable, className, methodName, descriptor);             } catch (IllegalAccessException | InvocationTargetException e) {             }         }     } }
  通过反射调用对性能会有所影响,特别是调用链路上每个方法都需要反射调用两个上报方法。
  可能不完全理解正确,但笔者试过这个方案确实可行。
  实现Agent与应用环境隔离
  为什么要实现隔离?
  隔离是避免Agent污染应用自身,使开发Java Agent无需考虑引入的jar包是否与目标应用引入的jar包冲突。
  Java Agent与Spring Boot应用相遇时会发生什么?
  Spring Boot应用打包后,将Agent附着到应用启动可能会抛出醒目的NoClassDefFoundError异常,这在IDEA中测试是不会发生的,而背后的原因是Agent与打包后的Spring Boot应用使用了不同的类加载器。
  我们可能会在Agent中调用被监控的SpringBoot应用的代码,也可能调用Agent依赖的第三方jar包的API,而这些jar包恰好在SpringBoot应用中也有导入,就可能会出现NoClassDefFoundError。
  Agent的jar包由AppClassLoader类加载器(系统类加载器)所加载。
  在IDEA中,项目的class文件和第三方库是通过AppClassLoader加载的,而使用-javaagent指定的jar也是通过AppClassLoader加载,所以在idea中测试不会遇到这个问题。
  SpringBoot应用打包后,JVM进程启动入口不再是我们写的main方法,而是SpringBoot生成的启动类。SpringBoot使用自定义的类加载器(LaunchedClassLoader)加载jar中的类和第三方jar包中的类,该类加载器的父类加载器为AppClassLoader。
  也就是说,SpringBoot应用打包后,加载javaagent包下的类使用的类加载器是SpringBoot使用的类加载器的父类加载器。
  如何实现隔离?
  让加载agent包不使用AppClassLoader加载器加载,而是使用自定义的类加载器加载。
  参考Alibaba开源的Arthas的实现,自定义URLClassLoader加载agent包以及agent依赖的第三方jar包。
  由于premain或者agentmain方法所在的类由jvm使用AppClassLoader所加载,所以必须将agent拆分为两个jar包。核心功能放在agent-core包下,premain或者agentmain方法所在的类放在agent-boot包下。在premain或者agentmain方法中使用自定义的URLClassLoader类加载器加载agent-core。
  第一步:
  自定义类加载器OnionClassLoader,继承URLClassLoader,如下代码所示:
  public class OnionClassLoader extends URLClassLoader {      public OnionClassLoader(URL[] urls) {         super(urls, ClassLoader.getSystemClassLoader().getParent());     }      @Override     protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {         final Class<?> loadedClass = findLoadedClass(name);         if (loadedClass != null) {             return loadedClass;         }         // 优先从parent(SystemClassLoader)里加载系统类,避免抛出ClassNotFoundException         if (name != null && (name.startsWith("sun.") || name.startsWith("java."))) {             return super.loadClass(name, resolve);         }         try {             Class<?> aClass = findClass(name);             if (resolve) {                 resolveClass(aClass);             }             return aClass;         } catch (Exception e) {             // ignore         }         return super.loadClass(name, resolve);     }  }
  同时在构造方法中指定OnionClassLoader的父类加载器为AppClassLoader的父类加载器。
  ClassLoader.getSystemClassLoader():获取系统类加载器(AppClassLoader)
  第二步:
  在premain或者agentmain方法中使用OnionClassLoader类加载器加载agent-core。
  // 1 File agentJarFile = new File(agentJar); final ClassLoader agentLoader = new OnionClassLoader(new URL[]{agentJarFile.toURI().toURL()}); // 2 Class<?> transFormer = agentLoader.loadClass("com.msyc.agent.core.OnionClassFileTransformer"); // 3 Constructor<?> constructor = transFormer.getConstructor(String.class); Object instance = constructor.newInstance(opsParams); // 4 instrumentation.addTransformer((ClassFileTransformer) instance);
  1、根据agent-core.jar所在绝对路径构造OnionClassLoader;2、加载agent-core.jar下的ClassFileTransformer;3、使用反射创建ClassFileTransformer实例;4、将ClassFileTransformer添加到Instrumentation;
  OnionClassFileTransformer类所依赖的agent-core包下的类,自然也会被使用OnionClassLoader类加载器加载,包括agent-core依赖的第三方jar包。
  适配webmvc框架
  生成分布式调用链日记的难点在于方法埋点和方法调用日记串连。
  分布式调用链日记串连的方式有多种,笔者采用的是最简单的方式:打点id+打点时间。
  对于同进程内的同线程,可用打点id将调用的方法串连起来,根据打点时间与一个累加器的值排序方法调用日记。
  对于不同进程,通过传递打点id可将不同应用的打点日记串连起来,根据打点时间排序。
  例如,适配webmvc框架的目的是从请求头获取调用来源传递过来的打点ID(事务ID)。对DispatcherServlet#doDispatch方法插桩,从HttpServletRequest参数获取请求头"S-Tid"。"S-Tid"是自定义的请求头参数,用于传递打点ID。
  笔者在实现适配webmvc和openfeign时都遇到了同样的问题,如在适配webmvc时,修改DispatcherServlet的doDispatch方法时,asm框架抛出java.lang.TypeNotPresentException。
  java.lang.TypeNotPresentException:当应用程序试图使用表示类型名称的字符串对类型进行访问,但无法找到带有指定名称的类型定义时,抛出该异常。
  其原因是,使用asm框架改写DispatcherServlet类时,asm会使用Class.forName方法加载符号引用的类,如果加载不到目标类则抛出TypeNotPresentException。
  默认asm会使用加载自身的类加载器去尝试加载当前改写类所依赖的一些类,而加载asm框架使用的类加载器与加载agent-core包使用的是同一个类加载器,DispatcherServlet则由SpringBoot的LaunchedClassLoader类加载器所加载。
  好在ClassFileTransformer#transform方法传递了用于加载当前类的类加载器:
  public class OnionClassFileTransformer implements ClassFileTransformer {     @Override     public byte[] transform(ClassLoader loader, String className,                             Class<?> classBeingRedefined,                             ProtectionDomain protectionDomain,                             byte[] classfileBuffer) {              // ......     } }
  如果当前需要改写的类是DispatcherServlet,则transform方法的第一个参数为即将用于加载DispatcherServlet类的类加载器;
  我们只需要指定asm使用ClassFileTransformer#transform方法传递进来的类加载器加载DispatcherServlet依赖的类即可。
  ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES) {        @Override        protected ClassLoader getClassLoader() {             return loader;        } };
  如代码所示,我们重写asm的ClassWriter类的getClassLoader方法,返回的类加载器是ClassFileTransformer#transform方法传递进来的类加载器。
  总结
  自实现Java探针需要牢记一句话:由父类加载器加载的类,不能引用子类加载器加载的类;
  通过自定义类加载加载agent,可实现agent与应用隔离,不让agent污染应用;
  通过ClassFileTransformer#transform改写类的字节码,记得重写asm框架的ClassWriter类的getClassLoader方法,使用ClassFileTransformer#transform传递进来的类加载器。

美运输机仓皇逃窜,武装直升机驱赶平民,3人空中掉落,1人更惨作者虹摄库尔斯克16日,阿富汗喀布尔机场出现了惨剧人寰的一幕!据阿富汗阿斯瓦卡通讯社报道,有人从一架从喀布尔机场仓皇逃窜的美军C17运输机上跌落,后确认在附近居民家中找到了3具尸体立陶宛有多作妖?小国兵弱,却屠杀犹太人,还跟着美国全世界作乱作者虹摄库尔斯克近日,波罗的海小国立陶宛引起了国内民众的广泛关注和愤慨。其不顾我方反复交涉晓以利害,宣布允许某岛在其设立代表处。而立陶宛的背后显然是美国拜登政府,立陶宛铁了心要死抱小心!美军岸舰导弹发射车首秀,无人驾驶机动部署,已击沉护卫舰作者虹摄库尔斯克最近美国海军陆战队有一则并不起眼的消息,但值得我们高度警惕。8月17日,美军宣布,美国海军陆战队参加夏威夷大部队2021(LSE)演习时,首次使用运输机和两栖气垫登卫星图来了!阿富汗空军集体出逃,战机一排排,会还给塔利班吗?作者虹摄库尔斯克近日,商业卫星照片拍到了逃往乌兹别克斯坦的阿富汗空军战机和直升机,大量飞机整齐地摆放在乌兹别克尔铁尔梅兹机场(Termez),这里几乎是一夜之间多了近50架飞机。据印度触手伸向阿富汗!送4架武装直升机,烂成这样,被塔利班缴获作者虹摄库尔斯克最近,阿富汗的战局急转直下,政府军遭到了塔利班的沉重打击,而随着美军的撤离,似乎政府军已经没有了继续战斗下去的信念。此前,印度还曾表示考虑支援阿富汗政府军武器装备,以为王者,其实青铜!印尼国产导弹艇下水被烧,时隔9年再造一艘作者虹摄库尔斯克据印度尼西亚媒体报道,2021年8月21日,印尼海军造船厂下水了一艘63米国产导弹艇。印度尼西亚海军参谋长尤多马尔戈诺出席了下水仪式,计划该艇将于今年10月开始测试塔利班横扫全境,阿富汗政府军46架战机仓皇出逃,撞下邻国战机作者虹摄库尔斯克据乌兹别克斯坦总检察院8月16日表示,8月15日,2架阿富汗空军EMB314超级巨嘴鸟轻型攻击机与1架乌兹别克斯坦空军的米格29战斗机在乌兹别克斯坦上空相撞,飞行员捡到宝!美军飞走,塔利班打开机库大门,赫然发现多架大型直升机作者虹摄库尔斯克美国总统拜登8月30日发表声明说,美国完成了从阿富汗的撤离任务。他表示,现在,我们在阿富汗20年的军事存在结束了。美国中央司令部司令肯尼思麦肯齐表示,美国东部时间8决策失误,印度亏死!80吨弹药到阿富汗,隔天就成塔利班战利品作者虹摄库尔斯克阿富汗战局的突变对于美军来说其实是早有预料,只是没有料到变化得那么快,没几天就丢掉了整个阿富汗,但对于本来就是在进行撤军的美军和美国既定的战略方针其实影响并不到。最俄军撤侨运输机,装4管尾炮,开火太壮观!还曾独创阿富汗降落法作者虹摄库尔斯克最近在阿富汗撤侨的运输机中,主要包括C5C17A400M等战略运输机,还有C130C160等中型战术运输机,这些飞机都是美国或者西方的,俄罗斯的机型并不多见。新的一哈里斯越南行,献花美国英雄!仔细一看太尴尬,美军飞行员在投降作者虹摄库尔斯克美国在阿富汗撤军中的作为让其国际信誉大打折扣,也让不少人看到了美国的丑恶嘴脸。最近,美国将目光从中亚的崇山峻岭转向了东南亚木棉花开之地,重返越南成为最近美国媒体高度
全球干旱加剧,保护环境迫在眉睫全球干旱加剧,一个国际科学家们组成的团队近期调查显示,世界河流总长度的一半以上每年至少有一天干涸,科学家们绘制了第一张断流世界地图,并分析间歇断流的多种后果。间歇性干旱影响的河流比研究困在格陵兰岛厚冰层下的水泡或能提供关键的水文见解据外媒报道,根据一项新研究,困在格陵兰冰盖厚厚的内部的水水泡可能为深入了解地球第二大冰体下面的水文网络以及气候变化可能如何破坏它的稳定提供关键的见解。每年,数千个天然融水湖在冰盖内专家视点马耀明加强多圈层立体观测与数值模拟应对青藏高原暖湿化中科院青藏高原研究所研究员马耀明青藏高原被称作地球第三极和亚洲水塔,是全球气候变化的敏感区域和天然实验室。近年来的研究发现,青藏高原地区暖湿化趋势明显。高原地区的地表温度和空气温度黑骑士卫星是真的存在吗,黑骑士卫星到底是什么在浩瀚的宇宙空间中,有很多神秘的卫星存在,早在1。3万年前黑骑士卫星就已在地球上空出现,那么黑骑士卫星是真的存在吗,这里神秘的黑骑士卫星到底是什么,下面一起来了解下。黑骑士卫星真的地球的模型地球仪1地球仪地球是一个巨大的球体,要观察它的全貌很难,于是,人们根据地球的形状,按一定比例将地球缩小,做成了地球模型,就是地球仪。借助地球仪,我们可以观察和认识地球。地球仪2经线纬线地材料的密度表观密度体积密度和堆积密度分别指的是什么材料的密度是指材料在绝对密实状态下单位体积的质量。表观密度是材料在包括闭口孔隙条件下单位体积的质量。体积密度是指材料在自然状态下的体积,包括材料实体及其开口与闭口孔隙条件下的单位体NASA还不确定毅力号是否获得了第一个岩石样本据外媒报道,NASA的毅力号(Perseverance)火星探测车通过钻探和采集岩石样本可能完成了其火星任务中的第一个重大任务,这些样本有朝一日可能会将其带回地球,不过这里面仍有一稳扎稳打,突破交会对接技术在成功实现载人航天飞行后,我国航天人按照载人航天第二步发展战略,继续朝着突破交会对接技术攀登。2015年10月成功发射了神舟六号载人飞船,实现多人多天多舱段在轨飞行。2008年9月太阳系到底有多大?可能和你想的不一样,太阳系半径在一光年左右大约在50亿年前,银河系中的一颗恒星死去了,没有人知道它的名字,也没有人知道它究竟是变成了黑洞,还是演变成了其他天体,我们只知道这个恒星的留下一团气体尘埃,这团气体尘埃的名字叫作太新的贝叶斯量子算法直接计算原子和分子的能量差左0和exp(iEt)1提供总能量E。紫色曲线箭头表示的及时阶段演化。右exp(ie0t)00和exp(ie1t)11之间的相位差直接提供能量差E1E0。蓝色和紫色的曲线箭头分别表9天上演绝地反击天舟二号两度推迟发射始末零距离9天上演绝地反击天舟二号两度推迟发射始末5月29日20时55分,搭载天舟二号货运飞船的长征七号遥三运载火箭在我国文昌航天发射场成功发射。郭文彬摄5月29日晚,长征七号遥三运载