在Class文件格式与执行引擎这部分中,用户的程序能直接影响的内容并不太多, Class文件以何种格式存储,类型什么时候加载、如何链接,以及虚拟机如何执行字节码指令等都是由虚拟机直接控制的行为,用户程序没法对其进行改变。能经过程序进行操做的,主要是字节码生成与类加载器这两部分的功能,但仅仅在如何处理这两点上,就已经出现了许多值得欣赏和借鉴的思路,这些思路后来成为了许多经常使用功能和程序实现的基础。java
主流的Java Web服务器 ,如Tomcat、Jetty、WebLogic、WebSphere或其余笔者没有列举的服务器,都实现了本身定义的类加载器(通常都不止一个)。由于一个功能健全的Web服务器 ,要解决以下几个问题:程序员
因为存在上述问题,在部署Web应用时 ,单独的一个ClassPath就没法知足需求了,因此各类Web服务器都“不约而同”地提供了好几个ClassPath路径供用户存放第三方类库,这些路径通常都以“lib”或“classes”命名。被放置到不一样路径中的类库,具有不一样的访问范围和服务对象,一般,每个目录都会有一个相应的自定义类加载器去加载放置在里面的Java类库。 如今 ,笔者就以Tomcat服务器为例,看一看Tomcat具体是如何规划用户类库结构和类加载器的。apache
在Tomcat目录结构中,有3组目录(“/common/*”、“/server/*”和“/shared/*”)能够存放Java类库,另外还能够加上Web应用程序自身的目录“/WEB-INF/*” ,一共4组 ,把Java类库放
置在这些目录中的含义分别以下。编程
为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,其关系如图9-1所示。数组
灰色背景的3个类加载器是JDK默认提供的类加载器,这3个加载器的做用在第7章中已经介绍过了。而CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat本身定义的类加载器,它们分别加载/common/*、/server/*、 /shared/*和/WebApp/WEB-INF/*中的Java类库。其中WebApp类加载器和Jsp类加载器一般会存在多个实例,每个Web应用程序对应一个WebApp类加载器,每个JSP文件对应一个Jsp类加载器。浏览器
从图9-1的委派关系能够看出,CommonClassLoader能加载的类均可以被Catalina ClassLoader和SharedClassLoader使用 ,而CatalinaClassLoader和SharedClassLoader本身能加载的类则与对方相互隔离。WebAppClassLoader可使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个Class , 它出现的目的就是为了被丢弃:当服务器检测到JSP文件被修改
时 ,会替掉目前的JasperLoader的实例,并经过再创建一个新的Jsp类加载器来实现JSP文件的HotSwap功能。缓存
对于Tomcat的6.x版本 ,只有指定了tomcat/conf/catalina.properties配置文件的server.loader和share.loader项后才会真正创建CatalinaClassLoader和SharedClassLoader的实例,不然会用到这两个类加载器的地方会用CommonClassLoader的实例代替,而默认的配置文件中没有设置这两个loader项 ,因此Tomcat 6.x瓜熟蒂落地把/common、/server和/shared三个目录默认合并到一块儿变成一个/lib目录,这个目录里的类库至关于之前/common目录中类库的做用。这是Tomcat设计团队为了简化大多数的部署场景所作的一项改进,若是默认设置不能知足须要, 用户能够经过修改配置文件指定server.loader和share.loader的方式从新启用Tomcat 5.x的加载器架构。tomcat
Tomcat加载器的实现清晰易懂,而且采用了官方推荐的“正统”的使用类加载器的方式。 若是读者阅读完上面的案例后,能彻底理解Tomcat设计团队这样布置加载器架构的用意,那说明已经大体掌握了类加载器“主流”的使用方式,那么笔者不妨再提一个问题让读者思考一下 :前面曾经提到过一个场景,若是有10个Web应用程序都是用Spring来进行组织和管理的话 ,能够把Spring放到Common或Shared目录下让这些程序共享。Spring要对用户程序的类进行管理 ,天然要能访问到用户程序的类,而用户的程序显然是放在/WebApp/WEB-INF目录中的 ,那么被CommonClassLoader或SharedClassLoader加载的Spring如何访问弃不在其加载范围内的用户程序呢?若是读过本书第7章的相关内容,相信读者能够很容易地回答这个问题。安全
Tomcat是Apache基金会中的一款开源的Java Web服务器 ,主页地址为: http://tomcat.apache.org。本案例中选用的是Tomcat 5.x服务器的目录和类加载器结构,在Tomcat6.x的默认配置下,/common、/server和/shared三个目录已经合并到一块儿了。服务器
Java程序社区中流传着这么一个观点:“学习JEE规 范 ,去看JBoss源 码 ;学习类加载器, 就去看OSGi源码”。尽管“JEE规范”和“类加载器的知识”并非一个对等的概念,不 过 ,既然 这个观点能在程序员中流传开来,也从侧面说明了OSGi对类加载器的运用确实有其独到之 处。
Java程序社区中流传着这么一个观点:“学习JEE规范 ,去看JBoss源码;学习类加载器, 就去看OSGi源码”。尽管“JEE规范”和“类加载器的知识”并非一个对等的概念,不过 ,既然这个观点能在程序员中流传开来,也从侧面说明了OSGi对类加载器的运用确实有其独到之处。
OSGi ( Open Service Gateway Initiative ) 是OSGi联盟 ( OSGi Alliance ) 制定的一个基于Java语言的动态模块化规范,这个规范最初由Sun、IBM、爰立信等公司联合发起,目的是使服务提供商经过住宅网关为各类家用智能设备提供各类服务,后来这个规范在Java的其余技术领域也有至关不错的发展,如今已经成为Java世界中“事实上”的模块化标准,而且已经有了Equinox、Felix等成熟的实现。OSGi在Java程序员中最著名的应用案例就是Eclipse IDE,另 外还有许多大型的软件平台和中间件服务器都基于或声明将会基于OSGi规范来实现,如IBM Jazz平台、GlassFish服务器、jBossOSGi等。
OSGi中的每一个模块(称为Bundle)与普通的Java类库区别并不太大,二者通常都以JAR格式进行封装,而且内部存储的都是Java Package和Class。可是一个Bundle能够声明它所依赖的Java Package(经过Import-Packagel描述),也能够声明它容许导出发布的Java Package(经过Export-Package描述)。在OSGi里面,Bundle之间的依赖关系从传统的上层模块依赖底层模块转变为平级模块之间的依赖(至少外观上如此),并且类库的可见性能获得很是精确的控制,一个模块里只有被Export过的Package才可能由外界访问,其余的Package和Class将会隐藏起来。除了更精确的模块划分和可见性控制外,**引入OSGi的另一个重要理
由是**,基于OSGi的程序极可能(只是极可能,并非必定会)能够实现模块级的热插拔功能 ,当程序升级更新或调试除错时,能够只停用、从新安装而后启用程序的其中一部分,这对企业级程序开发来讲是一个很是有诱惑力的特性。
OSGi之因此能有上述“诱人”的特色,要归功于它灵活的类加载器架构。OSGi的Bundle类加载器之间只有规则,没有固定的委派关系。例如 ,某个Bundle声明了一个它依赖的Package,若是有其余Bundle声明发布了这个Package,那么全部对这个Package的类加载动做都会委派给发布它的Bundle类加载器去完成。不涉及某个具体的Package时 ,各个Bundle加载器都是平级关系,只有具体使用某个Package和Class的时候,才会根据Package导入导出定义来构造Bundle间的委派和依赖。
另外,一个Bundle类加载器为其余Bundle提供服务时,会根据Export-Package列表严格控制访问范围。若是一个类存在于Bundle的类库中可是没有被Export,那么这个Bundle的类加载器能找到这个类,但不会提供给其余Bundle使用 ,并且OSGi平台也不会把其余Bundle的类加载请求分配给这个Bundle来办理。
咱们能够举一个更具体一些的简单例子,假设存在Bundle A、 Bundle B、 Bundle C三个模 块 ,而且这三个Bundle定义的依赖关系以下。
那么 ,这三个Bundle之间的类加载器及父类加载器之间的关系如图9-2所示。
因为没有牵扯到具体的OSGi实现 ,因此图9-2中的类加载器都没有指明具体的加载器实现,只是一个体现了加载器之间关系的概念模型,而且只是体现了OSGi中最简单的加载器委派关系。通常来讲,在OSGi中,加载一个类可能发生的查找行为和委派关系会比图9-2中 显示的复杂得多,类加载时可能进行的查找规则以下:
从图9-2中还能够看出,在OSGi里面,加载器之间的关系再也不是双亲委派模型的树形结构 ,而是已经进一步发展成了一种更为复杂的、运行时才能肯定的网状结构。这种网状的类加载器架构在带来更好的灵活性的同时,也可能会产生许多新的隐患。笔者曾经参与过将一个非OSGi的大型系统向Equinox OSGi平台迁移的项目,因为历史缘由,代码模块之间的依赖关系错综复杂,勉强分离出各个模块的Bundle后 ,发如今高并发环境下常常出现死锁。咱们很容易地找到了死锁的缘由:若是出现了Bundle A依赖Bundle B的Package B , 而Bundle B又依赖了Bundle A的Package A , 这两个Bundle进行类加载时就很容易发生死锁。具体状况是当 Bundle A加载Package B的类时,首先须要锁定当前类加载器的实例对象
(java.lang.ClassLoader.loadClass()是一个synchronized方法),而后把请求委派给Bundle B的加载器处理,若是这时候Bundle B也正好想加载Package A的类,它也先锁定本身的加载器再去请求Bundle A的加载器处理,这样 ,两个加载器都在等待对方处理本身的请求,而对方处理完以前本身又一直处于同步锁定的状态,所以它们就互相死锁,永远没法完成加载请求了。Equinox的Bug List中有关于这类问题的Bug, 也提供了一个以牺牲性能为代价的解决方案—–用户能够用osgi.classloader.singleThreadLoads参数来按单线程串行化的方式强制进行类加载动做。在JDK 1.7中 ,为非树状继承关系下的类加载器架构进行了一次专门的升级,目的是从底层避免这类死锁出现的可能。
整体来讲,OSGi描绘了 一个很美好的模块化开发的目标,并且定义了实现这个目标所须要的各类服务,同时也有成熟框架对其提供实现支持。对于单个虚拟机下的应用,从开发初期就创建在OSGi上是一个很不错的选择,这样便于约束依赖。但并不是全部的应用都适合采用OSGi做为基础架构,OSGi在提供强大功能的同时,也引入了额外的复杂度,带来了线程死锁和内存泄漏的风险。
“字节码生成”并非什么高深的技术,读者在看到“字节码生成”这个标题时也先没必要去想诸如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代码实际编写了代理类的“静态”代理而言的,它的优点不在于省去了编写代理类那一点工做量,而是实现了能够在原始类和接口还未知的时候,就肯定代理类的代理行为, 当代理类与原始类脱离直接联系后,就能够很灵活地重用于不一样的应用场景之中。
代码清单9-1演示了一个最简单的动态代理的用法,原始的逻辑是打印一句“hello world” ,代理类的逻辑是在原始类方法执行前打印一句“welcome”。咱们先看一下代码,而后再分析JDK是如何作到的。
代码清单9-1 动态代理的简单示例
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文件 ,反编译后能够看见如代码清单9-2所示的内容。
加入这句代码后再次运行程序,磁盘中将会产生一个名为“$Proxy0.class”的代理类Class文件 ,反编译后能够看见如代码清单9-2所示的内容。
代码清单9-2 反编译的动态代理类的代码
package org.fenixsoft.bytecode; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.lang.reflect.UndeclaredThrowableException; public final class $Proxy0 extends Proxy implements DynamicProxyTest.IHello { private static Method m3; private static Method m1; private static Method m0; private static Method m2; public $Proxy0(InvocationHandler paramInvocationHandler) throws { super(paramInvocationHandler); } public final void sayHello() throws { try { this.h.invoke(this, m3, null); return; } catch (RuntimeException localRuntimeException) { throw localRuntimeException; } catch (Throwable localThrowable) { throw new UndeclaredThrowableException(localThrowable); } } // 此处因为版面缘由,省略equals()、hashCode()、toString()三个方法的代码 // 这3个方法的内容与sayHello()很是类似。 static { try { m3 = Class.forName("org.fenixsoft.bytecode.DynamicProxyTest$IHello").getMethod("sayHello", new Class[0]); m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") }); m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]); m2 = Class.forName("java.lang.Object").getMethod("toString", 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的源
码。
通常来讲,以“作项目”为主的软件公司比较容易更新技术,在下一个项目中换一个技术框架、升级到最新的JDK版本 ,甚至把Java换成C#、C++来开发程序都是有可能的。但当公司发展壮大,技术有所积累,逐渐成为以“作产品”为主的软件公司后,自主选择技术的权利就会丧失掉,由于以前所积累的代码和技术都是用真金白银换来的,一个稳健的团队也不会随意地改变底层的技术。然而在飞速发展的程序设计领域,新技术老是突飞猛进、层出不穷 ,恰恰这些新技术又如鲜花之于蜜蜂同样,对程序员散发着自然的吸引力。
在Java世界里,每一次JDK大版本的发布,都伴随着一场大规模的技术革新,而对Java程序编写习惯改变最大的,无疑是JDK 1.5的发布。自动装箱、泛型、动态注解、枚举、变长参数、遍历循环(foreach循环)……事实上,在没有这些语法特性的年代,Java程序也照样能写,可是如今看来,上述每一种语法的改进几乎都是“必不可少”的。就如同习惯了24寸 液晶显示器的程序员,很难习惯在15寸纯平显示器上编写代码。但假如“不幸”由于要保护现有投资、维持程序结构稳定等,必须使用1.5之前版本的JDK呢 ?咱们没有办法把15寸显示器变成24寸的,但却能够跨越JDK版本之间的沟壑,把JDK 1.5中编写的代码放到JDK 1.4或1.3 的环境中去部署使用。为了解决这个问题,一种名为“Java逆向移植”的工具( Java Backporting Tools ) 应运而生,Retrotranslator是这类工具中较出色的一个。
Retrotranslator的做用是将JDK 1.5编译出来的Class文件转变为能够在JDK 1.4或1.3上部署的版本 ,它能够很好地支持自动装箱、泛型、动态注解、枚举、变长参数、遍历循环、静态导入这些语法特性,甚至还能够支持JDK 1.5中新增的集合改进、并发包以及对泛型、注解等的反射操做。了解了Retrotranslator这种逆向移植工具能够作什么之后,如今关心的是它是怎样作到的?
要想知道Retrotranslator如何在旧版本JDK中模拟新版本JDK的功能 ,首先要弄清楚JDK 升级中会提供哪些新的功能。JDK每次升级新增的功能大体能够分为如下4类 :
上述4类新功能中,Retrotranslator只能模拟前两类,对于后面两类直接在虚拟机内部实现的改进,通常全部的逆向移植工具都是无能为力的,至少不能完整地或者在可接受的效率上完成所有模拟,不然虚拟机设计团队也没有必要舍近求远地改动处于JDK底层的虚拟机。 在能够模拟的两类功能中,第二类模拟相对更容易实现一些,如JDK 1.5引入的 java.util.concurrent包 ,实际是由多线程大师Doug Lea开发的一套并发包,在JDK 1.5出现以前就已经存在(那时候名字叫作dl.util.concurrent, 引入JDK时由做者和JDK开发团队共同作了一些改进),因此要在旧的JDK中支持这部分功能,以独立类库的方式即可实现。 Retrotranslator中附带了一个名叫“backport-util-concurrent.jar”的类库来代替JDK 1.5的并发包。
至于JDK在编译阶段进行处理的那些改进,Retrotranslator则是使用ASM框架直接对字节码进行处理。因为组成Class文件的字节码指令数量并无改变,因此不管是JDK 1.三、 JDK 1.4仍是JDK 1.5,能用字节码表达的语义范围应该是一致的。固然,确定不可能简单地把Class的文件版本号从49.0改回48.0就能解决问题了,虽然字节码指令的数量没有变化,可是元数据信息和一些语法支持的内容仍是要作相应的修改。以枚举为例,在JDK 1.5中增长了enum关键字 ,可是Class文件常量池的CONSTANT_Class_info类型常量并无发生任何语义变化 ,仍然是表明一个类或接口的符号引用,没有加入枚举,也没有增长 过“CONSTANT_Enum_info”之类的“枚举符号引用”常量。因此使用enum关键字定义常量,虽然从Java语法上看起来与使用class关键字定义类、使用interface关键字定义接口是同一层次的 ,但实际上这是由Javac编译器作出来的假象,从字节码的角度来看,枚举仅仅是一个继承于java.lang.Enum、自动生成了values()和valueOf()方法的普通类而已。
Retrotranslator对枚举所作的主要处理就是把枚举类的父类从“java.lang.Enum”替换为它运行时类库中包含的“net.sf.retrotranslator.runtime.java.lang.Enum_”,而后再在类和字段的访问标志中抹去ACC_ENUM标志位。固然 ,这只是处理的整体思路,具体的实现要比上面说的复杂得多。能够想象既然两个父类实现都不同, values ( ) 和valueOf( )的方法天然须要重写 ,常量池须要引入大量新的来自父类的符号引用,这些都是实现细节。图9-3是一个使用JDK 1.5编译的枚举类与被Retrotranslator转换处理后的字节码的对比图。
不知道读者在作程序维护的时候是否遇到过这类情形:排查问题的过程当中,想查看内存中的一些参数值,却又没有方法把这些值输出到界面或日志中,又或者定位到某个缓存数据有问题 ,但缺乏缓存的统一管理界面,不得不重启服务才能清理这个缓存。相似的需求有一个共同的特色,那就是只要在服务中执行一段程序代码,就能够定位或排除问题,但就是恰恰找不到可让服务器执行临时代码的途径,这时候就会但愿Java服务器中也有提供相似Groovy Console的功能。
JDK 1.6以后提供了Compiler API , 能够动态地编译Java程序,虽然这样达不到动态语言的灵活度,但让服务器执行临时代码的需求就能够获得解决了。在JDK 1.6以前 ,也能够经过其余方式来作到,譬如写一个JSP文件上传到服务器,而后在浏览器中运行它,或者在服务端程序中加入一个BeanShell Script、JavaScript等的执行引擎(如Mozilla Rhino)去执行动态脚本。在本章的实战部分,咱们将使用前面学到的关于类加载及虚拟机执行子系统的知识去实如今服务端执行临时代码的功能。
首先 ,在实现“在服务端执行临时代码”这个需求以前,先来明确一下本次实战的具体目标 ,咱们但愿最终的产品是这样的:
看完上面列出的目标,你以为完成这个需求须要作多少工做呢?也许答案比大多数人所想的都要简单一些:5个类 ,250行代码(含注释),大约一个半小时左右的开发时间就能够了 ,如今就开始编写程序吧!
在程序实现的过程当中,咱们须要解决如下3个问题:
对于第一个问题,咱们有两种思路能够选择,一种是使用tools.jar包 (在SunJDK/lib目录下)中的com.sun.tools.javac.Main类来编译Java文件,这其实和使用Javac命令编译是同样的。 这种思路的缺点是引入了额外的JAR包 ,并且把程序“綁死”在Sun的JDK上了,要部署到其余公司的JDK中还得把tools.jar带上(虽然JRockit和J9虚拟机也有这个JAR包,但它总不是标准所规定必须存在的)。另一种思路是直接在客户端编译好,把字节码而不是Java代码传到服务端,这听起来好像有点投机取巧,通常来讲确实不该该假定客户端必定具备编译代码的能力,可是既然程序员会写Java代码去给服务端排查问题,那么很难想象他的机器上会连编译Java程序的环境都没有。
对于第二个问题,简单地一想:要执行编译后的Java代码,让类加载器加载这个类生成一个Class对象 ,而后反射调用一下某个方法就能够了(由于不实现任何接口,咱们能够借用一下Java中人人皆知的“main() ”方法)。但咱们还应该考虑得更周全些:一段程序每每不是编写、运行一次就能达到效果,同一个类可能要反复地修改、提交、执行。另外,提交上去的类要能访问服务端的其余类库才行。还有 ,既然提交的是临时代码,那提交的Java类在执行完后就应当能卸载和回收。
最后的一个问题,咱们想把程序往标准输出(Systemout)和标准错误输出( Systemerr ) 中打印的信息收集起来,但标准输出设备是整个虚拟机进程全局共享的资源,如桌使用System.setOut()/System.setErr()方法把输出流重定向到本身定义的PrintStream对象上当然能够收集输出信息,但也会对原有程序产生影响:会把其余线程向标准输出中打印的信息也收集了。虽然这些并非不能解决的问题,不过为了达到彻底不影响原程序的目的 ,咱们能够采用另一种办法,即直接在执行的类中把对System.out的符号引用替换为咱们准备的PnntStream的符号引用,依赖前面学习的知识,作到这一点并不困难。
在程序实现部分,咱们主要看一下代码及其注释。首先看看实现过程当中须要用到的4个支持类。第一个类用于实现“同一个类的代码能够被屡次加载”这个需求,即用于解决【目标】中列举的第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_UtfB_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。
/** * 为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()方法,若是期间出现任何异常,将异常信息打印到HackSystemout中,最后把缓冲区中的信息、做为方法的结果返回。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(); } }
远程执行功能的编码到此就完成了,接下来就要检验一下咱们的劳动成果了。若是只是测试的话,那么能够任意写一个Java类 ,内容无所谓,只要向System.out输出信息便可,取名为TestClass, 同时放到服务器C盘的根目录中。而后,创建一个JSP文件并加入如代码清单9- 8所示的内容,就能够在浏览器中看到这个类的运行结果了。
<%@ 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>"); %>
固然 ,上面的作法只是用于测试和演示,实际使用这个JavaExecuter执行器的时候,若是还要手工复制一个Class文件到服务器上就没有什么意义了。笔者给这个执行器写了一个“外壳”,是一个Eclipse插件 ,能够把Java文件编译后传输到服务器中,而后把执行器的返回结果输到Eclipse的Console窗口里,这样就能够在有灵感的时候随时写几行调试代码, 放到测试环境的服务器上当即运行了。虽然实现简单,但效果很不错,对调试问题也很是有用 ,如图9-4所示。