agent技术听着挺高大上的,实际上跟你在代码里面写一个方法a 然后再写个方法叫beforea,调用a之前先调用beforea是一样的,只不过这段代码的执行逻辑在jvm中而已.
那么在javaagent下这个a就是main,breforea就是premain,那么问题来了?总不能你在你的代码中随便命名了一个方法叫premain,人家就要给你去执行嘛,所以此处需要你去指定premain方法是哪个,而这个指定的属性就在ManiFest.mf中,我们来看看此文件具体的内容,非常好理解,见名知意,就不多解释了
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven
Built-By: root
Build-Jdk: 11.0.16
Automatic-Module-Name: org.jacoco.agent.rt
Implementation-Title: JaCoCo Java Agent
Implementation-Vendor: Mountainminds GmbH & Co. KG
Implementation-Version: 0.8.10
Premain-Class: org.jacoco.agent.rt.internal_4a7f17c.PreMain
那我们再来看看springboot项目中的MANIFEST.MF文件又长啥样
问题:这个jar包能执行吗?疑问的地方在于,既然premain需要指定,难道main就不需要指定了吗?
Manifest-Version: 1.0
Implementation-Title: demo-a
Implementation-Version: 0.0.1-SNAPSHOT
Built-By: 86180
Implementation-Vendor-Id: com.example
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_281
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
ot-starter-parent/demo-a
实际上你使用java -jar springboot.jar运行时候你会发现它会提示:springboot.jar中没有主清单属性
正如你所想的那样,main一样需要指定,不然同样不知道你应该执行哪个main方法作为入口,所以以上就不是一个可执行jar包,那我们再来看一个可执行jar包的MANIFEST.MF长什么样
Manifest-Version: 1.0
Implementation-Title: app-message
Implementation-Version: 0.0.1-SNAPSHOT
Built-By: 86180
Implementation-Vendor-Id: com.xxx.tech
Spring-Boot-Version: 2.1.3.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.xxx.message.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_281
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
ot-starter-parent/xxx-parent/xxx-framework/app-message
通过这份文件我们发现以下两个属性
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.xxx.message.Application
发现原来main-class并不是我们的启动类,而是一个JarLauncher,这个玩意不是本次的重点我们就不介绍了,然后是Start-Class这个是springboot扩展的,由于它占用了Main-Class属性,所以在定义个Start-Class来作为入口。
可以看到Premain-Class和Main-Class这两个是jvm提供的应用配置属性,我们可以猜测下它的逻辑,源码就不看了
伪代码如下:
//伪代码逻辑
main(){
//这里为什么是个数组,因为可以添加多个agent,理论上应该是循环javaagent的属性,从每个agent中去premain
premains=get("Premain-class")
for(i=0;i
知道了以上的逻辑,这个东西就没有那么神秘了。以下为premain的固定写法
public static void premain(String agentArgs, Instrumentation inst) {...}
我们可以看到这里是两个参数
注意:JVM在执行 premain 方法时,JVM 已经加载了所有的系统类和自定义类,包括我们想要转换的所有类。Transformer 调用的时机是在,在这些类被加载之后,并且在被链接(Linked)和初始化之前,也就是说在这些premain之前所有的类都已经被加载为java.lang.class对象了。
//伪代码逻辑
callbackTransformer(){
transformers=getTranformers();
for(i=0;i
而实现一个transformer只要继承ClassFileTransformer这个类实现它的transform方法即可,我们来讲讲transform中的方法参数
在 ClassFileTransformer 接口中,transform 方法
byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
它的参数如下:
loader: 类加载器对象,代表正在加载类的加载器。如果是启动类加载器加载的类,则 loader 为 null。
className: 要加载类路径+名称,如 java/lang/String。
classBeingRedefined: 如果是重定义(redefine)类,表示 java.lang.Class 对象,否则为 null,可以使用改参数判断类是否已经被修改过了,允不允许再次修改
protectionDomain: 字节码的保护域。它是一个 ProtectionDomain 对象,用于包含与类有关的安全策略信息。可以通过 getPermissions() 方法获取权限信息等。
classfileBuffer: 类文件的字节数组表示形式,即字节码数据。
这些参数提供了我们修改字节码的一些上下文信息。其中, classBeingRedefined 参数可以为 null,表示正在加载一个新的类;如果该参数非空,则表示正在重定义一个已经存在的类。在重定义类的时候,原有的类定义也会被加载器加载在 JVM 中,因此在操作时需要特别注意。而 protectionDomain 参数定义了类的安全策略,可以控制类的访问权限。这些上下文信息可以帮助我们编写更加灵活、健壮的字节码转换器。
注意,在实现字节码转换器时,必须保证返回的字节码是合法的 Java 字节码,否则会导致类加载失败、JVM 崩溃等严重后果。因此,在转换的过程中,需要保证不改变类的语法结构,遵守合法的字节码格式与 Java 虚拟机规范的要求。
注意在transform方法中一定要添加修改的类的限定条件,比如哪些包下的类需要被修改,可以使用通配符来指定,如果你不指定,你会发现transform方法类修改逻辑将不会被执行。(亲测结果)
我们来实现一个具体示例 logagent.jar,该示例通过agent修改Test类的test方法,在调用到test方法时执行下远程日志打印,在方法返回时执行下远程日志打印
需求上很简单,代码实现也很简单
pom引入asm,以及打包插件
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>org.qimogroupId>
<artifactId>logagentartifactId>
<version>1.0-SNAPSHOTversion>
<name>Archetype - logagentname>
<url>http://maven.apache.orgurl>
<packaging>jarpackaging>
<properties>
<asm.version>9.5asm.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.ow2.asmgroupId>
<artifactId>asm-bomartifactId>
<version>${asm.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>org.ow2.asmgroupId>
<artifactId>asmartifactId>
dependency>
<dependency>
<groupId>org.ow2.asmgroupId>
<artifactId>asm-commonsartifactId>
dependency>
<dependency>
<groupId>org.ow2.asmgroupId>
<artifactId>asm-treeartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-shade-pluginartifactId>
<configuration>
<createDependencyReducedPom>falsecreateDependencyReducedPom>
<finalName>${project.artifactId}-${project.version}-agentfinalName>
<filters>
<filter>
<artifact>*:*artifact>
<excludes>
<exclude>META-INF/**exclude>
excludes>
filter>
filters>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Premain-Class>com.qimo.PreMainPremain-Class>
<Can-Redefine-Classes>trueCan-Redefine-Classes>
<Can-Retransform-Classes>trueCan-Retransform-Classes>
manifestEntries>
transformer>
transformers>
configuration>
<executions>
<execution>
<phase>packagephase>
<goals>
<goal>shadegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
project>
定义一个PreMain.java
package com.qimo;
import java.lang.instrument.Instrumentation;
public class PreMain {
private PreMain(){
//防止被外部实例化
}
public static void premain(String agentArgs, Instrumentation inst) {
//需要外部传入参数时通过切割等方式转换agentArgs为map或者对象,我此处实现主流程就不麻烦了
System.out.println("执行premain方法");
System.out.println("添加TransFormer:LogClassTransformer");
inst.addTransformer(new LogClassTransformer());
System.out.println("premain执行结束");
}
}
定义一个LogClassTransFormer
package com.qimo;
import org.objectweb.asm.*;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
public class LogClassTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
//System.out.println("aaaaaaaaaaaaaaaaaaaaaaaaa");
//todo 此处有个疑惑,如果不进行className筛选,则后面的代码是无法执行的,这是社么原理
System.out.println(className + "::transForm方法回调");
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, cw) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
if (name.equals("test")) {
System.out.println("找到test方法,并进行字节码修改");
mv = new AddPrintlnMethodVisitor(mv);
}
return mv;
}
};
cr.accept(cv, ClassReader.EXPAND_FRAMES);
return cw.toByteArray();
}
}
插桩的具体逻辑AddPrintlnMethodVisitor.java
package com.qimo;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class AddPrintlnMethodVisitor extends MethodVisitor {
public AddPrintlnMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM5, mv);
}
@Override
public void visitCode() {
mv.visitCode();
mv.visitLdcInsn("{\"tag\":\"进入test方法\"}");
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/qimo/util/LogUtil", "log", "(Ljava/lang/String;)V", false);
}
@Override
public void visitInsn(int opcode) {
if (opcode == Opcodes.RETURN) {
mv.visitCode();
mv.visitLdcInsn("{\"tag\":\"从test方法return\"}");
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/qimo/util/LogUtil", "log", "(Ljava/lang/String;)V", false);
}
mv.visitInsn(opcode);
}
}
一个简单的日志收集类com/qimo/util/LogUtil
package com.qimo.util;
import okhttp3.*;
import java.io.IOException;
public class LogUtil {
public static final String url="http://localhost:8080/log";
public static void log(String data){
OkHttpClient client = new OkHttpClient();
MediaType mediaType = MediaType.parse("application/json");
RequestBody body = RequestBody.create(mediaType, data);
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
try {
client.newCall(request).execute();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
执行mvn clean package打包项目,打包完成后target目录下会出现两个jar文件
logagent-1.0-SNAPSHOT.jar
logagent-1.0-SNAPSHOT-agent.jar
我们选择第二个,第二个文件就是符合agent规范的ManiFest.mf的包
另起一个项目作为服务端,非常简单,我们称之为log-server
启动一个简单的springboot项目,写一个log接口,接收日志
@RestController
public class LogCollectController {
@PostMapping("/log")
public void collect(@RequestBody Map<Object,Object> data){
System.out.println(data.toString());
}
}
在请一个简单的项目,写一个Test类,并写个test方法,方法随便你写点啥,我们作为agent-client-demo
package com.asm;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class Test {
public void test(){
int i=10;
System.out.println(i);
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
写个controller调用该test方法
package com;
import com.asm.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@Autowired
Test test;
@GetMapping("/agent/test")
public void agentTest(){
test.test();
}
}
给该项目配置logagent.jar,为了方便调试我们顺便配置上jdwp
-javaagent:C:\\Users\\86180\\Desktop\\jacoco\\logagent-1.0-SNAPSHOT-agent.jar
-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005
-Dserver.port=8090
注意suspend=y时当项目启动时将会等待远程调试接入,所以如果你不需要调试启动过程可以设置suspend=n关闭它
启动项目,并且访问http://localhost:8090/agent/test 此时你会发现log-server的控制台会打印出两行日志
{tag=进入test方法}
{tag=从test方法return}
总体来说agent+字节码插桩还是特别简单的。