1.概述
2.开篇
Arthas提供retransform命令来重新加载已加载的类
,通过该命令可以在有限制的反编译已加载的类重新修改后生成class文件重新加载。
在测试环境中可以尝试着去使用这个命令,毕竟这个原理也是基于Instrumentation来达成的
。Instrumentation提供retransformClasses来实现上述的功能。
想操作字节码,Instrumentation基本上是逃不过的基础技术
。
3.基础知识点
在 Java SE6 里面,最大的改变是运行时的 Instrumentation 成为可能。在 Java SE 5 中,Instrument 要求在运行前利用命令行参数或者系统参数来设置代理类
,在实际的运行之中,虚拟机在初始化之时(在绝大多数的 Java 类库被载入之前),instrumentation 的设置已经启动,并在虚拟机中设置了回调函数,检测特定类的加载情况,并完成实际工作
。
但是在实际的很多的情况下,我们没有办法在虚拟机启动之时就为其设定代理,这样实际上限制了 instrument 的应用。而 Java SE 6 的新特性改变了这种情况,通过 Java Tool API 中的 attach 方式,我们可以很方便地 在运行过程中动态地设置加载代理类,以达到 instrumentation 的目的
。
Instrumentation提供retransformClasses重新转换提供的一组类。
该方法主要作用于已经加载过的class。可以用ClassFileTransformer对初始化过或者redifine过的class进行重新处理, 无论以前是否发生转换,此函数都将重新运行转换过程。转换后的类文件字节作为类的新定义安装。
4.Arthas的retransform
public class RetransformCommand extends AnnotatedCommand {
private static void initTransformer() {
if (transformer != null) {
return;
} else {
synchronized (RetransformCommand.class) {
if (transformer == null) {
transformer = new RetransformClassFileTransformer();
TransformerManager transformerManager = ArthasBootstrap.getInstance().getTransformerManager();
transformerManager.addRetransformer(transformer);
}
}
}
}
@Override
public void process(CommandProcess process) {
// 核心操作初始化Transformer
initTransformer();
Map<String, byte[]> bytesMap = new HashMap<String, byte[]>();
for (String path : paths) {
// 加载外部的class文件的字节流
RandomAccessFile f = null;
try {
f = new RandomAccessFile(path, "r");
final byte[] bytes = new byte[(int) f.length()];
f.readFully(bytes);
// 通过repackage-asm的依赖能够解析字节码获取类名
final String clazzName = readClassName(bytes);
bytesMap.put(clazzName, bytes);
} catch (Exception e) {
// 省略无关代码
} finally {
// 省略无关代码
}
}
List<RetransformEntry> retransformEntryList = new ArrayList<RetransformEntry>();
List<Class<?>> classList = new ArrayList<Class<?>>();
// 通过inst获取已加载的类并和指定重载的类进行比较,存在的才重新加载
for (Class<?> clazz : inst.getAllLoadedClasses()) {
if (bytesMap.containsKey(clazz.getName())) {
// 省略相关代码
ClassLoader classLoader = clazz.getClassLoader();
if (classLoader != null && hashCode != null
&& !Integer.toHexString(classLoader.hashCode()).equals(hashCode)) {
continue;
}
RetransformEntry retransformEntry = new RetransformEntry(clazz.getName(), bytesMap.get(clazz.getName()),
hashCode, classLoaderClass);
retransformEntryList.add(retransformEntry);
// 保存待重新加载的类
classList.add(clazz);
retransformModel.addRetransformClass(clazz.getName());
}
}
try {
addRetransformEntry(retransformEntryList);
// 重新触发字节类重新加载
inst.retransformClasses(classList.toArray(new Class[0]));
} catch (Throwable e) {
}
}
}
initTransforme方法负责初始化类转换对象RetransformCommand。
整个process核心逻辑加载class文件的字节码用于重新加载,前提是重新加载的类是和inst返回的已加载的类存在交集的。
通过inst.retransformClasses操作来触发类的重新加载。
public class RetransformCommand extends AnnotatedCommand {
static class RetransformClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className == null) {
return null;
}
className = className.replace('/', '.');
List<RetransformEntry> allRetransformEntries = allRetransformEntries();
// 倒序,因为要执行的配置生效
ListIterator<RetransformEntry> listIterator = allRetransformEntries
.listIterator(allRetransformEntries.size());
while (listIterator.hasPrevious()) {
RetransformEntry retransformEntry = listIterator.previous();
int id = retransformEntry.getId();
// 判断类名是否一致
boolean updateFlag = false;
// 类名一致,则看是否要比较 loader,如果不需要比较 loader,则认为成功
if (className.equals(retransformEntry.getClassName())) {
if (retransformEntry.getClassLoaderClass() != null || retransformEntry.getHashCode() != null) {
updateFlag = isLoaderMatch(retransformEntry, loader);
} else {
updateFlag = true;
}
}
if (updateFlag) {
retransformEntry.incTransformCount();
// 返回待重新加载的字节码
return retransformEntry.getBytes();
}
}
return null;
}
}
}
RetransformCommand核心在于retransformEntry.getBytes()返回待重新加载的字节码。