说到 Java Agent,有必要提一提 AOP,也就是面向切面编程思想。AOP 作为 OOP(面向对象编程思想)的一个补充,在技术实现上是没有规定和约束的。在 Java 中,最常见的实现方式就是 Filter 的责任链模式和 Spring AOP 的代理模式。
  
当然,AOP 也不一定非得像 Spring AOP 那样,在运行时通过动态生成代理对象来织入增强。一个 Java 类的生命周期,从编码开始,还需要经历编译、加载、连接、初始化、使用和卸载这些阶段。而在使用之前,每个阶段我们都可以对类进行增强。
编码阶段 在编码阶段,我们可以通过静态代理的方式对目标对象进行增强,但是缺点也很明显,就是不够灵活,属于一锤子买卖。
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 public  interface  Subject  {    void  operation () ; } public  class  SubjectImpl  implements  Subject  {    @Override      public  void  operation ()  {         System.out.println("operation..." );     } } public  class  SubjectProxy  implements  Subject  {    private  Subject target;     public  SubjectProxy (Subject target)  {         this .target = target;     }     @Override      public  void  operation ()  {         System.out.println("before..." );         target.operation();         System.out.println("after..." );     } } 
 
编译阶段 在编译阶段,我们可以通过使用特殊的编译器,在将源文件编译成字节码的时候进行增强,编译后的字节码本身就包含了增强的内容,这就是编译期织入 CTW(Compile-Time Weaving)。典型的工具库就是:AspectJ。
AspectJ 提供了两种切面的编写方式,其中一种是使用 AspectJ 特有的语法;另一种是使用注解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public  aspect SubjectAspect {         pointcut doBefore () :execution(void  SubjectImpl.operation(..));     pointcut doAfter () :execution(void  SubjectImpl.operation(..));     pointcut doAround () :execution(void  SubjectImpl.operation(..));               before(): doBefore() {         System.out.println("before..." );     }     after(): doAfter() {         System.out.println("after..." );     }          void  around () : doAround() {         System.out.println("around before..." );         proceed();         System.out.println("around after..." );     } } 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Aspect public  class  SubjectAspect  {    @Pointcut("execution(* org.example.SubjectImpl.*(..))")      public  void  pointcut ()  {}     @Before("pointcut()")      public  void  doBefore ()  {         System.out.println("before..." );     }     @After("pointcut()")      public  void  doAfter ()  {         System.out.println("after..." );     } } 
 
虽然使用 AspectJ 的特有语法来描述切面会更加灵活,但是由于它不兼容 java 语法,在 IDE 中需要安装插件来支持它的语法,再加上需要额外的学习成本,因此这种方式实际上使用的并不多,通常还是采用兼容 java 语法的注解来定义切面。
在定义好了切面以后,还需要使用 AspectJ 特定的编译器来编译代码。在 AspectJ 1.9.7 版本中,可以直接通过下载的 jar 包进行安装,安装后的程序包含 ajc 命令。当然也可以通过安装 IDE 插件或者使用构建工具(比如 Maven)的插件来编译。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <plugin >     <groupId > org.codehaus.mojo</groupId >      <artifactId > aspectj-maven-plugin</artifactId >      <version > 1.14.0</version >      <configuration >          <complianceLevel > 1.8</complianceLevel >          <source > 1.8</source >          <target > 1.8</target >          <showWeaveInfo > true</showWeaveInfo >          <verbose > true</verbose >          <Xlint > ignore</Xlint >          <encoding > UTF-8</encoding >      </configuration >      <executions >          <execution >              <goals >                  <goal > compile</goal >                  <goal > test-compile</goal >              </goals >          </execution >      </executions >  </plugin > 
 
下面就是通过 ajc 编译器编译后的代码,可以看到增强的部分直接被织入到了目标方法前后。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public  class  SubjectImpl  implements  Subject  {    public  SubjectImpl ()  {     }     public  void  operation ()  {         try  {             SubjectAspect.aspectOf().doBefore();             System.out.println("operation..." );         } catch  (Throwable var2) {             SubjectAspect.aspectOf().doAfter();             throw  var2;         }         SubjectAspect.aspectOf().doAfter();     } } 
 
aspectjrt.jar 包的主要作用是提供运行时环境,包括一些注解、静态方法等,在使用 AspectJ 时一般都需要引入。aspectjtools.jar 主要提供的是赫赫有名的 ajc 编译器,通常这个包会被封装到 IDE 插件或者自动化构建工具的插件中。aspectjweaver.jar 主要提供了一个 java agent 用于在类加载期间织入切面(LTW)。
 
加载阶段 由于类的加载本质上就是类加载器将从文件、网络或者其他渠道获取的字节流解析成 Class 对象的过程,因此我们只需要通过某种方式修改字节流,就可以实现类的增强。也就是说,在类加载阶段,我们只需要自定义一个类加载器,在类加载器读取字节流之前,利用一些字节码增强工具(比如:ASM、Javassist 等)对类进行增强,最后将增强后的字节流解析为 Class 对象即可。
下面使用 Javassist 简单演示一下在类加载阶段实现类增强的步骤。首先写一个类,添加一个方法 test,方便我们后续对该方法进行增强。
1 2 3 4 5 6 public  class  SomeThing  {    public  void  test  ()  {         System.out.println("test..." );     } } 
 
然后写一个方法,该方法接收字节流,内部通过 Javassist 的 API 对字节流进行修饰。
1 2 3 4 5 6 7 8 9 10 11 12 13 public  class  EnhanceMethod  {    public  static  byte [] printCost(byte [] classBytes) throws  Exception {         CtClass  ctClass  =  ClassPool.getDefault().makeClass(new  ByteArrayInputStream (classBytes));         CtMethod  ctMethod  =  ctClass.getMethod("test" , "()V" );         ctMethod.addLocalVariable("cost" , CtClass.longType);         ctMethod.insertBefore("cost = System.currentTimeMillis();" );         ctMethod.insertAfter("cost = System.currentTimeMillis() - cost; "  +                 "System.out.println(\"total cost: \" + cost);" );         return  ctClass.toBytecode();     } } 
 
接下来自定义一个类加载器,在读到原始类文件的流之后,调用该方法替换流。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public  class  MyClassLoader  extends  ClassLoader  {    private  String filePath;     public  MyClassLoader (String filePath)  {         this .filePath = filePath;     }     @Override      protected  Class<?> findClass(String name) throws  ClassNotFoundException {         String  fileUrl  =  filePath;         try  {             Path  path  =  Paths.get(new  URI (fileUrl + name + ".class" ));             byte [] bytes = Files.readAllBytes(path);             bytes = EnhanceMethod.printCost(bytes);             return  defineClass(name, bytes, 0 , bytes.length);         } catch  (Exception e) {             e.printStackTrace();         }         return  super .findClass(name);     } } 
 
最后就是使用自定义的类加载器加载原始类文件,然后调用对象的 test 方法即可。
1 2 3 4 5 6 7 8 9 10 public  class  Main  {    public  static  void  main (String[] args)  throws  Exception {         String  filePath  =  "file:/d:/code/myAgent/" ;         MyClassLoader  myClassLoader  =  new  MyClassLoader (filePath);         Class  clazz  =  myClassLoader.loadClass("SomeThing" );         Object  obj  =  clazz.newInstance();         Method  method  =  clazz.getMethod("test" );         method.invoke(obj);     } } 
 
Instrument Instrument 是 JDK 5 提供的一个新特性,用一句话来总结它的主要作用就是:实现了 JVM 级别的 AOP。通过这个特性,开发者可以构建一个独立于应用程序的代理程序,用来监测和协助运行在 JVM 上的应用。
JVMTI Instrument 的底层实现依赖于 JVMTI(JVM Tool Interface),它是 JVM 暴露出来为了方便用户扩展的接口集合。JVMTI 是基于事件驱动的,具体来说就是,JVM 在执行过程中触发了某些事件就会调用对应事件的回调接口(如果有的话),这些接口可以供开发者去扩展自己的逻辑。
JVMTIAgent JVMTIAgent 其实就是一个动态链接库。它利用 JVMTI 暴露出来的接口实现了一些特殊的功能,一般情况下,它会实现如下的一个或者多个接口。
1 2 3 4 5 6 7 8 JNIEXPORT jint JNICALL Agent_OnLoad (JavaVM *vm, char  *options, void  *reserved) ;JNIEXPORT jint JNICALL Agent_OnAttach (JavaVM* vm, char * options, void * reserved) ;JNIEXPORT void  JNICALL Agent_OnUnload (JavaVM *vm) ;
 
