java代理模式_Java代理

java代理模式

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

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

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)后,将按照在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社区提出了一些优秀的,非常成熟的库,例如JavassistASM ,仅举几例。 在这两种方法中,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构造函数。 听起来很吓人,但是使用ClassFileTransformerJavassist非常简单。 让我们看一下这样的转换器实现:

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:<path-to-jar>[=options]

其中<path-to-jar>是查找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( GoogleYahoo! )。尝试使用HTTP协议进行访问(其次是GoogleYahoo!搜索主页的内容大小):

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代理模式

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值