如何脚踏实地构建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

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

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

相关文章

ASP.NET Core MVC 之依赖注入 Controller

ASP.NET Core MVC 控制器应通过构造函数明确地请求它们地依赖关系,在某些情况下,单个控制器地操作可能需要一个服务,在控制器级别上的请求可能没有意义。在这种情况下,也可以将服务作为 Action 的参数。 依赖注入是一种如 Depend…

16g内存 32g内存游戏区别_电脑内存8G和16G的差别大吗?打游戏需要多大内存?

Hello大家好,我是兼容机之家的小牛。电脑内存是除了CPU之外最重要的元件之一,电脑内存的多少直接影响着运行大型软件、多任务时电脑的流畅程度,很多玩家在第一次购买电脑的时候并不知道应该购买内存多大的电脑,今天小牛就来和大家…

用友U9执行JS代码。

UFSoft.UBF.UI.AtlasHelper.RegisterAtlasStartupScript(part.Page, part.Page.GetType(), "JavaScriptExecQueue", "alert(地球即将毁灭,进入倒计时:5,4,3,2,1…嘣!嘣&#xff…

java培训学费_北京Java培训班学费很贵吗,包含了哪些收费项目

北京的Java培训班有很多,价格却是相差不多的,但培训的课程就参差不齐了,有的培训班就是为了赚钱而存在的,想要系统的学习Java,确保学习效果,那么你一定要挑选正规的Java培训班,挑选适合自己的Ja…

typora.io使用教程

引言:对于开发人员大部分都接触过.md文件,而typora.io就是专门编辑.md文件的工具,该工具对于编写接口文档特别方便,它提供了word类似的大纲视图,同时也提供了很多的功能,但是改软件本身却非常的小&#xff…

c#编译时提高兼容性_幻像类型提高了编译时的安全性

c#编译时提高兼容性介绍 使用幻像类型是一种非常简单的技术,可用于提高代码的编译时安全性。 有很多潜在的用例,其复杂性程度各不相同,但是即使幻像类型的轻量级使用也可以显着提高编译时的安全性。 幻像类型只是带有未使用类型参数的参数化类…

如何查看电脑显卡配置_3080显卡电脑配置清单(3700X/10700)

3080显卡昨天就解禁评测了,因为最近的老铁都很期待装3080的机器,我这边做了一些整理,方便各位老铁3080配置有综合的了解。3080显卡售价更加详尽的3080厂家整理可以参考:3080公版哪里买?(所有厂家整理&#…

问题 1074: 数字整除

题目描述定理:把一个至少两位的正整数的个位数字去掉,再从余下的数中减去个位数的5倍。当且仅当差是17的倍数时,原数也是17的倍数 。 例如,34是17的倍数,因为3-20-17是17的倍数;201不是17的倍数&#xff0c…

GWT HTTP请求替代

由于多种原因 ,许多GWT用户放弃了RPC机制,这是GWT提供的调用后端的标准方法。 他们发现,在GWT RequestBuilder与其他可能不适合其应用程序模型的外部库之间迷失了许多 。 这篇文章的目的是通过GWT中众所周知的HTTP / Rest库,以使情…

极限中0除以常数_基本不等式中常用公式百度作业帮

1. 基本不等式中常用公式基本不等式中常用公式:(1)√((ab)/2)≥(ab)/2≥√ab≥2/(1/a1/b)。(当且仅当ab时,等号成立)(2)√(ab)≤(ab)/2。(当且仅当ab时,等号成立)(3)ab≥2ab。(当且仅当ab时,等号成立)(4)ab≤(ab)/4。(当且仅当ab时…

NOIP模拟测试24「star way to hevaen·lost my music」

star way to heaven 题解 大致尝试了一下并查集,记忆化搜索,最小生成树 最小生成树是正解,跑最小生成树然后找到最大的值 欧几里德距离最小生成树学习 prim楞跑 至于为什么跑最小生成树不是跑最大生成树,你跑最大生成树连的边可能会^%$&$%!# 感性理解手膜吧,我理解但说不清…

NOIP模拟测试25「字符串·乌鸦喝水·所陀门王的宝藏(陀螺王)」

字符串 题解 没看出catalan怎么办 dp打表啊! 考虑大力dp拿到30分好成绩!顺便收获一张表 打表发现$C_{nm}^{m}-C_{nm}^{m-1}$ 仔细观察然后发现其实就是之前的网格那个题 那么我们回顾一下网格那个题 先看最简单的nm情况 求左下角走到右上角方案数,不能经过中间那条线 考虑大力容…

stackexchange_通过Spring Social推特StackExchange –第1部分

stackexchange本文将介绍一个快速的附带项目-一个自动从各种Q&A StackExchange网站上发布热门问题的机器人,例如StackOverflow , ServerFault , SuperUser等。我们将为StackExchange API构建一个简单的客户端,然后进行…

bandizip最后一个无广告版本_【软件来了】这是个无广告的旧版知乎

近两天有新加入粉丝团队的小伙伴在询问G先生旧版App的事情,文章已发过很久,但是知乎的旧版本还是头一次分享给大家,所以G先生就找到了两个版本的旧版知乎,知乎V3.1和知乎V3.9这两个知乎都支持在最高的系统,没有广告也没…

NOIP模拟测试26「嚎叫响彻在贪婪的机房·主仆见证了 Hobo 的离别·征途堆积出友情的永恒」...

题目比较神仙,注意是题目神仙 贪婪暗示贪心,堆积暗示堆优化$\%\%\%\%\%\%\%$ 两个乱搞$$一个堆优化$dp$ 嚎叫响彻在贪婪的机房 题解 对于一个序列来说只要他们差的$gcd$不为$1$就可以构成等差数列 例如 $2$ $4$ $16$ $2$与$4$差$2$ $4$与$16$差$1…

渴望订阅– RxJava常见问题解答

在教学和指导RxJava以及撰写本书之后 ,我注意到某些领域尤其成问题。 我决定发布一些简短的提示,以解决最常见的陷阱。 这是第一部分。 Observable和Flowable本质上是惰性的。 这意味着无论您在Flowable放置了多么繁琐或长时间运行的逻辑,仅…

WPScan高级用法和定制

自定义扫描选项 WordPress作为一个广泛使用的内容管理系统(CMS),为用户提供了丰富的自定义选项,使其能够根据特定需求创建个性化的网站。其中一个重要的自定义选项是自定义扫描选项,它允许用户对WordPress网站进行深度…

r语言中正定矩阵由于误差不正定_R语言之数据处理(一)

在上一篇小文中,提到了关于R语言导入数据的一些方法,之后的重点就转向了数据的处理上。数据处理其实在整个数据分析项目中所占用的时间是比较多的,所以根据处理的目的不同,也有不同的处理方法。在R语言中,我通常会将数…

【chromium】 渲染显示相关概念

DRM(Direct Rendering Manager) DRM 由两个部分组成:一是 Kernel 的子系统,这个子系统对硬件 GPU 操作进行了一层框架封装。二是提供了一个 libdrm 库,里面封装了一系列 API,用来进行图像显示。 说到这里,其实这么理解…

ASP.NET Core MVC 之区域(Area)

区域(Area)是一个 ASP.NET MVC 功能,用于将相关功能组织为一个单独的命名空间(用于路由)和文件结构(用于视图)。使用区域通过向控制器和操作添加 一个路由参数(area)来创…