美文网首页
Java黑魔法之Java Agent[译]

Java黑魔法之Java Agent[译]

作者: 库洛琪 | 来源:发表于2019-03-26 09:29 被阅读0次
    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

    相关文章

      网友评论

          本文标题:Java黑魔法之Java Agent[译]

          本文链接:https://www.haomeiwen.com/subject/semwvqtx.html