VM 是通过启动函数来启动 agent 的。如果 agent 是在 VM 启动时加载的,也就是说 agent 是在 java 命令中通过 -agentlib 指定的,那么 VM 就会在启动过程中去执行这个 agent 里的 Agent_OnLoad 函数来启动该 agent。如果 agent 不是在 VM 启动时加载的,而是在 VM 处于运行过程中时,先 attach 到目标进程上,然后向目标进程发送 load 命令来加载的,此时 VM 会在加载过程中会调用这个 agent 里的 Agent_OnAttach 函数来启动该 agent。而 Agent_OnUnload 函数会在 agent 卸载时被调用,一般很少实现它。
这里提到的 agent 程序和 java agent 不是同一概念。我们在使用 IDE 进行开发时,如果仔细观察,在控制台中会发现类似的命令:java.exe -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:62290,suspend=y,server=n,这个动态链接库 jdwp 同样也是一个 JVMTIAgent,它实现了程序调试相关的功能。
 
java agent java agent 的功能则是由一个叫做 instrument 的 JVMTIAgent 实现的,它由 JDK 内置提供,在 Linux 下对应的动态库是 libinstrument.so,在 Windows 下是 instrument.dll。由于它实现了 Agent_OnLoad 和 Agent_OnAttach 函数,因此可以在 JVM 启动时加载,也可以在运行时动态加载。其中,启动时加载还可以通过类似 -javaagent:agent.jar 的方式来间接加载 instrument agent。
对于开发人员来说,如果希望 agent 在目标 JVM 启动时加载,只需要编写一个类,然后实现以下方法:
1 2 public  static  void  premain (String agentArgs, Instrumentation inst) ;public  static  void  premain (String agentArgs) ;
 
如果希望目标 JVM 在运行时加载 agent,则需要实现以下方法:
1 2 public  static  void  agentmain (String agentArgs, Instrumentation inst) ;public  static  void  agentmain (String agentArgs) ;
 
上述方法中,JVM 会先寻找对应的第一个方法,如果没有找到则会去寻找对应的第二个方法。其中 agentArgs 是 premain 函数或 agentmain 函数得到的程序参数,由 -javaagent 指定。inst 是一个 Instrumentation 实例,由 JVM 传入,我们可以通过该参数进行类增强等操作。
接下来需要将这个 agent 打包成一个 jar 文件,同时 jar 文件中还要包含一个 MANIFEST.MF 描述文件,文件中需要指定 Premain-Class 或 Agent-Class 属性。
1 2 3 Manifest-Version: 1.0 Premain-Class: org.example.AgentApplication Agent-Class: org.example.AgentApplication 
 
