JDK1.6“新“特性Instrumentation之JavaAgent(推荐)

简介

Java Agent是在JDK1.5以后,我们可以使用agent技术构建一个独立于应用程序的代理程序(即为Agent),用来协助监测、运行甚至替换其他JVM上的程序。使用它可以实现虚拟机级别的AOP功能。

Agent分为两种,一种是在主程序之前运行的Agent,一种是在主程序之后运行的Agent(前者的升级版,1.6以后提供)。

JavaAgent的作用Agent给我们程序带来的影响.jpg

JDK1.6“新“特性Instrumentation之JavaAgent(推荐)_第1张图片

使用Agent-premain方法影响的程序效果图.jpg

JDK1.6“新“特性Instrumentation之JavaAgent(推荐)_第2张图片

使用Agent-agentmain方法影响的程序效果图.jpg

JDK1.6“新“特性Instrumentation之JavaAgent(推荐)_第3张图片

JavaAgent相关的API

在java.lang.instrument包下 给我们提供了相关的API

而最为主要的就是Instrumentation这个接口中的几个方法

JDK1.6“新“特性Instrumentation之JavaAgent(推荐)_第4张图片

public interface Instrumentation {
 
 /**
 * 添加Transformer(转换器) 
 * ClassFileTransformer类是一个接口,通常用户只需实现这个接口的 byte[] transform()方法即可;
 * transform这个方法会返回一个已经转换过的对象的byte[]数组
 * @param transformer  拦截器
 * @return canRetransform 是否能重新转换
 */
 	void addTransformer(ClassFileTransformer transformer, boolean canRetransform); 

 /**
 * 重新触发类加载,
 * 该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
 * @param classes  Class对象
 * @throws UnmodifiableClassException 异常
 */
 void retransformClasses(Class... classes) throws UnmodifiableClassException;

 /**
 * 直接替换类的定义
 * 重新转换某个对象,并已一个新的class格式,进行转化。
 * 该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
 * @param definitions  ClassDefinition对象[Class定义对象]
 * @throws ClassNotFoundException,UnmodifiableClassException 异常
 */
 void redefineClasses(ClassDefinition... definitions)throws ClassNotFoundException, UnmodifiableClassException;

 /**
 * 获取当前被JVM加载的所有类对象
 * @return Class[] class数组
 */
 Class[] getAllLoadedClasses();
}

后面我们会在代码中具体用到这些方法。再详细说明。

JavaAgent-premain方法1-初探效果:

实现main方法前执行业务逻辑

Agent1.java

public class Agent1 {
 public static void premain(String agent){
 System.out.println("Agent1 premain :" + agent);
 }
}

Demo1.java

public class Demo1 {

 /**
 * VM参数
 * -javaagent:D:\desktop\text\code\mycode\JavaAgentDemo\agent\target/agent.jar=input
 * */
 public static void main(String[] args) throws Exception {
 System.out.println("demo1");
 }
}

resources/META-INF/MANIFEST.MF

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: dahuoyzs
Created-By: Apache Maven 3.6.0
Build-Jdk: 1.8.0_171
Premain-Class: cn.bigfire.Agent1
Can-Retransform-Classes: true

运行效果

Agent1 premain :input
demo1

JavaAgent-premain方法2-实现修改代码逻辑效果:

实现 修改 程序源代码 hello -> hello agented

Agent2.java

public class Agent2 {
 /**
 * 可以运行在main方法启动前
 * @param agent  输入的参数
 * @param instrumentation  输入的参数
 */
 public static void premain(String agent, Instrumentation instrumentation){
 System.out.println("Agent2 premain 2param :" + agent);
 instrumentation.addTransformer(new ConsoleTransformer(),true);
 }

}

ConsoleTransformer.java

