文章目录
- 1. 概述
- 2. Tomcat:正统的类加载器结构
- 3. OSGi:灵活的类加载器架构
- 4. 字节码生成技术与动态代理的实现
- 5. Retrotranslator:跨越JDK版本
1. 概述
在Class文件格式与执行引擎这部分里,用户的程序能直接影响的内容并不太多,Class文件以何种格式存储,类型何时加载、如何连接,以及虚拟机如何执行字节码指令等都是由虚拟机直接控制的行为,用户程序无法对其进行改变。能通过程序进行操作的,主要是字节码生成与类加载器这两部分的功能,但仅仅在如何处理这两点上,就已经出现了许多值得欣赏和借鉴的思路,这些思路后来成为了许多常用功能和程序实现的基础。在本章中,我们将看一下前面所学的知识在实际开发之中是如何应用的。
2. Tomcat:正统的类加载器结构
主流的Java Web服务器,如Tomcat、Jetty、WebLogic、WebSphere或其他没有列举的服务器,都实现了自己定义的类加载器(一般都不止一个)。因为一个功能健全的Web服务器,都要解决如下几个问题:
- 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。这是最基本的需求,两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当可以保证两个应用程序的类库可以互相独立使用。
- 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享。这个需求也很常见,例如用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,如果把10份Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费 ——这主要倒不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到服务器内存,如果类库不能共享,虚拟机的 方法区很容易就会出现过度膨胀 的风险。
- 服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响。目前,有许多主流的Java Web服务器自身也是使用Java语言来实现的。因此服务器本身也有类库依赖的问题,一般来说,基于安全考虑,服务器所使用的类库应该与应用程序的类库互相独立。
- 支持JSP应用的Web服务器,十有八九都需要支持HotSwap功能。我们知道JSP文件最终要被编译成Java Class才能被虚拟机执行,但JSP文件由于其纯文本存储的特性,被运行时修改的概率远远大于第三方类库或程序自己的Class文件。而且ASP、PHP和JSP这些网页应用也把修改后无须重启作为一个很大的“优势”来看待,因此“主流”的Web服务器都会支持JSP生成类的热替换,当然也有“非主流”的,如运行在生产模式(Production Mode)下的WebLogic服务器默认就不会处理JSP文件的变化。
由于存在上述问题,在部署Web应用时,单独的一个ClassPath就无法满足需求了,所以各种Web服务器都不约而同地提供了好几个ClassPath路径供用户存放第三方类库,这些路径一般都以lib
或classes
命名。被放置到不同路径中的类库,具备不同的访问范围和服务对象,通常,每一个目录都会有一个相应的自定义类加载器去加载放置在里面的Java类库。现在,我们以Tomcat服务器为例,看一看Tomcat具体是如何规划用户的类库结构和类加载器的。
在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自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,其关系如图所示。
灰色背景的三个类加载器是JDK默认提供的类加载器,这三个加载器在 深入理解java虚拟机:虚拟机类加载机制(2) 中已经详细介绍过了。而CommonClassLoader
、CatalinaClassLoader
、SharedClassLoader
和WebappClassLoader
则是Tomcat自己定义的类加载器,它们分别加载/common/
、/server/
、/shared/
和WebApp/WEB-INF/
中Java类库的逻辑。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。
从上图的委派关系中可以看出,CommonClassLoader
能加载的类都可以被CatalinaClassLoader
和SharedClasLoader
使用,而CatalinaClassLoader
和SharedClasLoader
自己能加载的类则与对方相互隔离。WebAppClassLoader
可以使用SharedClassLoader
加载到的类,但各个WebAppClassLoader实例之间相互隔离。而JasperLoader
的加载范围仅仅是这个JSP文件所编译出来的那一个Class,它出现的目的就是为了被丢弃:当服务器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。
对于Tomcat的6.x版本,只有指定了 tomcat/conf/catalina.properties 配置文件的server.loader
和share.loader
项后才会真正建立CatalinaClassLoader和SharedClasLoader的实例,否则会用到这两个类加载器的地方都会用CommonClassLoader的实例来代替,而默认的配置文件中没有设置这两个loader项,所以Tomcat6.x顺理成章地把/common
、/server
和/shared
三个目录默认合并到一起变成一个Iib
目录,这个目录里的类库相当于以前/common目录中类库的作用。这是Tomcat设计团队为了简化大多数的部署场景所做的一项改进,如果默认设置不能满足需要,用户可以通过修改配置文件指定server.loader和share.loader的方式重新启用Tomcat5.x的加载器架构。
3. 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平台、GlassFlish服务器、Weblogic10.3所使用的mSA架构等。
OSGi中的每个模块(称为 Bundle )与普通的Java类库区别并不太大,两者一般都以 JAR 格式进行封装,并且内部存储的都是Java Package和Class。但是一个Bundle可以声明它所依赖的Java Package(通过Import-Package描述),也可以声明它允许导出发布的Java Package(通过Export–Package描述)。在OSGi里面,Bundle之间的依赖关系从传统的上层模块依赖底层模块转变为平级模块之间的依赖(至少外观上是如此),而且类库的可见性能得到了非常精确的控制,一个模块里只有被Export过的Package才可能被外界访问,其他的Package和Class将会被隐藏起来。除了更精确的模块划分和可见性控制外,引入OSGi的另外一个重要理由是,基于OSGi的程序很可能(只是很可能,并不是一定会)可以实现模块级的热插拔功能,当程序升级更新或调试除错时,可以只停用、重新安装然后启用程序的其中一部分,这对企业级程序开发来说是一个非常有诱惑力的特性。
4. 字节码生成技术与动态代理的实现
“字节码生成”并不是什么高深的技术,我们在看到“字节码生成”这个标题时也不必先去想诸如Javassist
、CGLib
和ASM
之类的字节码类库,因为JDK里面的 javac命令就是字节码生成技术的“老祖宗”,并且javac也是一个由Java语言写成的程序,它的代码存放在OpenJDK的jdk7/langtools/src/share/classes/com/sun/tools/javac
目录中。要深入了解字节码生成,阅读javac的源码是个很好的途径,不过javac对于我们这个例子来说太过庞大了。在Java里面除了javac和字节码类库外,使用到字节码生成的例子还有很多,如Web服务器中的JSP编译器,编译时织入的 AOP框架,还有很常用的 动态代理技术,甚至在使用反射的时候虚拟机都有可能会在运行时生成字节码来提高执行速度。我们选择其中相对简单的动态代理来看看字节码生成技术是如何影响程序运作的。
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 originObj){
this.originalObj = originObj;
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){
// jdk 8.0以前
System.getProperties().put("sum.misc.ProxyGenerator.saveGeneratedFiles","true");
// jdk 8.0以后
System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles","true");
IHello hello = (IHello) new DynamicProxy().bind(new Hello());
hello.sayHello();
}
}
/*
welcome
hello world
*/
代理类通过 JD-GUI 的反编译:查看$Proxy0.class
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 Test.IHello {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
public $Proxy0(InvocationHandler paramInvocationHandler) {
super(paramInvocationHandler);
}
public final boolean equals(Object paramObject) {
try {
return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue();
} catch (Error|RuntimeException error) {
throw null;
} catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
public final void sayHello() {
try {
this.h.invoke(this, m3, null);
return;
} catch (Error|RuntimeException error) {
throw null;
} catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
public final String toString() {
try {
return (String)this.h.invoke(this, m2, null);
} catch (Error|RuntimeException error) {
throw null;
} catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
public final int hashCode() {
try {
return ((Integer)this.h.invoke(this, m0, null)).intValue();
} catch (Error|RuntimeException error) {
throw null;
} catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
m3 = Class.forName("Test$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 noSuchMethodException) {
throw new NoSuchMethodError(noSuchMethodException.getMessage());
} catch (ClassNotFoundException classNotFoundException) {
throw new NoClassDefFoundError(classNotFoundException.getMessage());
}
}
}
具体的动态代理相关,参考 动态代理的介绍
5. Retrotranslator:跨越JDK版本
一般来说,以“做项目”为主的软件公司比较容易更新技术,在下一个项目中换一个技术框架、升级到最时髦的JDK版本、甚至把Java换成C#来开发都是有可能的。在Java的世界里,每一次JDK大版本的发布,就伴随着一场大规模的技术革新,而对Java程序编写习惯改变最大的,无疑是JDK1.5的发布。自动装箱、泛型、动态注解、枚举、变长参数、遍历循环(foreach循环)…事实上在没有这些语法特性的年代,Java程序也照样能写。由于客观原因,必须使用1.5以前版本的JDK呢?把JDK1.5中编写的代码放到JDK1.4或1.3的环境中去部署和使用。为了解决这个问题,一种名为 Java逆向移植 的工具(Java Backporting Tools)应运而生,Retrotranslator
是这类工具中最出色的一个。
Retrotranslator
的作用是将JDK1.5编译出来的Class文件转变为可以在JDK1.4或13上部署的版本,它可以很好地支持自动装箱、泛型、动态注解、枚举、变长参数、遍历循环、静态导人这些语法特性,甚至还可以支持JDK1.5中新增的集合改进、并发包及对泛型、注解等的反射操作…