java agent_如何脚踏实地构建Java Agent

java agent

在构建Plumbr的多年中,我们遇到了许多具有挑战性的问题。 在其他方面,使Plumbr Java Agent可靠地执行而不会危及客户的应用程序,是一个特别棘手的问题。 从实时系统中安全地收集所有需要的遥测会带来很多问题。 其中一些非常简单,而另一些则非常不明显。

在此博客文章中,我们想与您分享一些示例,这些示例演示了在为我们的探员需要处理的一些看似简单的方面提供支持时遇到的复杂性。 这些示例进行了简化,但摘录自我们前一段时间需要解决的现实问题。 实际上,这些只是等待尝试使用字节码工具或JVMTI的人的冰山一角。

示例1:检测一个简单的Web应用程序

让我们从一个非常简单的hello world网络应用开始 :

@Controller
public class HelloWorldController {@RequestMapping("/hello")@ResponseBodyString hello() {return "Hello, world!";}
}

如果启动应用程序并访问相关的控制器,则会看到以下内容:

$ curl localhost:8080/hello
Hello, world!

作为一个简单的练习,让我们将返回值更改为“ Hello,transformed world”。 自然,我们真正的Java代理不会对您的应用程序执行此类操作:我们的目标是在不更改观察到的行为的情况下进行监视。 但是为了使这个演示简短而简洁,请与我们联系。 要更改返回的响应,我们将使用ByteBuddy :

public class ServletAgent {public static void premain(String arguments, Instrumentation instrumentation) { // (1)new AgentBuilder.Default().type(isSubTypeOf(Servlet.class)) // (2).transform((/* … */) ->builder.method(named("service")) // (3).intercept(MethodDelegation.to(Interceptor.class) // (4))).installOn(instrumentation); // (5)}}

这里发生了什么事:

  1. 与Java代理一样,我们提供了一个pre-main方法。 这将在实际应用程序启动之前执行。 如果您想了解更多信息,ZeroTurnaround上有一篇很好的文章,提供了有关检测Java代理如何工作的更多信息。
  2. 我们发现所有类都是Servlet类的子类。 Spring的魔力最终也将融入Servlet。
  3. 我们找到一种名为“服务”的方法
  4. 我们拦截对该方法的调用,并将其委托给我们的自定义拦截器,该拦截器仅显示“ Hello,transformed world!” 到ServletOutputStream。
  5. 最后,我们告诉ByteBuddy根据上面的规则来检测加载到JVM中的类

las,如果我们尝试运行此命令,则应用程序将不再启动,并引发以下错误:

java.lang.NoSuchMethodError: javax.servlet.ServletContext.getVirtualServerName()Ljava/lang/String;at org.apache.catalina.authenticator.AuthenticatorBase.startInternal(AuthenticatorBase.java:1137)at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)

发生了什么? 我们只触摸了“ Servlet”类上的“ service”方法,但是现在JVM无法在另一个类上找到另一个方法。 腥。 让我们尝试看看在两种情况下该类的加载位置。 为此,我们可以将-XX:+ TraceClassLoading参数添加到JVM启动脚本中。 如果没有Java代理,则从Tomcat加载有问题的类:

[Loaded javax.servlet.ServletContext from jar:file:app.jar!/BOOT-INF/lib/tomcat-embed-core-8.5.11.jar!/]

但是,如果再次启用Java代理,则会从其他位置加载它:

[Loaded javax.servlet.ServletContext from file:agent.jar]

啊哈! 实际上,我们的代理直接依赖于Gradle构建脚本中定义的servlet API:

agentCompile "javax.servlet:servlet-api:2.5"

可悲的是,此版本与Tomcat期望的版本不匹配,因此出现错误。 我们用这种依赖性指定哪些类仪器:isSubTypeOf(Servlet ),但是这也造成了我们加载的servlet库的不兼容版本。 要摆脱这种情况实际上并不那么容易:要检查我们尝试检测的类是否是另一种类型的子类型,我们必须知道其所有父类或接口。

尽管有关直接父代的信息存在于字节码中,但传递继承却不存在。 实际上,在进行检测时,相关的类甚至可能尚未加载。 要解决此问题,我们必须在运行时找出客户端应用程序的整个类层次结构。 有效地收集类层次结构是一项艰巨的任务,它本身就有很多陷阱,但是这里的教训很明显:规范不应加载客户端应用程序可能也要加载的类,尤其是来自不兼容版本的类。

这只是一条小小的龙,当您尝试使用字节码或尝试与类加载器混为一谈时,它已远离军团等待着您。 我们已经看到了许多其他问题:类装入死锁,验证程序错误,多个代理之间的冲突,本机JVM结构膨胀,就这样吧!

但是,我们的代理并不限于使用Instrumentation API。 要实现某些功能,我们必须更深入。

示例2:使用JVMTI收集有关类的信息

可以采用多种不同方法来确定类型层次结构,但是在本文中,我们仅关注其中之一-JVMTI (JVM工具接口)。 它使我们能够编写一些本机代码,以访问JVM的更底层的遥测和工具功能。 除其他外,可以为应用程序或JVM本身中发生的各种事件订阅JVMTI回调。 我们当前感兴趣的是ClassLoad回调。 这是一个如何使用它来订阅类加载事件的示例 :

static void register_class_loading_callback(jvmtiEnv* jvmti) {jvmtiEventCallbacks callbacks;jvmtiError error;memset(&callbacks, 0, sizeof(jvmtiEventCallbacks));callbacks.ClassLoad = on_class_loaded;(*jvmti)->SetEventCallbacks(jvmti, &callbacks, sizeof(callbacks));(*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE, JVMTI_EVENT_CLASS_LOAD, (jthread)NULL);
}

这将使JVM在类加载的早期阶段执行我们定义的on_class_loaded函数。 然后,我们可以编写此函数,以便它通过JNI调用代理的java方法,如下所示:

void JNICALL on_class_loaded(jvmtiEnv *jvmti, JNIEnv* jni, jthread thread, jclass klass) {(*jni)->CallVoidMethod(jni, agent_in_java, on_class_loaded_method, klass);
}

为了简单起见,在Java Agent中,我们将只打印类的名称:

public static void onClassLoaded(Class clazz) {System.out.println("Hello, " + clazz);
}

闭上你的眼睛一分钟,尝试想象这里可能出什么问题。

你们中许多人可能认为这将崩溃。 毕竟,您在本机代码中犯的每个错误都有可能通过段错误使整个应用程序崩溃。 但是,在这个特定示例中,我们实际上将获得一些JNI错误和一个Java异常:

Error: A JNI error has occurred, please check your installation and try again
Error: A JNI error has occurred, please check your installation and try again
Hello, class java.lang.Throwable$PrintStreamOrWriter
Hello, class java.lang.Throwable$WrappedPrintStream
Hello, class java.util.IdentityHashMap
Hello, class java.util.IdentityHashMap$KeySet
Exception in thread "main" java.lang.NullPointerExceptionAt JvmtiAgent.onClassLoaded(JvmtiAgent.java:23)

让我们暂时将JNI错误放在一边,然后集中讨论Java异常。 真令人惊讶 在这里什么可以为空? 选项不多,所以让我们检查一下并再次运行:

public static void onClassLoaded(Class clazz) {if(System.out == null) {throw new AssertionError("System.out is null");}if(clazz == null) {throw new AssertionError("clazz is null");}System.out.println("Hello, " + clazz);
}

但是,a,我们仍然会遇到相同的异常:

Exception in thread "main" java.lang.NullPointerExceptionAt JvmtiAgent.onClassLoaded(JvmtiAgent.java:31)

让我们稍等一下,然后对代码进行另一个简单的更改:

public static void onClassLoaded(Class clazz) {System.out.println("Hello, " + clazz.getSimpleName());
}

输出格式的这种看似微不足道的变化导致了行为上的巨大变化:

Error: A JNI error has occurred, please check your installation and try again
Error: A JNI error has occurred, please check your installation and try again
Hello, WrappedPrintWriter
Hello, ClassCircularityError
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  Internal Error (systemDictionary.cpp:806), pid=82384, tid=0x0000000000001c03
#  guarantee((!class_loader.is_null())) failed: dup definition for bootstrap loader?

啊,终于崩溃了! 真高兴! 实际上,这为我们提供了很多信息,有助于查明根本原因。 具体来说,现在明显的ClassCircularityError和内部错误消息非常明显。 如果要查看JVM源代码的相关部分,您会发现一个非常复杂且混合在一起的算法,用于解析类。 它确实可以单独运行,但仍然很脆弱,但是通过执行一些不寻常的操作(如覆盖ClassLoader.loadClass或抛出一些JVMTI回调)很容易被破坏。

我们在这里所做的是将类加载潜入加载类的中间,这似乎是一项冒险的业务。 跳过故障排除过程,而该故障排除过程将自己撰写一篇博客文章,涉及很多本机挖掘工作,让我们仅概述第一个示例中发生的事情:

