欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

javaagent-通过Instrument的premain修改类字节码的实例

程序员文章站 2022-05-21 20:35:09
...

在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

 

完结。