上一篇: 深入理解Java虚拟机—虚拟机字节码执行引擎
下一篇:深入理解Java虚拟机—前端编译与优化
类加载及执行子系统的案例与实战
一. 概述
在Class文件格式与执行引擎这部分中,用户的程序能直接影响的内容并不太多, Class文件以何种格式存储,类型何时加载、如何连接,以及虚拟机如何执行字节码指令等都是由虚拟机直接控制的行为,用户程序无法对其进行改变。能通过程序进行操作的,主要是字节码生成与类加载器这两部分的功能,但仅仅在如何处理这两点上,就已经出现了许多值得欣赏和借鉴的思路,这些思路后来成为了许多常用功能和程序实现的基础
二. 案例分析
1.Tomcat:正统的类加载器架构
一个功能健全的web服务器,都要解决如下几个问题:
- 部署在同一个服务器上的Web应用程序所使用的Java类库可以实现相互隔离.这是最基本的需求,两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当可以保证两个应用程序的类库可以相互独立使用
- 部署在统一服务器上的两个Web应用程序所使用的Java类库可以互相共享.这个需求也很常见,例如用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,如果把10份Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费-这主要不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到服务器内存,如果类库不能共享,虚拟机的方法区很容易就会出现过渡膨胀的风险.
- 服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响.目前,有许多主流的JavaWeb服务器自身也是使用Java语言来实现的.因此服务器本事也有类库依赖的问题,一般来说,基于安全考虑,服务器所使用的类芦应该与应用程序的类库互相独立.
- 支持JSP应用的Web服务器,十有八九都需要支持HotSwap功能.我们知道JSP文件最终要被编译成Java Class才能被虚拟机执行,但JSP文件由于其纯文本存储的特性,被运行时修改的概率远远大于第三方类库或程序自己的Class文件.而且ASP,PHP和JSP这些网页应用也把修改后无须重启作为一个很大的"优势"来看待,因此"主流"的Web服务器都会支持JSP生成类的热替换,当然也有"非主流"的,如运行在生产模式(Production Mode)下的WebLogic服务器默认就不会处理JSP文件的变化
在Tomcat目录结构中,有三组目录("/common/","/server/“和”/shared/")可以存放Java类库,另外还可以加上Web应用程序自身的目录"/WEB-INF/",一共四组,把Java类库放置在这些目录中的含义分别是:
- 放置在/common目录中:类库可被Tomcat和所有的Web应用程序共同使用
- 放置在/server目录中:类库可被Tomcat使用,对所有的Web应用程序都不可见
- 放置在/shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见
- 放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见
为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,
这些类加载器按照经典的双亲委派模型来实现,如下图所示
从委托关系可以看出,CommonClassLoader能加载的类都可以被CtatlinaClassLoader和SharedClassLoader使用,而CtatlinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。WeAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离(这里面用户使用第三方库且版本可能不一样),JasperLoader的加载范围仅仅是JSP文件所编译出来的那一个Class,他出现的目的仅仅是为了被丢弃:当服务器检测到JSP文件被修改时,会替换当前的jasperLoader的实例(多个实例),并通过再建立一个新的JSP类加载器去实现JSP文件的HotSwap功能。
但对于tomcat6及以后的版本,只有指定了tomcat/conf/catalina.properties配置文件的server.loader和share.loader 项才会真正建立CtatlinaClassLoader和SharedClassLoader实例,否则将会用到他们的地方被CommonClassLoader的实例所代替,默认配置没有设置这两项,所以把/common/、/server/、/shared/*的目录都一起变成一个/lib目录,相当于以前的/Common目录
2. OSGI 灵活的类加载器
java世界的模块化标准,广泛应用。
- 没有父子关系,所有的bundle(模块)为平级关系;
- 根据发布和引用package,来决定引用依赖关系
- 其逻辑结构由树(双亲委派)-> 图
- 可能死锁:循环引用
- 解决:单线程串行加载
OSGI(Open Service Gateway Initiative)是一个由OSGI联盟联合制定的一个基于Java语言的动态模块化规范,而后这个规范得到不断发展,并具备成熟的特性,最著名的应用案例就是: Eclipse IDE 另外许多大型软件平台和服务器都是基于OSGI规范来实现的。
OSGI中的 每个模块(Bundle) 与普通的Java类库区别不大,2者都是由JAP格式封装,并且内部存储的都是Java Package和Class。但一个Bundle可以声明它所依赖的Java Package(通过Import Package描述),也可以声明它所允许的导出的Java Package(由Export-Package描述)
在OSGI中,模块称之为bundle,一个bundle在物理上而言就是一个jar包。Jar包中有一个描述jar包的信息文件,位于jar内部的META-INF目录下的MANIFEST.MF文件。OSGI通过MANIFEST这个文件获取模块的定义信息,比如模块间的互访信息,模块的版本信息等。
一个bundle中一般包含如下的东西:
部署描述文件(MANIFEST.MF,必要的),各类资源文件(如html、xml等,非必须的),还有类文件。这与一个普通的jar包没有任何的区别。但是,除此之外,bundle里还可以放入其它的jar包,用于提供给bundle内部的类引用,即bundle内部的lib库。(跟一个war很类似)。
Note:实际上bundle里可以存放任何的内容,但是在bundle内部不会有嵌套的bundle,即上面提到的存放于bundle中的jar包就只会当成是一个普通的jar包,不管这些jar包中是否含有bundle定义的信息
在OSGI里面,Bundle之间的依赖关系从传统的上层模块依赖下层模块转变为 平级模块之间的依赖 ,而且类库的可见性得到严格的控制,一个模块里只有被Export的Package才能被外界访问,其它的Package和Class会被隐藏起来。引入OSGI的另一个理由,可能实现热插拔功能,当程序升级时,只需要重新启动其中一小部分
OSGI之所以有如此优势,主要归功于它灵活的类加载架构。即Bundle类加载器之间只有规则,没有固定的委派原则。如,一个Bundle声明了一个它依赖的Package,如果其它的Bundle声明发布了这个Package,那么对这个Package类加载请求提交给发布过它的Bundle类加载器去完成
一个例子:
- BundleA: 声明发布了PackageA ,依赖java.*包
- BundleB: 声明依赖了PackageA PackageC 同时也依赖java.*包
- BundleC: 声明发布了PackageC 依赖PackageA
相应的类加载关系:
- 以java.*开头的类,委派给父类加载器完成
- 否则,委派给列表单内的类,委派给父类加载器完成
- 否则,Import列表中的类,委派给Export这个类的Bundle类加载完成
- 否则,查找当前Bundle的Classpath 使用自己的类加载器加载
- 否则,查找当前是否在自己的Fragment Bundle中,如果是则委派给Fragment Bundle 的 类加载器加载
3. 字节码生成技术与动态代理的实现
“ 字节码生成 ” 并不是什么高深的技术,读者在看到“字节码生成”这个标题时也先不必去想诸如Javassist、CGLib、ASM值类的字节码类库,因为JDK里面的javac命令就是字节码生成技术的“老祖宗”,并且javac也是一个由Java语言写成的程序,他的代码存放在OpenJDK的langtools/src/share/classes/com/sun/tools/javac目录中。要深入了解字节码生成,阅读javac和源码是个很好的途径。不过Javac对于我们这个例子来说太过庞大了。在Java里面除了javac和字节码类库外,使用字节码生成的例子还有很多,如Web服务器中的JSP编译器,编译时植入的AOP框架,还有很常用的动态代理技术,甚至在使用反射的时候虚拟机都有可能会在运行时生成字节码来提高执行速度。我们选择其中相对简单的动态代理来看看字节码生成技术是如何影响程序运作的。
相信许多Java开发人员都是用过动态代理,即时没有直接使用过java.lang.reflect.Proxy或实现过java.lang.reflect.InvocationHandler接口,应该也用过Spring来做过Bean的组织管理。如果使用过Spring,那大多数情况都会用过动态代理,因为如果Bean是面向接口编程,那么在Spring内部都是通过动态代理的方式来对Bean进行增强的。动态代理中所谓的“动态”,是针对使用Java代码实际编写了代理类的“静态”代理而言的,他的优势不在于省去了编写代码类那一点工作量,而是实现了可以在原始类和接口还未知的时候,就确定代理类的代理行为,当代理类与原始类脱离直接联系后,就可以很灵活的重用于不同的应用场景之中。
下面代码演示了一个最简单的动态代理的用法,原始的逻辑是打印一句“hello world”,代理类的逻辑是在原始类方法执行前打印一句“welcome”。我们先看一下代码,然后再分析JDK是如何做到的,
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) {
IHello hello = (IHello) new DynamicProxy().bind(new Hello());
hello.sayHello();
}
运行结果:
welcome
hello world
上述代码里,唯一的“黑匣子”就是Proxy.newProxyInstance()方法,除此之外再没有任何特殊之处。这个方法返回一个实现了IHello接口,并且代理了new Hello()实例行为的对象。跟踪这个方法的源码,可以看到程序进行了验证、优化、缓存、同步、生成字节码、显示类加载等操作,前面的步骤并不是我们关注的重点,而最后他调用了sun.misc.ProxyGenerator.generateProxyClass()方法来完成生成字节码的动作,这个方法可以在运行时产生一个描述代理类的字节码byte[]数组。如果想看一看这个在运行时产生的代理类中写了些什么,可以在main()方法中加入下面这句:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
加入这句代码后再次运行程序,磁盘中将会产生一个名为“$Proxy0.class”的代理类Class文件,反编译后可以看见如下所示的内容:
package org.fenixsoft.bytecode;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
final class $Proxy0 extends Proxy
implements DynamicProxyTest.IHello
{
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
public $Proxy0(InvocationHandler paramInvocationHandler)
throws
{
super(paramInvocationHandler);
}
public final boolean equals(Object paramObject)
throws
{
try
{
return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue();
}
catch (Error|RuntimeException localError)
{
throw localError;
}
catch (Throwable localThrowable)
{
throw new UndeclaredThrowableException(localThrowable);
}
}
public final void sayHello()
throws
{
try
{
this.h.invoke(this, m3, null);
return;
}
catch (Error|RuntimeException localError)
{
throw localError;
}
catch (Throwable localThrowable)
{
throw new UndeclaredThrowableException(localThrowable);
}
}
public final String toString()
throws
{
try
{
return (String)this.h.invoke(this, m2, null);
}
catch (Error|RuntimeException localError)
{
throw localError;
}
catch (Throwable localThrowable)
{
throw new UndeclaredThrowableException(localThrowable);
}
}
public final int hashCode()
throws
{
try
{
return ((Integer)this.h.invoke(this, m0, null)).intValue();
}
catch (Error|RuntimeException localError)
{
throw localError;
}
catch (Throwable localThrowable)
{
throw new UndeclaredThrowableException(localThrowable);
}
}
static
{
try
{
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
m3 = Class.forName("org.fenixsoft.bytecode.DynamicProxyTest$IHello").getMethod("sayHello", new Class[0]);
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
return;
}
catch (NoSuchMethodException localNoSuchMethodException)
{
throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
}
catch (ClassNotFoundException localClassNotFoundException)
{
throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
}
}
}
这个代理类的实现代码也很简单,他为传入接口中的每一个方法,以及从java.lang.Object中继承来的equals()、hashCode()、toString()方法都生成了对应的实现,并且统一调用了InvocationHandler对象的invoke()方法(代码中的“this.h”就是父类Proxy中保存的InvocationHandler实例变量)来实现这些方法的内容,各个方法的区别不过是传入的参数和Method对象有所不同而已,所以无论调用动态代理的哪一个方法,实际上都是在执行InvocationHandler.invoke()中的代理逻辑。
这个例子中并没有讲到generateProxyClass()方法具体是如何产生代理类“$Proxy0.class”的字节码的,大致的生成过程其实就是根据Class文件的格式规范去拼装字节码,但在实际开发中,以byte为单位直接拼装出字节码的应用场合很少见,这种生成方式也只能产生一些高度模板化的代码。对于用户的程序代码来说,如果有要大量操作字节码的需求,还是使用封装好的字节码类库比较合适。如果读者对动态代理的字节码拼装过程很感兴趣,可以在OpenJDK的jdk/src/share/classes/sun/misc目录下找到sun.misc.ProxyGenerator的源码
三. 自己动手实现远程执行功能
1. 目标
首先 在实现“在服务端执行临时代码”这个需求之前 先来明确一下本次实战的具体目标 我们希望最终的产品是这样的:
- 不依赖JDK版本 能在目前还普遍使用的JDK中部署 也就是使用JDK 1.4~JDK 1.7都可以运行
- 不改变原有服务端程序的部署 不依赖任何第三方类库
- 不侵入原有程序 即无须改动原程序的任何代码 也不会对原有程序的运行带来任何影响
- 考虑到BeanShell Script或JavaScript等脚本编写起来不太方便 “临时代码”需要直接支持Java语言
- “临时代码”应当具备足够的自由度 不需要依赖特定的类或实现特定的接口 这里写的是“不需要”而不是- “不可以” 当“临时代码”需要引用其他类库时也没有限制 只要服务端程序能使用的 临时代码应当都能直接引用
- “临时代码”的执行结果能返回到客户端 执行结果可以包括程序中输出的信息及抛出的异常等
2. 实现
五个工具类:
package org.jvm;
import java.io.*;
/**
* 对字节数组操作的工具类
*/
public class ByteUtils {
public static int byte2Int(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[] origialBytes,int offset,int len,byte[] replaceBytes){
byte[] newBytes=new byte[origialBytes.length+(replaceBytes.length-len)];
System.arraycopy(origialBytes,0,newBytes,0,offset);
System.arraycopy(replaceBytes,0,newBytes,offset,replaceBytes.length);
System.arraycopy(origialBytes,offset+len,newBytes,offset+replaceBytes.length,origialBytes.length-offset-len);
return newBytes;
}
}
package org.jvm;
import java.io.*;
/**
* 对测试类class文件的字节数组执行替换,将oldStr替换成newStr
*/
public class ClassModifier {
private static final int CONSTANT_POOL_COUNT_INDEX=8;
private static final int CONSTANT_UTF8_info=1;
private static final int[] CONSTANT_ITEM_LENGTH={-1,-1,5,-1,5,9,9,3,3,5,5,5,5};
private final int u1=1;
private final int u2=2;
private byte[] classByte;
public ClassModifier(byte[] classByte){
this.classByte=classByte;
}
public byte[] modiftyUTF8Constant(String oldStr,String newStr){
int cpc=getConstantPoolCount();
int offset=CONSTANT_POOL_COUNT_INDEX+u2;
for(int i =0;i<cpc;i++){
//取出CONSTANT_UTF8_info中标志部分
int tag= ByteUtils.byte2Int(classByte, offset, u1);
//判断是否为CONSTANT_UTF8_info数据类型
if(tag==CONSTANT_UTF8_info){
//取出CONSTANT_UTF8_info中字符串的长度
int len=ByteUtils.byte2Int(classByte,offset+u1,u2);
offset+=(u1+u2);
//取出CONSTANT_UTF8_info中的字符串部分
String str=ByteUtils.bytes2String(classByte,offset,len);
//通过字符串部分比较是否为需要修改的CONSTANT_UTF8_info
if(str.equalsIgnoreCase(oldStr)){
//将新字符串的值打散成字节数组
byte[] strBytes=ByteUtils.string2Bytes(newStr);
//将表示字符串长度值的两个字节分别以16进制的形式装在字节数组中
byte[] strLen=ByteUtils.int2Bytes(newStr.length(),u2);
//将CONSTANT_UTF8_info中表示length部分进行替换
classByte=ByteUtils.bytesReplace(classByte,offset-u2,u2,strLen);
//将CONSTANT_UTF8_info中字符串部分进行替换
classByte=ByteUtils.bytesReplace(classByte,offset,len,strBytes);
return classByte;
//如不是需要修改的CONSTANT_UTF8_info,则跳过这个类型,接着循环
}else {
offset+=len;
}
//如果不是CONSTANT_UTF8_info数据类型,根据tag跳转CONSTANT_ITEM_LENGTH中定义的字节数
}else {
offset+=CONSTANT_ITEM_LENGTH[tag];
}
}
return classByte;
}
public int getConstantPoolCount(){
return ByteUtils.bytes2Int(ClassByte,CONSTANT_POOL_COUNT_INDEX,u2)
}
}
package org.jvm;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintStream;
/**
* 用于替换System的输出,将测试类中每次System.out的内容输出到字节数组流中,最后一次性输出到页面
*/
public class HackSystem {
public final static InputStream in=System.in;
private static ByteArrayOutputStream buffer=new ByteArrayOutputStream();
public static final PrintStream out=new PrintStream(buffer);
public static final PrintStream err=out;
public static String getBuffer(){
return buffer.toString();
}
public static void clearBuffer(){
buffer.reset();
}
}
package org.jvm;
/**
* 测试类的类加载器,通过字节数组的方式进行加载
*/
public class HotSwapClassloader extends ClassLoader{
public HotSwapClassloader(){
super(HotSwapClassloader.class.getClassLoader());
}
public Class loadByte(byte[] classByte){
return defineClass(null,classByte,0,classByte.length);
}
}
package org.jvm;
import java.lang.reflect.Method;
/**
* 执行类,通过反射调用测试类中的main方法,最后取出HackSystem中字节数组流中的数据进行返回
*/
public class JavaClassExecuter {
public static String executer(byte[] classByte) throws NoSuchMethodException { HackSystem.clearBuffer();
ClassModifier classModifier=new ClassModifier(classByte);
byte[] modiByte=classModifier.modiftyUTF8Constant("java/lang/System","org/jvm/HackSystem");
HotSwapClassloader loader=new HotSwapClassloader();
Class cs=loader.loadByte(modiByte);
try {
Method method=cs.getMethod("main", new Class[]{String[].class});
method.invoke(null,new String []{null});
}catch (Throwable throwable){
throwable.printStackTrace(HackSystem.out);
}
return HackSystem.getBuffer();
}
}
测试类
package org.jvm;
/**
* 测试类,在此类中打印想要在页面看到的内容,System.out输出的内容会存在HackSystem的字节数组输出流中
*/
public class TestClass {
public static void main(String[] args) {
System.out.println("-----this is test class out println----");
}
}
jsp页面
<%@ page import="java.lang.*" %>
<%@ page import="java.io.*" %>
<%@ page import="org.jvm.*" %>
<%
InputStream inputStream=new FileInputStream("/opt/TestClass.class");
byte[] b=new byte[inputStream.available()];
inputStream.read(b);
inputStream.close();
out.println(JavaClassExecuter.executer(b));
%>
使用方法:
-
将 ByteUtils ClassModifier HackSystem HotSwapClassloader JavaClassExecuter TestClass 这六个.java文件上传到服务器通过javac进行编译成.class 文件
-
将编译好的TestClass放在/opt目录中
-
在tomcat的项目位置的WEB-INF/classes/中新建org/jvm文件夹,再将编译好的 ByteUtils ClassModifier HackSystem HotSwapClassloader JavaClassExecuter 放在WEB-INF/classes/org/jvm中
-
将test.jsp放在项目中能访问到的位置,如项目的根路径中
-
在浏览器中访问jsp页面即可,如http://192.168.3.235:8080/test.jsp即可看到页面中会输出