前段时间做动态无侵入拦截的工作,对于“即时加载”新类有了一些较深入的理解,已经写出两篇文章在这里。
我们已经解决了“如何修改”的问题,但是另一个问题是“能修改成什么样子”。
利用Instrumentation来动态redefine的类,只能修改方法,即在原有的方法代码插入代码来实现我们需要的逻辑。
却无法增加,删除方法和字段,即修改类定义的结构。
目前真正能做到“即时”加载结构已经被修改的新的class的工具就是javarebel.但这个工具是一个收费的(免费的功能有限)
工具,其它还没见到有相同功能的工具,其实这种需求是很强烈的,特别是在开发阶段,新修改一个类的实现,特别是类的结构。
希望能在不重启的情况下能即时看到效果。而实际上只javarebel能做到,说明这个技术还是有些小难度的。
经过非人的折磨(看混淆过的反编译码还不如直接看字节码更明白),基本弄懂了某个工具的实现原理(传说为了研究目的进行
某些工作可以原谅的,也正是因为这个原因可能会引起不必要的麻烦,所以不写具体的代码,只做原理性的记录在这里)。
当然,对于从class读懂了它的字节码,crack是相当简单滴了。
首先,把JDK中几个基本类的源码拿过来修改。
ClassLoader,Class,Enum,Constructor,Field,Method,Proxy.这几个类,其实还有其它几个ObjectStreamClass,URLClassLoader,PropertyResourceBundle,ResourceBundle$Control这几个类都需要修改。但是
从原理上讲对于最小模型我们先动ClassLoader,Class,Enum,Constructor,Field,Method,Proxy.
现在假设有一个class:
class A{
int x = 0;
}
要加载这个类,一定不能被JRE的默认ClassLoader加载,因为类一旦在JVM中露头,你在redefin时只能修改它的方法体而不能
修改它的结构了。所以我们要修改ClassLoader,当加载类A的时候,我们利用字节码生成器生成一个A$$ver1.class,其中A的
所有方法都在A$$ver1中实现,并反回这个A$$ver1的Class引用。
这里我们就需要修改Class,Constructor,Field,Method这几个基础类了。比如Class的getName(),当A$$ver1.class.getName()时
在return name前需要
int index = name.indexOf("$$ver");
if(index != -1) name = name.subString(0,index);
而Class.forName(String name)时我们则需要
修改成name = name + "$$verx";//这个x可以用一个全局的map保存原始类和当前版本的对应表。
还有Constructor,Method,Field这些反射方法,都要修改,即load A时,我实际是动态生成了A$$ver1.class,所以这些类中对应的方法中
要修改成实际上找A$$ver1来对应A的逻辑。
这时如果我们修改了A:
class A{
int x = 0;
int y = 0;
}
当扫描发现类结构被修改后,被crack过的ClassLoader又动态生成一个A$$ver2.class,当然修改后的方法和字段都生成在A$$ver2.class中,这里映射表中把A的对应类换成A$$ver2,所以Class类,Constructor,Field,Method,Proxy这些基础类访问A.class时都知道直接去访问A$$ver2.class,原来的A$$ver1.class就放弃了。
这样实际上每次修改A时底层实际上是生成了不同版本的它的替身,然后通过被crack过的基础类ClassLoader,Class,Enum,Constructor,Field,Method,Proxy就能达到动态“加载”目的。
前提是这几个被crack过的基础类一定要在Boot-Classpath中,先于JRE的原始的基础类工作。