`

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

    博客分类:
  • java
 
阅读更多

在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

 

完结。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics