java agent使用全解析

今天打算写一下 Java agent,一开始我对它的概念也比较陌生,后来在别人口中听到 字节码插桩,bTrace,Arthas后面才逐渐了解到Java还提供了这么个工具。

JVM启动前静态Instrument

Java agent 是什么?

Java agent是java命令的一个参数。参数 javaagent 可以用于指定一个 jar 包,并且对该 java 包有2个要求:

  1. 这个 jar 包的 MANIFEST.MF 文件必须指定 Premain-Class 项。
  2. Premain-Class 指定的那个类必须实现 premain() 方法。

premain 方法,从字面上理解,就是运行在 main 函数之前的的类。当Java 虚拟机启动时,在执行 main 函数之前,JVM 会先运行-javaagent所指定 jar 包内 Premain-Class 这个类的 premain 方法 。

在命令行输入 java可以看到相应的参数,其中有 和 java agent相关的:

-agentlib:<libname>[=<选项>] 加载本机代理库 <libname>, 例如 -agentlib:hprof
 另请参阅 -agentlib:jdwp=help 和 -agentlib:hprof=help
-agentpath:<pathname>[=<选项>]
 按完整路径名加载本机代理库
-javaagent:<jarpath>[=<选项>]
 加载 Java 编程语言代理, 请参阅 java.lang.instrument

在上面-javaagent参数中提到了参阅java.lang.instrument,这是在rt.jar 中定义的一个包,该路径下有两个重要的类:

该包提供了一些工具帮助开发人员在 Java 程序运行时,动态修改系统中的 Class 类型。其中,使用该软件包的一个关键组件就是 Javaagent。从名字上看,似乎是个 Java 代理之类的,而实际上,他的功能更像是一个Class 类型的转换器,他可以在运行时接受重新外部请求,对Class类型进行修改。

从本质上讲,Java Agent 是一个遵循一组严格约定的常规 Java 类。 上面说到 javaagent命令要求指定的类中必须要有premain()方法,并且对premain方法的签名也有要求,签名必须满足以下两种格式:

public static void premain(String agentArgs, Instrumentation inst)

public static void premain(String agentArgs)

JVM 会优先加载 带 Instrumentation 签名的方法,加载成功忽略第二种,如果第一种没有,则加载第二种方法。这个逻辑在sun.instrument.InstrumentationImpl 类中:

Instrumentation 类 定义如下:

public interface Instrumentation {

  //增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
  void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

  //在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
  void addTransformer(ClassFileTransformer transformer);

  //删除一个类转换器
  boolean removeTransformer(ClassFileTransformer transformer);

  boolean isRetransformClassesSupported();

  //在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
  void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

  boolean isRedefineClassesSupported();

  void redefineClasses(ClassDefinition... definitions)
    throws ClassNotFoundException, UnmodifiableClassException;

  boolean isModifiableClass(Class<?> theClass);

  @SuppressWarnings("rawtypes")
  Class[] getAllLoadedClasses();

  @SuppressWarnings("rawtypes")
  Class[] getInitiatedClasses(ClassLoader loader);

  //获取一个对象的大小
  long getObjectSize(Object objectToSize);

  void appendToBootstrapClassLoaderSearch(JarFile jarfile);

  void appendToSystemClassLoaderSearch(JarFile jarfile);

  boolean isNativeMethodPrefixSupported();

  void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}

最为重要的是上面注释的几个方法,下面我们会用到。

如何使用javaagent?

使用 javaagent 需要几个步骤:

  1. 定义一个 MANIFEST.MF 文件,必须包含 Premain-Class 选项,通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项。
  2. 创建一个Premain-Class 指定的类,类中包含 premain 方法,方法逻辑由用户自己确定。
  3. 将 premain 的类和 MANIFEST.MF 文件打成 jar 包。
  4. 使用参数 -javaagent: jar包路径 启动要代理的方法。

在执行以上步骤后,JVM 会先执行 premain 方法,大部分类加载都会通过该方法,注意:是大部分,不是所有。当然,遗漏的主要是系统类,因为很多系统类先于 agent 执行,而用户类的加载肯定是会被拦截的。也就是说,这个方法是在 main 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,那么就可以去做重写类这样的操作,结合第三方的字节码编译工具,比如ASM,javassist,cglib等等来改写实现类。

