java_logo

前言

java.lang.instrument包是Java中来增强JVM上的应用的一种方式,机制是在JVM启动前或启动后attach上去进行修改方法字节码的方式。
instrument包的用途很多,主要体现在对代码侵入低的优点上,例如一些监控不方便修改业务代码,但是可以使用这种方式在方法中植入特定逻辑,这种方式能够直接修改JVM中加载的字节码的内容,而不需要在像Spring AOP实现中创建新的代理类,所以在底层侵入更高,但是对开发者更透明。用于自动添加getter/setter方法的工具lombok就使用了这一技术。另外btrace和housemd等动态诊断工具也是用了instrument技术。

创建一个简单的Agent

关于premain的介绍:

通常agent的包里面MATE-INF目录下的MANIFEST.MF中会有这样一段声明

Premain-Class: cn.org.javaweb.test.Agent

在你通过启动命令添加-javaagent:xxx.jar 的时候,JVM会去xxx.jar中找其中Premain-Class是你在MANIFEST.MF中声明的cn.org.javaweb.test.Agent这个类中的public static void premain(String agentOps, Instrumentation instrumentation)或者public static void premain(String agentOps)方法.

注意这里,public static void premain(String agentOps, Instrumentation instrumentation)public static void premain(String agentOps)这两个是有优先级的,其中当premain有两个参数,也就是public static void premain(String agentOps, Instrumentation instrumentation)的时候优先级最高,如果两个都存在,public static void premain(String agentOps)则会被忽略。

其中agentOps将获得程序的参数,会随着-javaagent一起传入,如下:

java -javaagent:agent-0.0.1.jar="Hello World.?"  -jar agent-1.0-SNAPSHOT.jar

将会将Hello World.?传入进去,和main方法不一样的是,该处传入的是一串完整的字符串,并不会传入解析以后的字符串,所以此处如果有需要的话,那么此处传入字符串将由agent完成解析

其中instrumentationjava.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。java.lang.instrument.Instrumentationinstrument 包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。


了解了以上知识以后,我们来创建一个agent。

创建agent

首先创建一个名为cn.org.javaweb.test的包,然后在该包下创建一个为Agent的类,
如下:

package cn.org.javaweb.test;
/*
 * Copyright sky 2018-11-20 Email:[email protected].
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @author sky
 */
public class Agent {
}

然后写一个premain方法

public static void premain(String agentOps, Instrumentation inst) {
        System.out.println("=======this is agent premain function=======");
    }

将其使用maven打包为jar文件,pom.xml文件中build的内容为:

<plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.0.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Premain-Class>
                                cn.org.javaweb.test.Agent
                            </Premain-Class>
                            <can-redefine-classes>
                                true
                            </can-redefine-classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
</plugins>

然后使用maven中的clean install命令安装即可(或者使用mvn clean package),将会生成一个agent.jar的文件。

创建测试程序

此时我们随便新建一个带有main方法的程序,

package cn.org.javaweb.test;
/*
 * Copyright sky 2018-11-20 Email:[email protected].
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @author sky
 */
public class TestAgent {

    public static void main(String[] args) {
        System.out.print("Hello,This is TestAgent Program!");
    }
}

pom.xml文件中build的内容是


        <plugins>
            <plugin>
                <artifactId>maven-shade-plugin</artifactId>
                <groupId>org.apache.maven.plugins</groupId>
                <version>1.2.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>cn.org.javaweb.test.TestAgent</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
</plugins>

然后使用maven生成jar文件。

测试agent

这时候我们会得到两个jar文件,

agent.jar --> agent生成的jar文件
test.jar  --> 测试程序

我们先来运行下测试程序,看是否正常

java -jar test.jar 

输出

Hello,This is TestAgent Program!

然后我们加入agent运行试试看

java -javaagent:agent.jar -jar test.jar

输出

=======this is agent premain function=======
Hello,This is TestAgent Program!

可以看到premain会在程序的main方法之前运行,此时我们可以简单的看下jvm中加入agent以及不加入agent的运行过程。

不加入agent的运行过程

-w509

加入agent的运行过程

-w498

可以简单明了的看到agent会在运行程序之前运行,但是其后面涉及到其他复杂的逻辑,这里不做解释。

使用instrumentation参数

在上面我们只是进入了premain方法,其中premain方法的instrumentation参数我们并没有用到,这里我们将简单的介绍使用instrumentation参数。

关于Transformer解释

此方法的实现可以转换提供的类文件,并返回一个新的替换类文件。
有两种装换器,由 Instrumentation.addTransformer(ClassFileTransformer,boolean)canRetransform 参数确定:

  • 可重转换 转换器,将 canRetransform 设为 true 可添加这种转换器
  • 不可重转换 转换器,将 canRetransform 设为 false 或者使用 Instrumentation.addTransformer(ClassFileTransformer) 可添加这种转换器

在转换器使用 addTransformer 注册之后,每次定义新类和重定义类时都将调用该转换器。
每次重转换类时还将调用可重转换转换器。
对新类定义的请求通过 ClassLoader.defineClass 或其本机等价方法进行。
对类重定义的请求通过 Instrumentation.redefineClasses 或其本机等价方法进行。
对类重转换的请求将通过 Instrumentation.retransformClasses 或其本机等价方法进行。
转换器是在验证或应用类文件字节之前的请求处理过程中调用的。
当存在多个转换器时,转换将由 transform 调用链组成。 也就是说,一个 transform 调用返回的 byte 数组将成为下一个调用的输入(通过 classfileBuffer 参数)。

转换将按以下顺序应用:

  • 不可重转换转换器
  • 不可重转换本机转换器
  • 可重转换转换器
  • 可重转换本机转换器

对于重转换,不会调用不可重转换转换器,而是重用前一个转换的结果。对于所有其他情况,调用此方法。在每个这种调用组中,转换器将按照注册的顺序调用。本机转换器由 Java 虚拟机 Tool 接口中的 ClassFileLoadHook 事件提供。

ClassLoader loader                  ----> 这个没什么解释的,就是classloader
String className                    ----> 包名(jvm中包名是/而不是.)
Class<?> classBeingRedefined        ----> 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
ProtectionDomain protectionDomain   ----> 保护域
byte[] classfileBuffer              ----> 二进制字节码

添加一个新的Transformer类

我们新建一个名为TestTransformer的类,内容入下:

/*
 * Copyright sky 2018-11-20 Email:[email protected].
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.org.javaweb.test;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

/**
 * @author sky
 */
public class TestTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

        System.out.println(className.replace("/", "."));
        return classfileBuffer;
    }
}

在Agent.java中修改premain方法为

System.out.println("=======this is agent premain function=======");
inst.addTransformer(new TestTransformer());

将我们新建的TestTransformer注册进去
这时候我们在重新生成agent后运行如下命令

java -javaagent:agent.jar -jar test.jar

将会输出

=======this is agent premain function=======
sun.launcher.LauncherHelper
cn.org.javaweb.test.TestAgent
java.lang.Void
Hello,This is TestAgent Program!
java.lang.Shutdown
java.lang.Shutdown$Lock

可以看到会输出很多包名,这是因为我们在TestTransformer中将包名打印了出来。

结束语

关于agent的简单使用就介绍到了此处,此文仅抛砖引玉,更多用法可以发挥想象,比如性能监控,日志监控、管理会话、安全过滤、请求管理等。

文章中涉及到的代码全部都在下面地址:

https://gitlab.com/iiusky/javaagent

参考:

最后修改:2021 年 11 月 29 日
如果觉得我的文章对你有用,请随意赞赏