Java黑魔法之Java Agent[译]

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类(CtClassCtConstructor)来自于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崩溃,可能会丢失您的应用程序此时拥有的重要数据。

6. 原文地址

Java Agents

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,001评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,210评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,874评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,001评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,022评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,005评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,929评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,742评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,193评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,427评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,583评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,305评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,911评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,564评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,731评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,581评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,478评论 2 352

推荐阅读更多精彩内容