通过上面的步骤我们用代码实现来实现。实现 javaagent 你需要搭建两个工程,一个工程是用来承载 javaagent类,单独的打成jar包;一个工程是javaagent需要去代理的类。即javaagent会在这个工程中的main方法启动之前去做一些事情。

1.首先来实现javaagent工程。

工程目录结构如下:

-java-agent
----src
--------main
--------|------java
--------|----------com.rickiyang.learn
--------|------------PreMainTraceAgent
--------|resources
-----------META-INF
--------------MANIFEST.MF

第一步是需要创建一个类,包含premain 方法:

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

/**
 * @author: rickiyang
 * @date: 2019/8/12
 * @description:
 */
public class PreMainTraceAgent {

  public static void premain(String agentArgs, Instrumentation inst) {
    System.out.println("agentArgs : " + agentArgs);
    inst.addTransformer(new DefineTransformer(), true);
  }

  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;
    }
  }
}

上面就是我实现的一个类,实现了带Instrumentation参数的premain()方法。调用addTransformer()方法对启动时所有的类进行拦截。

然后在 resources 目录下新建目录:META-INF,在该目录下新建文件:MANIFREST.MF:

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: PreMainTraceAgent

注意到第5行有空行。

说一下MANIFREST.MF文件的作用,这里如果你不去手动指定的话,直接 打包,默认会在打包的文件中生成一个MANIFREST.MF文件:

Manifest-Version: 1.0
Implementation-Title: test-agent
Implementation-Version: 0.0.1-SNAPSHOT
Built-By: yangyue
Implementation-Vendor-Id: com.rickiyang.learn
Spring-Boot-Version: 2.0.9.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.rickiyang.learn.LearnApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.5.2
Build-Jdk: 1.8.0_151
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
 ot-starter-parent/test-agent

这是默认的文件,包含当前的一些版本信息,当前工程的启动类,它还有别的参数允许你做更多的事情,可以用上的有:

  • Premain-Class :包含 premain 方法的类(类的全路径名)
  • Agent-Class :包含 agentmain 方法的类(类的全路径名)
  • Boot-Class-Path :设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径。(可选)
  • Can-Redefine-Classes :true表示能重定义此代理所需的类,默认值为 false(可选)
  • Can-Retransform-Classes :true 表示能重转换此代理所需的类,默认值为 false (可选)
  • Can-Set-Native-Method-Prefix: true表示能设置此代理所需的本机方法前缀,默认值为 false(可选)

即在该文件中主要定义了程序运行相关的配置信息,程序运行前会先检测该文件中的配置项。

一个java程序中-javaagent参数的个数是没有限制的,所以可以添加任意多个javaagent。所有的java agent会按照你定义的顺序执行,例如:

java -javaagent:agent1.jar -javaagent:agent2.jar -jar MyProgram.jar

程序执行的顺序将会是:

MyAgent1.premain -> MyAgent2.premain -> MyProgram.main

说回上面的 javaagent工程,接下来将该工程打成jar包,我在打包的时候发现打完包之后的 MANIFREST.MF文件被默认配置替换掉了。所以我是手动将上面我的配置文件替换到jar包中的文件,这里你需要注意。

另外的再说一种不去手动写MANIFREST.MF文件的方式,使用maven插件:

<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>
        <Premain-Class>com.rickiyang.learn.PreMainTraceAgent</Premain-Class>
        <Agent-Class>com.rickiyang.learn.PreMainTraceAgent</Agent-Class>
        <Can-Redefine-Classes>true</Can-Redefine-Classes>
        <Can-Retransform-Classes>true</Can-Retransform-Classes>
      </manifestEntries>
    </archive>
  </configuration>
</plugin>

用这种插件的方式也可以自动生成该文件。

agent代码就写完了,下面再重新开一个工程,你只需要写一个带 main 方法的类即可:

public class TestMain {

