9.3.3 实现

在程序实现部分,我们主要看一下代码及其注释。首先看看实现过程中需要用到的4个支持类。第一个类用于实现“同一个类的代码可以被多次加载”这个需求,即用于解决9.3.1节中列举的第2个问题的HotSwapClassLoader,具体程序如代码清单9-3所示。

代码清单9-3 HotSwapClassLoader的实现


/**

*为了多次载入执行类而加入的加载器<br>

*把defineClass方法开放出来,只有外部显式调用的时候才会使用到loadByte方法

*由虚拟机调用时,仍然按照原有的双亲委派规则使用loadClass方法进行类加载

*

*@author zzm

*/

public class HotSwapClassLoader extends ClassLoader{

public HotSwapClassLoader(){

super(HotSwapClassLoader.class.getClassLoader());

}

public Class loadByte(byte[]classByte){

return defineClass(null,classByte,0,classByte.length);

}

}


HotSwapClassLoader所做的事情仅仅是公开父类(即java.lang.ClassLoader)中的protected方法defineClass(),我们将会使用这个方法把提交执行的Java类的byte[]数组转变为Class对象。HotSwapClassLoader中并没有重写loadClass()或findClass()方法,因此如果不算外部手工调用loadByte()方法的话,这个类加载器的类查找范围与它的父类加载器是完全一致的,在被虚拟机调用时,它会按照双亲委派模型交给父类加载。构造函数中指定为加载HotSwapClassLoader类的类加载器作为父类加载器,这一步是实现提交的执行代码可以访问服务端引用类库的关键,下面我们来看看代码清单9-3。

第二个类是实现将java.lang.System替换为我们自己定义的HackSystem类的过程,它直接修改符合Class文件格式的byte[]数组中的常量池部分,将常量池中指定内容的CONSTANT_Utf8_info常量替换为新的字符串,具体代码如代码清单9-4所示。ClassModifier中涉及对byte[]数组操作的部分,主要是将byte[]与int和String互相转换,以及把对byte[]数据的替换操作封装在代码清单9-5所示的ByteUtils中。

代码清单9-4 ClassModifier的实现


/**

*修改Class文件,暂时只提供修改常量池常量的功能

*@author zzm

*/

public class ClassModifier{

/**

*Class文件中常量池的起始偏移

*/

private static final int CONSTANT_POOL_COUNT_INDEX=8;

/**

*CONSTANT_Utf8_info常量的tag标志

*/

private static final int CONSTANT_Utf8_info=1;

/**

*常量池中11种常量所占的长度,CONSTANT_Utf8_info型常量除外,因为它不是定长的

*/

private static final int[]CONSTANT_ITEM_LENGTH={-1,-1,-1,5,5,9,9,3,3,5,5,5,5};

private static final int u1=1;

private static final int u2=2;

private byte[]classByte;

public ClassModifier(byte[]classByte){

this.classByte=classByte;

}

/**

*修改常量池中CONSTANT_Utf8_info常量的内容

*@param oldStr修改前的字符串

*@param newStr修改后的字符串

*@return修改结果

*/

public byte[]modifyUTF8Constant(String oldStr,String newStr){

int cpc=getConstantPoolCount();

int offset=CONSTANT_POOL_COUNT_INDEX+u2;

for(int i=0;i<cpc;i++){

int tag=ByteUtils.bytes2Int(classByte,offset,u1);

if(tag==CONSTANT_Utf8_info){

int len=ByteUtils.bytes2Int(classByte,offset+u1,u2);

offset+=(u1+u2);

String str=ByteUtils.bytes2String(classByte,offset,len);

if(str.equalsIgnoreCase(oldStr)){

byte[]strBytes=ByteUtils.string2Bytes(newStr);

byte[]strLen=ByteUtils.int2Bytes(newStr.length(),u2);

classByte=ByteUtils.bytesReplace(classByte,offset-u2,u2,strLen);

classByte=ByteUtils.bytesReplace(classByte,offset,len,strBytes);

return classByte;

}else{

offset+=len;

}

}else{

offset+=CONSTANT_ITEM_LENGTH[tag];

}

}

return classByte;

}

/**

*获取常量池中常量的数量

*@return常量池数量

*/

public int getConstantPoolCount(){

return ByteUtils.bytes2Int(classByte,CONSTANT_POOL_COUNT_INDEX,u2);

}

}


代码清单9-5 ByteUtils的实现


/**

*Bytes数组处理工具

*@author

*/

