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)后,将按照在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 方法的类。 此属性是必需的,如果不存在该代理,则不会启动代理。 |
引导类路径 | 引导类加载器要搜索的路径列表。 路径代表目录或库。 |
可以重新定义类 | true 或false 值,不区分大小写,并且定义是否具有重新定义此代理所需的类的能力。 此属性是可选的,默认值为false 。 |
可以重新转换类 | true 或false 值,不区分大小写,并且定义是否具有重新转换此代理所需的类的能力。 此属性是可选的,默认值为false 。 |
可以设置本机方法前缀 | true 或false 值,不区分大小写,并且定义是否可以设置此代理所需的本机方法前缀。 此属性是可选的,默认值为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
类( CtClass
, CtConstructor
)都来自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代理模式