java高级开发jvmjava基础

Java探针(javaagent)

2022-08-05  本文已影响0人  老鼠AI大米_Java全栈

JDK1.5开始引入了Agent机制(即启动java程序时添加“-javaagent”参数,Java Agent机制允许用户在JVM加载class文件的时候先加载自己编写的Agent文件,通过修改JVM传入的字节码来实现注入自定义的代码。采用这种方式时,必须在容器启动时添加jvm参数,所以需要重启Web容器。
JDK1.6新增了attach方式,可以对运行中的java进程附加agent,提供了动态修改运行中已经被加载的类的途径。一般通过VirtualMachine的attach(pid)方法获得VirtualMachine实例,随后可调用loadagent方法将JavaAgent的jar包加载到目标JVM中。

什么是java agent?

在JVM中运行中,类是通过classLoader加载.class文件进行生成的。在类加载加载.class文件生成对应的类对象之前时,我们可以通过修改.class文件内容(就是字节码修改技术),达到修改类的目的。JDK提供了对字节码进行操作的一系列api,而使用这些api开发出的程序就可以称之为java agent。

java agent能做什么?

不修改目标应用达到代码增强的目的,就好像spring的aop一样,但是java agent是直接修改字节码,而不是通过创建代理类。例如skywalking就是使用java agent技术,为目标应用代码植入监控代码,监控代码进行数据统计上报的。这种方式实现了解耦,通用的功能。

javaagent作用

javaagent使用方式

java -javaagent:D:\workspace\javaagent.jar=hello1 
-javaagent:D:\workspace\javaagent.jar=hello2 -jar D:\workspace\myTest.jar

注: hello1是参数

javaagent其他功能

静态agent与动态agent

Agent分为如下两种:

静态Instrument(启动时)加载Instrument过程

动态Instrument运行时加载Instrument过程

通过JVM的attach机制来请求目标JVM加载对应的agent,过程大致如下:

示例1: 简单例子

agent程序

1.提供premain方法

package com.example.a;
 
import java.lang.instrument.Instrumentation;
 
public class DemoAgent {
    /**
     * 该方法在main方法之前运行,与main方法运行在同一个JVM中
     */
    public static void premain(String arg, Instrumentation instrumentation) {
        System.out.println("agent的premain(String arg, Instrumentation instrumentation)方法");
    }
 
    /**
     * 若不存在 premain(String agentArgs, Instrumentation inst),
     * 则会执行 premain(String agentArgs)
     */
    public static void premain(String arg) {
        System.out.println("agent的premain(String arg)方法");
    }
}

2.提供META-INF/MANIFEST.MF

在src/main/java的同级目录下新建META-INF文件夹,在里边新建MANIFEST.MF文件(注意最后一行必须是空行)

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.example.a.DemoAgent

3.将其打包为jar包

步骤1:打包的配置入口

File=> Project Structure=> Project Settings=> Artifacts=> + => JAR=> From modules with dependencies..


image.png

步骤2:打包的配置


image.png
步骤3:打包

Build=> Build Artifacts...=> Build

此时会生成out目录,并生成jar包:


image.png

也可使用maven配置META-INF/MANIFEST.MF

使用maven,打包方便,而且不用手写META-INF/MANIFEST.MF,用插件即可:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5.1</version>
            <!-- 指定maven编译的jdk版本。若不指定,maven3默认用jdk 1.5 maven2默认用jdk1.3 -->
            <configuration>
                <source>8</source>
                <target>8</target>
            </configuration>
        </plugin>
 
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>3.2.0</version>
            <configuration>
                <archive>
                    <!--自动添加META-INF/MANIFEST.MF -->
                    <manifest>
                        <addClasspath>true</addClasspath>
                    </manifest>
                    <manifestEntries>
                        <Menifest-Version>1.0</Menifest-Version>
                        <Premain-Class>com.example.a.DemoAgent</Premain-Class>
                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                    </manifestEntries>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>

maven的项目结构为:


image.png

应用程序
项目结构


image.png

1.提供main程序

package com.example.a;
public class Demo {
    public static void main(String[] args) {
        System.out.println("应用的main方法");
    }
}

测试
java -javaagent:D:\tmp\demo_javaagent.jar -jar demo_java.jar

结果:


image.png

示例2:统计方法的执行时间

需求:写一个agent,统计应用的某个方法的执行时间。(本处要统计的方法是:TimeTest#test方法)

agent程序

agent代码

package com.example.a;
 
import java.lang.instrument.Instrumentation;
 
public class DemoAgent {
    /**
     * 该方法在main方法之前运行,与main方法运行在同一个JVM中
     */
    public static void premain(String arg, Instrumentation instrumentation) {
        System.out.println("agent的premain(String arg, Instrumentation instrumentation)方法");
 
        instrumentation.addTransformer(new MyTransformer());
    }
 
    /**
     * 若不存在 premain(String agentArgs, Instrumentation inst),
     * 则会执行 premain(String agentArgs)
     */
    public static void premain(String arg) {
        System.out.println("agent的premain(String arg)方法");
    }
}

Transformer代码

package com.example.a;
 
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 MyTransformer implements ClassFileTransformer {
    private final String injectedClass = "com.example.a.TimeTest";
    private final String injectedMethod = "test";
 
    @Override
    public byte[] transform(ClassLoader loader,
                            String className,
                            Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) throws IllegalClassFormatException {
 
        String realClassName = className.replace("/", ".");
 
        if (realClassName.equals(injectedClass)) {
            CtClass ctClass;
            try {
                // 使用全称,取得字节码类<使用javassist>
                ClassPool classPool = ClassPool.getDefault();
                ctClass = classPool.get(realClassName);
 
                // 得到方法实例
                CtMethod ctMethod = ctClass.getDeclaredMethod(injectedMethod);
                // 添加变量
                ctMethod.addLocalVariable("time", CtClass.longType);
                ctMethod.insertBefore("System.out.println(\"------------ Before --------\");");
                ctMethod.insertBefore("time = System.currentTimeMillis();");
 
                ctMethod.insertAfter("System.out.println(\"Elapsed Time(ms): \" + (System.currentTimeMillis() - time));");
                ctMethod.insertAfter("System.out.println(\"------------- After --------\");");
 
                return ctClass.toBytecode();
            } catch (Throwable e) { //这里要用Throwable,不要用Exception
                System.out.println(e.getMessage());
                e.printStackTrace();
            }
        }
 
        // 返回原类字节码
        return classfileBuffer;
    }
}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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.example</groupId>
  <artifactId>demo_javaagent</artifactId>
  <version>1.0-SNAPSHOT</version>
 
  <dependencies>
    <dependency>
      <groupId>org.javassist</groupId>
      <artifactId>javassist</artifactId>
      <version>3.28.0-GA</version>
    </dependency>
  </dependencies>
 
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.5.1</version>
        <!-- 指定maven编译的jdk版本。若不指定,maven3默认用jdk 1.5 maven2默认用jdk1.3 -->
        <configuration>
          <source>8</source>
          <target>8</target>
        </configuration>
      </plugin>
 
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.2.0</version>
        <configuration>
          <archive>
            <!--自动添加META-INF/MANIFEST.MF -->
            <manifest>
              <addClasspath>true</addClasspath>
            </manifest>
            <manifestEntries>
              <Menifest-Version>1.0</Menifest-Version>
              <Premain-Class>com.example.a.DemoAgent</Premain-Class>
              <Can-Redefine-Classes>true</Can-Redefine-Classes>
              <Can-Retransform-Classes>true</Can-Retransform-Classes>
            </manifestEntries>
          </archive>
        </configuration>
      </plugin>
    </plugins>
  </build>
 
</project>

应用程序
main类

package com.example.a;
 
public class Demo {
    public static void main(String[] args) {
        System.out.println("应用的main方法");
        new TimeTest().test();
    }
}

测试类

package com.example.a;
 
public class TimeTest {
    public void test() {
        System.out.println("开始执行TimeTest#test");
        System.out.println("sleep开始");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sleep结束");
    }
}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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.example</groupId>
    <artifactId>demo_maven</artifactId>
    <version>1.0-SNAPSHOT</version>
 
    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.28.0-GA</version>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <!-- 指定maven编译的jdk版本。若不指定,maven3默认用jdk 1.5 maven2默认用jdk1.3 -->
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
 
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.example.a.Demo</mainClass>
                        </manifest>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <appendAssemblyId>false</appendAssemblyId>
 
                </configuration>
 
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>assembly</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

测试
java -javaagent:D:\tmp\demo_javaagent-1.0-SNAPSHOT.jar -jar demo_maven-1.0-SNAPSHOT.jar

结果:


image.png

Agentmain(attach)

在 Java SE 6 的 Instrumentation 当中,提供了一个新的代理操作方法:agentmain,可以在 main 函数开始运行之后再运行。
跟premain函数一样, 开发者可以编写一个含有agentmain函数的 Java 类:

//采用attach机制,被代理的目标程序VM有可能很早之前已经启动,当然其所有类已经被加载完成,
//这个时候需要借助Instrumentation#retransformClasses(Class<?>... classes)
//让对应的类可以重新转换,从而激活重新转换的类执行ClassFileTransformer列表中的回调
public static void agentmain (String agentArgs, Instrumentation inst)

public static void agentmain (String agentArgs)

agentMain 主要用于对java程序的监控,调用java进程,将自己编写的agentMain 注入目标完成对程序的监控,修改。

创建agentmain

public class TestMainAgent {
    public static void agentmain(String agentArgs, Instrumentation instrumentation) {
        System.out.println("loadagent after main run.args=" + agentArgs);

        Class<?>[] classes = instrumentation.getAllLoadedClasses();

        for (Class<?> cls : classes)
        {
            System.out.println(cls.getName());
        }

        System.out.println("agent run completely.");
    }

    static class DefineTransformer implements ClassFileTransformer {

        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            System.out.println("premain load Class:" + className);
            return classfileBuffer;
        }
    }
}

添加maven插件打包

 <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <!--自动添加META-INF/MANIFEST.MF -->
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Agent-Class>com.tttiger.TestMainAgent</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

测试agentMain插桩到其他类
另外启用了一个jvm进程,找到需要attach的jvm进程,让它加载agentMain,那么agentMain就会被加载到对方jvm执行。arthas就是使用这种方式attach进jvm进程,开启一个socket然后进行目标jvm的监控。

    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, InterruptedException {
        //获取当前系统中所有 运行中的 虚拟机
        System.out.println("running JVM start ");
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) {
            //如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid
            //然后加载 agent.jar 发送给该虚拟机
            System.out.println(vmd.displayName());
            if (vmd.displayName().endsWith("com.tttiger.TestJVM")) {
                System.out.println(vmd.id());
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                virtualMachine.loadAgent("e:/test-agentMain-1.0-SNAPSHOT.jar");
                virtualMachine.detach();
                System.out.println("attach");
            }
        }
        Thread.sleep(10000L);
    }

VirtualMachine 字面意义表示一个Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了获取系统信息(比如获取内存dump、线程dump,类信息统计(比如已加载的类以及实例个数等), loadAgent,Attach 和 Detach (Attach 动作的相反行为,从 JVM 上面解除一个代理)等方法,可以实现的功能可以说非常之强大 。该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上 。

代理类注入操作只是它众多功能中的一个,通过loadAgent方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理。

VirtualMachineDescriptor 则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能

通过VirtualMachine类的attach(pid)方法,便可以attach到一个运行中的java进程上,之后便可以通过loadAgent(agentJarPath)来将agent的jar包注入到对应的进程,然后对应的进程会调用agentmain方法。


image.png

Instrumentation的局限性
大多数情况下,我们使用Instrumentation都是使用其字节码插桩的功能,或者笼统说就是类重定义(Class Redefine)的功能,但是有以下的局限性:

  1. premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
  2. 类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses()方法,此方法有以下限制:

除了上面的方式,如果想要重新定义一个类,可以考虑基于类加载器隔离的方式:创建一个新的自定义类加载器去通过新的字节码去定义一个全新的类,不过也存在只能通过反射调用该全新类的局限性。

上一篇下一篇

猜你喜欢

热点阅读