public class ByteUtils{

public static int bytes2Int(byte[]b,int start,int len){

int sum=0;

int end=start+len;

for(int i=start;i<end;i++){

int n=((int)b[i])&0xff;

n<<=(—len)*8;

sum=n+sum;

}

return sum;

}

public static byte[]int2Bytes(int value,int len){

byte[]b=new byte[len];

for(int i=0;i<len;i++){

b[len-i-1]=(byte)((value>>8*i)&0xff);

}

return b;

}

public static String bytes2String(byte[]b,int start,int len){

return new String(b,start,len);

}

public static byte[]string2Bytes(String str){

return str.getBytes();

}

public static byte[]bytesReplace(byte[]originalBytes,int offset,int len,byte[]replaceBytes){

byte[]newBytes=new byte[originalBytes.length+(replaceBytes.length-len)];

System.arraycopy(originalBytes,0,newBytes,0,offset);

System.arraycopy(replaceBytes,0,newBytes,offset,replaceBytes.length);

System.arraycopy(originalBytes,offset+len,newBytes,offset+replaceBytes.length,originalBytes.length-offset-len);

return newBytes;

}

}


经过ClassModifier处理后的byte[]数组才会传给HotSwapClassLoader.loadByte()方法进行类加载,byte[]数组在这里替换符号引用之后,与客户端直接在Java代码中引用HackSystem类再编译生成的Class是完全一样的。这样的实现既避免了客户端编写临时执行代码时要依赖特定的类(不然无法引入HackSystem),又避免了服务端修改标准输出后影响到其他程序的输出。下面我们来看看代码清单9-4和代码清单9-5。

最后一个类就是前面提到过的用来代替java.lang.System的HackSystem,这个类中的方法看起来不少,但其实除了把out和err两个静态变量改成使用ByteArrayOutputStream作为打印目标的同一个PrintStream对象,以及增加了读取、清理ByteArrayOutputStream中内容的getBufferString()和clearBuffer()方法外,就再没有其他新鲜的内容了。其余的方法全部都来自于System类的public方法,方法名字、参数、返回值都完全一样,并且实现也是直接转调了System类的对应方法而已。保留这些方法的目的,是为了在Sytem被替换成HackSystem之后,执行代码中调用的System的其余方法仍然可以继续使用,HackSystem的实现如代码清单9-6所示。

代码清单9-6 HackSystem的实现


/**

*为JavaClass劫持java.lang.System提供支持

*除了out和err外,其余的都直接转发给System处理

*

*@author zzm

*/

public class HackSystem{

public final static InputStream in=System.in;

private static ByteArrayOutputStream buffer=new ByteArrayOutputStream();

public final static PrintStream out=new PrintStream(buffer);

public final static PrintStream err=out;

public static String getBufferString(){

return buffer.toString();

}

public static void clearBuffer(){

buffer.reset();

}

public static void setSecurityManager(final SecurityManager s){

System.setSecurityManager(s);

}

public static SecurityManager getSecurityManager(){

return System.getSecurityManager();

}

public static long currentTimeMillis(){

return System.currentTimeMillis();

}

public static void arraycopy(Object src,int srcPos,Object dest,int destPos,int length){

System.arraycopy(src,srcPos,dest,destPos,length);

}

public static int identityHashCode(Object x){

return System.identityHashCode(x);

}

//下面所有的方法都与java.lang.System的名称一样

//实现都是字节转调System的对应方法

//因版面原因,省略了其他方法

}


至此,4个支持类已经讲解完毕,我们来看看最后一个类JavaClassExecuter,它是提供给外部调用的入口,调用前面几个支持类组装逻辑,完成类加载工作。JavaClassExecuter只有一个execute()方法,用输入的符合Class文件格式的byte[]数组替换java.lang.System的符号引用后,使用HotSwapClassLoader加载生成一个Class对象,由于每次执行execute()方法都会生成一个新的类加载器实例,因此同一个类可以实现重复加载。然后,反射调用这个Class对象的main()方法,如果期间出现任何异常,将异常信息打印到HackSystem.out中,最后把缓冲区中的信息作为方法的结果返回。JavaClassExecuter的实现代码如代码清单9-7所示。

代码清单9-7 JavaClassExecuter的实现


/**

*JavaClass执行工具

*

*@author zzm

*/

public class JavaClassExecuter{

/**

*执行外部传过来的代表一个Java类的byte数组<br>

*将输入类的byte数组中代表java.lang.System的CONSTANT_Utf8_info常量修改为劫持后的HackSystem类

*执行方法为该类的static main(String[]args)方法,输出结果为该类向System.out/err输出的信息

*@param classByte代表一个Java类的byte数组

*@return执行结果

*/

public static String execute(byte[]classByte){

HackSystem.clearBuffer();

ClassModifier cm=new ClassModifier(classByte);

byte[]modiBytes=cm.modifyUTF8Constant("java/lang/System","org/fenixsoft/classloading/execute/HackSystem");

HotSwapClassLoader loader=new HotSwapClassLoader();

Class clazz=loader.loadByte(modiBytes);

try{

Method method=clazz.getMethod("main",new Class[]{String[].class});

method.invoke(null,new String[]{null});

}catch(Throwable e){

e.printStackTrace(HackSystem.out);

}

return HackSystem.getBufferString();

}

}