public class ConsoleTransformer implements ClassFileTransformer {
 @Override
 public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
 if (className.equals("cn/bigfire/Console")){
  String root = StrUtil.subBefore(System.getProperty("user.dir"), "JavaAgentDemo", true);
  String classFile = root + "JavaAgentDemo/agent/src/main/resources/Console.class";
  return FileUtil.readBytes(classFile);
 }
 return classfileBuffer;
 }
}

Demo2.java

public class Demo2 {

 /**
 * VM参数
 * -javaagent:D:\desktop\text\code\mycode\JavaAgentDemo\agent\target/agent.jar=input
 * */
 public static void main(String[] args) throws Exception {
 new Thread(()->{
  while (true){
  Console.hello();// public static void hello(){System.out.println("hello"); }
  ThreadUtil.sleep(2000);
  }
 }).start();
 }
}

resources/META-INF/MANIFEST.MF

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: dahuoyzs
Created-By: Apache Maven 3.6.0
Build-Jdk: 1.8.0_171
Premain-Class: cn.bigfire.Agent2
Can-Retransform-Classes: true

运行效果

Agent2 premain 2param :input
满足条件
hello  agented
hello  agented
hello  agented
hello  agented

JavaAgent-premain方法3-无侵入动态修改程序源代码实现方法耗时统计效果:

实现main方法外的所有方法统计时间

Agent3.java

public class Agent3 {
 /**
 * 可以运行在main方法启动前
 * @param agent    输入的参数
 * @param instrumentation  instrumentation对象由JVM提供并传入
 */
 public static void premain(String agent, Instrumentation instrumentation) {
 System.out.println("Agent3 premain :" + agent);
 instrumentation.addTransformer(new TimeCountTransformer());
 }

 /**
 * 时间统计Transformer 给要代理的方法添加时间统计
 */
 private static class TimeCountTransformer implements ClassFileTransformer {
 @Override
 public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
  try {
  className = className.replace("/", ".");
  if (className.equals("cn.bigfire.Demo3")) {
   //使用全称,用于取得字节码类<使用javassist>
   CtClass ctclass = ClassPool.getDefault().get(className);
   //获得方法列表
   CtMethod[] methods = ctclass.getDeclaredMethods();
   //给方法设置代理
   Stream.of(methods).forEach(method-> agentMethod(ctclass,method));
   //CtClass转byte[]数组
   return ctclass.toBytecode();
  }
  } catch (Exception e) {
  e.printStackTrace();
  }
  return null;
 }
 }

 /**
 * 代理方法,把传入的方法经写代理,并生成带时间统计的方法,
 * @param ctClass    javassist的Class类
 * @param ctMethod   javassist的ctMethod方法
 * */
 public static void agentMethod(CtClass ctClass,CtMethod ctMethod){
 try {
  String mName = ctMethod.getName();
  if (!mName.equals("main")){//代理除了main方法以外的所有方法
  String newName = mName + "$Agent";
  ctMethod.setName(newName);
  CtMethod newMethod = CtNewMethod.copy(ctMethod, mName, ctClass, null);
  // 构建新的方法体
  String bodyStr = "{\n" +
   "long startTime = System.currentTimeMillis();\n" +
   newName + "();\n" +
   "long endTime = System.currentTimeMillis();\n" +
   "System.out.println(\""+newName+"() cost:\" +(endTime - startTime));\n" +
   "}";
  newMethod.setBody(bodyStr);// 替换新方法
  ctClass.addMethod(newMethod);// 增加新方法
  }
 }catch (Exception e){
  e.printStackTrace();
 }
 }

}

Demo3.java

public class Demo3 {
 
 /**
 * VM参数
 * -javaagent:D:\desktop\text\code\mycode\JavaAgentDemo\agent\target/agent.jar=input
 */
 public static void main(String[] args) throws Exception {
 sleep1();
 sleep2();
 }

 public static void sleep1(){
 ThreadUtil.sleep(1000);
 }

 public static void sleep2(){
 ThreadUtil.sleep(2000);
 }

}

