1 案例分析
1.1 Tomcat:正统的类加载器架构
- Java Web 服务器:部署在同一个服务器上的两个 Web 应用程序所使用的 Java 类库可以实现相互隔离又要可以互相共享;尽可能保证自身的安全不受部署的 Web 应用程序影响;要支持 JSP 生成类的热替换;
- 上图中,灰色背景的三个类加载器是 JDK 默认提供的类加载器,而 CommonClassLoader、CatalinaClassLoader、SharedClassLoader 和 WebappClassLoader 是 Tomcat 自己定义的类加载器,分别加载 /common/(可被 Tomcat 和 Web 应用共用)、/server/(可被 Tomcat 使用)、/shared/(可被 Web 应用使用)和 /WebApp/WEB-INF/(可被当前 Web 应用使用)中的 Java 类库,Tomcat 6.x 把前面三个目录默认合并到一起变成一个/lib目录(作用同原先的 common 目录);
1.2 OSGI:灵活的类加载架构
- OSGI 的每个模块称为 Bundle,可以声明它所依赖的 Java Package(通过 Import-Package 描述),也可以声明它允许导出发布的 Java Package(通过 Export-Package 描述);
- 除了更精确的模块划分和可见性控制外,引入 OSGI 的另外一个重要理由是基于 OSGI 的程序很可能可以实现模块级的热插拔功能;
- OSGI 的类加载器之间只有规则,没有固定的委派关系;加载器之间的关系更为复杂、运行时才能确定的网状结构,提供灵活性的同时,可能会产生许多的隐患;
1.3 字节码生成技术与动态代理的实现
- 在 Java 里面除了 javac 和字节码类库外,使用字节码生成的例子还有 Web 服务器中的JSP编译器、编译时植入的 AOP 框架和很常用的动态代理技术等,这里选择其中相对简单的动态代理来看看字节码生成技术是如何影响程序运作的;
- 动态代理的优势在于实现了在原始类和接口还未知的时候就确定类的代理行为,可以很灵活地重用于不同的应用场景之中;
- 以下的例子中生成的代理类“$Proxy0.class”文件可以看到代理为传入接口的每一个方法统一调用了 InvocationHandler 对象的 invoke 方法;其生成代理类的字节码大致过程其实就是根据 Class 文件的格式规范去拼接字节码;
public class DynamicProxyTest {
interface IHello {
void sayHello();
}
static class Hello implements IHello {
@Override
public void sayHello() {
System.out.println("Hello world");
}
}
static 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 static void main(String[] args) {
// add this property to generate proxy class file
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
IHello hello = (IHello) new DynamicProxy().bind(new Hello());
hello.sayHello();
}
}
1.4 Retrotranslator:跨越 JDK 版本
- Retrotranslator 的作用是将 JDK 1.5 编译出来的 Class 文件转变为可以在 JDK 1.4 或 JDK 1.3 部署的版本,它可以很好地支持自动装箱、泛型、动态注解、枚举、变长参数、遍历循环、静态导入这些语法特性,甚至还可以支持JDK 1.5 中新增的集合改进、并发包以及对泛型、注解等的反射操作;
- JDK 升级通常包括四种类型:编译器层面的做的改进、Java API 代码增强、需要再字节码中进行支持的活动以及虚拟机内部的改进,Retrotranslator 只能模拟前两类,第二类通过独立类库实现,第一类则通过 ASM 框架直接对字节码进行处理;
2 实战:自己动手实现远程执行功能
2.1 目标
不依赖 JDK 版本、不改变原有服务端程序的部署,不依赖任何第三方类库、不侵入原有程序、临时代码的执行结果能返回到客户端;
2.2 思路
- 如何编译提交到服务器的 Java 代码(客户端编译好上传 Class 文件而不是 Java 代码)
- 如何执行编译之后的 Java 代码(要能访问其他类库,要能卸载)
- 如何收集 Java 代码的执行结果(在执行的类中把 System.out 的符号引用替换为我们准备的 PrintStream 的符号引用)
2.3 具体实现
- HotSwapClassLoader 用于实现同一个类的代码可以被多次加载,通过公开父类 ClassLoader 的 defineClass 实现
- HackSystem 是为了替换 java.lang.System,它直接修改 Class 文件格式的 byte[] 数组中的常量池部分,将常量池中指定内容的 CONSTANT_Utf8_info 常量替换为新的字符串
- ClassModifier 涉及对 byte[] 数组操作的部分,主要是将 byte[] 与 int 和 String 互相转换,以及把对 byte[] 数据的替换操作封装在 ByteUtils 类中
- 经过 ClassModifier 处理过的 byte[] 数组才会传给 HotSwapClassLoader.loadByte 方法进行类加载
- 而 JavaClassExecutor 是提供给外部调用的入口
public class JavaClassExecutor {
public static String execute(byte[] classByte) {
HackSystem.clearBuffer();
ClassModifier cm = new ClassModifier(classByte);
byte[] modifiedBytes = cm.modifyUTF8Constant("java/lang/System",
"org/fenixsoft/classloading/execute/HackSystem");
HotSwapClassLoader hotSwapClassLoader = new HotSwapClassLoader();
Class clazz = hotSwapClassLoader.loadByte(modifiedBytes);
try {
Method method = clazz.getMethod("main", new Class[] { String[].class });
method.invoke(null, new String[] { null });
} catch (Throwable t) {
t.printStackTrace(HackSystem.out);
}
return HackSystem.getBufferString();
}
}
用于测试的JSP
<%@page import="java.lang.*" %>
<%@page import="java.io.*" %>
<%@page import="org.fenixsoft.classloading.execute.*" %>
<%
InputStream is = new FileInputStream("c:/TestClass.class");
byte[] b = new byte[is.available()];
is.read(b);
is.close();
out.println(JavaClassExecutor.execute(b));
%>