  public static void main(String[] args) {
    System.out.println("main start");
    try {
      Thread.sleep(3000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("main end");
  }
}

很简单,然后需要做的就是将上面的 代理类 和 这个测试类关联起来。有两种方式:

如果你用的是idea,那么你可以点击菜单: run-debug configuration,然后将你的代理类包 指定在 启动参数中即可:

另一种方式是不用 编译器,采用命令行的方法。与上面大致相同,将 上面的测试类编译成 class文件,然后 运行该类即可:

 #将该类编译成class文件
 > javac TestMain.java

 #指定agent程序并运行该类
 > java -javaagent:c:/alg.jar TestMain

使用上面两种方式都可以运行,输出结果如下:

D:\soft\jdk1.8\bin\java.exe -javaagent:c:/alg.jar "-javaagent:D:\soft\IntelliJ IDEA 2019.1.1\lib\idea_rt.jar=54274:D:\soft\IntelliJ IDEA 2019.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\soft\jdk1.8\jre\lib\charsets.jar;D:\soft\jdk1.8\jre\lib\deploy.jar;D:\soft\jdk1.8\jre\lib\ext\access-bridge-64.jar;D:\soft\jdk1.8\jre\lib\ext\cldrdata.jar;D:\soft\jdk1.8\jre\lib\ext\dnsns.jar;D:\soft\jdk1.8\jre\lib\ext\jaccess.jar;D:\soft\jdk1.8\jre\lib\ext\jfxrt.jar;D:\soft\jdk1.8\jre\lib\ext\localedata.jar;D:\soft\jdk1.8\jre\lib\ext\nashorn.jar;D:\soft\jdk1.8\jre\lib\ext\sunec.jar;D:\soft\jdk1.8\jre\lib\ext\sunjce_provider.jar;D:\soft\jdk1.8\jre\lib\ext\sunmscapi.jar;D:\soft\jdk1.8\jre\lib\ext\sunpkcs11.jar;D:\soft\jdk1.8\jre\lib\ext\zipfs.jar;D:\soft\jdk1.8\jre\lib\javaws.jar;D:\soft\jdk1.8\jre\lib\jce.jar;D:\soft\jdk1.8\jre\lib\jfr.jar;D:\soft\jdk1.8\jre\lib\jfxswt.jar;D:\soft\jdk1.8\jre\lib\jsse.jar;D:\soft\jdk1.8\jre\lib\management-agent.jar;D:\soft\jdk1.8\jre\lib\plugin.jar;D:\soft\jdk1.8\jre\lib\resources.jar;D:\soft\jdk1.8\jre\lib\rt.jar;D:\workspace\demo1\target\classes;E:\.m2\repository\org\springframework\boot\spring-boot-starter-aop\2.1.1.RELEASE\spring-
...
...
...
1.8.11.jar;E:\.m2\repository\com\google\guava\guava\20.0\guava-20.0.jar;E:\.m2\repository\org\apache\commons\commons-lang3\3.7\commons-lang3-3.7.jar;E:\.m2\repository\com\alibaba\fastjson\1.2.54\fastjson-1.2.54.jar;E:\.m2\repository\org\springframework\boot\spring-boot\2.1.0.RELEASE\spring-boot-2.1.0.RELEASE.jar;E:\.m2\repository\org\springframework\spring-context\5.1.3.RELEASE\spring-context-5.1.3.RELEASE.jar com.springboot.example.demo.service.TestMain
agentArgs : null
premain load Class     :java/util/concurrent/ConcurrentHashMap$ForwardingNode
premain load Class     :sun/nio/cs/ThreadLocalCoders
premain load Class     :sun/nio/cs/ThreadLocalCoders$1
premain load Class     :sun/nio/cs/ThreadLocalCoders$Cache
premain load Class     :sun/nio/cs/ThreadLocalCoders$2
premain load Class     :java/util/jar/Attributes
premain load Class     :java/util/jar/Manifest$FastInputStream
...
...
...
premain load Class     :java/lang/Class$MethodArray
premain load Class     :java/lang/Void
main start
premain load Class     :sun/misc/VMSupport
premain load Class     :java/util/Hashtable$KeySet
premain load Class     :sun/nio/cs/ISO_8859_1$Encoder
premain load Class     :sun/nio/cs/Surrogate$Parser
premain load Class     :sun/nio/cs/Surrogate
...
...
...
premain load Class     :sun/util/locale/provider/LocaleResources$ResourceReference
main end
premain load Class     :java/lang/Shutdown
premain load Class     :java/lang/Shutdown$Lock

Process finished with exit code 0

上面的输出结果我们能够发现:

  • 执行main方法之前会加载所有的类,包括系统类和自定义类;
  • 在ClassFileTransformer中会去拦截系统类和自己实现的类对象;
  • 如果你有对某些类对象进行改写,那么在拦截的时候抓住该类使用字节码编译工具即可实现。

下面是使用javassist来动态将某个方法替换掉:

package com.rickiyang.learn;

import javassist.*;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

/**
 * @author rickiyang
 * @date 2019-08-06
 * @Desc
 */
public class MyClassTransformer implements ClassFileTransformer {
  @Override
  public byte[] transform(final ClassLoader loader, final String className, final Class<?> classBeingRedefined,final ProtectionDomain protectionDomain, final byte[] classfileBuffer) {
    // 操作Date类
    if ("java/util/Date".equals(className)) {
      try {
        // 从ClassPool获得CtClass对象
        final ClassPool classPool = ClassPool.getDefault();
        final CtClass clazz = classPool.get("java.util.Date");
        CtMethod convertToAbbr = clazz.getDeclaredMethod("convertToAbbr");
        //这里对 java.util.Date.convertToAbbr() 方法进行了改写,在 return之前增加了一个 打印操作
        String methodBody = "{sb.append(Character.toUpperCase(name.charAt(0)));" +
            "sb.append(name.charAt(1)).append(name.charAt(2));" +
            "System.out.println(\"sb.toString()\");" +
            "return sb;}";
        convertToAbbr.setBody(methodBody);

        // 返回字节码,并且detachCtClass对象
        byte[] byteCode = clazz.toBytecode();
        //detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载
        clazz.detach();
        return byteCode;
      } catch (Exception ex) {
        ex.printStackTrace();
      }
    }
    // 如果返回null则字节码不会被修改
    return null;
  }
}

JVM启动后动态Instrument

上面介绍的Instrumentation是在 JDK 1.5中提供的,开发者只能在main加载之前添加手脚,在 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 方法中带Instrumentation参数的方法也比不带优先级更高。开发者必须在 manifest 文件里面设置“Agent-Class”来指定包含 agentmain 函数的类。

在Java6 以后实现启动后加载的新实现是Attach api。Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach 包里面:

  1. VirtualMachine 字面意义表示一个Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了获取系统信息(比如获取内存dump、线程dump,类信息统计(比如已加载的类以及实例个数等), loadAgent,Attach 和 Detach (Attach 动作的相反行为,从 JVM 上面解除一个代理)等方法,可以实现的功能可以说非常之强大 。该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上 。代理类注入操作只是它众多功能中的一个,通过loadAgent方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理。
  2. VirtualMachineDescriptor 则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。

attach实现动态注入的原理如下:

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

既然是两个进程之间通信那肯定的建立起连接,VirtualMachine.attach动作类似TCP创建连接的三次握手,目的就是搭建attach通信的连接。而后面执行的操作,例如vm.loadAgent,其实就是向这个socket写入数据流,接收方target VM会针对不同的传入数据来做不同的处理。

我们来测试一下agentmain的使用:

工程结构和 上面premain的测试一样,编写AgentMainTest,然后使用maven插件打包 生成MANIFEST.MF。

package com.rickiyang.learn;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

/**
 * @author rickiyang
 * @date 2019-08-16
 * @Desc
 */
public class AgentMainTest {

  public static void agentmain(String agentArgs, Instrumentation instrumentation) {
    instrumentation.addTransformer(new DefineTransformer(), true);
  }

  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;
    }
  }
}
<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.rickiyang.learn.AgentMainTest</Agent-Class>
    <Can-Redefine-Classes>true</Can-Redefine-Classes>
    <Can-Retransform-Classes>true</Can-Retransform-Classes>
   </manifestEntries>
  </archive>
 </configuration>
</plugin>

将agent打包之后,就是编写测试main方法。上面我们画的图中的步骤是:从一个attach JVM去探测目标JVM,如果目标JVM存在则向它发送agent.jar。我测试写的简单了些,找到当前JVM并加载agent.jar。

package com.rickiyang.learn.job;

import com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List;

/**
 * @author rickiyang
 * @date 2019-08-16
 * @Desc
 */
public class TestAgentMain {

