假设您有一个正在生产中运行的应用程序。有时,它会进入损坏状态,很难重现该错误,并且您需要从应用程序中获取更多信息。
那么您是否想知道解决方案?
您可以做的是动态地将一些代码集附加到您的应用程序,然后仔细地重写它,以便代码转储您可以记录的其他信息,否则您可以将应用程序阶段转储到文本文件中。Java为我们提供了使用Java Agent进行此操作的便利。
您是否曾经想过如何在我们的IDE中热交换我们的Java代码?这是因为代理商。关于Java代理的另一个有趣的事实是,应用程序探查器在后端使用相同的技术来收集有关内存使用,内存泄漏和方法执行时间的信息。 那么什么是Java代理?
Java代理是一种特殊的类,通过使用Java Instrumentation API,它可以拦截在JVM上运行的应用程序,从而修改其字节码。Java代理功能非常强大,也很危险。
在深入探讨之前,我将使用简单的HelloWorld示例说明Java Agent如何拦截类。
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代理 Java代理基于来自Java平台的工具,并且该工具的入口点是一个java.lang instrument软件包,该软件包提供允许代理检测运行在JVM上的程序的服务。该软件包非常简单且自包含,因为它包含几个异常类,一个数据类,类定义和两个接口。在这两个之中,classFileTransformer如果我们要编写Java代理,则仅需要实现接口。
有两种定义代理的方法。
第一个是静态代理,这意味着我们构建我们的代理时会将其打包为jar文件,并且在启动Java应用程序时,我们传入一个称为的特殊JVM参数javaagent。然后,我们为其指定代理jar在磁盘上的位置,然后JVM发挥作用。
javaagent
$ 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方法有两个参数:
premain
agentArgs
instrumentation
ClassFileTransformer
您不需要编写启动应用程序的方式的方法,而是编写一小段代码,将其连接并连接到现有的JVM,并告诉它加载特定的代理。
VirtualMachine vm = VirtualMachine.attach(vmPid); vm.load(agentFilePath); vm.detach();
此参数agentFilePath与静态代理方法中的参数完全相同。它必须是代理jar的文件名,因此没有输入流没有字节。这种方法有两个警告 。第一个是这是生活在com sun空间下的私有API,通常适用于热点实现。第二个问题是,使用Java 9进行排序时,您将无法再使用此代码将其附加到正在运行的JVM。
agentFilePath
类转换 这是我们需要为代理实现的接口,以转换类。
public interface ClassFileTransformer { byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException; }
有点说不过去,但是我将在方法签名中解释必要的参数。第一个重要的是className此参数的主要目的是帮助查找和区分您要拦截的正确类和其他类。显然,您可能不想拦截应用程序中的每个类,而最简单的方法是使用条件语句进行检查。
然后ClassLoader,它通常用于基本应用程序没有平面类空间的环境中,您可能无需看就可以逃脱,但是一旦遇到更复杂的东西或模块化平台,就需要查看ClassLoader。classfileBuffer是在进行检测之前该类的当前定义。要拦截它,您需要使用库读取此字节数组并拦截您的代码,然后必须再次转换回字节码以返回。
有几个字节代码生成库。您需要进行研究并自行决定是高级API还是低级API,社区规模和许可证。我在下面放置的演示是Javassist,因为我认为它在高级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; }
在这里,由于我想使用method classPool,因此可以直接通过绕过该类。我们遍历类定义中的所有方法,并获得所需的类。我们根本不需要使用字节码。我们可以简单地向它传递一些合法的Java代码,然后Javassist将对其进行编译以生成新的字节码并提供给我们该定义。 classfileBuffermain
method classPool
classfileBuffermain
有三种方法可以将一些Java代码插入该方法中。insertAfter(..)在正文末尾插入字节码。它在正文末尾插入字节码。insertAt(..)在主体的指定行中insertBefore(..)插入字节码,并在主体的开头插入字节码。
insertAfter(..)
insertBefore(..)
动手使用Java Agent
Java Agent
mvn clean install
-dependencies.jar
$ java -jar <path of the packaged jar>
$ java -javaagent:<path of agent jar file> -jar <path of the packaged jar file you want to intercept>
Logging using Agent
总而言之,如果要实现Java代理,请执行以下操作:
ClassFileTransformer(CustomTransformer)
transform
CustomTransformer
我和Java代理 我正在为WSO2 Identity Server开发某种调试器,该调试器从服务器的Authentication流中获取重要的变量。正如我提到的,在一开始,不可能更改我们要截取的整个代码。因此,很容易将某些代码集动态附加到Server并仔细地重写它,以便代码激发可用于调试的其他信息。这种无需启动Java调试或任何代码操作即可进行调试的体系结构令我感到惊讶,因此我想到了一些有关此令人惊叹的工具的信息。
结论 在本文中,我们研究了Java开发人员工具带中功能极其强大的条目:Java代理。它具有访问加载到JVM中的类的能力。您可能想知道我们是否做了太多的工作却收效甚微。答案将是坚定的“否”。首先,您必须记住,此处阐述了Hello world示例,以解释Java代理的用法。Java代理可以完成的工作非常繁多,而当要重写的代码很复杂时,它们会派上用场。我只是从头开始了解Java代理可以实现的目标,但是希望阅读本文之后,您现在将了解它们的存在并可以进行进一步的研究。但是,对于持久性和适当的监视,构建可靠的Java代理是一项需要由专门的工程师团队解决的任务。
原文链接:http://codingdict.com