【JVM.8】类加载及执行子系统的案例与实战

时间:2022-12-25 14:22:54

一. 案例分析

1.  Tomcat:正统的类加载器架构

  主流的Java Web服务器,如Tomcat、Jetty、WebLogic、WebSphere或其他服务器,都实现了自己定义的类加载器(一般都不止一个)。因为一个功能健全的Web服务器,要解决如下问题:

  •   部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。服务器应当保证两个应用程序的类库可以互相独立使用。
  •   部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享。如果部分类库不能共享,虚拟机的方法区就会很容易出现过度膨胀的风险。
  •   服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响。基于安全考虑,服务器所使用的类库应该与应用程序的类库互相独立。
  •   支持JSP应用的Web服务器,大多数需要支持HotSwap功能。我们知道,JSP文件最终要编译成Java Class 才能由虚拟机执行,但JSP文件由于其纯文本存储的特性,运行时修改的概率远远大于第三方类库或程序自身Class文件。

  由于存在上述问题,在部署Web应用时,单独的一个ClassPath就无法满足需求了,所以各种Web服务器都“不约而同”地提供了好几个ClassPath路径供用户存放第三方类库,这些路径一般都以“lib”或“classes”命名。不同路径的类库,具备不同的访问范围和服务对象。

  

  在Tomcat目录结构中,有3组目录(“/common/*”、“/server/*”和“/shared/*”)可以存放Java类库,另外加上Web应用程序自身目录“WEB-INF/*”,一共4组,把Java类库放置在这些目录中的含义分别是:

  •   放置在/common目录中: 类库可被Tomcat和所有的Web应用程序共同使用。
  •   放置在/server目录中: 类库可被Tomcat使用,对所有Web应用程序都不可见。
  •   放置在/shared目录中: 类库可被所有的Web应用程序共同使用,但对Tomcat不可见。
  •   放置在/WebApp/WEB-INF目录中: 类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。

      【JVM.8】类加载及执行子系统的案例与实战

  为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现。

   从图中的委派关系,可以看出,CommonClassLoader能加载的类都可以被CatelinaClassLoaderSharedClassLoader使用,而CatelinaClassLoaderSharedClassLoader自己能加载的类则与对方相互隔离。

  WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。

  而JasperLoader的加载范围则仅仅是这个JSP文件所编译出来的哪一个Class,它出现的目的就是为了被抛弃:当服务器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。

   注意:对于Tomcat6.x的版本,只有指定了 tomcat/conf/catalina.properties 配置文件的 server.loader share.loader 项才会真正建立对应的 *ClassLoader实例,否则会用到这两个类加载器的地方使用CommonClassLoader 的实例来代替,默认配置中没有设置这两个loader 项。

 

2.  OSGI:灵活的类加载器架构

  在OSGI里面,Bundle之间的依赖关系从传统的上层模块依赖底层模块转变为平级模块之间的依赖。

  OSGI特点,要归功于它灵活的类加载架构。OSGI的Bundle类加载器之间只有规则,没有固定的委派关系。

 

3.  字节码生成技术与动态代理的实现

  相信许多Java开发人员都是用过动态代理,例如 java.lang.reflect.Proxy 或实现过 java.lang.reflect.InvocationHandler 接口。

   下面一个例子,在方法前面打印一句“welcome”。

  

public interface IHello {
    void sayHello();
    void sayHi();
}

public class Hello implements IHello{

    @Override
    public void sayHello() {
        System.out.println("hello world");
    }

    @Override
    public void sayHi() {
        System.out.println("hi world");
    }
}

public class DynamicProxy implements InvocationHandler {

    Object originalObj;

    Object bind(Object originalObj) {
        this.originalObj = originalObj;
        return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(), originalObj.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("welcome");
        return method.invoke(originalObj,args);
    }
}

public class DynamicProxyTest {
    public static void main(String[] args) {
        IHello hel = (IHello) new DynamicProxy().bind(new Hello());
        hel.sayHello();
        hel.sayHi();
    }
}

运行结果

welcome
hello world
welcome
hi world

 

4.  Retrotranslator:跨越JDK版本

  把JDK1.5 中编写的代码放到 JDK1.4 或 1.3 的环境去部署使用。为了解决这个问题,一种名为“Java逆转移植”的工具(Java Backporting Tools)应运而生,Retrotranslator 是这类工具中较为出色的一个。

 

二. 实战:自己动手实现远程执行功能

  我们将使用前面学到的关于类加载及虚拟机执行子系统的知识去实现在服务端执行临时代码的能力。

1.   目标

  首先,在实现“在服务端执行临时代码”这个需求之前,先明确一下本次实战的具体目标,我们希望最终的产品是这样的:

  •   不依赖JDK版本,能在目前普遍使用的JDK中部署。
  •   不改变原有服务端程序的部署,不依赖任何第三方类库。
  •   不侵入原有程序,即无需改动原程序的任何代码,也不会对原有程序运行带来任何影响。
  •   “临时代码”应当具备足够的*度,不需要依赖热定的类或实现特定的接口。
  •   “临时代码”的执行结果能返回到客户端,执行结果可以包括程序中输出的信息及抛出的异常等。

 

2.  思路

  在程序实现的过程中,我们需要解决以下3个问题:

  •   如何编译提交到服务器的Java代码?
  •   如何执行编译后的Java代码?
  •   如何收集Java代码的执行结果?

 

3.  实现

  第一个类用于实现“同一个类的代码可以被多次加载”这个需求,具体代码如下:

package org.swift.framework.RemotePlugin;

/**
 * 为了多次载入执行类而加入的加载器
 * 把defineClass方法开放出来,只有外部显式调用的时候才会使用到loadByte方法
 * 由虚拟机调用时,仍然按照原有的双亲委派规则使用loadClass方法进行加载
 * zww
 */
public class HotSwapClassLoader extends ClassLoader {

    public HotSwapClassLoader() {
        super(HotSwapClassLoader.class.getClassLoader()); //使用父类的加载器
    }

    public Class loadByte(byte[] classByte) {
        return defineClass(null, classByte, 0, classByte.length);
    }

}

  HotSwapClassLoader 所做的事情仅仅是公开父类中的defineClass() ,这个类加载器的类查找范围与它的父类加载器是完全一致的。

 

  第二个类实现将 java.lang.System 替换为我们自己定义的HackSystem 类的过程,它直接修改符合Class 文件格式的 byte[] 数组中的常量池部分,将常量池中指定内容的 CONSTANT_Utf8_info 常量替换为新的字符串。

package org.swift.framework.RemotePlugin;

/**
 * 修改Class文件,暂时只提供修改常量池常量的功能
 */
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;
    }

    public byte[] modifyUTF8Constant(String oldStr, String newStr) {
        int cpc = getConstantPoolCount();   //常量的数量
        int offset = CONSTANT_POOL_COUNT_INDEX + u2;    //CONSTANT_POOL 起始位置
        for (int i = 0; i < cpc; i++) {
            int tag = ByteUtils.bytes2Int(classByte, offset, u1);   //获取常量型
            if (tag == CONSTANT_Utf8_info) {    //判断常量型类型是否是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
     */
    private int getConstantPoolCount() {
        return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX, u2);
    }

}

  ByteUtils 工具的实现:

package org.swift.framework.RemotePlugin;

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++) {
            // 因为当系统检测到byte可能会转化成int或者说byte与int类型进行运算的时候,
            // 就会将byte的内存空间高位补1(也就是按符号位补位)扩充到32位
            // 如果b[i]为负数时:例如:10000001 & 11111111  ==》 1111111111111111111111111 10000001 & 11111111 = 000000000000000000000000 10000001
            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) {
        // ||        |~offset|    ~len   ||      ||
        byte[] newBytes = new byte[originalBytes.length - len + replaceBytes.length];
        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() 方法进行类加载

 

  最后一个类就是前面提到的代替 java.lang.System 的 HackSystem ,这个类除了把 out 和 err 两个静态变量修改了,其他都来自于 System类的 public方法。

package org.swift.framework.RemotePlugin;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintStream;

/**
 * 为JavaClass 劫持 java.lang.System 提供支持
 * 除了 out 和 err 外,其余的都直接转发给 System 处理
 */
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();
    }

    //下面所有的方法都与 java.lang.System 的名称一样
    //实现都是字节调System的对应方法
    //因版面原因,省略其他方法

}

  至此,4个支持类已经讲解完毕,我们来看看最后一个类 JavaClassExecuter ,它是提供外部调用的入口