  public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
    //获取当前系统中所有 运行中的 虚拟机
    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.rickiyang.learn.job.TestAgentMain")) {
        VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
        virtualMachine.loadAgent("/Users/yangyue/Documents/java-agent.jar");
        virtualMachine.detach();
      }
    }
  }

}

list()方法会去寻找当前系统中所有运行着的JVM进程,你可以打印vmd.displayName()看到当前系统都有哪些JVM进程在运行。因为main函数执行起来的时候进程名为当前类名,所以通过这种方式可以去找到当前的进程id。

注意:在mac上安装了的jdk是能直接找到 VirtualMachine 类的,但是在windows中安装的jdk无法找到,如果你遇到这种情况,请手动将你jdk安装目录下:lib目录中的tools.jar添加进当前工程的Libraries中。

运行main方法的输出为:

可以看到实际上是启动了一个socket进程去传输agent.jar。先打印了“running JVM start”表名main方法是先启动了,然后才进入代理类的transform方法。

instrument原理

instrument的底层实现依赖于JVMTI(JVM Tool Interface),它是JVM暴露出来的一些供用户扩展的接口集合,JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者去扩展自己的逻辑。JVMTIAgent是一个利用JVMTI暴露出来的接口提供了代理启动时加载(agent on load)、代理通过attach形式加载(agent on attach)和代理卸载(agent on unload)功能的动态库。而instrument agent可以理解为一类JVMTIAgent动态库,别名是JPLISAgent(Java Programming Language Instrumentation Services Agent),也就是专门为java语言编写的插桩服务提供支持的代理。

