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

Tomcat:正统的类加载器架构

一个功能健全的类加载器,都要解决以下几个问题:
(1)部署在同一服务器上的两个web应用程序所使用的java类库可以实现相互隔离。这是最基本的需求,两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当可以保证两个应用程序的类库可以相互独立使用。
(2)部署在同一个服务器上的两个web应用程序所使用的java类库可以相互共享,这个需求也很常见,例如用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,如果把10分Spring分别放在各个应用程序的隔离目录中,将会是很大的资源浪费-----主要到不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到服务器内存,如果类库不能共享,虚拟机的方法区很容易就会出现过度膨胀的危险。
(3)服务器需要尽可能的保证自身的安全不受部署的web应用程序影响,目前,很多主流的java web服务器自身也是使用java语言来实现的,因此服务器本身也有类库依赖问题,一般来说,基于安全考虑,服务器所使用的类库应该与应用程序的类库互相独立。
(4)支持JSP应用的web服务器,十有八九都需要支持HotSwap功能,我们知道JSP文件最终要被编译成java Class文件才能被虚拟机执行,但JSp文件由于其纯文本存储的特性,被运行时修改的概率远远大于第三方类库或程序自己的Class文件,而且ASP、PHP、JSP这些网页应用也把修改后无须重启作为一个很大的“优势”来看待,因此“主流”web服务器都会支持JSP生成类的热替换,当然也有“非主流”,如运行在生产模式下的webLogic服务器默认就不会处理JSP文件的变化。

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

在Tomcat目录结构中,有三组目录(“/common/”、“/server/”、和“/shared/”)可以存放在java类库中,另外还可以加上web应用程序自身的目录“/WEB-INF/”,一共四组,把java类库放置在这些目录中的含义分别是:
(1)放置在/common目录中;类库可被Tomcat和所有的web应用程序共同使用。
(2)放置在/server目录中:类库可被Tomcat使用,对所有的Web应用程序都不可见。
(3)放置在/shared目录中:类库可被所有web应用程序共同使用,但对Tomcat自己不可见。
(4)放置在/WebApp/WEB-INF目录中:类库仅仅可以被此web应用程序使用,对Tomcat和其他web应用程序都不可见。

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

  灰色背景的三个类加载器时JDK默认提供的类加载器。而CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/、/server/、/shared/*和/WebApp/WEB-INF/*中java类库的逻辑,其中webApp类加载器和JSP类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个JSP类加载器。

  从上图的委派关系可以看出,CommonClassLoader能加载的类都可以被CatalinaClassLoader和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项,所以Tomcat6.x顺理成章的把/common、/server、/shared三个目录默认合并到一起变成一个/lib目录,这个目录里的类库相当于以前/commom目录张类库的作用。这是tomcat设计团队为了简化大多数的部署场景所做的一项改进,如果默认设置不能满足需要,用户可以通过修改配置文件制定server.loader和share.loader的方式重新启用Tomcat5.x的加载器架构。

OSGI:灵活的类加载器架构

  OSGI中的每个模块(称为Bundle)与普通的java类库区别并不太大,两者一般都以JAR格式进行封装,并且内部存储的都是Java Package和Class。但是一个Bundle可以声明它所依赖的Java Package(通过Import-Package描述),也可以声明它允许导出发布的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间的委派和依赖。在OSGI里面,加载器之间的关系不再是双亲委派模型的树形结构,而是进一步发展成了一种运行时才能确定的网状结构。这种网站结构的类加载器在带来更优秀的灵活性的同时,也可能产生许多新的隐患。

字节码生成技术与动态代理的实现

  如果使用过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[]数组。

Retrotranslator:跨越JDK版本

  Retrotranslator的作用是将JDK 1.5编译出来的Class文件转变为在JDK 1.4huo1.3上部署的版本,他可以很好的支持自动装箱、泛型、动态注解、枚举、变长参数、遍历循环、静态导入这些语法特性,甚至还可以支持JDK 1.5中新增的集合改进、并发包以及对泛型、注解等的反射操作。
  以枚举为例,在JDK 1.5中增加了enum关键字,但是Class文件常量池的CONSTANT_Class_info类型常量并没有发生任何语义变化,仍然是代表一个类或接口的符号引用,没有加入枚举,也没有增加“CONSTANT_Enum_info”之类的“枚举符号引用”常量。所以使用enum关键字定义常量,虽然从Java语法上看起来与使用class关键字定义类、使用interface关键字定义接口是同一层次的,但实际上这是由Javac编译器做出来的假象,从字节码的角度来看,枚举仅仅是一个继承于java.lang.Enum、自动生成了values()和valueOf()方法的普通Java类而已。
  Retrotranslator对枚举所做的主要处理就是把枚举类的父类从“java.lang.Enum”替换为他运行时类库中包含的“net.sf.retrotranslator.runtime.java.lang.Enum_”,然后再在类和字段的访问标志中抹去ACC_ENUM标志位。当然,这只是处理的总体思路,具体的实现要比上面说的复杂得多。可以想想既然两个父类实现都不一样,values()和valueOf()的方法自然需要重写,常量池需要引入大量新的来自父类的符号引用,这些都是实现细节。下图是一个使用JDK 1.5编译的枚举类与被Retrotranslator转换处理后的字节码的对比图。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灬一抹丶清风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值