了解Java代理


假设您有一个正在生产中运行的应用程序。有时,它会进入损坏状态,很难重现该错误,并且您需要从应用程序中获取更多信息。

那么您是否想知道解决方案?

您可以做的是动态地将一些代码集附加到您的应用程序,然后仔细地重写它,以便代码转储您可以记录的其他信息,否则您可以将应用程序阶段转储到文本文件中。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),这意味着程序的字节码的结构和形状可以有很大的不同。

1-pcs603ss2v2b5y-lvyvooa.jpeg

实施Java代理 Java代理基于来自Java平台的工具,并且该工具的入口点是一个java.lang instrument软件包,该软件包提供允许代理检测运行在JVM上的程序的服务。该软件包非常简单且自包含,因为它包含几个异常类,一个数据类,类定义和两个接口。在这两个之中,classFileTransformer如果我们要编写Java代理,则仅需要实现接口。

有两种定义代理的方法。

第一个是静态代理,这意味着我们构建我们的代理时会将其打包为jar文件,并且在启动Java应用程序时,我们传入一个称为的特殊JVM参数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 —字符串参数,无论用户选择将什么作为参数传递给Java代理调用。
  • instrumentation来自java.lang工具包,我们可以添加一个新ClassFileTransformer对象,其中包含我们Agent的实际逻辑。 第二个选项称为动态代理。

您不需要编写启动应用程序的方式的方法,而是编写一小段代码,将其连接并连接到现有的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,社区规模和许可证。我在下面放置的演示是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

有三种方法可以将一些Java代码插入该方法中。insertAfter(..)在正文末尾插入字节码。它在正文末尾插入字节码。insertAt(..)在主体的指定行中insertBefore(..)插入字节码,并在主体的开头插入字节码。

动手使用Java Agent

  1. 从指出的链接下载 示例应用程序和Java Agent
  2. 使用进入路径构建两个仓库,并执行命令 mvn clean install
  3. 现在,您将在目标中获取jar文件。复制的路径.jar中的示例应用程序文件和复制的路径-dependencies.jar中JavaAgent文件。
  4. 首先,仅使用示例应用程序使用命令运行该应用程序,$ java -jar <path of the packaged jar>然后观察输出。Hi I am main. 将打印在控制台中。
  5. 然后,使用命令运行Java代理附带的应用程序,$ java -javaagent:<path of agent jar file> -jar <path of the packaged jar file you want to intercept>并观察输出。Logging using Agent 将在控制台中另外打印。这样可以确保已拦截Java代理并将其添加到main方法的主体中。 使用代理输出记录

1-ybp-qkuvfr9ztwrhumn8aq.png 总而言之,如果要实现Java代理,请执行以下操作:

  1. 您需要创建两个Java类。一个带有withpremain方法(JavaAgent),另一个带有扩展ClassFileTransformer(CustomTransformer)的类
  2. 在premain方法主体内部,您需要添加扩展对象的类的对象。ClassFileTransformer
  3. 然后,您需要transformCustomTransformer中的重写方法内添加逻辑。
  4. 在transform方法中转换字节码时,您可能需要根据目的使用字节码生成库。
  5. 您需要premain在清单中指定类并构建jar。
  6. 使用javaagent标记将您要拦截的应用程序加载到代理中。

我和Java代理 我正在为WSO2 Identity Server开发某种调试器,该调试器从服务器的Authentication流中获取重要的变量。正如我提到的,在一开始,不可能更改我们要截取的整个代码。因此,很容易将某些代码集动态附加到Server并仔细地重写它,以便代码激发可用于调试的其他信息。这种无需启动Java调试或任何代码操作即可进行调试的体系结构令我感到惊讶,因此我想到了一些有关此令人惊叹的工具的信息。

结论 在本文中,我们研究了Java开发人员工具带中功能极其强大的条目:Java代理。它具有访问加载到JVM中的类的能力。您可能想知道我们是否做了太多的工作却收效甚微。答案将是坚定的“否”。首先,您必须记住,此处阐述了Hello world示例,以解释Java代理的用法。Java代理可以完成的工作非常繁多,而当要重写的代码很复杂时,它们会派上用场。我只是从头开始了解Java代理可以实现的目标,但是希望阅读本文之后,您现在将了解它们的存在并可以进行进一步的研究。但是,对于持久性和适当的监视,构建可靠的Java代理是一项需要由专门的工程师团队解决的任务。


原文链接:http://codingdict.com