深入理解Java虚拟机-9.2 案例分析- 高飞网

9.2 案例分析

2017-02-18 00:37:06.0

9.2.1 Tomcat:正统的类加载器架构

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

    □ 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。这是最基本的需求,两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当可以保证两个应用程序的类库可以相互独立使用。
    □ 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以想到共享。这个需求也很常见,例如用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,如果把10份Spring分别放在各个应用程序的隔离目录中,将会是很大的资源浪费——主要不是磁盘空间,而是类库在使用时都要被加载到服务器内存,如果类库不能共享,虚拟机的方法区很容易就会出现过度膨胀的风险。
    □ 服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响。目前,有许多主流的Java Web服务器自身也是Java语言实现的。因此服务器本身也有类库依赖的问题,一般来说,基于安全考虑,服务器所使用的类库应该与应用程序的类库相互独立。
    □ 支持JSP应用的Web服务器,十有八九都需要支持HotSwap功能。

    在Tomcat目录结构中(此处是Tomcat5.x),有三组目录("/common/*"、"/server/*"和"/shared/*")可以存放Java类库,另外还可以加上Web应用程序自身的目录"/WEB-INF/*",一共四级,把Java类库旋转在这些目录中的含义分别是:

    □ 放置在/server目录:类库可被Tomcat使用,对所有的Web应用都不可见
    □ 放置在/common目录:类库可被Tomcat使用,所有的Web应用程序共同使用
    □ 放置在/shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见
    □ 放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。

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

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

    JasperLoader加载器的加载范围仅仅是这个JSP文件所编译出来的那一个Class,它出现的目的就是为了被丢弃:当服务器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器实现Jsp的HotSwap功能。

    对于Tomcat6.x版本,只有指定了tomcat/conf/catalina.properties配置文件的server.loader和share.loader项后才会真正建立CatalinaClassLoader和SharedClassLoader的实例,否则会用到这两个类加载器的地方都会用CommonClassLoader的实例来代替,而默认的配置文件中没有设置这两个loader项,所以Tomcat6.x顺理成章地把/common、/server和shared三个目录 默认合并到一起变成/lib目录。这个目录里的类库相当于以前/common目录中类库的作用。

9.2.2 OSGi:灵活的类加载器架构

    OSGi(Open Service Gateway Initiiative)是OSGi聪明制定的一个基于Java语言的动态模块化规范。

    OSGi中的每个模块(称为Bundle)与普通的Java类库区别并不太大,再者一般都以JAR格式进行封装,并且内存存储的都是Java Package和Class。但是一个Bundle可以声明它所依赖的Java Package(通过Import-Package描述),也可以声明它允许导出发布的Java Package(通过Export-Package描述)。在OSGi里面,Bundle之间的依赖关系从传统的上层模块依赖底层模块转变为平级模块之间的依赖,而且类库的可见性能得到了非常精确的控制,一个模块里只有被Export过的Package才可能被外界访问,其他的Package和Class将会被隐藏起来。另外,OSGi的程序很可能可以实现模块级的热插拔功能,当程序升级更新或调试除错时,可以只停用,重新安装然后启用程序的其中一部分。

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

    除了最常想到的Javassist、CGLib和ASM之类的字节码类库,javac命令本身也是字节码生成技术,javac是一个由Java语言写成的程序,放在jdk7/lib目录下的tools.jar包中com/sun/tools/javac。如下面的命令可以编译java代码:

java -cp tools.jar com.sun.tools.javac.Main Hello.java
D:\software\Java\jdk1.7.0_71\lib>java Hello
Hello world!

    在Java里面除了javac和字节码类库外,使用到字节码生成的例子还有很多,比如Web服务器的JSP编译器,编译时织入的AOP框架,还有很常用的动态代理技术。以下以动态代理为例学习下字节码的生成。

import java.lang.reflect.*;
public class DynamicProxyTest{
    interface IHello{
        void sayHello();
    }

    static class Hello implements IHello{
        public void sayHello(){
            System.out.println("Hello World!");
        }
    }

    static class MyProxy implements InvocationHandler{
        Object originalObj;

        public Object invoke(Object proxy, Method method, Object[] args)throws Throwable{
            System.out.println("welcome!");
            return method.invoke(originalObj,args); 
        }
        public Object bind(Object originalObj){
            this.originalObj = originalObj;
            return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(),originalObj.getClass().getInterfaces(),this);
        }

    }
    public static void main(String[] args){
        IHello h = (IHello)(new MyProxy().bind(new Hello()));
        h.sayHello();
    }
}

    该类最终由sun.misc.ProxyGenerator.generateProxyClass()方法完成生成字节码的动作,同时生成一个byte[]数组。如果想得到这个代理类中写了些什么,可以在main中加上下面的代码。

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");

    在当前路径下会生成一个$Proxy0.class:

    反编译后是:

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);
    }
  }

  public final boolean equals(Object paramObject)
    throws 
  {
    //忽略
  }

  public final int hashCode()
    throws 
  {
    //忽略
} public final String toString() throws { //忽略
} static { try { m3 = Class.forName("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()); } } }

    这个例子中没有讲到generateProxyClass()方法具体是如何产生代理类"$Proxy0.class"的,可以通过OpenJDK的jdk/src/share/classes/sun/misc目录下找到sun.misc.ProxyGenerator的源码。

9.2.4 Retrotranslator:跨越JDK版本

    了为解决“Java逆向移植”的问题,Retrotranslator是这类工具。