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 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();
}
}
}
}
}
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
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 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;
}
}
}
import java.util.HashSet;
import java.io.File;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class AgentMain {
private static Set
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 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");
}
}
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
public String getFiClsFolderPath() {
return fiClsFolderPath;
}
public void setFiClsFolderPath(String fiClsFolderPath) {
this.fiClsFolderPath = fiClsFolderPath;
}
public Set
return fiClsFileNames;
}
public void setFiClsFileNames(Set
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-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也是基于此原理。
此文完。