类加载及执行子系统的案例与实战

代码编译的结果从本地机器码转变为字节码是存储格式发展的一小步,却是编程语言发展的一大步。

1.概述

Class文件与执行引擎这部分里,用户的程序能直接影响的内容兵不太多,Class文件以何种格式存储,类型何时加载,如何连接,以及虚拟机如何执行字节码指令等都是由虚拟机直接控制的行为,用户程序无法对其进行改变。能通过程序进行操作的,主要是字节码生成与类加载器这两部分的功能 ,但仅仅在如何如理这两点上,就已经出现了许多值得欣赏和借鉴的思路,这些思路后来成为了许多常用功能和程序实现的基础。在本章中,我们将看一下前面所学的知识在实际开发之中是如何应用的。

2.案例分析

在案例分析部分,笔者准备了四个例子,关于类加载器和字节码的案例各有两个。并且这两个领域的案例中各有一个案例是大多数java开发人员都使用过的工具或技术,另外一个案例虽然不一定每个人都使用过,但特别精彩迪演绎出这个领域中的技术特性。希望这些案例能引起你的思考,并给你的日常工作带来灵感。

2.1Tomcat:正统的类加载器结构

主流的java Web服务器,如Tomcat,JettyWeblogic,WebSphere或其他笔者没有列举的服务器,都实现了自己定义的类加载器(一般都不止一个)。因为一个功能健全的Web服务器,都要解决如下几个问题:

l 部署在同一个服务器上的两个Web应用程序锁使用的Java垒许可以实现相互隔离。这是最基本的需求,两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当可以保证两个应用程序的类库可以相互独立使用。

l 部署在同一个服务器上的两个web应用程序锁使用的Java类库可以互相共享。这个需求也很常见,例如用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,如果把10Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费——这主要倒不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到服务器内存,如果类库不能共享,虚拟机的方法去容易就会出现过度膨胀的风险。

l 服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响。目前,有许多主流的javaWeb服务器自身也是使用java语言实现的。因此服务器本身也有类库依赖的问题,一般来说,基于安全考虑,服务器锁使用的类库应该与应用程序的类库相互独立。

l 支持jsp应用的Web服务器,十有八九都需要支持HotSpot功能。我们知道JSP文件最终要被编译成Java Class才能被虚拟机执行,但Jsp文件由于其纯文本存储的特性,被运行时修改的概率远远大于第三方类库或程序自己的Class文件。而且ASP,PHP,和JSP这些网页应用也把修改后无需重启作为一个很大的“优势”来看待,因此“主流”的服务器都会支持JSP生成类的热替换,当然也有非主流的,如运行在生产模式(Production Mode)下的WebLogic服务器默认就不会处理JSP文件的变化。

由于存在上述问题,在部署Web应用时,单独的一个ClassPath就无法满足需求了,所以各种Web服务器都不约而同地提供了好几个ClassPath路径供用户存在第三方类库,这些路径一般都以“lib”或“classes”命名。被放置到不同路径中的类库,具备不同的访问范围和服务对象,通常,每一个目录都会有一个相应的自定义类加载器去加载放置在里面的Java类库。现在,笔者就以Tomcat服务器为例,看一看Tomcat具体是如何规划用户的类库结构和类加载器的。

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

l 放置在/common目录中:类库可被Tomcat和所有的Web应用程序共同使用。

l 放置在/server目录中:类库可被Tomcat使用,岁所有的Web应用程序都不可见。

l 放置在、shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。

l 放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。

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

    

灰色背景的三个类加载器是JDK默认提供的类加载器,这三个加载器在以前详细介绍过了。而CommonClassLoaderCatalinaClassLoader,SharedClassLoaderWebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/*,/server/*,/shared/*,/WebApp/WEB-INF/*Java类库的逻辑。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个Jsp文件对应一个Jsp类加载器。从图9-1的委派关系中可以看出,CommonClassLoader能加载的类都可以被CatalinaClassLoaderSharedClassLoader使用,而CatalinaClassLoaderSharedClassLoader自己能加载的类则与对方相互隔离。WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的哪一个Class,它出现的目的就是为了被丢弃:当服务器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。

对于Tomcat6.x的版本,只有指定了tomcat/conf/catalina.properties配置文件的server.loadershare.loader项后才会真正建立CatalinaClassLoaderSharedClassLoader的实例,否则会用到这两个类加载器的地方都会用CommonCLassLoader的实例来代替,而默认的配置文件中并没有设置这个两个loader项,所以Tomcat6.x顺理成章地把/common,/server/shared三个目录默认合并到一起变成一个/lib目录,这个目录里的类库相当于以前/common目录中类库的作用。这个Tomcat设计团队为了简化大多数的部署场景所做的一项改进,如果默认设置不能满足需要,用户可以通过修改配置文件指定server.loadershare.loader的方式重新启用Tomcat5.x的加载器架构。

Tomcat加载器的实现清晰易懂,并且采用了官方推荐的“正统”的使用类加载器的方式。如果读者阅读完上面的案例后,能毫不费力地完全理解Tomcat设计团队这样布置加载器架构的用意,那说明您已经掌握了类加载器“主流”的使用方式,那么笔者不妨再提一个问题让各位思考一下:前面曾经提到过的一个场景,如果有10Web应用程序都是用Spring来进行组织和管理的话,可以把Spring放到CommonShared目录下让这些程序共享。Spring要对用户程序的类进行管理,自然要能访问到用户程序的类,而用户的程序显然是在/WebApp/WEB-INF目录中的。那么被CommonClassLoaderSharedClassLoader加载的Spring如何访问并不在其加载范围内的用户程序呢?

2.2OSGi:灵活的类加载器结构

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平台,GlassFlish服务器,Weblogic 10.3所使用的mSA架构等。

OSGi中的每个模块(称为Bundle)与普通的Java类库区别并不太大,两者一般都是Java Package和Class.但是一个Bundle可以声明它所依赖的java Package(通过Import-Package描述),也可以声明它允许导出发布的Java Package(通过Export-Package描述)。在OSGi里面,Bundle之间的依赖关系从传统的上层模块依赖底层模块转变为平级模块之间的依赖(至少外观上是如此),而且类库的可见性能得到了非常准确的控制,一个模块里只有被Export过的Package才可能被外界访问,其他的Package和Class将会被隐藏起来。除了更精确的模块划分和可见性控制外,引入OSGi的另外一个重要理由是,基于OSGi的程序很可能(只是很可能,并不是一定会)可以实现模块级的热插拔功能,当程序升级更新或调试出错时,可以只停用,重新安装然后启用程序的其中一部分,这对企业级程序来说是一个非常有诱惑力的特性。

OSGi之所以能有上述诱人的特点,要归功于它灵活的类加载器结构。OSGi的Bundle类加载器之间只有规则,没有固定的委派关系。例如,某个Bundel声明了一个它依赖的Package,如果有其他Bundle声明发布了这个Package后,那个对这个Package的所有类加载动作都会给发布它的Bundle类加载器去完成。不涉及某个具体的Package时,各个Bundle加载器都是平级的关系,只有具体使用到某个Package和Class的时候,才会根据Package导入导出定义来构造Bundle间的委派。

另外,一个Bundle类加载器为其他Bundle提供服务时,会根据Export-Package列表严格控制访问范围。如果一个类存在于Bundle的类加载器能找到这个类,但不会提供给其他Bundle使用,而且OSGi平台也不会把其他Bundle的类加载请求分配给这个Bundle来处理。

我们可以举一个更具体一点的简单例子,加入存在BundleA,BundleB和BundleC三个模块,并且这三个Bundle定义的依赖关系为:

  • Bundle A:声明发布了packageA,依赖了java.*包;
  • Bundel B:声明依赖了PackageA和PackageC,同时也依赖了java.*包;
  • Bundle C:声明发布了packageC,依赖了PackageA.
那么,这三个Bundle之间的类加载器及父类 加载器之间的关系如图9-2所示。
  
由于没有牵扯到哦具体的OSGi实现,图中的类加载器都没有指明具体的加载器实现,只是一个体现了加载器间关系的概念模型,并且只是体现了OSGi中最简单的加载器委派关系。一般来说,在OSGi里加载一个雷可能发生的查找行为和委派关系会比图9-2中显示的复杂的多,类加载时可能进行的查找规则如下:
  • 以java.*开头的类,委派给父类加载器加载。
  • 否则,委派列表名单内的类,委派给父类加载器加载。
  • 否则,Import列表中的类,委派给Export这个类的Bundle的类加载器加载
  • 否则,查找当前Bundle的Classpath,使用自己的类加载器加载。
  • 否则,查找是否在自己的Fragment Bundle中,如果是则委派给Fragment Bundle的类加载器加载。
  • 否则,查找Dynamic Import列表的Bundle,委派给对应的Bundle类加载器加载。
  • 否则,查找失败。
从图9-2中还可以看出,在OSGi里面,加载器之间的关系不再是双亲委派模型的树形结构,而是已经进一步发展成了一种运行时才能确定的网状结构。这个网状结构的类加载器架构在带来更优秀的灵活性的同时,也可能会产生许多新的隐患。笔者曾经参与过讲一个非OSGi的大型系统向Equinox OSG平台迁移的项目,由于历史原因,代码模块之间的依赖关系错综复杂,勉强分离出各个模块的Bundle后,发现在 高并发环境下经常出现死锁。我们很容易就找到了死锁的原因:如果出现了Bundle A依赖Bundle B的Package B,而Bundle B又依赖了Bundel A的Package A,这两个Bundle进行类加载时就很容易发生死锁。具体情况是当Bundel A 加载Package B时,首先需要锁定但钱磊加载器的实例对象(java.lang.ClassLoader.loadClass()是一个synchronized方法),然后把请求委派给Bundle B的加载器处理,但如果这时候Bundle B的也正好想加载Package A的类,它也先锁定自己的加载器再去请求Bundle A的加载器处理,这样两个加载器都在等待对方处理自己的请求,而对方处理完之前自己又一直处于同步锁定的状态,因此他们就互相死锁,永远无法完成加载请求了。Equinox的Bug List中也有关于这类问题的Bug,并提供了一个以牺牲戏能为代价的解决方法-用户可以启用osgi.classloader.singleThreadLoads参数来按单线程串行化的方式强制进行类加载动作。在JDK1.7中,将会为非树状继承关系下的类加载器架构进行一次专门的升级,希望从底层避免这类死锁的状况,这个动作也是为将实现“官方”模块化规范-JSR-296,JSR-277座准备。
总体来说,OSGi描绘了一个很美好的模块化开发的目标,而且定义了实现这个目标所需要的各种服务,同时也有成熟框架对其提供实现支持。对于单个虚拟机下的应用,从开发初期就建立在OSGi上是一个很不错的选择,这样便于约束依赖。但并非所有的应用都适合采用OSGi作为架构基础,OSGi在提供强大功能的同时也引入的额外的复杂度,带来了线程死锁和内存泄漏的风险。
2.3字节码生成技术与动态代理的实现
“字节码生成”并不是什么高深的技术读者在看到“字节码生成”这个标题时也不必先去想诸如Javassist,CGLib和ASM之类的字节码类库,因为JDK里面的javac命令就是字节码生成技术的“老祖宗”,并且javac也是一个由java语言写成的程序,它的代码存放在OpenJDK的jdk7/langtools/src/share/classes/com/sun/tools/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是如何做到的。
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();
	}
}
上述代码中,唯一的“黑匣子”就是Proxy.newProxyInstance()方法,除此之外再没有任何特殊之处。这个方法返回一个实现了IHello的接口。并且代理了new Hello()实例行为的对象。跟踪这个方法的源码,可以看到程序进行了验证,优化,缓存,同步,生成字节码和显式类加载等操作,前面的步骤并不是我们关注的重点,而最后它调用了sun.misc.ProxyGenerator.generateProxyClass()方法来完成生成字节码的动作 .
3.自己动手实现远程执行功能
不知道读者在做程序维护的时候是否遇到过这类情形:排查问题的过程中,想查看内存中的一些参数值,却又没有办法把这些值输出到界面或日志中。又或者定位到某个缓存数据有问题,但缺少缓存的统一管理界面,不得不重启服务才能清理掉这个缓存。类似的需求有一个共同的特点,就是只要在服务中执行一段程序代码,就可以定位或排除问题,但就是偏偏找不到可以让服务器执行临时代码的途径,这时候希望Java服务器中也有提供类似Groovy console的功能啊。
JDK1.6之后提供了Compiler API,可以动态地编译Java程序,这样虽然达不到动态语言的灵活度,但让服务器执行临时代码的需求是可以得到解决了。在JDK1.6之前,也可以通过“曲线救国”的方式来做到,譬如写一个JSP文件上传到服务器,然后再浏览器中运行它,或者在服务端的程序中加入一个BeanShellScript,javaScripte等的执行引擎(如Mozilla Rhino)去执行动态脚本。在本章的实战部分,我们将使用前面学到的关于类加载及虚拟机执行子系统的知识去完成在服务端执行临时代码的功能。
3.1目标
在实现“在服务端执行临时代码”这个需求之前,先来明确一下本次实战的具体目标,我们希望最终的产品是这样的:
。不依赖JDK版本,能在目前还被普遍使用的JDK中部署,也就是实用JDK1.4至1.7都可以运行。
。不改变原有服务端程序的部署,不依赖任何第三方类库。
。不侵入原有程序,即无须改动原程序的代码。也不会对原有程序的运行带来任何影响。
。考虑到BeanShell Script或javaScript等脚本编写起来不太方便,“临时代码”需要直接支持Java语言。
。“临时代码”应当具备足够的自由度,不需要依赖特定的类或实现特定的接口。这里写的是“不需要”而不是“不可以”,当“临时代码”需要引用其他类库时也没有限制,只要服务端程序能使用的,临时代码应当都能直接引用。
。“临时代码”的执行结果能返回到客户端,执行结果可以包括程序中输出的信息及抛出的异常等。
看完上面列出的目标,你觉得完成这个需求需要做多少工作呢?也许答案比大多数人所想的都要 简单一些:5个类,250行代码(含注释),大约一个半小时左右的开发时间就可以了,现在就开始编写程序吧!
3.2思路
在程序实现的过程中,我们需要解决以下三个问题:
  • 如何编译提交到服务器的Java代码?
  • 如何执行编译后的Java代码?
  • 如何收集Java代码的执行结果?


对于第一个问题,我们有两种方案可以选择,一个是使用tools.jar包(在Sun JDK/lib目录下)中的com.sun.tools.javac.Main类来编译Java文件,这其实和使用Javac命令来编译时一样的。这种思路的缺点是引入了额外的JAR包,而且把程序死绑在SUn的JDK上了,要部署到其他公司的JDK中还得把tools.jar带上(虽然JRockit和J9虚拟机也有这个JAR包但它总不是标准所规定必须存在的)。另外一种思路是直接在客户端编译好,把字节码而不是Java代码传到服务端,这听起来好像有点投机取巧,一般来说确实不应该假定客户端一定具有编译代码的能力,但是既然程序猿会写Java代码去给服务端排查问题,那么很难想象他的机器身上会连编译Java程序的环境都没有。

对于第二个问题,简单地想一想:要执行编译后的Java代码,让类加载器加载这个类生成一个Class对象,然后又反射调用某个方法就可以了(因为不实现任何接口,我们可以借用一下Java中人人皆知的“mian()”方法)。但我们还应该考虑的更周全一些:一段程序往往不是编写和运行一次就能达到效果,同一个类可能要被反复地修改,提交和执行。另外,提交上去的类要能访问到服务端的其他类库才行,还有就是,既然提交的是临时代码,那提交的Java类在执行完后就应当被卸载和回收掉。

最后一个问题,我们想把程序往标准输出(System.out)和标准错误输出(System.err)中打印的信息收集起来。但标准输出设备是整个虚拟机进程级别的资源,如果使用System.setOut()/System.setErr()方法把输出流重定向到自己定义的PrintStream对象上固然可以收集到输出信息,但也会对原有程序产生影响:会把其他进程向标准输出中打印的信息也收集了。虽然这些并不是不能解决的问题,不过为了达到完全不影响原程序的目的,我们可以采用另外一种方法:直接在执行的类中把对System.out的符号引用替换为我们准备的PrintSteam的符号引用,依赖前面虚席到的知识做到这点并不难。

3.3实现

在程序实现部分,我们主要来看看代码及其中的注释。首先看看实现过程中需要用到的四个支持类。第一个类用于实现同一个类的代码可以被多次加载,即用于解决上一节列举的第二个问题的HotSwapClassLoader,具体程序如下面的代码清单9-3所示。

/**
 * 为了多次载入执行类而加入的加载器
 * 把defineClass方法开放出来,只有外部显式调用的时候才会使用到loadByte方法
 * 由虚拟机调用时,仍然按照原有的双亲委派规则使用loadClass方法进行类加载
 */
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类的类加载器作为父类加载器,这一步时实现提交的执行代码可以访问服务端引用类库的关键。

第二个类是实现将java.lang.System替换为我们自己定义的HackSystem类的过程,它直接修改符合Class文件格式的byte[]数组中的常量池部分,将常量池中指定内容的CONSTANT_Utf8_info常量替换为新的字符串,具体代码如下面的代码清单9-4所示。ClassModifier中涉及对byte[]数组操作的部分,主要是将byte[]与int和String互相转换,以及把对byte[]数据的替换操作封装在代码清单9-5所示的TyteUtils中。
/**
 * 修改Class文件,暂时只提供修改常量池常量的功能
 * @author liuzhe
 *
 */
public class ClassModifier {
	/**
	 * Class文件中常量池的起始偏移
	 */
	private static final int CONSTANT_POOL_COUNT_INDEX = 8;
	/**
	 * CONSTANT_Uuf8_info常量的tag标志
	 */
	private static final int CONSTANT_Utf8_info = 1;
	/**
	 * 常量池中11种常量所占的长度,CONSTANT_Utf8_info型常量除外,因为它不是定长的
	 */
	private static final int[] CONSTANT_ITEM_LENGTH = {-1,-1,
		5,-1,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);
	}
}

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),又避免了服务端修改标准输出后影响到其他程序的输出。
最后一个类就是前面提到过的用来代理java.lang.System的HackSystem,这个类的方法看起来不少,但其实除了把out和err两个静态变量改成使用ByteArrayOutputStream作为打印目标的同一个PrintStream对象,以及增加了读取,清理ByteArrayOutputStream中的内容的getBufferString()和clearBuffer()方法外,就再没有其他新鲜的内容了。其余的方法全部都来自于System类的public方法,方法名字,参数和返回值都完全一样,并且实现也是直接转调了System类的对应方法而已。保留这些方法的目的,是为了在System被替换成成HackSystem之后,执行代码中调用的System的其余方法仍然可以继续使用,HackSystem的实现如代码清单9-6所示:
/**
 * 为JavaClass劫持java.lang.System提供支持
 * 除了out和err外,其余的都直接转发给System处理
 * @author liuzhe
 *
 */
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的对应方法,因版面原因,省略了其他方法
}

