FiAttach传入两个参数,一个是agent.jar的路径,一个是存放希望运行时进行替换的类文件的文件夹路径。

程序自动检测当前的Java应用,将agent.jar附着到虚拟机进程,并将文件夹下的类文件动态替换进去(用新的类替换虚拟机中原来加载的类)。

import java.io.IOException;
import java.util.List;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

public class FiAttach {
   public static void main(String[] args) {
    List vmdList = VirtualMachine.list();
     if (args.length < 2) {
      System.out.println( "Error! Run Command: java com.taobao.fi.FiAttach agentJarPath agentArgs");
       return;
    }
    
    String agentJarPath = args[0];
    String agentArgs = args[1];
    
    System.out.println( "agentJarPath: " + agentJarPath);
    System.out.println( "agentArgs: " + agentArgs);
    
     for (VirtualMachineDescriptor vmd : vmdList) {
       // 注意,目前只支持jboss和tomcat,否则判断会失效!
       // vmd.displayName(): org.jboss.Main -b 0.0.0.0 -Djboss.server.home.dir=/home/admin/deploy/.default -Djboss.server.home.url=file:/home/admin/deploy/.default
       if (vmd.displayName().startsWith( "org.jboss.Main") || vmd.displayName().startsWith( "org.apache.catalina.startup.Bootstrap")) {
         try {
          VirtualMachine vm = VirtualMachine.attach(vmd);
          vm.loadAgent(agentJarPath, agentArgs);
          vm.detach();
        } catch (AttachNotSupportedException e) {
          e.printStackTrace();
        } catch (IOException e) {
          e.printStackTrace();
        } catch (AgentLoadException e) {
          e.printStackTrace();
        } catch (AgentInitializationException e) {
          e.printStackTrace();
        }
      }
    }
  }
}
程序编译时,需要依赖JDK_HOME/lib/tools.jar

下面看agent.jar的实现,AgentMain.java:
import java.util.Set;
import java.util.HashSet;
import java.io.File;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class AgentMain {

   private static Set fiClsFileNames = new HashSet();

   private static Transformer transformer = new Transformer();

   // 标识是否之前做过故障注入
   private static boolean hasFi = false;

   private static void updateClsFileNames(String fiClassFolderPath) {
    fiClsFileNames.clear();
    File fiClassFolderFile = new File(fiClassFolderPath);
     if (!fiClassFolderFile.isDirectory()) {
       return;
    }

    File[] fiClassFiles = fiClassFolderFile.listFiles();
     for (File fiClassFile : fiClassFiles) {
      fiClsFileNames.add(fiClassFile.getName());
    }
  }

   // 判断是否是已经进行过故障注入的类 或者是 将要进行故障注入的类
   private static boolean isPrevFiCls(String clsName) {
    String clsFileName = clsName + ".class";
     return fiClsFileNames.contains(clsFileName);
  }

   // 判断是否是将要进行故障注入的类(注意:在这之前,需要调用updateCurrClsFileNames())
   private static boolean isWillingFiCls(String clsName) {
    String clsFileName = clsName + ".class";
     return fiClsFileNames.contains(clsFileName);
  }

   public static void agentmain(String agentArgs, Instrumentation inst)
       throws ClassNotFoundException, UnmodifiableClassException,
      InterruptedException {

    System.out.println( "AgentMain::agentmain!!");

     synchronized (AgentMain. class) {

      String fiClsFolderPath = agentArgs;

       if (hasFi) {
        inst.removeTransformer(transformer);

        Class[] classes = inst.getAllLoadedClasses();
         for (Class cls : classes) {
          System.out.println( "AgentMain::agentmain, recover class: "
              + cls.getName());
           if (isPrevFiCls(cls.getName())) {
             // 触发已加载的类 还原对类的更改
            inst.retransformClasses(cls);
          }
        }
      }

      updateClsFileNames(fiClsFolderPath);
      
      transformer.setFiClsFolderPath(fiClsFolderPath);
       // 这里应该不存在线程安全隐患,因为attach动作总是人为触发的
      transformer.setFiClsFileNames(fiClsFileNames);

       // 添加转换器
      inst.addTransformer(transformer, true);

       // 更改当前已加载的类
      Class[] classes = inst.getAllLoadedClasses();
       for (Class cls : classes) {
         if (isWillingFiCls(cls.getName())) {
          System.out
              .println( "AgentMain::agentmain, transform class: "
                  + cls.getName());
          inst.retransformClasses(cls);
        }
      }

      hasFi = true;
    }
  }
}

Transformer.java:
import java.util.Set;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class Transformer implements ClassFileTransformer {

   private String fiClsFolderPath;

   private Set fiClsFileNames = null;

   public String getFiClsFolderPath() {
     return fiClsFolderPath;
  }

   public void setFiClsFolderPath(String fiClsFolderPath) {
     this.fiClsFolderPath = fiClsFolderPath;
  }

   public Set getFiClsFileNames() {
     return fiClsFileNames;
  }

   public void setFiClsFileNames(Set fiClsFileNames) {
     this.fiClsFileNames = fiClsFileNames;
  }

   private boolean isFiCls(String clsName) {
    String clsFileName = clsName + ".class";
     return fiClsFileNames.contains(clsFileName);
  }

   public static byte[] getBytesFromFile(String fileName) {
    System.out.println( "[Transformer]: getBytesFromFile: " + fileName);
     try {
       // precondition
      File file = new File(fileName);
      InputStream is = new FileInputStream(file);
       long length = file.length();
       byte[] bytes = new byte[( int) length];

       // Read in the bytes
       int offset = 0;
       int numRead = 0;
       while (offset < bytes.length
          && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
        offset += numRead;
      }

       if (offset < bytes.length) {
         throw new IOException( "Could not completely read file "
            + file.getName());
      }
      is.close();
       return bytes;
    } catch (Exception e) {
      System.out.println( "error occurs in _ClassTransformer!"
          + e.getClass().getName());
       return null;
    }
  }

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

     // 如果不是将要进行故障注入的类,直接返回null,意即不做任何的转换处理
     if (!isFiCls(className.replace( "/", "."))) {
       return null;
    }

     return getBytesFromFile(fiClsFolderPath + File.separator
        + className.replace( "/", ".") + ".class");
  }
}

类文件名的命名格式,举例:com.taobao.A.class这样。

这里,如果转换后的类(更改后的类)需要依赖某个类(记为类B) ,可以将这个类B的源码放置到agent工程,随着agent.jar打包进去。虚拟机在加载agent.jar后,也会将该类装载进去。
这样,转换后的类也可以访问到类B。

注意,为了打成agent,需要在源码目录下新建META-INF文件夹
文件夹内新建文件MANIFEST.MF,内容如下:
Manifest-Version: 1.0
Agent-Class: com.taobao.fi.AgentMain
Can-Redefine-Classes: false
Can-Retransform-Classes: false
Boot-Class-Path: fiagent.jar

特别注意,此文件是空格敏感的。每一行不容许有多余的空格。否则,打包出来的agent.jar,虚拟机会不认的。

利用eclipse的导出jar包时,记得要选择使用该工程源码目录下的MANIFEST.MF文件。

如果你想还原成原来的类,只需要将类文件夹下的类删除,然后,重新执行FiAttach即可。

本文是本人实作了 Java故障注入测试 工具后的总结,供业界同仁参考。题外话,像btrace也是基于此原理。

此文完。