package org.swift.framework.RemotePlugin;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * JavaClass 执行工具
 */
public class JavaClassExecuter {

    /**
     * 执行外部传过来的代表一个Java类的byte数组
     * 将输入类byte数组中代表 java.lang.System的CONTANT_Utf8_info常量修改为劫持后的HackSystem类
     * 执行方法为该类的 main 方法,输出结构为该类向System.out/err输出的信息
     * @param classByte
     * @return
     */
    public static String execute(byte[] classByte) {
        HackSystem.clearBuffer();
        ClassModifier classModifier = new ClassModifier(classByte);
        //修改Class字节码,把HackSystem 替代 System
        byte[] modiBytes = classModifier.modifyUTF8Constant("java.lang.System", "org.swift.framework.RemotePlugin.HackSystem");
        HotSwapClassLoader loader = new HotSwapClassLoader();
        Class clazz = loader.loadByte(modiBytes);
        try {
            //调用其main方法
            Method method = clazz.getMethod("main", new Class[] { String[].class});
            method.invoke(null, new String[] {null});
        } catch (Exception e) {
            e.printStackTrace(HackSystem.out);
        }
        return HackSystem.getBufferString();
    }

}

 

4.  验证

  任意写一个Jaca类,只需要向外System.out 信息即可,同事放到指定路径 C://TestClass.class ,然后建立一个Jsp 文件,在浏览器可以看到这个类的运行结果。

package org.swift.framework.RemotePlugin;

public class TestClass {
    public static void main(String[] args) {
        System.out.println("this is a test class");
    }
}
<%@ page import="java.lang.*" %>
<%@ page import="java.io.*" %>
<%@ page import="org.swift.framework.RemotePlugin.*" %>
<%
    InputStream is = new FileInputStream("C:/TestClass.class");
    byte[] b = new byte[is.available()];
    is.read(b);
    is.close();

    out.println("<textarea style='width:1000;height:800'>");
    out.println(JavaClassExecuter.execute(b));
    out.println("</textarea>");
%>

 

其中主要需要学习的就是对 class文件的内容进行修改替换,并可以正常提供使用。