四个支持类已经讲解完毕,我们来看下最后一个类JavaClassExecuter,它是为外部调用提供的入口,调用前面几个支持类组装逻辑,完成类加载工作。JavaClassExecuter只有一个execute()方法,用输入的符合Class文件格式的byte[]数组替换掉java.lang.System的符号引用后,使用HotSwapClassLoader加载生成一个Class对象,由于每次执行execute()方法都会生成一个新的类加载器实例,因此同一个类可以实现重复加载。然后反射调用这个Class对象的main()方法,如果期间出现任何异常,将异常信息打印到HackSystem.out中,最后把缓冲区中的信息作为方法的结果来返回。JavaClassExecuter的实现代码如下清单9-7
/**
 * JavaClass执行工具
 * @author liuzhe
 */
public class JavaClassExecuter {
	/**
	 * 执行外部传过来的代表一个java类的Byte数组
	 * 将输入类的byte数组中代表java.lang.System的CONSTANT_Utf8_info常量修改为劫持后的HackSystem类
	 * 执行方法为该类的static mian(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();
	}
}

3.4验证
远程执行功能的编码到此就完成了,接下来就来校验一下我们的劳动成果。只是作为测试的话,任意写一个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>");
%>

4本章小结
本书第6~9章介绍了Class文件格式,类加载及虚拟机执行引擎及部分内容,这些内容是虚拟机中必不可少的组成部分,了解了虚拟机如何执行程序,才能更好地理解怎样才能写出优秀的代码。
关于虚拟机执行子系统的介绍到此为止就结束了,通过这4章的讲解,我们描绘了一个虚拟机应该如何运行Class文件的概念模型。对于具体到某个虚拟机的实现,为了使实现简单清晰,或者为了更快的运行速度,在虚拟机内部的运作跟概念模型可能会有非常大的差异,但从最终的执行结果来看应该是一致的。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值