1. 介绍
在这最后一篇教程中我们将来介绍Java agent,这是普通Java开发者的黑魔法。Java agent能通过直接修改字节码侵入正运行于JVM上的Java应用。它危险而强大:它几乎能够做所有事情,但一旦出错,可以轻易地导致JVM崩溃。
这部分的目标是通过解释它如何工作,如何运行它并通过一些能展示它优势的简单的例子来揭开Java agent的什么面纱。
2. Java Agent基础
在本质上,Java agent是一个遵循一组严格约定的普通类。agent类必须实现public static void premain(String agentArgs, Instrumentation inst) 方法,从而成为一个代理入口(就像普通类的main方法)。
一旦JVM初始化,每一个agent的premain(String agentArgs, Instrumentation inst)方法就会在JVM启动时按照指定的顺序被调用。当初始化步骤完成了,真正的Java应用的main方法就会被调用。
然而,如果类没有实现public static void premain(String agentArgs, Instrumentation inst) 方法,JVM会寻找并调用重载版本public static void premain(String agentArgs)。需要注意的是每一个premain方法必须要返回,以便JVM的启动过程能够继续下去。
最后但很重要的是,Java agent也可能有public static void agentmain(String agentArgs, Instrumentation inst)或者public static void agentmain(String agentArgs)方法将会在JVM启动之后执行。
初看起来很简单,但是Java agent实现的时候必须要提供manifest文件。通常位于META-INF文件夹下名为MANIFEST.MF,包含了与包分发相关的各种元数据。
下表是为打包成JAR文件的Java agent定义的一些属性:
Manifest Attribute | Description |
---|---|
Premain-Class | When an agent is specified at JVM launch time this attribute defines the Java agent class: the class containing the premain method. When an agent is specified at JVM launch time this attribute is required. If the attribute is not present the JVM will abort. |
Agent-Class | If an implementation supports a mechanism to start Java agents sometime after the JVM has started then this attribute specifies the agent class: the class containing the agentmain method. This attribute is required and the agent will not be started if this attribute is not present. |
Boot-Class-Path | A list of paths to be searched by the bootstrap class loader. Paths represent directories or libraries. |
Can-Redefine-Classes | A value of true or false , case-insensitive and defines if the ability to redefine classes needed by this agent. This attribute is optional, the default is false . |
Can-Retransform-Classes | A value of true or false , case-insensitive and defines if the ability to retransform classes needed by this agent. This attribute is optional, the default is false . |
Can-Set-Native-Method-Prefix | A value of true or false , case-insensitive and defines if the ability to set native method prefix needed by this agent. This attribute is optional, the default is false . |
3. Java agent与Instrumentation
Java agent的检测功能是无限的,值得关注的但不仅仅限于这些:
- 运行时重新定义类的能力。重定义将可能改变方法体、常量池和属性。重定义不能增加、删除或者重命名域和方法,不能修改方法的签名、不能改变继承关系。
- 运行时重新转换类的能力。转换将可能改变方法体、常量池和属性。转换不能增加、删除或者重命名域和方法,不能修改方法的签名、不能改变继承关系。
需要注意的是转换和重定义类的字节码是没有验证的,当转换和重定义执行完成后类直接被虚拟机装入了,如果这些字节码是错误的,将会抛出异常并导致JVM崩溃。
4. 编写你的第一个Java agent
在这一节中将通过实现了自己的类转换器的Java agent。话虽如此,使用Java代理的唯一缺点是,为了完成或多或少有用的转换,需要直接的字节码操作技能。并且,不幸的是,标准的Java库并没有提供能进行这些字节码操作的API。
为了填补这个空白,非常有创造力的Java社区,提出了一些优秀的,现在已经非常成熟的库,例如Javassist和ASM。这两者之间,Javassist更易于使用,这也是我们将使用它作为字节码操作解决方案的原因。
下面的例子非常简单,但却是来自于真实案例。我们将获取每一个由Java应用打开的HTTP连接的URL。通过直接修改源代码会有许多方式来实现它,但假设因为许可证或者其他的,我们没法获取到源代码。
一个创建HTTP连接的典型例子如下所示:
public class SampleClass {
public static void main( String[] args ) throws IOException {
fetch("http://www.baidu.com");
fetch("http://www.tencent.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 agent非常适合解决这个问题。仅仅需要定义一个transformer,通过注入代码到sun.net.www.protocol.http.HttpURLConnection的构造器中来输出URL到控制台。听起来很恐怖,但有了ClassFileTransformer和Javassist,这非常简单。下面是一个transformer的实现:
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包。这里的transformer非常原始,但作为示例已经足够了。首先,因为我们只对HTTP感兴趣,sun.net.www.protocol.http.HttpURLConnection该类负责HTTP连接。
首先需要注意className使用的是“/”分隔符而不是“.”。然后找到HttpURLConnection类通过注入System.out.println(this.getURL());来修改了它所有的构造器。最后返回被修改类的字节码,这将被JVM用以替换原来的类。
这样,Java agent的premain方法的作用就是将SimpleClassTransformer的实例添加到Instrumentation的上下文中
public class SimpleAgent {
public static void premain(String agentArgs, Instrumentation inst) {
final SimpleClassTransformer transformer = new SimpleClassTransformer();
inst.addTransformer(transformer);
}
}
为了完成Java agent,需要提供适当的MANIFEST.MF,以便JVM能获取到正确的类。下面是所需属性的最小集合。
Manifest-Version: 1.0
Premain-class: com.javacodegeeks.advanced.agent.SimpleAgent
(使用maven-jar-plugin可以直接配置manifest的相关属性。)
5. 运行Java Agent
从命令行执行时,通过使用参数-javaagent将Java agent传递给JVM实例,语法如下:
-javaagent:<path-to-jar>[=options]
<path-to-jar>是Java agent JAR文件的路径,options是一些其他传递给Java agent的参数,即是agentArgs参数。举个例子来说,在我们的Java agent中,可以使用以下方式:
java -javaagent:advanced-java-part-15-java7.agents-0.0.1-SNAPSHOT.jar
使用Java agent advanced-java-part-15-java7.agents-0.0.1-SNAPSHOT.jar来运行SampleClass,可以在控制台打印尝试使用HTTP协议的URL。
http://www.baidu.com
Content size: 2309
http://www.tencent.com
Content size: 180
不使用Java agent来运行SimpleClass将只会在控制台打印内容大小,不会有URL。
Content size: 2309
Content size: 180
JVM使得运行Java agent十分容易。但是,需要注意,任何错误的字节码会导致JVM崩溃,可能会丢失您的应用程序此时拥有的重要数据。
网友评论