从源代码解析启动时加载 在创建 JVM 时,JVM 会进行启动参数的解析,我们这里重点关注 -agentlib、-agentpath 和 -javaagent。
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 } else  if  (match_option (option, "-agentlib:" , &tail) ||       (is_absolute_path = match_option (option, "-agentpath:" , &tail))) {   if (tail != NULL ) {     const  char * pos = strchr (tail, '=' );     size_t  len = (pos == NULL ) ? strlen (tail) : pos - tail;     char * name = strncpy (NEW_C_HEAP_ARRAY (char , len + 1 , mtInternal), tail, len);     name[len] = '\0' ;     char  *options = NULL ;     if (pos != NULL ) {       options = strcpy (NEW_C_HEAP_ARRAY (char , strlen (pos + 1 ) + 1 , mtInternal), pos + 1 );     } #if  !INCLUDE_JVMTI     if  (valid_hprof_or_jdwp_agent (name, is_absolute_path)) {       jio_fprintf (defaultStream::error_stream (),         "Profiling and debugging agents are not supported in this VM\n" );       return  JNI_ERR;     } #endif       add_init_agent (name, options, is_absolute_path);   } } else  if  (match_option (option, "-javaagent:" , &tail)) { #if  !INCLUDE_JVMTI   jio_fprintf (defaultStream::error_stream (),     "Instrumentation agents are not supported in this VM\n" );   return  JNI_ERR; #else    if (tail != NULL ) {     char  *options = strcpy (NEW_C_HEAP_ARRAY (char , strlen (tail) + 1 , mtInternal), tail);     add_init_agent ("instrument" , options, false );   } #endif   } 
 
ref: hotspot/src/share/vm/runtime/arguments.cpp
 
1 2 3 4 static  AgentLibraryList _agentList;static  void  add_init_agent (const  char * name, char * options, bool  absolute_path)    { _agentList.add (new  AgentLibrary (name, options, absolute_path, NULL )); }
 
ref: hotspot/src/share/vm/runtime/arguments.hpp
 
这里我们只需要关注 add_init_agent 方法,该方法将解析好的参数放入了一个 AgentLibraryList 类型的链表中,以备后续使用。
接下来我们回到创建 JVM 的方法,在这个方法中,我省略了部分代码,重点关注解析参数后的加载过程即可。
1 2 3 4 5 6 7 8 9 10 11 12 jint Threads::create_vm (JavaVMInitArgs* args, bool * canTryAgain)   {        jint parse_result = Arguments::parse (args);   if  (parse_result != JNI_OK) return  parse_result;         if  (Arguments::init_agents_at_startup ()) {     create_vm_init_agents ();   }    } 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void  Threads::create_vm_init_agents ()   {  extern  struct  JavaVM_  main_vm;   AgentLibrary* agent;   JvmtiExport::enter_onload_phase ();      for  (agent = Arguments::agents (); agent != NULL ; agent = agent->next ()) {     OnLoadEntry_t  on_load_entry = lookup_agent_on_load (agent);     if  (on_load_entry != NULL ) {              jint err = (*on_load_entry)(&main_vm, agent->options (), NULL );       if  (err != JNI_OK) {         vm_exit_during_initialization ("agent library failed to init" , agent->name ());       }     } else  {       vm_exit_during_initialization ("Could not find Agent_OnLoad function in the agent library" , agent->name ());     }   }   JvmtiExport::enter_primordial_phase (); } 
 
1 2 3 4 5 6 static  OnLoadEntry_t lookup_agent_on_load (AgentLibrary* agent)   {     const  char  *on_load_symbols[] = AGENT_ONLOAD_SYMBOLS;   return  lookup_on_load (agent, on_load_symbols, sizeof (on_load_symbols) / sizeof (char *)); } 
 
ref: hotspot/src/share/vm/runtime/thread.cpp
 
可以看到,在加载过程中,又调用了 lookup_agent_on_load 方法,该方法的主要作用是加载 agent 对应的动态链接文件。我们回忆刚才分析的代码,也就是说,当指定了 -javaagent 参数时,这里会加载 instrument 这个动态链接文件,最终还会调用它的 Agent_OnLoad 方法,因此,我们接下来要分析 Agent_OnLoad 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 JNIEXPORT jint JNICALL Agent_OnLoad (JavaVM *vm, char  *tail, void  * reserved)   {    initerror = createNewJPLISAgent (vm, &agent);     if  ( initerror == JPLIS_INIT_ERROR_NONE ) {                  if  (parseArgumentTail (tail, &jarfile, &options) != 0 ) {           fprintf (stderr, "-javaagent: memory allocation failure.\n" );           return  JNI_ERR;         }         attributes = readAttributes (jarfile);         premainClass = getAttribute (attributes, "Premain-Class" );                  convertCapabilityAtrributes (attributes, agent);                  initerror = recordCommandLineData (agent, premainClass, options);     } } 
 
ref: jdk/src/share/instrument/InvocationAdapter.c
 
以上是精简后的代码,大概的流程就是:先创建一个 JPLISAgent,然后将 ManiFest 文件中设定的一些参数解析出来, 比如 Premain-Class 等。在创建了 JPLISAgent 之后,还会调用 initializeJPLISAgent 方法对这个 Agent 进行初始化操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 JPLISInitializationError initializeJPLISAgent (   JPLISAgent *    agent,                         JavaVM *        vm,                         jvmtiEnv *      jvmtienv)   {         if  ( jvmtierror == JVMTI_ERROR_NONE ) {         jvmtiEventCallbacks callbacks;         memset (&callbacks, 0 , sizeof (callbacks));         callbacks.VMInit = &eventHandlerVMInit;         jvmtierror = (*jvmtienv)->SetEventCallbacks ( jvmtienv,                                                      &callbacks,                                                      sizeof (callbacks));         check_phase_ret_blob (jvmtierror, JPLIS_INIT_ERROR_FAILURE);         jplis_assert (jvmtierror == JVMTI_ERROR_NONE);     } } 
 
在该方法中,我们只关注 callbacks.VMInit = &eventHandlerVMInit; 这行代码,这里设置了一个 VMInit 事件的回调函数,表示在 JVM 初始化的时候 会回调 eventHandlerVMInit 函数。
1 2 3 4 5 6 7 8 9 10 11 12 void  JNICALLeventHandlerVMInit ( jvmtiEnv *      jvmtienv,                     JNIEnv *        jnienv,                     jthread         thread)   {         if  ( environment != NULL  ) {         jthrowable outstandingException = preserveThrowable (jnienv);         success = processJavaStart ( environment->mAgent,                                     jnienv);         restoreThrowable (jnienv, outstandingException);     } } 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 jboolean processJavaStart (   JPLISAgent *    agent,                     JNIEnv *        jnienv)   {    jboolean    result;     result = initializeFallbackError (jnienv);     if  ( result ) {                  result = createInstrumentationImpl (jnienv, agent);         jplis_assert (result);     }          if  ( result ) {         result = setLivePhaseEventHandlers (agent);         jplis_assert (result);     }          if  ( result ) {         result = startJavaAgent (agent, jnienv,                                 agent->mAgentClassName, agent->mOptionsString,                                 agent->mPremainCaller);     }     return  result; } 
 
可以看到,这个 VMInit 事件发生时,虚拟机的主要操作是实例化了一个 sun.instrument.InstrumentationImpl,然后设置了 ClassFileLoadHook 事件的回调函数,最后加载 java agent 同时调用它的 premain 方法。其中,InstrumentationImpl 是 java.lang.instrument.Instrumentation 接口的实现类。此时我们很容易就会想到 premain 方法中的 inst 参数,没错,在调用 premain 方法时,由虚拟机传入的 inst 参数就是它。
从源代码解析运行时加载 与启动时加载 Agent 相比,运行时加载 Agent 显得更有吸引力,因为运行时加载 Agent 给我们提供了很强的动态性,我们可以在需要的时候加载 Agent 来进行一些工作。tools.jar 中提供了一个 com.sun.tools.attach.VirtualMachine 类,通过它可以实现虚拟机在运行时动态加载 agent。以下代码来自美团技术博客。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private  void  attachAgentToTargetJVM ()  throws  Exception {    List<VirtualMachineDescriptor> virtualMachineDescriptors = VirtualMachine.list();     VirtualMachineDescriptor  targetVM  =  null ;     for  (VirtualMachineDescriptor descriptor : virtualMachineDescriptors) {         if  (descriptor.id().equals(configure.getPid())) {             targetVM = descriptor;             break ;         }     }     if  (targetVM == null ) {         throw  new  IllegalArgumentException ("could not find the target jvm by process id:"           + configure.getPid());     }     VirtualMachine  virtualMachine  =  null ;     try  {         virtualMachine = VirtualMachine.attach(targetVM);         virtualMachine.loadAgent("{agent}" , "{params}" );     } catch  (Exception e) {         if  (virtualMachine != null ) {             virtualMachine.detach();         }     } } 
 
接下来我们回到创建虚拟机的过程,在上面我们分析过了这个过程的部分操作,其中我们忽略了一个操作:
1 2 3 4 5 jint Threads::create_vm (JavaVMInitArgs* args, bool * canTryAgain)   {     os::signal_init ();    } 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void  os::signal_init ()   {  if  (!ReduceSignalUsage) {          const  char  thread_name[] = "Signal Dispatcher" ;     Handle string = java_lang_String::create_from_str (thread_name, CHECK);          { MutexLocker mu (Threads_lock)  ;       JavaThread* signal_thread = new  JavaThread (&signal_thread_entry);            }          os::signal (SIGBREAK, os::user_handler ());   } } 
 
这个方法创建了一个名为:Signal Dispatcher 的线程,这个线程的入口方法为:signal_thread_entry。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static  void  signal_thread_entry (JavaThread* thread, TRAPS)   {  while  (true ) {     int  sig;     {       sig = os::signal_wait ();     }     if  (sig == os::sigexitnum_pd ()) {                return ;     }     switch  (sig) {       case  SIGBREAK: {         if  (!DisableAttachMechanism && AttachListener::is_init_trigger ()) {           continue ;         }                }     }   } } 
 
在这个方法中,如果 Signal Dispatcher 线程接收到 SIGBREAK 信号时,就执行接下来的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 bool  AttachListener::is_init_trigger ()   {  if  (init_at_startup () || is_initialized ()) {     return  false ;   }   char  fn[PATH_MAX+1 ];   sprintf (fn, ".attach_pid%d" , os::current_process_id ());   int  ret;   struct  stat64  st;   RESTARTABLE (::stat64 (fn, &st), ret);   if  (ret == -1 ) {     snprintf (fn, sizeof (fn), "%s/.attach_pid%d" ,              os::get_temp_directory (), os::current_process_id ());     RESTARTABLE (::stat64 (fn, &st), ret);   }   if  (ret == 0 ) {               if  (st.st_uid == geteuid ()) {       init ();       return  true ;     }   }   return  false ; } 
 
ref: hotspot\src\os\linux\vm\attachListener_linux.cpp
 
上面这部分代码是 linux 平台下的实现,可以看到,在该方法中会先检查 JVM 是否已经启动了 Attach Listener,如果没有,会在 /tmp 目录下创建一个叫做 .attach_pid{pid} 的文件,然后执行 AttachListener 的 init 函数。
1 2 3 4 5 6 7 8 9 10 void  AttachListener::init ()   {     const  char  thread_name[] = "Attach Listener" ;   Handle string = java_lang_String::create_from_str (thread_name, CHECK);   { MutexLocker mu (Threads_lock)  ;     JavaThread* listener_thread = new  JavaThread (&attach_listener_thread_entry);        } } 
 
与 Signal Dispatcher 线程类似,这里也创建了一个线程:Attach Listener。
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 static  void  attach_listener_thread_entry (JavaThread* thread, TRAPS)   {  for  (;;) {     AttachOperation* op = AttachListener::dequeue ();     if  (op == NULL ) {       return ;     }     if  (strcmp (op->name (), AttachOperation::detachall_operation_name ()) == 0 ) {       AttachListener::detachall ();     } else  {              AttachOperationFunctionInfo* info = NULL ;       for  (int  i=0 ; funcs[i].name != NULL ; i++) {         const  char * name = funcs[i].name;         assert (strlen (name) <= AttachOperation::name_length_max, "operation <= name_length_max" );         if  (strcmp (op->name (), name) == 0 ) {           info = &(funcs[i]);           break ;         }       }              if  (info != NULL ) {                  res = (info->func)(op, &st);       } else  {         st.print ("Operation %s not recognized!" , op->name ());         res = JNI_ERR;       }     }   } } 
 
在这个入口函数中,首先通过 dequeue 方法拉取操作,这个拉取的方法在不同的平台有不同的实现,在 linux 下,Attach Listener 线程会监听某个端口,通过 accept 方法来接收一个连接,然后从连接中读取请求并封装成一个 AttachOperation 类型的对象,然后到对应的操作列表中去匹配,最后执行相应的函数。以下是这个操作列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 static  AttachOperationFunctionInfo funcs[] = {  { "agentProperties" ,  get_agent_properties },   { "datadump" ,         data_dump },   { "dumpheap" ,         dump_heap },   { "load" ,             JvmtiExport::load_agent_library },   { "properties" ,       get_system_properties },   { "threaddump" ,       thread_dump },   { "inspectheap" ,      heap_inspection },   { "setflag" ,          set_flag },   { "printflag" ,        print_flag },   { "jcmd" ,             jcmd },   { NULL ,               NULL  } }; 
 
经过上面的分析,我们应该能够隐约知道 VirtualMachine 的 attach 方法的大概逻辑,下面通过源代码来验证一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public  static  VirtualMachine attach (String var0)  throws  AttachNotSupportedException, IOException {    if  (var0 == null ) {         throw  new  NullPointerException ("id cannot be null" );     } else  {         List  var1  =  AttachProvider.providers();         if  (var1.size() == 0 ) {             throw  new  AttachNotSupportedException ("no providers installed" );         } else  {             AttachNotSupportedException  var2  =  null ;             Iterator  var3  =  var1.iterator();             while (var3.hasNext()) {                 AttachProvider  var4  =  (AttachProvider)var3.next();                 try  {                     return  var4.attachVirtualMachine(var0);                 } catch  (AttachNotSupportedException var6) {                     var2 = var6;                 }             }             throw  var2;         }     } } 
 
1 2 3 4 5 public  VirtualMachine attachVirtualMachine (String var1)  throws  AttachNotSupportedException, IOException {    this .checkAttachPermission();     this .testAttachable(var1);     return  new  LinuxVirtualMachine (this , var1); } 
 
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 45 46 47 48 49 50 LinuxVirtualMachine(AttachProvider var1, String var2) throws  AttachNotSupportedException, IOException {     super (var1, var2);     int  var3;     try  {         var3 = Integer.parseInt(var2);     } catch  (NumberFormatException var25) {         throw  new  AttachNotSupportedException ("Invalid process identifier" );     }     this .path = this .findSocketFile(var3);     if  (this .path == null ) {         File  var4  =  this .createAttachFile(var3);         try  {             int  var5;             if  (isLinuxThreads) {                 try  {                     var5 = getLinuxThreadsManager(var3);                 } catch  (IOException var24) {                     throw  new  AttachNotSupportedException (var24.getMessage());                 }                 assert  var5 >= 1 ;                 sendQuitToChildrenOf(var5);             } else  {                 sendQuitTo(var3);             }             var5 = 0 ;             long  var6  =  200L ;             int  var8  =  (int )(this .attachTimeout() / var6);             do  {                 try  {                     Thread.sleep(var6);                 } catch  (InterruptedException var23) {                 }                 this .path = this .findSocketFile(var3);                 ++var5;             } while (var5 <= var8 && this .path == null );             if  (this .path == null ) {                 throw  new  AttachNotSupportedException ("Unable to open socket file: target process not responding or HotSpot VM not loaded" );             }         } finally  {             var4.delete();         }     }     checkPermissions(this .path);     int  var27  =  socket();     try  {         connect(var27, this .path);     } finally  {         close(var27);     } } 
 
1 2 3 4 private  String findSocketFile (int  var1)  {    File  var2  =  new  File ("/tmp" , ".java_pid"  + var1);     return  !var2.exists() ? null  : var2.getPath(); } 
 
attachVirtualMachine 方法在不同的平台有不同的实现,上面的代码是 linux 平台下的实现。大体逻辑是,首先检查 /tmp 目录下是否存在 java_pid{pid} 文件。如果已经存在了,则说明 Attach 机制已经准备就绪,可以接受客户端的命令了,这个时候客户端就可以通过 connect 方法连接到目标 JVM 进行命令的发送。如果 java_pid{pid} 文件还不存在,则通过 sendQuitTo 方法向目标 JVM 发送一个 SIGBREAK 信号,让它初始化 Attach Listener 线程并准备接受客户端连接。可以看到,发送了信号之后客户端会循环等待 java_pid{pid} 这个文件,之后再通过 connect 连接到目标 JVM 上。
instrument 实例 在上面我们分析了 agent 技术的实现,在实际使用中,我们只需要编写 premain 或者 agentmain 方法,然后在其中通过 Instrument API 来完成类的动态修改即可。Instrument 接口的 addTransformer 方法可以添加一个类转换器(也就是 ClassFileTransformer 接口),该接口只有一个方法:transform,当类被加载时,虚拟机就会调用它进行类的转换。
下面我们通过 Byte Buddy 这个开源库来写一个简单的实例,这个 java agent 能够实现打印指定包中所有方法的执行耗时。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public  class  MyAgent  {    public  static  void  premain (String agentArgs, Instrumentation inst)  {         System.out.println("this is a java agent" );         System.out.println("arguments: "  + agentArgs);         AgentBuilder.Transformer  transformer  =  (builder, typeDescription, classLoader, javaModule) -> builder                                  .method(ElementMatchers.any())                                  .intercept(MethodDelegation.to(ExecuteTimeInterceptor.class));         new  AgentBuilder                  .Default()                                  .type(ElementMatchers.nameStartsWith("org.example.agent.demo" ))                 .transform(transformer)                 .installOn(inst);     } } 
 
1 2 3 4 5 6 7 8 9 10 11 12 public  class  ExecuteTimeInterceptor  {    @RuntimeType      public  static  Object intercept (@Origin  Method method, @SuperCall  Callable<?> callable)  throws  Exception {         long  start  =  System.currentTimeMillis();         try  {                          return  callable.call();         } finally  {             System.out.println(method.getName() + ":"  + (System.currentTimeMillis() - start) + " ms" );         }     } } 
 
与加载时不同,运行时需要通过 redefineClasses 方法进行类的重定义,同时使用该方法不能添加、删除或者重命名字段和方法,也不能修改方法的签名或者类的继承关系。
参考 
Java 动态调试技术原理及实践