resources/META-INF/MANIFEST.MF

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: dahuoyzs
Created-By: Apache Maven 3.6.0
Build-Jdk: 1.8.0_171
Class-Path: ../javassist-3.12.1.GA.jar
Premain-Class: cn.bigfire.Agent3
Can-Retransform-Classes: true

运行效果

Agent3 premain :input
sleep1$Agent() cost:1005
sleep2$Agent() cost:2001

JavaAgent-agentmain方法1-实现运行时修改程序效果:

实现运行时 修改程序 hello -> hello agented

Agent4.java

public class Agent4 {

 public static void premain(String agent){
 System.out.println("Agent4 premain 1param:" + agent);
 }

 public static void premain(String agent, Instrumentation instrumentation) {
 System.out.println("Agent4 premain 2param:" + agent);
 //premain时,由于堆里还没有相应的Class。所以直接addTransformer,程序就会生效。
// instrumentation.addTransformer(new ConsoleTransformer(),true);
 }

 public static void agentmain(String agent, Instrumentation instrumentation){
 System.out.println("Agent4 agentmain 2param :" + agent);
 instrumentation.addTransformer(new ConsoleTransformer(),true);
 //agentmain运行时 由于堆里已经存在Class文件,所以新添加Transformer后
 // 还要再调用一个 inst.retransformClasses(clazz); 方法来更新Class文件
 for (Class clazz:instrumentation.getAllLoadedClasses()) {
  if (clazz.getName().contains("cn.bigfire.Console")){
  try {
   instrumentation.retransformClasses(clazz);
  } catch (Exception e) {
   e.printStackTrace();
  }
  }
 }
 }

 public static void agentmain(String agent){
 System.out.println("Agent4 agentmain 1param :" + agent);
 }

}

Demo4

public class Demo4 {
 /**
 * 打包agent4 -> 先运行demo2 -> 运行demo4 ->选择程序demo2结尾的程序,即可运行时修改文件
 * VM参数
 * -javaagent:D:\desktop\text\code\mycode\JavaAgentDemo\agent\target/agent.jar=input
 * */
 public static void main(String[] args) throws Exception {
 while (true){
  List list = VirtualMachine.list();
  for (int i = 0; i < list.size(); i++) {
  VirtualMachineDescriptor jvm = list.get(i);;
  System.out.println("[" +i+ "]ID:"+jvm.id()+",Name:"+jvm.displayName());
  }
  System.out.println("请选择第几个");
  Scanner scanner = new Scanner(System.in);
  int s = scanner.nextInt();
  VirtualMachineDescriptor virtualMachineDescriptor = list.get(s);
  VirtualMachine attach = VirtualMachine.attach(virtualMachineDescriptor.id());
  String root = StrUtil.subBefore(System.getProperty("user.dir"), "JavaAgentDemo", true);
  String agentJar = root + "JavaAgentDemo\\agent\\target\\agent.jar";
  File file = new File(agentJar);
  System.out.println(file.exists());
  attach.loadAgent(agentJar,"param");
  attach.detach();
 }
 }
}

resources/META-INF/MANIFEST.MF

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: dahuoyzs
Created-By: Apache Maven 3.6.0
Build-Jdk: 1.8.0_171
Premain-Class: cn.bigfire.Agent4
Agent-Class: cn.bigfire.Agent4
Can-Retransform-Classes: true
Can-Redefine-Classes: true

此时的运行顺序
打包agent4 -> 先运行demo2 -> 运行demo4 ->选择程序demo2结尾的程序,即可运行时修改文件

运行效果
Demo2

Agent4 premain 2param:input
hello
hello

Demo4

[0]ID:12480,Name:cn.bigfire.Demo2
[1]ID:14832,Name:org.jetbrains.kotlin.daemon.KotlinCompileDaemon --daemon-runFilesPath xxx
[2]ID:14864,Name:
[3]ID:3952,Name:cn.bigfire.Demo4
[4]ID:14852,Name:org.jetbrains.idea.maven.server.RemoteMavenServer36
[5]ID:11928,Name:org.jetbrains.jps.cmdline.Launcher xxx
请选择第几个
0
true

Demo2

Agent4 premain 2param:input
hello
hello
Agent4 agentmain 2param :param
hello agented
hello agented
hello agented

JavaAgent-agentmain方法2-实现动态修改日志级别效果:

实现运行时 修改程序 模拟项目中的动态日志 info <-> debug

Agent5.java

public class Agent5 {

 public static void premain(String agent, Instrumentation instrumentation){
 System.out.println("Agent5 premain 2param :" + agent);
 instrumentation.addTransformer(new StartTransformer(),true);

 //这个方式不行。因为启动时Class都还没有呢。
// for (Class clazz:inst.getAllLoadedClasses()) {
//  if (clazz.getName().equals("cn.bigfire.LogLevelStarter")){
//  try {
//   switchDebug(clazz);
//   instrumentation.retransformClasses(clazz);
//  } catch (Exception e) {
//   e.printStackTrace();
//  }
//  }
// }
 }

 public static void agentmain(String agent, Instrumentation instrumentation){
 System.out.println("Agent5 agentmain 2param :" + agent);
 for (Class clazz:instrumentation.getAllLoadedClasses()) {
  if (clazz.getName().equals("cn.bigfire.LogLevelStarter")){
  try {
   switchAtomicDebug(clazz);
   instrumentation.retransformClasses(clazz);
  } catch (Exception e) {
   e.printStackTrace();
  }
  }
 }
 }

 public static class StartTransformer implements ClassFileTransformer {
 @Override
 public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
  //此时由于classBeingRedefined是空,所以还是不能用这个Class修改属性呢,只能通过 读取byte[]往堆里丢,才能用。
  if (className.equals("cn/bigfire/LogLevelStarter")){
  //【这是一个错误的思路】 premain的时候 classBeingRedefined是空的因为很多的Class还没加载到堆中
//  if (classBeingRedefined!=null){
//   switchDebug(classBeingRedefined);
//   return toBytes(classBeingRedefined);
//  }
  //正常的读取一共文件byte[]数组
  String root = StrUtil.subBefore(System.getProperty("user.dir"), "JavaAgentDemo", true);
  String classFile = root + "JavaAgentDemo/agent/src/main/resources/LogLevelStarter.class";
  return FileUtil.readBytes(classFile);
  }
  return classfileBuffer;
 }
 }

 /**
 * 可序列化对象转byte[]数组
 * @param clazz  要转byte[]数组的对象
 * @return byte[]  返回byte[]数组
 */
 public static byte[] toBytes(Serializable clazz){
 try {
  ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
  ObjectOutputStream stream = new ObjectOutputStream(byteArrayOutputStream);
  stream.writeObject(clazz);
  return byteArrayOutputStream.toByteArray();
 }catch (Exception e){
  e.printStackTrace();
 }
 return null;
 }

 public static void switchDebug(Class clazz){
 try {
  Field field1 = clazz.getDeclaredField("isDebug");
  field1.setAccessible(true);
  boolean debug = field1.getBoolean(clazz);
  field1.setBoolean(clazz,!debug);
 }catch (Exception e){
  e.printStackTrace();
 }
 }

 public static void switchAtomicDebug(Class clazz){
 try {
  Field field2 = clazz.getDeclaredField("atomicDebug");
  field2.setAccessible(true);
  AtomicBoolean atomicDebug = (AtomicBoolean)field2.get(clazz);
  atomicDebug.set(!atomicDebug.get());
 }catch (Exception e){
  e.printStackTrace();
 }
 }

}

注意,需要先把LogLevelStarter.java中的isDebug 改为true编译一下。放到src/main/resources/目录下;

LogLevelStarter.java

public class LogLevelStarter {

 public static volatile boolean isDebug = false;
 public static AtomicBoolean atomicDebug = new AtomicBoolean(false);

 /**
 * VM参数
 * -javaagent:D:\desktop\text\code\mycode\JavaAgentDemo\agent\target/agent.jar=input
 */
 public static void main(String[] args) throws Exception {
 new Thread(()->{
  for (;;){
  //死循环,每隔两秒打印一个日志。
  System.out.print(isDebug ? "volatile debug" : "volatile info");
  System.out.print("\t");
  System.out.println(atomicDebug.get() ? "atomicDebug debug" : "atomicDebug info");
  ThreadUtil.sleep(2000);
  }
 }).start();
 }
}

resources/META-INF/MANIFEST.MF

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: dahuoyzs
Created-By: Apache Maven 3.6.0
Build-Jdk: 1.8.0_171
Premain-Class: cn.bigfire.Agent5
Agent-Class: cn.bigfire.Agent5
Can-Retransform-Classes: true
Can-Redefine-Classes: true

此时的运行顺序
打包agent5 -> 先运行LogLevelStarter -> 运行demo4 ->选择程序LogLevelStarter结尾的程序,即可运行时修改文件

运行效果

LogLevelStarter

Agent5 premain 2param :input
volatile debug atomicDebug info
volatile debug atomicDebug info

Demo4

[0]ID:12592,Name:cn.bigfire.LogLevelStarter
[1]ID:12880,Name:cn.bigfire.Demo4
[2]ID:14832,Name:org.jetbrains.kotlin.daemon.KotlinCompileDaemon --daemon-runFilesPath xxx
[3]ID:14864,Name:
[4]ID:14852,Name:org.jetbrains.idea.maven.server.RemoteMavenServer36
[5]ID:8116,Name:org.jetbrains.jps.cmdline.Launcher xxx
请选择第几个
0
true

LogLevelStarter

Agent5 premain 2param :input
volatile debug	atomicDebug info
volatile debug	atomicDebug info
Agent5 agentmain 2param :param
volatile debug	atomicDebug debug
volatile debug	atomicDebug debug

在Agent5中,其实使用premain和agentmain。

premain把volatile修饰的isDbug给修改为true了。

而agentmain时把atomicDebug的值进行多次取反操作。

自己实现一个热部署功能的大致思路

当运行完本项目中的几个demo之后。

读者可能对Java Agent有了一些基本的概念

最起码我们知道了premain是可以运行在main函数前的。

agentmain是可以在程序运行时,修改程序内的一些类文件的。

那么热部署很明显就是使用的agentmain这个特性了

那么热部署具体应该怎么实现呢?

这里先有个大概的思路。后续如果有经历,可以简单按照这个思路实现一下

思路

当我们文件发生修改的时候,项目会重新加载我们的类。

那么这里肯定会涉及到文件变化的观察。 即 观察者设计模式跑不了

首先递归当前项目目录。并根据文件类型,如(.java ,xml,yml等)将此类文件注册观察者模式。

当文件内容发生变化时,会调用 监听器中的回调方法;

在回调中完成如下(具体实现时未必需要)

使用Java1.6的JavaCompiler编译Java文件;
自定义ClassLoader 装载 编译好的Class到堆中

使用agentmain修改原Class文件替换成新的Class文件

完成热加载

JavaAgent的应用场景

apm:(Application Performance Management)应用性能管理。pinpoint、cat、skywalking等都基于Instrumentation实现
idea的HotSwap、Jrebel等热部署工具
应用级故障演练
Java诊断工具Arthas、Btrace等

源代码

{
	"author": "大火yzs",
	"title": "【JavaAgent】JavaAgent入门教程",
	"tag": "JavaAgent,Instrumentation,运行时动态修改源程序",
	"createTime": "2020-08-02 18:30"
}

总结

到此这篇关于JDK1.6“新“特性Instrumentation之JavaAgent的文章就介绍到这了,更多相关JDK1.6“新“特性Instrumentation内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

你可能感兴趣的:(JDK1.6“新“特性Instrumentation之JavaAgent(推荐))