Java代理

本文是我们名为“ 高级Java ”的学院课程的一部分。

本课程旨在帮助您最有效地使用Java。 它讨论了高级主题,包括对象创建,并发,序列化,反射等。 它将指导您完成Java掌握的过程! 在这里查看 !

目录

1.简介
2. Java代理基础
3. Java代理和规范
4.编写您的第一个Java代理
5.运行Java代理
6.接下来
7.下载源代码

1.简介

在本教程的最后一部分中,我们将讨论Java代理,这对于在那里的常规Java开发人员来说是真正的魔咒。 Java代理能够通过直接修改字节码来“侵入”运行时在JVM上运行的Java应用程序的执行。 Java代理的功能和危险一样强大:它们几乎可以执行所有操作,但是如果出现问题,它们很容易使JVM崩溃。

本部分的目的是通过解释Java代理如何工作,如何运行它们以及展示一些Java代理具有明显优势的简单示例来使Java代理神秘化。

2. Java代理基础

本质上,Java代理是遵循一组严格约定的常规Java类。 代理类必须实现一个public static void premain(String agentArgs, Instrumentation inst)方法,该方法成为代理的入口点(类似于常规Java应用程序的main方法)。

初始化Java虚拟机(JVM)后, premain(String agentArgs, Instrumentation inst)在JVM启动时指定代理的顺序调用每个代理的每个此类premain(String agentArgs, Instrumentation inst)方法。 完成此初始化步骤后,将调用实际的Java应用程序main方法。

但是,如果该类未实现public static void premain(String agentArgs, Instrumentation inst)方法,则JVM将尝试查找并调用另一个重载版本的public static void premain(String agentArgs) 请注意,每个premain方法必须返回才能启动阶段。

最后但并非最不重要的一点是,Java代理类还可以具有在JVM启动后启动代理时使用的public static void agentmain(String agentArgs, Instrumentation inst)public static void agentmain(String agentArgs)方法。

乍看之下看起来很简单,但Java代理实现还应提供其他一些内容作为其包装的一部分:清单。 清单文件通常位于META-INF文件夹中,名为MANIFEST.MF ,包含与包分发有关的各种元数据。

我们在本教程中并未讨论清单,因为大多数时候它们都不是必需的,但是Java代理不是这种情况。 为打包为Java归档(或简称JAR)文件的Java代理定义了以下属性:

清单属性 描述
初级班 在JVM启动时指定了代理时,此属性定义Java代理类:包含premain方法的类。 在JVM启动时指定了代理时,此属性是必需的。 如果该属性不存在,JVM将中止。
特工级 如果实现支持在JVM启动后的某个时间启动Java代理的机制,则此属性指定代理类:包含agentmain方法的类。 此属性是必需的,如果不存在该代理,则不会启动代理。
引导类路径 引导类加载器要搜索的路径列表。 路径代表目录或库。
可以重新定义类 truefalse值,不区分大小写,并且定义是否具有重新定义此代理所需的类的能力。 此属性是可选的,默认值为false
可以重新转换类 truefalse值,不区分大小写,并且定义是否具有重新转换此代理所需的类的能力。 此属性是可选的,默认值为false
可以设置本机方法前缀 truefalse值,不区分大小写,并且定义是否可以设置此代理所需的本机方法前缀。 此属性是可选的,默认值为false

有关更多详细信息,请随时查阅专用于Java代理和工具的官方文档 。

3. Java代理和规范

Java代理的检测功能确实是无限的。 最引人注意的内容包括但不限于:

  • 能够在运行时重新定义类。 重新定义可能会更改方法主体,常量池和属性。 重新定义不得添加,删除或重命名字段或方法,更改方法的签名或更改继承。
  • 能够在运行时重新转换类。 重新转换可能会更改方法主体,常量池和属性。 重新转换不得添加,删除或重命名字段或方法,更改方法的签名或更改继承。
  • 通过允许重命名使用前缀来修改本机方法解析的失败处理的能力。

请注意,在应用转换或重新定义之后,不会检查,验证和安装重新转换或重新定义的类字节码。 如果生成的字节码错误或不正确,则将引发异常,并可能使JVM完全崩溃。

4.编写您的第一个Java代理

在本节中,我们将通过实现我们自己的类转换器来编写一个简单的Java代理。 话虽如此,使用Java代理的唯一缺点是,为了完成或多或少的有用转换,需要直接字节码操作技能。 而且,不幸的是,Java标准库没有提供任何API(至少是文档化的API)来使这些字节码操作成为可能。

为了填补这一空白,富有创造力的Java社区提出了一些优秀的,非常成熟的库,例如Javassist和ASM ,仅举几例。 在这两种方法中,Javassist使用起来更简单,这就是为什么它成为我们将要用作字节码操作解决方案的原因。 到目前为止,这是我们第一次无法在Java标准库中找到合适的API,除了使用社区提供的API之外,别无选择。

我们将要处理的示例非常简单,但它取材于实际的用例。 假设我们要捕获Java应用程序打开的每个HTTP连接的URL。 有很多方法可以通过直接修改Java源代码来做到这一点,但是让我们假设由于许可证策略或其他原因,源代码不可用。 打开HTTP连接的类的典型示例如下所示:

