使用javaagen进行字节码插桩实现日志收集

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<premains.size;i++){
    execute(premain[i]);
  }
  main=get("Main-Class")
  execute(main)
}

知道了以上的逻辑,这个东西就没有那么神秘了。以下为premain的固定写法

public static void premain(String agentArgs, Instrumentation inst) {...}

我们可以看到这里是两个参数

  • 第一个参数是接收从外部传进来的agent组件的参数,具体怎么解析可以自定义,你可以传一个配置文件进来,然后去解析配置文件也行
  • 第二个参数是 Java Agent 操作的核心类,它提供了关于类动态修改、资源管理等的API;我们可以往里面注册类修改器transformer。这里仅仅是注册,那么它的回调逻辑是隐藏在jvm中的,所以我们此处我们也是无法看到的,但是我们同样可以猜测下它的逻辑;

注意:JVM在执行 premain 方法时,JVM 已经加载了所有的系统类和自定义类,包括我们想要转换的所有类。Transformer 调用的时机是在,在这些类被加载之后,并且在被链接(Linked)和初始化之前,也就是说在这些premain之前所有的类都已经被加载为java.lang.class对象了。

//伪代码逻辑
callbackTransformer(){
    transformers=getTranformers();
    for(i=0;i<tansfromers;i++){
        transformers[i].transform
    }
}

而实现一个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.0</modelVersion>
  <groupId>org.qimo</groupId>
  <artifactId>logagent</artifactId>
  <version>1.0-SNAPSHOT</version>
  <name>Archetype - logagent</name>
  <url>http://maven.apache.org</url>
  <packaging>jar</packaging>

  <properties>
    <asm.version>9.5</asm.version>
  </properties>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.ow2.asm</groupId>
        <artifactId>asm-bom</artifactId>
        <version>${asm.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>org.ow2.asm</groupId>
      <artifactId>asm</artifactId>
    </dependency>
    <dependency>
      <groupId>org.ow2.asm</groupId>
      <artifactId>asm-commons</artifactId>
    </dependency>
    <dependency>
      <groupId>org.ow2.asm</groupId>
      <artifactId>asm-tree</artifactId>
    </dependency>

  </dependencies>
  <build>
    <plugins>
      <!-- 使用 maven-shade-plugin 插件打包 -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <configuration>
          <createDependencyReducedPom>false</createDependencyReducedPom>
          <finalName>${project.artifactId}-${project.version}-agent</finalName>
          <filters>
            <filter>
              <artifact>*:*</artifact>
              <excludes>
                <exclude>META-INF/**</exclude>
              </excludes>
            </filter>
          </filters>
          <transformers>
            <!-- 使用 manifest-only 插件生成 MANIFEST.MF 文件 -->
            <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
              <manifestEntries>
                <Premain-Class>com.qimo.PreMain</Premain-Class>
                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                <Can-Retransform-Classes>true</Can-Retransform-Classes>
              </manifestEntries>
            </transformer>
          </transformers>
        </configuration>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </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+字节码插桩还是特别简单的。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值