假设您在生产中运行了一个应用程序。每隔一段时间,它就会进入崩溃状态,错误很难重现,您需要从应用程序中获得更多的信息。
你想知道解决方案吗?
您可以做的是将一些代码动态地附加到您的应用程序中,然后仔细地重写它,以便代码转储您可以记录的其他信息,或者您可以将应用程序阶段转储到一个文本文件中。Java为我们提供了使用Java代理.
您有没有想过我们的Java代码是如何在IDE中进行热交换的?是因为探员。关于Java代理的另一个有趣的事实是,ApplicationProfile在后端使用了相同的技术来收集有关内存使用、内存泄漏和方法执行时间的信息。
那么什么是Java代理呢?
Java代理是一种特殊类型的类,通过使用Java仪器API,可以拦截在JVM上运行的应用程序,修改它们的字节码。Java代理非常强大,也很危险。
在深入研究之前,我将解释Java代理如何使用简单的HelloWorld示例拦截一个类.
public class Hello {
public static void main(String[] args){
System.out.println("hello world");
}
}
如下图所示,分类装载机负责将类从二进制加载到内存中。在运行已编译的HelloWorld应用程序(HelloWorld.class)时,可以将代理视为在运行时拦截类加载器行为的一种方法。您可能会想,如何重新构造java字节代码,以便代理可以在正确的位置添加相关的代码。有趣的是,因为Java程序,字节码的结构真的很接近原始的Java程序源代码。因此,虽然我们不对Java程序本身进行测试,但我们使用了非常接近的表示形式。需要注意的一点是,有一些非Java语言可以编译成Java字节码(例如Scala、Clojure和Kotlin),这意味着程序字节码的结构和形状可能非常不同。
实现Java代理
ava代理基于设施,来自Java平台,入口点是java.lang instrumentPackage,它提供允许代理检测JVM上运行的程序的服务。这个包非常简单,并且是独立的,因为它包含了几个异常类、一个数据类、类定义和两个接口。在这两种情况下,我们只需要实现classFileTransformer接口,如果我们想编写Java代理。
有两种方法可以定义代理。
第一个是静态剂,这意味着我们构建代理并将其打包为JAR文件,当我们启动Java应用程序时,我们传递一个名为javaagent。然后我们给出代理JAR在磁盘上的位置,然后JVM执行它的魔术。
$ java -javaagent:<path of agent jar file> -jar <path of the packaged jar file you want to intecept>
我们需要添加一个特殊的清单条目,称为pre-main类,当然,这是一个完全限定的名称类定义。
Premain-Class : org.example.JavaAgent
public class JavaAgent {
/**
* As soon as the JVM initializes, This method will be called.
*
* @param agentArgs The list of agent arguments
* @param instrumentation The instrumentation object
* @throws InstantiationException
*/
public static void premain(String agentArgs, Instrumentation instrumentation) throws InstantiationException {
InterceptingClassTransformer interceptingClassTransformer = new InterceptingClassTransformer();
interceptingClassTransformer.init();
instrumentation.addTransformer(interceptingClassTransformer);
}
}
premain 方法有两个参数:
agentArgs-String参数,无论用户选择将其作为参数传递给Java代理调用。
instrumentation来自java.lang工具包,我们可以添加一个新的ClassFileTransformer对象,它包含我们代理的实际逻辑。
第二个选项称为动态代理.
您可以做的不是检测启动应用程序的方式,而是编写一小部分代码,这些代码接受并连接到现有的JVM,并告诉它加载某个代理。
VirtualMachine vm = VirtualMachine.attach(vmPid);
vm.load(agentFilePath);
vm.detach();
这个论点agentFilePath与静态代理方法中的完全相同。它必须是代理JAR的文件名,所以没有输入流,没有字节。确实有两个注意事项用这种方法。第一种情况是,这是生活在COM Sun空间下的私有API,它通常适用于热点实现。第二个问题是,使用Java 9进行排序,您就不能再使用这段代码来附加到它正在运行的JVM。
类变换
为了转换类,我们需要为代理实现这个接口。
public interface ClassFileTransformer {
byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
}
这有点让人费解,但我将在方法签名中解释必要的参数。第一个重要的问题是className此参数的主要目的是帮助查找和区分要拦截的正确类和其他类。显然,您可能不想拦截应用程序中的每个类,最简单的方法就是使用条件语句进行检查。
然后ClassLoader它主要用于没有基本应用程序的平面类空间的环境中,您可能不需要查看它就可以逃脱,但是一旦遇到更复杂或模块化的平台,您就需要查看ClassLoader。classfileBuffer被检测之前类的当前定义。要拦截它,您需要使用库读取这个字节数组并拦截代码,然后必须再次转换回字节码才能返回。
有几个字节代码生成库。您需要进行研究,并根据它是高级API还是低级API、社区规模和许可证来决定。下面的演示是爪哇因为我认为它在高级和低级API之间有一个很好的平衡,而且它也是一个三重许可证,所以它应该可以供几乎任何人使用。因此,这就是实现ClassFileTransformer .
@Override
public byte[] transform(ClassLoader loader, ..)
throws .. {
byte[] byteCode = classfileBuffer;
// If you wanted to intercept all the classs then you can remove this conditional check.
if (className.equals("Example")) {
try {
ClassPool classPool = scopedClassPoolFactory.create(loader, rootPool,
ScopedClassPoolRepositoryImpl.getInstance());
CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
CtMethod[] methods = ctClass.getDeclaredMethods();
for (CtMethod method : methods) {
if (method.equals("main")) {
method.insertAfter("System.out.println(\"Logging using Agent\");");
}
}
byteCode = ctClass.toBytecode();
ctClass.detach();
} catch (Throwable ex) {
log.log(Level.SEVERE, "Error in transforming the class: " + className, ex);
}
}
return byteCode;
}
ctClass.detach();
} catch (Throwable ex) {
log.log(Level.SEVERE, "Error in transforming the class: " + className, ex);
}
}
return byteCode;
}
在这里,从classPool,我们可以直接让类绕过classfileBuffer因为我想用这个方法main。我们循环遍历类定义中的所有方法,得到我们想要的类。我们根本不必使用字节码。我们可以简单地传递一些合法的Java代码,然后爪哇将编译它生成新的字节码并给出定义。
有三种方法可以将一些Java代码插入到方法中。insertAfter(…)在正文的末尾插入字节码。它在身体的末端插入字节码。insertAt(…)在正文中的指定行插入字节码,并且insertBefore(…)在主体的开头插入字节码。
与Java代理打交道
下载样本应用和Java代理从链接中指出。
使用进入路径并执行命令来构建repomvn clean install
现在,您将在目标中获取JAR文件。复制.jar文件并复制-dependencies.jarJavaAgent中的文件。
首先,只使用示例应用程序使用以下命令运行应用程序$ java -jar
然后,使用命令运行与java代理连接的应用程序。$ java -javaagent:
总之,如果您想实现Java代理:
您需要创建两个Java类。带着premain方法(JavaAgent)和扩展ClassFileTransformer(海关变压器)
在身体内premain方法时,需要添加类的对象,该类扩展了ClassFileTransformer
然后,需要在重写的方法中添加逻辑。transform内部自定义变压器。
在转换方法中转换字节码时,可能需要根据您的目的使用字节码生成库。
您需要指定premain类并构建JAR。
使用javaagent标记来加载代理,并将您想要拦截的应用程序加载到该代理中。
Me和Java代理
我正在为WSO2Identity Server开发某种调试器,它从服务器获得身份验证流中的重要变量。正如我所提到的,在一开始,不可能更改我们想要拦截的全部代码。因此,动态地将一些代码附加到Server并仔细重写它是很容易的,这样代码就可以激发用于调试的附加信息。这个架构在不启动Java调试或任何代码操作的情况下进行调试,这让我感到惊讶,所以我想就这个神奇的工具发表一些看法。
结语
在这篇文章中,我们研究了Java开发人员工具箱中非常强大的条目:Java代理。它有权访问加载到JVM中的类。你可能会想,我们所做的一切工作是否做得太多而收效甚微呢?答案将是坚定的“不”。首先,您必须记住,本文详细介绍了HelloWorld示例,以解释Java代理的使用。使用java代理可以完成的事情是巨大的,当需要重写复杂的代码时,它们就会派上用场。我只是触及了Java代理所能实现的目标的表面,但希望在阅读完这篇文章之后,您将了解它们的存在,并进一步研究它们的存在。然而,对于持久性和适当的监控,构建一个可靠的java代理是一项需要由一组专门的工程师来完成的任务。告诉我你是怎么上的!