public class SampleClass {
    public static void main( String[] args ) throws IOException {
        fetch("http://www.google.com");
        fetch("http://www.yahoo.com");
    }

    private static void fetch(final String address) 
            throws MalformedURLException, IOException {

        final URL url = new URL(address);                
        final URLConnection connection = url.openConnection();
        
        try( final BufferedReader in = new BufferedReader(
                new InputStreamReader( connection.getInputStream() ) ) ) {
            
            String inputLine = null;
            final StringBuffer sb = new StringBuffer();
            while ( ( inputLine = in.readLine() ) != null) {
                sb.append(inputLine);
            }       
            
            System.out.println("Content size: " + sb.length());
        }
    }
}

Java代理非常适合解决此类挑战。 我们只需要定义一个转换器,即可通过注入代码以将输出生成到控制台来稍微修改sun.net.www.protocol.http.HttpURLConnection构造函数。 听起来很吓人,但是使用ClassFileTransformer和Javassist非常简单。 让我们看一下这样的转换器实现:

public class SimpleClassTransformer implements ClassFileTransformer {
  @Override
  public byte[] transform( 
      final ClassLoader loader, 
      final String className,
      final Class classBeingRedefined, 
      final ProtectionDomain protectionDomain,
      final byte[] classfileBuffer ) throws IllegalClassFormatException {
        
    if (className.endsWith("sun/net/www/protocol/http/HttpURLConnection")) {
      try {
        final ClassPool classPool = ClassPool.getDefault();
        final CtClass clazz = 
          classPool.get("sun.net.www.protocol.http.HttpURLConnection");
                
        for (final CtConstructor constructor: clazz.getConstructors()) {
          constructor.insertAfter("System.out.println(this.getURL());");
        }
    
        byte[] byteCode = clazz.toBytecode();
        clazz.detach();
              
        return byteCode;
      } catch (final NotFoundException | CannotCompileException | IOException ex) {
        ex.printStackTrace();
      }
    }
        
    return null;
  }
}

ClassPool和所有CtXxx类( CtClassCtConstructor )都来自Javassist库。 我们所做的转换是非常幼稚的,但这只是出于示范目的。 首先,由于我们仅对HTTP通信感兴趣,因此sun.net.www.protocol.http.HttpURLConnection是来自标准Java库的类。

请注意,而不是“。” 分隔符, className带有“ /”。 其次,我们寻找HttpURLConnection类,并通过注入System.out.println(this.getURL());修改其所有构造函数System.out.println(this.getURL()); 最后的声明。 最后,我们返回了该类转换后的版本的新字节码,因此它将由JVM使用,而不是原始版本。

这样,Java代理premain方法的作用就是将SimpleClassTransformer类的实例SimpleClassTransformer到检测上下文中:

public class SimpleAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        final SimpleClassTransformer transformer = new SimpleClassTransformer();
        inst.addTransformer(transformer);
    }
}

而已。 看起来很容易,同时又有些令人恐惧。 为了完成Java代理,我们必须提供适当的MANIFEST.MF,以便JVM能够选择正确的类。 这是必需属性的相应最小集合(更多信息,请参考Java Agent Basics部分):

Manifest-Version: 1.0
Premain-Class: com.javacodegeeks.advanced.agent.SimpleAgent

这样一来,Java代理程序就可以为真正的战斗做好准备了。 在本教程的下一部分中,我们将介绍一种与Java应用程序一起运行Java代理的方法。

5.运行Java代理

从命令行运行时,可以使用具有以下语义的-javaagent参数将Java代理传递给JVM实例:

-javaagent:[=options]

其中是查找Java代理JAR归档文件的路径,而options包含可以通过agentArgs参数更准确地传递给Java代理的其他选项。 例如,从编写您的第一个Java代理 (使用Java 7版本)部分运行我们的Java代理的命令行如下所示(假设代理JAR文件位于当前文件夹中):

java -javaagent:advanced-java-part-15-java7.agents-0.0.1-SNAPSHOT.jar

当与advanced-java-part-15-java7.agents-0.0.1-SNAPSHOT.jar Java代理一起运行SampleClass类时,该应用程序将在控制台上打印所有URL( Google和Yahoo! )。尝试使用HTTP协议访问(其次是Google和Yahoo!搜索首页的内容大小):

http://www.google.com
Content size: 20349
http://www.yahoo.com
Content size: 1387

在未指定Java代理的情况下运行相同的SampleClass类将仅在控制台上输出内容大小,而不输出URL(请注意,内容大小可能会有所不同):

Content size: 20349
Content size: 1387

JVM使运行Java代理变得简单。 但是,请注意,任何错误或不正确的字节码生成都可能使JVM崩溃,并可能丢失此时您的应用程序可能保存的重要数据。

6.接下来

到最后,高级Java教程也结束了。 希望您发现它是有用,实用和有趣的。 有许多主题尚未涵盖,但是非常欢迎您继续深入探讨Java语言,平台,生态系统和不可思议的社区的奇妙世界。 祝好运!

7.下载源代码

您可以在此处下载本课程的源代码: advanced-java-part-15

翻译自: https://www.javacodegeeks.com/2015/09/java-agents.html

你可能感兴趣的:(Java代理)