java.lang.instrument包是Java中来增强JVM上的应用的一种方式,机制是在JVM启动前或启动后attach上去进行修改方法字节码的方式。
instrument包的用途很多,主要体现在对代码侵入低的优点上,例如一些监控不方便修改业务代码,但是可以使用这种方式在方法中植入特定逻辑,这种方式能够直接修改JVM中加载的字节码的内容,而不需要在像Spring AOP实现中创建新的代理类,所以在底层侵入更高,但是对开发者更透明。用于自动添加getter/setter方法的工具lombok就使用了这一技术。另外btrace和housemd等动态诊断工具也是用了instrument技术。
通常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完成解析
其中 instrumentation
是 java.lang.instrument.Instrumentation
的实例,由 JVM 自动传入。 java.lang.instrument.Instrumentation
是 instrument
包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。
了解了以上知识以后,我们来创建一个agent。
首先创建一个名为 cn.org.javaweb.test
的包,然后在该包下创建一个为 Agent
的类,
如下:
package cn.org.javaweb.test; /* * Copyright sky 2018-11-20 Email:sky@03sec.com. * * 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:sky@03sec.com. * * 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文件。
这时候我们会得到两个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的运行过程
加入agent的运行过程
可以简单明了的看到agent会在运行程序之前运行,但是其后面涉及到其他复杂的逻辑,这里不做解释。
在上面我们只是进入了premain方法,其中premain方法的instrumentation参数我们并没有用到,这里我们将简单的介绍使用instrumentation参数。
此方法的实现可以转换提供的类文件,并返回一个新的替换类文件。
有两种装换器,由 Instrumentation.addTransformer(ClassFileTransformer,boolean)
的 canRetransform
参数确定:
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 ----> 二进制字节码
我们新建一个名为TestTransformer的类,内容入下:
/* * Copyright sky 2018-11-20 Email:sky@03sec.com. * * 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
参考: