当前位置:   article > 正文

JAVA Agent 简析及开发示例_java agent架构设计

java agent架构设计

        Java Instrumentation指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等java SE5中使用JVM TI替代了JVM PI和JVM DI。提供一套代理机制,支持独立于JVM应用程序之外的程序以代理的方式连接和访问JVMInstrumentation 的最大作用就是类定义的动态改变和操作。在 Java SE 5 及其后续版本当中,开发者可以在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 – javaagent 参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。

premain方式
         在Java SE5时代,Instrument只提供了premain一种方式,即在真正的应用程序(包含main方法的程序)main方法启动前启动一个代理程序。例如使用如下命令:

java -javaagent:agent_jar_path[=options] java_app_name  
  • 1

可以在启动名为java_app_name的应用之前启动一个agent_jar_path指定位置的agent jar。 实现这样一个agent jar包,必须满足两个条件:

        1.在这个jar包的manifest文件中包含Premain-Class属性,并且改属性的值为代理类全路径名。
        2.代理类必须提供一个

public static void premain(String args, Instrumentation inst)
  • 1

public static void premain(String args) 
  • 1

方法。
        当在命令行启动该代理jar时,VM会根据manifest中指定的代理类,使用于main类相同的系统类加载器(即ClassLoader.getSystemClassLoader()获得的加载器)加载代理类。在执行main方法前执行premain()方法。如果premain(String args, Instrumentation inst)和premain(String args)同时存在时,优先使用前者。其中方法参数args即命令中的options,类型为String(注意不是String[]),因此如果需要多个参数,需要在方法中自己处理(比如用”;”分割多个参数之类);inst是运行时由VM自动传入的Instrumentation实例,可以用于获取VM信息。

premain实例-打印所有的方法调用
        下面实现一个打印程序执行过程中所有方法调用的功能,这个功能可以通过AOP其他方式实现,这里只是尝试使用Instrumentation进行ClassFile的字节码转换实现:

构造agent类

premain方式的agent类必须提供premain方法,代码如下:

package test;  
import java.lang.instrument.Instrumentation;  
public class Agent {  
 public static void premain(String args, Instrumentation inst){  
System.out.println("Hi, I'm agent!");  
inst.addTransformer(new TestTransformer());  
}  
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

premain有两个参数,args为自定义传入的代理类参数,inst为VM自动传入的Instrumentation实例。 premain方法的内容很简单,除了标准输出外,只有

inst.addTransformer(new TestTransformer());  
  • 1

这行代码的意思是向inst中添加一个类的转换器。用于转换类的行为。

构造Transformer

        下面来实现上述过程中的TestTransformer来完成打印调用方法的类定义转换。

package test;  
import java.lang.instrument.ClassFileTransformer;  
import java.lang.instrument.IllegalClassFormatException;  
import java.security.ProtectionDomain;  
import org.objectweb.asm.ClassReader;  
import org.objectweb.asm.ClassWriter;  
import org.objectweb.asm.Opcodes;  
import org.objectweb.asm.tree.ClassNode;  
import org.objectweb.asm.tree.FieldInsnNode;  
import org.objectweb.asm.tree.InsnList;  
import org.objectweb.asm.tree.LdcInsnNode;  
import org.objectweb.asm.tree.MethodInsnNode;  
import org.objectweb.asm.tree.MethodNode;  

public class TestTransformer implements ClassFileTransformer {  

  @Override  
  public byte[] transform(ClassLoader arg0, String arg1, Class<?> arg2,  
         ProtectionDomain arg3, byte[] arg4)  
        throws IllegalClassFormatException {  
      ClassReader cr = new ClassReader(arg4);  
      ClassNode cn = new ClassNode();  
      cr.accept(cn, 0);  
      for (Object obj : cn.methods) {  
        MethodNode md = (MethodNode) obj;  
      if ("<init>".endsWith(md.name) || "<clinit>".equals(md.name)) {  
           continue;  
        }  
      InsnList insns = md.instructions;  
          InsnList il = new InsnList();  
       il.add(new FieldInsnNode(Opcodes.GETSTATIC, "java/lang/System",  
                   "out", "Ljava/io/PrintStream;"));  
       il.add(new LdcInsnNode("Enter method-> " + cn.name+"."+md.name));  
        il.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL,  
                 "java/io/PrintStream", "println", "(Ljava/lang/String;)V"));  
          insns.insert(il);  
          md.maxStack += 3;  
      }  
      ClassWriter cw = new ClassWriter(0);  
     cn.accept(cw);  
      return cw.toByteArray();  
   } 
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

        TestTransformer实现了ClassFileTransformer接口,该接口只有一个transform方法,参数传入包括该类的类加载器,类名,原字节码字节流等,返回被转换后的字节码字节流。 TestTransformer主要使用ASM实现在所有的类定义的方法中,在方法开始出添加了一段打印该类名和方法名的字节码。在转换完成后返回新的字节码字节流。
设置MANIFEST.MF
        设置MANIFEST.MF文件中的属性,文件内容如下:

Manifest-Version: 1.0  
Premain-Class: test.Agent  
Created-By: 1.6.0_29  
  • 1
  • 2
  • 3
  • 4

测试
         代码编写完成后将代码编译打成agent.jar。 编写测试代码:

public class TestAgent {  
public static void main(String[] args) {  
TestAgent ta = new TestAgent();  
 ta.test();  
}  
 public void test() {  
System.out.println("I'm TestAgent");  
  }  
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

         从命令行执行该类,并设置agent.jar

java -javaagent:agent.jar TestAgent  
  • 1

将打印出程序运行过程中实际执行过的所有方法名:

Hi, I'm agent!  
Enter method-> test/TestAgent.main  
Enter method-> test/TestAgent.test  
I'm TestAgent 
。。。。。 
  • 1
  • 2
  • 3
  • 4
  • 5

从输出中可以看出,程序首先执行的是代理类中的premain方法(不过代理类自身不会被自己转换,所以不能打印出代理类的方法名),然后是应用程序中的main方法。

agentmain方式

参见:http://blog.csdn.net/u010039929/article/details/70117018

        代理 (agent) 是在你的main方法前的一个拦截器 (interceptor),也就是在main方法执行之前,执行agent的代码。agent的代码与你的main方法在同一个JVM中运行,并被同一个system classloader装载,被同一的安全策略 (security policy) 和上下文 (context) 所管理。叫代理(agent)这个名字有点误导的成分,它与我们一般理解的代理不大一样。java agent使用起来比较简单。

        怎样写一个java agent? 只需要实现premain这个方法

public static void premain(String agentArgs, Instrumentation inst)
  • 1

JDK 6 中如果找不到上面的这种premain的定义,还会尝试调用下面的这种premain定义:

public static void premain(String agentArgs)
  • 1
  1. Agent 类必须打成jar包,然后里面的 META-INF/MAINIFEST.MF 必须包含 Premain-Class这个属性。
    下面是一个MANIFEST.MF的例子:
Manifest-Version: 1.0
Premain-Class:MyAgent1
Created-By:1.6.0_06
  • 1
  • 2
  • 3

然后把MANIFEST.MF 加入到你的jar包中。

  1. 所有的这些Agent的jar包,都会自动加入到程序的classpath中。所以不需要手动把他们添加到classpath。 除非你想指定classpath的顺序。

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

 java -javaagent:MyAgent1.jar -javaagent:MyAgent2.jar -jar MyProgram.jar
  • 1

假设MyProgram.jar里面的main函数在MyProgram中。
MyAgent1.jar, MyAgent2.jar, 这2个jar包中实现了premain的类分别是MyAgent1, MyAgent2
程序执行的顺序将会是
MyAgent1.premain -> MyAgent2.premain -> MyProgram.main
5. 另外,放在main函数之后的premain是不会被执行的,

例如

java -javaagent:MyAgent1.jar  -jar MyProgram.jar -javaagent:MyAgent2.jar
  • 1

MyAgent2 和MyAgent3 都放在了MyProgram.jar后面,所以MyAgent2的premain都不会被执行,

  1. 每一个java agent 都可以接收一个字符串类型的参数,也就是premain中的agentArgs,这个agentArgs是通过java option中定义的。
    如:
java -javaagent:MyAgent2.jar=thisIsAgentArgs -jar MyProgram.jar
  • 1

MyAgent2中premain接收到的agentArgs的值将是”thisIsAgentArgs” (不包括双引号)

  1. 参数中的Instrumentation:

通过参数中的Instrumentation inst,添加自己定义的ClassFileTransformer,来改变class文件。

这里自定义的Transformer实现了transform方法,在该方法中提供了对实际要执行的类的字节码的修改,甚至可以达到执行另外的类方法的地步

  1. 通过java agent就可以不用修改原有的java程序代码,通过agent的形式来修改或者增强程序了,或者做热启动等等。

    其实是执行在premain中添加的ClassFileTransformer的Transform方法(有可能需要对被执行的类的字节码做修改)
    最后才是执行类的main方法

Premain 加载:

想调用ASM API (用于字节码处理的开源API)对字节码进行处理,目标是实现对Java程序运行时各种对象的动态跟踪,并进一步分析各个对象之间的关系(研究前提是目前的UML锁阐释的whole-part relation 是比较混乱的)。由于ASM相关内容又可以延伸很远,在此文中略过。

在完成了能对字节码进行处理的ASM调用以后,需要考虑如何将这些功能与正常的java程序整合到一起。

首先,我考虑到了自定义ClassLoader的方法即在程序的main入口处,首先加载自定义的classloader,然后通过reflect技术使用这个classloader加载并调用测试程序(就是被跟踪的程序,姑且称之为测试程序)的入口类。

这个方法一个很大的限制,在于每次都必须先找到测试程序的入口类,而对于有的封装成jar的程序集合,这一点相对比较难控制。

于是,有了这里介绍的方法:通过 java.lang.instrument 实现的java agent对象操作字节码,也就试所谓的AOP(面向方面编程)

原理

        JVMTI(Java Virtual Machine Tool Interface)是一套本地编程接口集合,它提供了一套”代理”程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能。
        java.lang.instrument 包的实现,也就是基于这种机制的:在 Instrumentation 的实现当中,存在一个 JVMTI 的代理程序,通过调用 JVMTI 当中 Java 类相关的函数来完成 Java 类的动态操作。

Instrumentation 的最大作用,就是类定义动态改变和操作。在 Java SE 5 及其后续版本当中,开发者可以在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 – javaagent参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。
步骤
1.编写java代理类。
这个类中,premain方法是关键,对比于一般的入口main一样,这里的premain是在main之前执行的。它会告诉JVM如何处理加载上来的java字节码。如下例:

public static void premain(String agentArgs, Instrumentation inst) {  
                        Trace.BeginTrace(); // it's important for trace files   
                inst.addTransformer(new ASMAgent());  
            }  
  • 1
  • 2
  • 3
  • 4
  public static void premain(String agentArgs, Instrumentation inst) {  
                        Trace.BeginTrace(); // it's important for trace files  
                inst.addTransformer(new ASMAgent());  
            }  
  • 1
  • 2
  • 3
  • 4

值得注意的是,addTransformer实现了对字节码处理的方法的回调。

    inst.addTransformer(new ASMAgent());  
  • 1

类ASMAgent包含着实现对java字节码处理的方法:transform()。它来自于ClassFileTransformer接口。为了方便,博主这里将对ClassFileTransformer接口的实现跟ASMAgent类放在了一起。

public byte[] transform(ClassLoader loader, String className,Class<?> classBeingRedefined, ProtectionDomain protectionDomain,byte[] classfileBuffer)  
                throws IllegalClassFormatException {  
                byte[] retVal = null;  
                if(isInstrumentable(className)){  
                    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);  
                    ASMClassAdapter mca = new ASMClassAdapter(cw);   
                    ClassReader cr = new ClassReader(classfileBuffer);  
                    cr.accept(mca, 0);  
                    retVal = cw.toByteArray();  
                }else{  
                        retVal = classfileBuffer ;  
                    }  
                return retVal;  
        }  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

2.打包代理类。

只有合理打包并在manifest文件中记录下相应的键值对之后,才能正常执行premain的内容。

manifest文件中需要添加的键值对是:

   Premain-Class : test.asm.ASMAgent  
  • 1

另外,如果对字节码的处理有应用到了其他的类,需要在manifest中增加路径。键值对为:

    Class-Path: asm-3.0.jar  
  • 1

3.执行

        执行测试程序时,添加“-javaagent:代理类的jar[=传入premain的参数]”选项。

java -javaagent:ASMInstrument.jar   -jar XXXX.jar  xxxx
  • 1

其中ASMInstrument.jar 是第二步中打包的程序, XXX.jar是需要测试的程序, xxx是XXX.jar 执行时可能的命令行参数。
如果只是执行某.class文件中的类,我们假设是在当前目录下的一个XXXX类,则是:

java -javaagent:ASMInstrument.jar   -cp ./  XXXX xxx
  • 1

其中xxx是可能的命令行参数。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/349111
推荐阅读
相关标签
  

闽ICP备14008679号