  1. 我们尝试加载一个类,例如launcher.LauncherHelper
  2. 为了打印出来,我们尝试加载io.PrintStream类,递归到相同的方法。 由于递归是通过JVM内部以及JVMTI和JNI进行的,因此在任何堆栈跟踪中都看不到它。
  3. 现在也必须打印出PrintStream。 但是还没有完全加载,所以我们收到一个JNI错误
  4. 现在,我们继续尝试继续打印。 要连接字符串,我们需要加载lang.StringBuilder。 重复同样的故事。
  5. 最后,由于类加载不多,我们得到了一个空指针异常。

好吧,那很复杂。 但是毕竟,JVMTI文档非常明确地表示我们应该格外小心:

“此事件是在加载课程的早期阶段发送的。 因此,该类应谨慎使用。 请注意,例如,方法和字段尚未加载,因此对方法,字段,子类等的查询不会给出正确的结果。 请参见Java语言规范中的“类和接口的加载”。 对于大多数目的, ClassPrepare 事件将更加有用。”

确实,如果我们使用此回调,那么就不会有这样的困难。 但是,在设计用于监视目的的Java代理时,有时会被迫进入JVM的非常暗的区域以支持我们所需的产品功能,而开销却足以降低生产部署的成本。

带走

这些示例说明了一些看似无辜的设置和构建Java代理的幼稚方法如何以令人惊讶的方式让您大吃一惊。 实际上,以上内容几乎不涉及我们多年来发现的内容。

再加上数量众多的不同平台,此类代理将需要完美运行(不同的JVM供应商,不同的Java版本,不同的操作系统),并且本来就很复杂的任务变得更具挑战性。

但是,通过尽职调查和适当的监视,构建可靠的Java代理是一项可以由一组敬业工程师解决的任务。 我们在自己的产品中自信地运行Plumbr Agent,并且不会因此而睡不着。

翻译自: https://www.javacodegeeks.com/2017/06/shoot-foot-building-java-agent.html

java agent

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/335119.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python中链表和数组_数据结构笔记(一):数组、链表|python基础教程|python入门|python教程...

https://www.xin3721.com/eschool/pythonxin3721/(一)数组数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。1、数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。通过 a[i]_address a[0]_address i*元…

旋转散点图_聚类分析的结果如何用散点图展示出来?

SPSS系统聚类输出的树状图广受用户喜爱,二阶聚类也可以输出一系列美观的可视化图形用来观察聚类效果,但我们发现Kmeans均值聚类没有提供可视化程度高的图形,那怎么办,我们自己来制作。数据小兵推荐使用3D散点图全方位观察K均值聚类…

async 打包异常_重新打包流中的异常

async 打包异常Java 8已有两年历史,但是仍然存在社区尚未为其开发好的解决方案库的用例,甚至边缘用例。 如何处理流管道中的检查异常就是这样一个问题。 Stream操作接受的功能接口不允许实现抛出已检查的异常,但是我们可能要调用许多方法。 显…

补码基础

关于补码,有如下比较有趣的演化过程: 假如计算机中使用 4 位的二进制表示数据,如图-2,最多能表示 0 到 15(10 进制),之后有牛人做了 一个细微改动,如图-3,将所有二进制以 1 开头的数(大于 7 的数)放到 0 之…

身份验证错误错误指定的句柄无效_基于 Web 端的人脸识别身份验证「实践」

作者:沫沫 政采云前端团队转发链接:https://mp.weixin.qq.com/s/fRDpXixnLIy9c0Uh2tMezQ前言近些年来,随着生物识别技术的逐渐成熟,基于深度学习的人脸识别技术取得了突破性进展,准确率显著提高。现阶段,人…

打开 谷歌浏览器exe_专治各种网银不服:两步开启微软Edge浏览器IE兼容模式

此前微软已经预告过Microsoft Edge将支持IE模式,即可以在该浏览器下使用IE模式加载某些特定的需要的网站。不过在后续更新中微软又突然改口不再面向普通用户提供此功能,而企业级用户若要使用还需要管理员提前配置。现在这款浏览器的正式版已经发布&#…

java单例枚举_Java增强枚举的用例

java单例枚举Brian Goetz在消息“ 增强枚举-用例 ”中写道:“我们希望就现在实现的功能[ 增强枚举 ]获得用户反馈。” 他陈述了他的消息的第一个目的:“开始工作,这是一些通用枚举可能有用的典型用例。” 所提供的两个示例中的第一个示例是重…

爱python网_Python

一、PIL库简介1.安装PIL库PIL库是Python的第三方库,需要手动通过pip工具安装。可通过cmd命令:pip install pillow 进行安装。(注意:出现pip不是内部处理命令时需要配置一下环境)。2.PIL库的功能PIL库支持图像存储、显示和处理,它…

python计算互信息_互信息公式及概述

在概率论和信息论中,两个随机变量的互信息(Mutual Information,简称MI)或转移信息(transinformation)是变量间相互依赖性的量度。不同于相关系数,互信息并不局限于实值随机变量,它更加一般且决定着联合分布 p(X,Y) 和分解的边缘分…

队列和消息队列_消息队列概述[幻灯片]

队列和消息队列昨天,我进行了一次演讲,探讨了使用消息队列的所有方面。 我以前曾写过“您可能不需要消息队列” –现在的结论有些细微差别,但我仍然坚持简单性的观点。 演讲探讨了使用消息队列的各种好处和用例,并讨论了典型“消…

反码、原码、补码的观点阐述

我自己是不认可所谓的原码、反码的说法,但是很多人在计算负数的二进制时,都会使用反码和原码的概念,我这里就简单梳理下吧。 十进制2的二进制数(按8位的二进制举例)如下: 原码:0000 0010 补码&…

aspose word 获取标题_Word干货|多级标题的自动编号怎么添加?

在对Word文档进行排版时,大家普遍认为的一个难点就是对多级标题添加自动编号,本期Word妹与大家分享相关技巧的使用。1、添加样式选中文本,点击开始——样式——选择标题1,相同的样式则可以借用F4来实现。PS:以同样方式…

python使用elasticsearch_python中使用ElasticSearch(二)

一、数据库和elasticsearch的对比分析二、kibana中常用的命令。1.新建数据。2.查询上一步中存进去的数据。3.put修改字段(这个操作要小心,容易误操作,清楚其他的字段)4.post修改字段5.查询的基本操作。(1)全部查询(类似于select * from fruit)(2)有条件的…

vue.jsr入门_JSR 365更新:深入CDI 2.0

vue.jsr入门上下文和依赖注入2.0( JSR 365 )是CDI 1.2的更新,CDI 1.2目前是Java EE 7平台的一部分。 目前处于公开审查阶段 。 对于不熟悉CDI的那些人,它定义了一组功能强大的免费服务,这些服务可充当凝胶,…

Java集合类梳理

文章目录集合框架CollectionListList常用方法ArrayListArrayList常用方法LinkedListLinkedList常用方法VectorVector 常用方法StackStack 常用方法SetHashSetHashSet 常用方法LinkedHashSetLinkedHashSet 常用方法TreeSetTreeSet常用方法EnumSetEnumSet 常用方法MapHashMapHash…

增加第三方插件_AE插件排行!!

大家好是万能的懒懒酱After effects为视觉效果艺术家和动画设计师带来了大量的效果。然而,第三方开发人员提供了更多独特插件,供After Effects使用。在这里可以帮助你了解哪些插件是最流行的最受欢迎的。第10名:Looks(多功能调色插…

mysql 连续签到天数_签到功能实现,没有你想的那么复杂(一)

1 签到定义以及作用签到,指在规定的簿册上签名或写一“到”字,表示本人已经到达。在APP中使用此功能,可以增加用户粘性和活跃度.2 技术选型redis为主写入查询,mysql辅助查询. 传统签到多数都是直接采用mysql为存储DB,在大数据的情况下数据库的压力较大.查…

java包装项目_项目包装组织

java包装项目程序包是Java的基本概念,是您开始用该语言编程时偶然发现的第一件事。 作为一个初学者,您可能不太关注软件包的结构,但是随着您成为经验丰富且成熟的软件开发人员,您开始考虑可以采取哪些措施来提高其效率。 有几个主…

如何开发 Servlet 程序

文章目录如何开发 Servlet步骤 1:写一个类步骤 2:编译步骤 3:打包步骤 4:部署步骤 5:启动服务器步骤 6:访问 servletServlet 开发示例不使用 IDE 开发(手动编译和部署)步骤 1&#x…

报任安书文言现象_语文老师精心总结【文言文常考点】够你从初一用到初四!...

点击本号菜单栏 免费获取学习资料▼今天给大家整理了初中文言文的一些常用知识点:特殊句式和古今异义,这些只是文言文学习模块中的一部分,除此之外,其他大家需要在平时积累的文言文知识点有下面这些:文言文高频词、古代…