理解Java代理

135 篇文章 0 订阅
4 篇文章 5 订阅

假设您在生产中运行了一个应用程序。每隔一段时间,它就会进入崩溃状态,错误很难重现,您需要从应用程序中获得更多的信息。

你想知道解决方案吗?

您可以做的是将一些代码动态地附加到您的应用程序中,然后仔细地重写它,以便代码转储您可以记录的其他信息,或者您可以将应用程序阶段转储到一个文本文件中。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 观察输出。Hi I am main. 将被打印在控制台中。
然后,使用命令运行与java代理连接的应用程序。$ java -javaagent: -jar 观察输出。Logging using Agent 将额外打印在控制台中。这确保了java代理已被截获并添加到main方法。
在这里插入图片描述
总之,如果您想实现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代理是一项需要由一组专门的工程师来完成的任务。告诉我你是怎么上的!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值