类加载及执行子系统的案例与实战
概述
Class文件以何种格式存储、字节码指令如何执行等都是由JVM控制
字节码生成与类加载器这两部分的功能,可由用户自定义,接下来将对一些实际应用进行介绍
类加载器案例
Tomcat
主流的Java Web服务器,如Tomcat、Jetty等自定义了类加载器(且不止一个),为了实现如下需求
- 服务器上的两个Web应用的Java类库需要隔离(可能使用不同版本)或共享(避免重复加载同一类库)
- 服务器需保证自身安全不受影响,也应与应用所使用的类库互相独立
- JSP最终要编译成Class文件,且JSP支持热替换
由于上述问题,部署Web应用时需提供多个ClassPath存放第三方类库,其以lib或classes命名
- /common中的类库可被Tomcat和所有的Web应用程序共享
- /server中的类库只能被Tomcat使用
- /shared中的类库可被所有的Web应用程序共享
- /WebApp/WEB-INF的类库仅可被该Web应用程序使用
上图为Tomcat中的类加载器,按照双亲委派模型实现
- Common、Catalina、Shared、Webapp加载器分别对应加载上面路径的类库
- Common加载的类都可以被Catalina和Shared使用
- Catalina和Shared加载的类相互隔离
- WebApp可使用Shared加载的类,一个Web应用程序对应一个WebAppClassLoader,相互隔离
- 一个JSP文件对应一个JasperLoader,当JSP文件被修改时,会生成新的JasperLoader替换,以此实现HotSwap
tomcat 6之后
- /common、/server和/shared合并成/lib,相当于原来的/common
- 只有指定tomcat/conf/catalina.properties中的server.loader和share.loader才会建立Catalina和Shared,否则使用Common
OSGi
OSGi(Open Service Gateway Initiative)是OSGi联盟制订的一个基于Java的动态模块化规范(JDK 9的JPMS是静态的模块系统)
OSGi中的每个模块(称为Bundle)与普通的Java类库类似,以JAR格式进行封装,内部存储的Java的Package和Class
Bundle可以声明它所依赖的Package(Import-Package),也可以声明它允许导出的Package(Export-Package),从传统的上层模块依赖底层模块转变为平级模块之间的依赖
OSGi的优点是可以实现模块热插拔,类加载器无固定的委派关系,对Package的类加载都会委派给发布它的Bundle类加载器去完成,若对于
- Bundle A:发布了packageA,依赖java.*
- Bundle B:依赖了packageA和packageC,依赖java.*
- Bundle C:发布了packageC,依赖packageA
则它们的类加载关系如下
类加载的查找规则如下
- java.*开头的类,委派给父类加载器加载
- 否则,委派列表名单内的类,委派给父类加载器加载
- 否则,Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
- 否则,查找当前Bundle的Classpath,使用自己的类加载器加载
- 否则,查找是否在自己的Fragment Bundle中,如果是则委派给Fragment Bundle的类加载器加载
- 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
- 否则,类查找失败
OSGi中的加载器不再是双亲委派模型的树形结构,而是网状结构
JDK7之前,加载需要锁定当前类加载器,若出现循环依赖可能导致死锁,而JDK7后将锁定对象降低为类名级别,从而避免死锁
字节码案例
动态代理
动态代理中的"动态",是相对与编写代理类的"静态"代理而言,其优势不仅是省去编码的工作量,而是实现了在原始类和接口未知时就确定代理类的代理行为
public class Test {
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) {
IHello hello = (IHello) new DynamicProxy().bind(new Hello());
hello.sayHello();
}
}
如上代码通过代理,在hello world之前加上welcome
welcome
hello world
Proxy.newProxyInstance()方法返回一个实现了IHello的接口,并且代理了new Hello()实例行为的对象
代理通过调用sun.misc.ProxyGenerator::generateProxyClass()方法来生成字节码,在main中加入如下代码,可在磁盘中生成一个名为$Proxy0的代理类Class文件
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
将其反编译为如下
final class $Proxy0 extends Proxy implements IHello {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final void sayHello() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m3 = Class.forName("Test$IHello").getMethod("sayHello");
m2 = Class.forName("java.lang.Object").getMethod("toString");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
代理类为接口及Object中继承的方法都生成了对应的实现并调用了InvocationHandler对象(即h)的invoke()方法,所以无论调用DynamicProxy的哪一个方法都会回调invoke()
Java逆向移植工具
当需要把高版本JDK编写的代码放到低版本JDK环境中去部署使用,可使用
-
Retrotranslator:将JDK5编译的Class文件转变为可在JDK1.4/3上部署的版本
-
Retrolambda:将JDK 8的Lambda表达式和try-resources语法转变为可以在JDK5、JDK 6、JDK 7中使用的形式
每次JDK升级新增功能可分为五大类
- 增强Java类库,如concurren、invoke包
- 改进前端编译器,称为语法糖,如自动装箱拆箱
- 字节码改动,如invokedynamic
- JDK整体结构改动,如模块化系统
- JVM内部改动,如更换垃圾收集器
对于第一类,可使用其他可代替的包去实现,如Retrotranslator中存在backport-util-concurrent.jar代替concurren
对于第二类,JDK在编译阶段进行的改进,Retrotranslator使用ASM框架对字节码进行处理,如Retrotranslator将枚举的父类Enum转为自身类库的net.sf.retrotranslator.runtime.java.lang.Enum_,再去掉ACC_ENUM标志位
对于第三类,invokedynamic实现Lambda,Retrolambda生成一组匿名内部类来替代Lambda
而第四第五类,使用逆向移植工具比较难处理
实战------远程执行功能
有时候需要在服务中执行一小段程序代码定位或排除问题(如查看内存的参数值),但却没有让服务器执行临时代码的途径,可通过
- BTrace这类JVMTI工具去动态修改程序中某一部分的运行代码
- JDK 6的Compiler API可以动态地编译Java程序
- 写一个JSP文件在服务器浏览器中运行它,或者在服务端程序中新增BeanShell Script、JavaScript等的执行引擎去执行动态脚本
- 在应用程序中内置动态执行的功能
目标
实现在服务端执行临时代码
- 不依赖于JDK版本(1.4以上)
- 不改变原有服务端程序的部署,不依赖第三方类库
- 无须改动原程序代码,也不会影响源程序运行
- 临时代码支持Java
- 不需要依赖特定的类或实现特定的接口
- 执行结果能返回到客户端
思路
如何编译提交到服务器的Java代码?
- JDK6后可使用Compiler API,JDK6前可使用tool.jar,但是引入依赖
- 直接把编译好的字节码而不是Java代码上传
如何执行编译之后的java代码
- 让类加载器加载这个类生成一个Class对象,然后通过反射调用
如何收集Java代码的执行结果
- 使用System.out和System.err将输出重定向,但会收集到其他应用信息
- 可将System.out的符号引用替换为自定义PrintStream的符号引用
实现
HotSwapClassLoader用于实现用一个类的代码可以被多次加载的需求,用loadByte()方法公开defineClass(),把byte[]数组转变为Class对象
public class HotSwapClassLoader extends ClassLoader {
public HotSwapClassLoader() {
super(HotSwapClassLoader.class.getClassLoader());
}
public Class loadByte(byte[] classByte) {
return defineClass(null, classByte, 0, classByte.length);
}
}
ClassModifier的modifyUTF8Constant()方法将Class数组流的常量替换为指定常量,从而实现将System替换为自定义的HackSystem
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;
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;
}
public int getConstantPoolCount() {
return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX, u2);
}
}
ByteUtils用于对byte[]数组进行替换
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);
return newBytes;
}
}
HackSystem将out和err改为用PrintStream对象,以及增加了读取、清理ByteArrayOutputStream中内容的getBufferString()和clearBuffer(),其余方法调用原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();
}
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);
}
}
JavaclassExecuter组合前面的类完成类加载
public class JavaclassExecuter {
public static String execute(byte[] classByte) {
HackSystem.clearBuffer();
ClassModifier cm = new ClassModifier(classByte);
byte[] modiBytes = cm.modifyUTF8Constant("java/lang/System", "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();
}
}
验证(未完成,不会写JSP)
任意书写一个TestClass类,向System.out输出信息,再建立个JSP文件用于再浏览器中看TestClass的运行结果
<%@ 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("<textarea style='width:1000;height=800'>");
out.println(JavaclassExecuter.execute(b));
out.println("</textarea>");
%>