javaagent-通过Instrument的premain修改类字节码的实例
在Java SE5时代,Instrument只提供了premain一种方式,即在真正的应用程序(包含main方法的程序)main方法启动前启动一个代理程序。而且JDK5之后又提供了类似的新特性,大家百度上找吧。
第1步:DEMO APP
我有一个读文件的,或者是发送URL的请求,例如我想知道读这文件,或者URL请求的耗时情况。
这里有个前提就是无代码侵入。
代码:
package jl.demo; import java.io.File; import java.io.IOException; import org.apache.commons.io.FileUtils; /** * Hello world! * */ public class App { public static void main(String[] args) throws IOException { String content = FileUtils.readFileToString(new File("C:\\test\\openstack.txt")); System.out.println("Hello World!"); } }
FileUtils为apache的 commons-io的2.5版本的类。
其中readFileToString方法如下:
public static String readFileToString(final File file) throws IOException { return readFileToString(file, Charset.defaultCharset()); }
如果把埋点放在这个里面,这样所有调用这个方法的请求的耗时情况,都会统计进来。
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>jl</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>demo</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.5</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.0.0</version> <configuration> <archive> <manifest> <mainClass>jl.demo.App</mainClass> </manifest> </archive> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
如上,需求准备完毕,开始埋点。
第2步:Agent
修改原理就是premain方法,使用的Jar包就是javassist,也有asm可以用,自己随便选。
agent:
package jl.agent; import java.lang.instrument.Instrumentation; public class Agent { /** * This method is called before the application’s main-method is called, when * this agent is specified to the Java VM. **/ public static void premain(String agentArgs, Instrumentation inst) { inst.addTransformer(new MyClassFileTransformer()); } }
transformer:
package jl.agent; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtBehavior; import javassist.CtClass; import javassist.NotFoundException; public class MyClassFileTransformer implements ClassFileTransformer { public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] oldClassBuffer) throws IllegalClassFormatException { if (!className.equals("org/apache/commons/io/FileUtils")) { return oldClassBuffer; } System.out.println("Transforming class:" + className); CtClass ctClass = null; try { ctClass = ClassPool.getDefault().makeClass(new java.io.ByteArrayInputStream(oldClassBuffer)); if (ctClass.isInterface() == false) { CtBehavior[] methods = ctClass.getDeclaredBehaviors(); for (int i = 0; i < methods.length; i++) { if (!isTargetMethod(methods[i])) { continue; } transformMethod(methods[i]); } return ctClass.toBytecode(); } } catch (Exception e) { e.printStackTrace(); } finally { if (ctClass != null) { ctClass.detach(); } } return oldClassBuffer; } private boolean isTargetMethod(CtBehavior method) throws NotFoundException { if (!"readFileToString".equals(method.getName())) { return false; } CtClass[] parameterTypes = method.getParameterTypes(); if (parameterTypes.length == 1) { // 这里只验证一下个数,省事了,重名的方法,我们调用的只有一个参数 return true; } return false; } private void transformMethod(CtBehavior method) throws NotFoundException, CannotCompileException { System.out.println("Transforming method:" + method.getName()); // 第1种方式: // 这里必须先定义本地变量,网上的没有这1步,直接是insertBefore=long // stime=System.nanoTime();,不晓得他们是怎么跑的通的 method.addLocalVariable("stime", CtClass.longType); method.insertBefore("stime=System.nanoTime();"); method.insertAfter("System.out.println(\"" + method.getName() + " cost:\"+(System.nanoTime()-stime));"); // 第2种方式:可以使用ExprEditor直接修改方法体,没有验证过 // method.instrument(new ExprEditor() { // public void edit(MethodCall m) throws CannotCompileException { // m.replace("{ long stime = System.nanoTime(); $_ = $proceed($$); // System.out.println(\"" // + m.getClassName() + "." + m.getMethodName() + // ":\"+(System.nanoTime()-stime));}"); // } // }); } }
MANIFEST.MF
Manifest-Version: 1.0 Built-By: Administrator Class-Path: javassist-3.12.1.GA.jar Created-By: Apache Maven 3.5.3 Build-Jdk: 1.8.0_121 Premain-Class: jl.agent.Agent
pom.xml,这里注意,配置了MANIFEST.MF位置,因为Premain-Class在这个plugin指定不了,所以这样配置了。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>jl</groupId> <artifactId>agent</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>agent</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>javassist</groupId> <artifactId>javassist</artifactId> <version>3.12.1.GA</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.0.0</version> <configuration> <archive> <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile> <manifest> <addClasspath>true</addClasspath> </manifest> </archive> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
第3步:打包
run as --> manven install后会在target目录下生成对应的打包文件。
第4步:运行
第1种方式:eclipse中
在App.java的run configuractions中配置arguments-->vm arguments:
-javaagent:E:\workspaces\eclipse\agent\target\agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar
然后直接run支持,结果如下:
Transforming class:org/apache/commons/io/FileUtils Transforming method:readFileToString readFileToString cost:6521989 Hello World!
可以看到已经成功拦截
第2种方式:命令行
进入demo的jar包所在目录:E:\workspaces\eclipse\demo\target
命令:
PS E:\workspaces\eclipse\demo\target> java -javaagent:E:\workspaces\eclipse\agent\target\agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar -cp demo-0.0.1-SNAPSHOT-jar-with-dependencies .jar jl.demo.App
输出:
PS E:\workspaces\eclipse\demo\target> java -javaagent:E:\workspaces\eclipse\agent\target\agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar -cp demo-0.0.1-SNAPSHOT-jar-with-dependencies .jar jl.demo.App Transforming class:org/apache/commons/io/FileUtils Transforming method:readFileToString readFileToString cost:6736224 Hello World! PS E:\workspaces\eclipse\demo\target>
参考:
实战的:
https://blog.csdn.net/pwlazy/article/details/5109742
如下这个详细描述了Instrument相关功能,有很多其他特性及实例参考,非常好。
其实原先参考的那一篇文章,忘了名字,没有搜索到,发现这篇是一样的,记录下,方便参考。
http://zhxing.iteye.com/blog/1703305
有一往篇介绍原理的,图文配,很好理解,介绍了字码节切入的时机,没有找到这篇文件。。。
回头
再看一下我们拦截的方法:
public static String readFileToString(final File file) throws IOException { return readFileToString(file, Charset.defaultCharset()); }
我们在方法前插入了句,在方法后插入了一句,来统计耗时。但是,对于这个只有一个return内容的方法,在方法后到底插入到了哪里?是否真正统计到了该方法的全部执行过程的耗时。
因此,在MyClassFileTransformer中,将替换好的字节码:return ctClass.toBytecode();保存到文件FileUtils
中。
然后使用命令:
PS C:\test> javap -v FileUtils >> aaa.txt
将字节码反编译的内容保存到aaa.txt中,打开这个文件,找到拦截的方法java.lang.String readFileToString(java.io.File) ,可以看到我们插入的内容在哪里,在其调用readFileToString方法的前面和后面:
public static java.lang.String readFileToString(java.io.File) throws java.io.IOException; descriptor: (Ljava/io/File;)Ljava/lang/String; flags: ACC_PUBLIC, ACC_STATIC Code: stack=8, locals=5, args_size=1 0: invokestatic #1167 // Method java/lang/System.nanoTime:()J 3: lstore_1 4: aload_0 5: invokestatic #103 // Method java/nio/charset/Charset.defaultCharset:()Ljava/nio/charset/Charset; 8: invokestatic #214 // Method readFileToString:(Ljava/io/File;Ljava/nio/charset/Charset;)Ljava/lang/String; 11: goto 14 14: astore 4 16: getstatic #1170 // Field java/lang/System.out:Ljava/io/PrintStream; 19: new #1172 // class java/lang/StringBuffer 22: dup 23: invokespecial #1174 // Method java/lang/StringBuffer."<init>":()V 26: ldc_w #1176 // String readFileToString cost: 29: invokevirtual #1179 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer; 32: invokestatic #1167 // Method java/lang/System.nanoTime:()J 35: lload_1 36: lsub 37: invokevirtual #1182 // Method java/lang/StringBuffer.append:(J)Ljava/lang/StringBuffer; 40: invokevirtual #1184 // Method java/lang/StringBuffer.toString:()Ljava/lang/String; 43: invokevirtual #1189 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 46: aload 4 48: areturn LineNumberTable: line 1800: 4 LocalVariableTable: Start Length Slot Name Signature 0 14 0 file Ljava/io/File; 0 14 1 stime J StackMapTable: number_of_entries = 1 frame_type = 255 /* full_frame */ offset_delta = 14 locals = [ class java/io/File, long ] stack = [ class java/lang/String ] Exceptions: throws java.io.IOException Deprecated: true RuntimeVisibleAnnotations: 0: #575()
javap命令,及字节码的参考:
https://www.cnblogs.com/royi123/p/3569926.html
完结。