Java Agent笔记

1,083 阅读6分钟

Java Agent简介

Java Agent 是从 JDK1.5 开始引入的, 它本身就是 java 命令的一个参数(即 -javaagent)。-javaagent 参数之后需要指定一个 jar 包,这个 jar 包需要同时满足下面两个条件:

  1. 在 META-INF 目录下的 MANIFEST.MF 文件中必须指定 premain-class 配置项。
  2. premain-class 配置项指定的类必须提供了 premain() 方法。

在 Java 虚拟机启动时,执行 main() 函数之前,虚拟机会先找到 -javaagent 命令指定 jar 包,然后执行 premain-class 中的 premain() 方法。

使用 Java Agent 的步骤大致如下:

  1.  定义一个 MANIFEST.MF 文件,在其中添加 premain-class 配置项。

  2.  创建  premain-class 配置项指定的类,并在其中实现 premain() 方法,方法签名如下:

public static void premain(String agentArgs, Instrumentation inst){
 		
}

3.  将 MANIFEST.MF 文件和  premain-class 指定的类一起打包成一个 jar 包。 4.  使用 -javaagent 指定该 jar 包的路径即可执行其中的 premain() 方法。

创建Java Agent

创建一个Maven项目,用于创建java agent

public class JavaAgentTest {

    public static void premain(String agentArgs,
                               Instrumentation inst)throws Exception {
        System.out.println("this is a java agent only two args");
        System.out.println("参数:" + agentArgs + "\n");
    }

    public static void premain(String agentArgs) {
        System.out.println("this is a java agent only one args");
        System.out.println("参数:" + agentArgs + "\n");
    }
}

创建 MANIFEST.MF 文件并打包,代码如下:

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>2.4</version>
                <configuration>
                    <appendAssemblyId>false</appendAssemblyId>
                    <!-- 将所有依赖包都打到jar包中-->
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifest><!-- 添加MANIFEST.MF中的各项配置-->
                            <!-- 添加 mplementation-*和Specification-*配置项-->
                            <addDefaultImplementationEntries>true
                            </addDefaultImplementationEntries>
                            <addDefaultSpecificationEntries>true
                            </addDefaultSpecificationEntries>
                        </manifest>
                        <!-- 将 premain-class 配置项设置为com.dragon.TestAgent-->
                        <manifestEntries>
                            <Premain-Class>com.dragon.TestAgent</Premain-Class>
                        </manifestEntries>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <!-- 绑定到package生命周期阶段上 -->
                        <phase>package</phase>
                        <goals>
                            <!-- 绑定到package生命周期阶段上 -->
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

打包

mvn package -Dcheckstyle.skip -DskipTests

完成打包之后,我们可以解压 target 目录下的 test-agent.jar,在其 META-INF 目录下可以找到 MANIFEST.MF 文件,其内容如下:

Manifest-Version: 1.0
Implementation-Title: JavaAgentTest
Implementation-Version: 1.0-SNAPSHOT
Archiver-Version: Plexus Archiver
Built-By: wangjinlong
Premain-Class: com.dragon.JavaAgentTest
Specification-Title: JavaAgentTest
Implementation-Vendor-Id: com.dragon
Can-Retransform-Classes: true
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_265
Specification-Version: 1.0-SNAPSHOT

使用Java Agent

再创建一个普通的 Maven 项目,用于使用Java Agent Main方法如下

public class Main {
    public static void main(String[] args) throws Exception {
        Thread.sleep(1000);
        System.out.println("Hello World Main!");
    }
}

在程序运行时指定javaagent参数

-javaagent:/Users/wangjinlong/IdeaProjects-Communicity/TestAgent/target/test-agent.jar

原理分析

premain() 方法有两个重载,如下所示,如果两个重载同时存在,【1】将会被忽略,只执行【2】:

public static void premain(String agentArgs) [1]
public static void premain(String agentArgs, 
      Instrumentation inst); [2]

确定 premain() 方法的两个重载优先级的逻辑在 sun.instrument.InstrumentationImpl.java 中实现。 代理类必须有一个premain或者 agentamin方法,这个方法可以有一个或2个参数。这个是按以下顺序来判断。

  1. declared with a signature of (String, Instrumentation)
  2. declared with a signature of (String)
  3. inherited with a signature of (String, Instrumentation)
  4. inherited with a signature of (String) 所以,当前类中声明的方法会比继承的方法的优先级高,两个参数的方法的优先级比一个参数的方法的优先级高。 相关代码如下:
    // WARNING: the native code knows the name & signature of this method
    private void
    loadClassAndCallPremain(    String  classname,
                                String  optionsString)
            throws Throwable {

        loadClassAndStartAgent( classname, "premain", optionsString );
    }


    // Attempt to load and start an agent
    private void
    loadClassAndStartAgent( String  classname,
                            String  methodname,
                            String  optionsString)
            throws Throwable {

        ClassLoader mainAppLoader   = ClassLoader.getSystemClassLoader();
        Class<?>    javaAgentClass  = mainAppLoader.loadClass(classname);

        Method m = null;
        NoSuchMethodException firstExc = null;
        boolean twoArgAgent = false;

        // The agent class must have a premain or agentmain method that
        // has 1 or 2 arguments. We check in the following order:
        //
        // 1) declared with a signature of (String, Instrumentation)
        // 2) declared with a signature of (String)
        // 3) inherited with a signature of (String, Instrumentation)
        // 4) inherited with a signature of (String)
        //
        // So the declared version of either 1-arg or 2-arg always takes
        // primary precedence over an inherited version. After that, the
        // 2-arg version takes precedence over the 1-arg version.
        //
        // If no method is found then we throw the NoSuchMethodException
        // from the first attempt so that the exception text indicates
        // the lookup failed for the 2-arg method (same as JDK5.0).

        try {
            m = javaAgentClass.getDeclaredMethod( methodname,
                                 new Class<?>[] {
                                     String.class,
                                     java.lang.instrument.Instrumentation.class
                                 }
                               );
            twoArgAgent = true;
        } catch (NoSuchMethodException x) {
            // remember the NoSuchMethodException
            firstExc = x;
        }

        if (m == null) {
            // now try the declared 1-arg method
            try {
                m = javaAgentClass.getDeclaredMethod(methodname,
                                                 new Class<?>[] { String.class });
            } catch (NoSuchMethodException x) {
                // ignore this exception because we'll try
                // two arg inheritance next
            }
        }

        if (m == null) {
            // now try the inherited 2-arg method
            try {
                m = javaAgentClass.getMethod( methodname,
                                 new Class<?>[] {
                                     String.class,
                                     java.lang.instrument.Instrumentation.class
                                 }
                               );
                twoArgAgent = true;
            } catch (NoSuchMethodException x) {
                // ignore this exception because we'll try
                // one arg inheritance next
            }
        }

        if (m == null) {
            // finally try the inherited 1-arg method
            try {
                m = javaAgentClass.getMethod(methodname,
                                             new Class<?>[] { String.class });
            } catch (NoSuchMethodException x) {
                // none of the methods exists so we throw the
                // first NoSuchMethodException as per 5.0
                throw firstExc;
            }
        }

        // the premain method should not be required to be public,
        // make it accessible so we can call it
        // Note: The spec says the following:
        //     The agent class must implement a public static premain method...
        setAccessible(m, true);

        // invoke the 1 or 2-arg method
        if (twoArgAgent) {
            m.invoke(null, new Object[] { optionsString, this });
        } else {
            m.invoke(null, new Object[] { optionsString });
        }

        // don't let others access a non-public premain method
        setAccessible(m, false);
    }


Instrumentation

Instrumentation 位于 java.lang.instrument 包中,通过这个工具包,我们可以编写一个强大的 Java Agent 程序,用来动态替换或是修改某些类的定义。

  • addTransformer()/removeTransformer() 方法:注册/注销一个 ClassFileTransformer 类的实例,该 Transformer 会在类加载的时候被调用,可用于修改类定义。
  • redefineClasses() 方法:该方法针对的是已经加载的类,它会对传入的类进行重新定义。
  • **getAllLoadedClasses()方法:**返回当前 JVM 已加载的所有类。
  • getInitiatedClasses() 方法:返回当前 JVM 已经初始化的类。
  • getObjectSize()方法:获取参数指定的对象的大小。

