我们都知道java是一门解释型语言,大致执行过程如下图所示,首先需要将.java文件编译成字节码文件,然后再通过jvm解释执行,最终运行在对应的操作系统上。这也是java号称一次编译,到处运行的关键。同时我们也能看到,其中的关键是java通过遵循jvm规范而编译出来的字节码文件,它是实际被解释执行的逻辑。
图1
图2
图3
package com.tantan.demo;
import javassist.*;
public class DoAop {
public static void main(String[] args) {
// 获取Ctclass容器
ClassPool pool = ClassPool.getDefault();
try {
// 从容器中根据Demo类的全限定名,获取CtClass对象
CtClass ct = pool.get("com.tantan.demo.Demo");
// 获取Demo的test方法
CtMethod cm = ct.getDeclaredMethod("test");
// 给Demo类加一个int类型的c字段
ct.addField(new CtField(CtClass.intType,"c",ct));
// 在test方法插入一行代码, {}在只有一行代码的时候可以不添加,如果插入多行代码,那么每一行前后都要{}
cm.insertBefore("{ System.out.println(\"method begin\");}");
// 在test方法后边插入一行代码
cm.insertAfter("{ System.out.println(\"method end\");}");
// 把修改后的类写入class文件
ct.writeFile("/Users/demo/projects/java/test/src/main/java/");
} catch (Exception e) {
e.printStackTrace();
}
}
}
上边代码有几个主要的类介绍一下ClassPool,CtClass,CtField
ClassPool:可以简单理解为存放类的pool,所有CtClass都要从pool中获取;
CtClass:表示一个类文件,可以通过一个类的全限定名来获取一个CtClass,并对他做些操作;
CtMethod:对应的类中的方法,可以通过CtClass获取编辑指定方法;
CtField:对应类中的属性,可以通过CtClass获取编辑指定属性;
图4
图5
Manifest-Version: 1.0
Implementation-Title: demo
Implementation-Version: 1.0
Premain-Class: com.tantan.demo.Agent
Agent-Class: com.tantan.demo.Agent
premain:作用于JVM加载的所有类,所有类首次加载并且进入程序main()方法之前,premain方法会被激活,然后所有被加载的类都会执行ClassFileTransformer列表中的回调。
agentmain:采用attach机制,需要借助Instrumentation#retransformClasses(Class< ?>… classes)让对应的类重新转换,激活重新转换的类执行ClassFileTransformer列表中的回调。
这样通过java agent这个通道,我们就可以对jvm做很多事了,比如无侵入的代码监控(如sky working),动态修改正在运行的服务代码(如 Arthas),还有java软件的逆向破解等等,下边我就用一个无侵入代码统计耗时的例子,来将上述功能串起来。
目录结构如下图所示
图6
首先是MANIFEST.MF文件,内容如下图所示,指定了Premain-Class入口类com.tantan.agent.TimeAgent
Manifest-Version: 1.0
Implementation-Title: demo
Implementation-Version: 1.0
Premain-Class: com.tantan.agent.TimeAgent
其次是TimeAgent类,
package com.tantan.agent;
import com.tantan.agent.service.TimeAgentService;
import java.lang.instrument.Instrumentation;
public class TimeAgent {
public static void premain(String args, Instrumentation inst){
// 添加transformer
inst.addTransformer(new TimeAgentService());
}
}
再就是TimeAgentService:
继承ClassFileTransformer实现了transform接口
这里传的className是/拼接的,需要做下转换,转成xx.xx的全限定类名
然后做些自己的逻辑,我这里是做了个判断,使用javassist只对com.tantanapp.demo.controller.WorkController类的testSend方法做一个字节码增强,当然你也可以通过ctClass和获取他的类的所有方法做增强,亦或者对所有类的所有方法做增强,也可以自定义一个规则。
将修改后的ct class返回
package com.tantan.agent.service;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class TimeAgentService implements ClassFileTransformer {
// 返回修改后的字节码
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
//
className = className.replace("/",".");
if (className.equals("com.tantanapp.demo.controller.WorkController")) {
try {
CtClass ctClass = ClassPool.getDefault().getCtClass(className);
CtMethod ctmethod = ctClass.getDeclaredMethod("testSend");
addTimeAop(ctmethod);
return ctClass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
private void addTimeAop(CtMethod method) throws Exception {
method.addLocalVariable("start",CtClass.longType);
method.addLocalVariable("name",ClassPool.getDefault().get("java.lang.String"));
method.insertBefore("start = System.currentTimeMillis();");
method.insertBefore("name =\""+method.getLongName()+"\";");
method.insertAfter("System.out.println(name+\"----time cost:\"+(System.currentTimeMillis()-start));");
}
}
pom文件:主要是用了maven-shade-plugin和maven-jar-plugin插件,将项目打包成符合agent规范的jar包
<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>com.tantan</groupId>
<artifactId>demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>
</dependencies>
<build>
<finalName>demo</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<compilerArguments>
<verbose/>
</compilerArguments>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
编译打包之后,会生成一个demo.jar
然后我们这边还有一个web服务的一个controller,也就是上边代码中写的com.tantanapp.demo.controller.WorkController这个类
看图可以知道,有一个testSend方法,代码逻辑就是sleep 100ms,那么启动测试下吧
图7
启动命令 java -javaagent:/Users/demo/projects/java/demo/target/demo.jar -jar worker-api.jar
图8