启动时加载instrument agent过程:

1.创建并初始化 JPLISAgent;

2.监听 VMInit 事件,在 JVM 初始化完成之后做下面的事情:

  1. 创建 InstrumentationImpl 对象 ;
  2. 监听 ClassFileLoadHook 事件 ;
  3. 调用 InstrumentationImpl 的loadClassAndCallPremain方法,在这个方法里会去调用 javaagent 中 MANIFEST.MF 里指定的Premain-Class 类的 premain 方法 ;

3.解析 javaagent 中 MANIFEST.MF 文件的参数,并根据这些参数来设置 JPLISAgent 里的一些内容。

运行时加载instrument agent过程:

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

1.创建并初始化JPLISAgent;

2.解析 javaagent 里 MANIFEST.MF 里的参数;

3.创建 InstrumentationImpl 对象;

4.监听 ClassFileLoadHook 事件;

5.调用 InstrumentationImpl 的loadClassAndCallAgentmain方法,在这个方法里会去调用javaagent里 MANIFEST.MF 里指定的Agent-Class类的agentmain方法。

Instrumentation的局限性

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

1.premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。

2.类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses()方法,此方法有以下限制:

  1. 新类和老类的父类必须相同;
  2. 新类和老类实现的接口数也要相同,并且是相同的接口;
  3. 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致;
  4. 新类和老类新增或删除的方法必须是private static/final修饰的;
  5. 可以修改方法体。

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

以上就是javaagent使用全解析的详细内容,更多关于javaagent 使用的资料请关注我们其它相关文章!

(0)

相关推荐

  • java request.getHeader("user-agent")获取浏览器信息的方法

    一.User Agent的含义 User Agent中文名为用户代理,简称 UA,它是一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本.CPU 类型.浏览器及版本.浏览器渲染引擎.浏览器语言.浏览器插件等. 一些网站常常通过判断 UA 来给不同的操作系统.不同的浏览器发送不同的页面,因此可能造成某些页面无法在某个浏览器中正常显示,但通过伪装 UA 可以绕过检测. 浏览器的 UA 字串 标准格式为: 浏览器标识 (操作系统标识; 加密等级标识; 浏览器语言) 渲染引擎标识 版本信息 浏

  • java agent 使用及实现代码

    java agent的作用 在字节码这个层面对类和方法进行修改的技术,能够在不影响编译的情况下,修改字节码.可以理解spring的aop技术 如何实现 1.实现javaagent需要实现premain方法 2.必须在MANIFEST.MF文件中有Premain-Class demo实现 agent package com.xueyou.demo.agent; import javassist.ClassPool; import javassist.CtClass; import javassis

  • Java Agent入门学习之动态修改代码

    前言 最近用了一下午总算把Java agent给跑通了,本篇文章记录一下具体的操作步骤,以免遗忘.下面话不多说,来一起看看详细的介绍: 通过java agent可以动态修改代码(替换.修改类的定义),进行AOP. 目标: 为所有添加@ToString注解的类实现默认的toString方法 需要两个程序,一个是用来测试的程序,一个agent用于修改代码. 1. 测试程序 被测试的程序包括: - ToString.Java - Foo.java - Main.java 具体代码如下: ToStrin

  • Java使用agent实现main方法之前的实例详解

    Java使用agent实现main方法之前的实例详解 创建Agent项目 PreMainExecutor 类,在main方法之前执行此方法 public class PreMainExecutor { public static void premain(String agentOps, Instrumentation inst){ System.out.println("premain execute.........."); } } META-INF/MANIFEST.MF Man

  • java.lang.Instrument 代理Agent使用详细介绍

    java.lang.Instrument 代理Agent使用 java.lang.Instrument包是在JDK5引入的,程序员通过修改方法的字节码实现动态修改类代码.这通常是在类的main方法调用之前进行预处理的操作,通过java指定该类的代理类来实现.在类的字节码载入JVM前会调用ClassFileTransformer的transform方法,从而实现修改原类方法的功能,实现AOP,这个的好处是不会像动态代理或者CGLIB技术实现AOP那样会产生一个新类,也不需要原类要有接口. (1)

  • java 解析user-agent 信息

    解析http user-agent信息,使用uasparser-0.6.1.jar和jregex-1.2_01.jar两个包 import cz.mallat.uasparser.OnlineUpdater; import cz.mallat.uasparser.UASparser; import cz.mallat.uasparser.UserAgentInfo; import java.io.IOException; /** * Created by Edward on 2016/7/1.

  • java agent使用全解析

    今天打算写一下 Java agent,一开始我对它的概念也比较陌生,后来在别人口中听到 字节码插桩,bTrace,Arthas后面才逐渐了解到Java还提供了这么个工具. JVM启动前静态Instrument Java agent 是什么? Java agent是java命令的一个参数.参数 javaagent 可以用于指定一个 jar 包,并且对该 java 包有2个要求: 这个 jar 包的 MANIFEST.MF 文件必须指定 Premain-Class 项. Premain-Class

  • Java 泛型全解析

    泛型简介 什么是泛型? 参化类型,数是JDK1.5的新特性.(定义泛型时使用参数可以简单理解为形参),例如List<E>,Map<K,V> 编译时的一种类型,此类型仅仅在编译阶段有效,运行时无效.例如List<String>在运行时String会被擦除,最终系统会认为都是Object. 为什么要使用泛型? 泛型是进行类型设计或方法定义时的一种约束规范,基于此规范可以: 提高编程时灵活性(有点抽象,后续结合实例理解). 提高程序运行时的性能.(在编译阶段解决一些运行时需要

  • Java信号量全解析

    前言: Semaphore(信号量) 是一个线程同步结构,用于在线程间传递信号,以避免出现信号丢失(译者注:下文会具体介绍),或者像锁一样用于保护一个关键区域.自从5.0开始,jdk在java.util.concurrent包里提供了Semaphore 的官方实现,因此大家不需要自己去实现Semaphore.但是还是很有必要去熟悉如何使用Semaphore及其背后的原理 内容主题: 一.简单的Semaphore实现 下面是一个信号量的简单实现: public class Semaphore {

  • Java Mybatis框架由浅入深全解析下篇

    目录 前言 什么是Maven Maven环境配置 Maven 构建生命周期 Maven项目的创建 目录结构 pom.xml文件 什么是pom.xml文件 加入项目所需依赖 添加资源文件的指定 总结 前言 上一篇我们第一次测试了Mybatis框架,并且成功了. 本想直接推进学习框架配置,但是很多小伙伴对Maven不了解,今天就来浅谈一下Maven. 今天我们就来剖析pom.xml配置文件,这个pom.xml文件,是我们构建maven项目的配置文件,既然我们使用到了,就利用本篇文章学习一下吧.这里只

  • Java Mybatis框架由浅入深全解析中篇

    目录 前言 添加框架的步骤 在idea中添加数据库的可视化 添加jdbc.properties属性文件(数据库配置) 添加SqlMapCongig.xml 创建实体类Student用来封装数据 添加增删改查 创建测试类进行功能测试 总结 前言 上一篇我们了解了框架相关知识,并且导入依赖配置了核心文件,今天就可以开始写代码测试了. 添加框架的步骤 在idea中添加数据库的可视化 这里需要注意:很多小伙伴链接不成功,这个时候要修改一下自己的驱动版本,尽量与数据库版本一致 添加jdbc.propert

  • Java Mybatis框架由浅入深全解析上篇

    目录 学习路线 什么是三层架构 常用的SSM框架(了解) 什么是框架 什么是Mybatis框架 添加框架的步骤 1.新建库建表 2.新建maven项目 3.修改目录 4.修改pom.xml文件 5.修改pom.xml文件 总结 学习路线 什么是三层架构 在项目开发中,遵循一种形式模式,分为三层. 界面层: 用来接收客 户端的输入,调用业务逻辑层进行功能处理,返回结果给客户端.过去的servlet就是界面层的功能. **业务逻辑层:**用来进行整个项目的业务逻辑处理,向上为界面层提供处理结果,向下

  • C# 单元测试全解析

    目录 1.前言 2.单元测试 2.1 单元测试的定义 2.2 单元测试的好处 2.3 单元测试的原则 3..NET 中的测试框架 3.1 MS Test 3.2 NUnit 3.3 XUnit 4.XUnit 的基本使用 5.其他 1.前言 "不会写单元测试的程序员不是合格的程序员,不写单元测试的程序员不是优秀的工程师." 那么问题来了,什么是单元测试,如何做单元测试. 2.单元测试 2.1 单元测试的定义 按照维基百科上的说法,单元测试(Unit Testing)又称为模块测试, 是

  • java spi最全使用总结

    目录 前言 一.JDK中SPI的使用规范 案例展示 SPI优点 SPI缺点 SPI机制在实际生产中的一个应用 二.DUbbo中SPI的使用 Dubbo的SPI举例 三.springboot中SPI思想的使用 前言 在开发过程中,经常要用到第三方提供的SDK来完成一些业务扩展功能,比如调用第三方的发短信.图片验证码.人脸识别等等功能,但问题是,第三方SDK只是提供了标准的功能实现,某些场景下,开发者还想基于这些SDK做一些个性化的定制和扩展,那要怎么办呢? 于是,一些优秀的SDK就通过SPI的机制

  • 通过使用Byte Buddy便捷创建Java Agent

    目录 Java字节码与类文件格式 克服字节码的不足 ByteBuddy简介 通过委托实现Instrumentation 实现方法级别的安全性 实现安全功能的Javaagent 关于作者 Java agent 是在另外一个 Java 应用(“目标”应用)启动之前要执行的 Java 程序,这样 agent 就有机会修改目标应用或者应用所运行的环境.在本文中,我们将会从基础内容开始,逐渐增强其功能,借助字节码操作工具 Byte Buddy,使其成为高级的 agent 实现. 在最基本的用例中,Java

  • Java Agent (代理)探针技术详情

    目录 前言: Java Agent 技术简介 Java Agent 功能介绍 Java Agent 实现原理 Java Agent 案例 前言: Java 中的 Agent 技术可以让我们无侵入性的去进行代理,最常用于程序调试.热部署.性能诊断分析等场景,现如今比较火热的分布式链路追踪项目Skywalking,就是通过探针技术去捕获日志,将数据上报OAP观察分析平台. Java Agent 技术简介 Java Agent 直译为 Java 代理,也常常被称为 Java 探针技术. Java Ag

随机推荐