实现类的转换

首先定义要转换的类和转换后的类

转换的类

public class TestClass {
    public String hello() {
        return "I am TestClass";
    }
}

转换后的类(类的包名和类名必须一样)

public class TestClass{
    public String hello() {
        return "I am TestClass from agent";
    }
}

类转换器

class Transformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader l, String className,
                            Class<?> c, ProtectionDomain pd, byte[] b)  {
        if (!c.getSimpleName().equals("TestClass")) {
            return null; // 只修改TestClass的定义
        }
        // 读取 TestClass.class这个 class文件,作为 TestClass类的新定义
        return getBytesFromFile("Class文件所在绝对路径");
    }

    public static byte[] getBytesFromFile(String fileName) {
        try {
            // precondition
            File file = new File(fileName);
            InputStream is = new FileInputStream(file);
            long length = file.length();
            byte[] bytes = new byte[(int) length];

            // Read in the bytes
            int offset = 0;
            int numRead = 0;
            while (offset < bytes.length
                    && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
                offset += numRead;
            }

            if (offset < bytes.length) {
                throw new IOException("Could not completely read file "
                        + file.getName());
            }
            is.close();
            return bytes;
        } catch (Exception e) {
            System.out.println("error occurs in _ClassTransformer!"
                    + e.getClass().getName());
            return null;
        }
    }
}

编写 premain() 方法,并注册一个 Transformer 对象:、

public class TestAgent {
    public static void agentmain(String agentArgs,
                               Instrumentation inst)throws Exception {
        // 注册一个 Transformer,该 Transformer在类加载时被调用
        inst.addTransformer(new com.dragon.Transformer(), true);
        inst.retransformClasses(TestClass.class);
        System.out.println("premain done");
    }

    public static void premain(String agentArgs) {
        System.out.println("this is a java agent only one args");
        System.out.println("参数:" + agentArgs + "\n");
    }
}

在 maven-assembly-plugin 插件中添加 Can-Retransform-Classes 参数:

<manifestEntries>
    <Premain-Class>com.dragon.TestAgent</Premain-Class>
    <Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>

使用类

-javaagent:/路径/test-agent.jar=option1=value2,option2=value2

结果

premain done
I am TestClass from transformer
I am TestClass from transformer
I am TestClass from transformer
I am TestClass from transformer
I am TestClass from transformer
I am TestClass from transformer
I am TestClass from transformer
I am TestClass from transformer
I am TestClass from transformer

agentmain

在 Java 5 中,Java 开发者只能通过 Java Agent 中的 premain() 方法在 main() 方法执行之前进行一些操作,这种方式在一定程度上限制了灵活性。Java 6 针对这种状况做出了改进,提供了一个 agentmain() 方法,Java 开发者可以在 main() 方法执行以后执行 agentmain() 方法实现一些特殊功能。

agentmain() 方法同样有两个重载,它们的参数与 premain() 方法相同,而且前者优先级也是高于后者的:

public static void agentmain (String agentArgs, 
      Instrumentation inst);[1]

public static void agentmain (String agentArgs); [2]

agentmain() 方法主要用在 JVM Attach 工具中,Attach API 是 Java 的扩展 API,可以向目标 JVM “附着”(Attach)一个代理工具程序,而这个代理工具程序的入口就是 agentmain() 方法。

Attach API 中有 2 个核心类:

  • VirtualMachine 是对一个 Java 虚拟机的抽象,在 Attach 工具程序监控目标虚拟机的时候会用到该类。VirtualMachine 提供了 JVM 枚举、Attach、Detach 等基本操作。
  • VirtualMachineDescriptor 是一个描述虚拟机的容器类,后面示例中会介绍它如何与 VirtualMachine 配合使用。