FiAttach传入两个参数,一个是agent.jar的路径,一个是存放希望运行时进行替换的类文件的文件夹路径。 程序编译时,需要依赖JDK_HOME/lib/tools.jar Transformer.java:
程序自动检测当前的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<VirtualMachineDescriptor> 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(); } } } } }
下面看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<String> fiClsFileNames = new HashSet<String>(); 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; } } }
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<String> fiClsFileNames = null; public String getFiClsFolderPath() { return fiClsFolderPath; } public void setFiClsFolderPath(String fiClsFolderPath) { this.fiClsFolderPath = fiClsFolderPath; } public Set<String> getFiClsFileNames() { return fiClsFileNames; } public void setFiClsFileNames(Set<String> 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也是基于此原理。
此文完。