如何脚踏实地构建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)}}
这里发生了什么事:
- 正如典型的Java代理一样,我们提供了pre-main方法。 这将在实际应用程序启动之前执行。 如果您想了解更多信息,ZeroTurnaround上有一篇很好的文章,提供了有关检测Java代理如何工作的更多信息。
- 我们发现所有类都是Servlet类的子类。 Spring的魔力最终也将出现在Servlet中。
- 我们找到一种名为“服务”的方法
- 我们拦截对该方法的调用,并将其委托给我们的自定义拦截器,该拦截器仅显示“ Hello,transformed world!”。 到ServletOutputStream。
- 最后,我们告诉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回调。
我们在这里所做的是将类加载潜入加载类的中间,这似乎是一项冒险的业务。 跳过故障排除过程,而该故障排除过程将自己撰写一篇博客文章,涉及很多本机挖掘工作,让我们仅概述第一个示例中发生的事情:
- 我们尝试加载一个类,例如launcher.LauncherHelper
- 为了打印出来,我们尝试加载io.PrintStream类,递归到相同的方法。 由于递归是通过JVM内部以及JVMTI和JNI进行的,因此在任何堆栈跟踪中都看不到它。
- 现在也必须打印出PrintStream。 但是还没有完全加载,所以我们收到一个JNI错误
- 现在,我们继续尝试继续打印。 要连接字符串,我们需要加载lang.StringBuilder。 重复同样的故事。
- 最后,由于类加载不多,我们得到了一个空指针异常。
好吧,那很复杂。 但是毕竟,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相关推荐
- java agent_如何脚踏实地构建Java Agent
java agent 在构建Plumbr的多年中,我们遇到了许多具有挑战性的问题. 在其他方面,使Plumbr Java Agent可靠地执行而不会危及客户的应用程序,是一个特别棘手的问题. 从实时系 ...
- idea git 过滤target_IDEA + maven 零基础构建 java agent 项目
Java Agent(java 探针)虽说在 jdk1.5 之后就有了,但是对于绝大多数的业务开发 javaer 来说,这个东西还是比较神奇和陌生的:虽说在实际的业务开发中,很少会涉及到 agent ...
- java agent 开发_IDEA + maven 零基础构建 java agent 项目
Java Agent(java 探针)虽说在 jdk1.5 之后就有了,但是对于绝大多数的业务开发 javaer 来说,这个东西还是比较神奇和陌生的:虽说在实际的业务开发中,很少会涉及到 agent ...
- 我的天,你工作5年了,连Java agent都不知道...
点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 # 引言 在本篇文章中,我会通过几个简单的程序来说明 agent ...
- .net byte转java byte_Java Web安全 || Java基础 Java Agent
点击上方"凌天实验室","星标或置顶公众号" 漏洞.技术还是其他,我都想第一时间和你分享 " [历史]已连载更新全部内容:[菜单栏]-[JAVA SE ...
- java agent简介热部署SDK接入
思考: 我们在平时使用arthas的类方法监控,类增强,到底是怎么在应用启动后还能对类进行修改的呢,他到底是基于什么场景下孕育出来的呢,今天我们就来聊一聊java-agent,当了解完了agent机制 ...
- WAP 构建 Java 应用 和 WAP经验总结
WAP 构建 Java 应用 充分利用现有的 EJB 使移动工作群体变得强大 文档选项 将此页作为电子邮件发送 未显示需要 JavaScript 的文档选项 级别: 初级 Aashish Patil ...
- Jenkins教程(2)使用Maven构建Java应用程序
本教程将向你展示如何使用Jenkins编排并构建一个使用Maven管理的简单Java应用程序. 如果你是使用Maven的Java开发人员,并且对CI/CD概念不熟悉,或者你可能熟悉这些概念,但不知道如 ...
- 基于Java Agent实现APM
一.APM概述 APM系统(Application Performance Management,即应用性能管理),用于对应用系统做实时监控,目的是实现对应用性能管理和故障定位. 1.1.为什么需要A ...
最新文章
- Partition List
- 阿里Druid数据库连接池配置解释
- 自动化测试8大元素定位之xpath语法
- key位置 win10生成的ssh_Windows 10:如何在OpenSSH中生成SSH密钥
- tomcat Connector 连接器
- windows 停止nginx
- python类takes no arguments_Python构造函数报错:TypeError: People() takes no arguments
- 文件读、写、追加的区别 Python
- 在.NET2.0中上传文件操作(解决了上传文件大小和多文件限制)--转
- MyApps接口引擎,打破跨系统间的壁垒
- 注塑机服务器显示e011,一种注塑机工业4.0联网系统以及监控方法
- 笔记本独显无输出_双显卡笔记本独显消失怎么回事|笔记本双显卡独显没了怎么解决|笔记本双显卡切换独显方法...
- (三)shp路网数据导入postgres中
- Ubuntu 16.04 和18.04 命令行配置802.1x无线网络连接方法(针对北邮校园网BUPT-mobile)
- 考虑储能削峰填谷的含DG配电网可靠性评估
- JAVAScript——JQuery—$ ( )
- gpg 的加密与解密
- 从程序员到asp.net架构师转变(转载)
- 用c语言实现cos x 函数
- 财务自由之路——我的投资史(2)