《深入理解Java虚拟机》读书笔记(八)–类加载及执行子系统案例(Tomcat类加载、OSGI、动态代理)

《深入理解Java虚拟机》读书笔记(八)--类加载及执行子系统案例(Tomcat类加载、OSGI、动态代理)

一、Tomcat类加载器架构

作为一个web服务器,需要解决以下几个问题:

  • 部署在同一个服务器上的web应用程序所使用的Java类库可以实现相互隔离。
  • 部署在同一个服务器上的两个web应用程序所使用的Java类库可以互相共享。
  • 服务器需要尽可能保证自身的安全不受部署的web应用程序影响。
  • JSP需要支持热更新

由于存在上述问题,单独的一个classpath无法满足需求,所以web服务器都提供了好几个classpath路径供用户存放第三方类库。在Tomcat目录结构中,有3组目录(“/common/*”、“/server/*”和“/shared/*”),再加上web应用程序自身的目录”/WEB-INF/*“。

  • /common目录:类库可被Tomcat和所有的Web应用程序共同使用。

  • /server目录:类库可被Tomcat使用,对所有的Web应用程序都不可见。

  • /shared目录:类库可被所有的web应用程序共同使用,但对Tomcat自己不可见。

  • /webapp/WEB-INF目录:类库仅可以被此微web应用程序使用,对Tomcat和其它web应用程序都不可见 。

Tomcat自定义了多个类加载器,这些类加载按照经典的双亲委派模型来实现,其关系如图所示:

《深入理解Java虚拟机》读书笔记(八)–类加载及执行子系统案例(Tomcat类加载、OSGI、动态代理)
Tomcat 5.x 类加载器结构(网图)

其中,顶层的启动类加载器、扩展类加载器、应用程序类加载器都是JDK默认提供的类加载器。另外的CommonClassLoader负责加载”/common/*“中的Java类库,CatalinaClassLoader负责加载”/server/*“中的Java类库,SharedClassLoader负责加载”/shared/*“中的Java类库,而WebAppClassLoader负责加载各自的”/WebApp/WEB-INF/*“中的Java类库,最后每一个jsp文件都对应一个类加载器。

注:Tomcat的6.x版本后,common、server、shared都默认合并到了lib目录,这个目录里的类库相当于以前common目录中类库起到的作用。如果默认设置不能满足要求,用户可以通过修改配置文件指定server.loader和share.loader的方式重新启用5.x的加载器结构

从上图可以看出,CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader类加载器加载,而CatalinaClassLoader和SharedClassloader自己能加载的类则与对方相互隔离。WebClassLoader可以使用SharedClassLoader(包括上层类加载器)加载到的类,但是各个WebAppClassLoader实例之间相互隔离。而JasperLoader的加载范围仅是这个jsp文件所编译出来的那一个class,当服务器检测到jsp文件被修改时,会替换掉目前的JasperLoader实例,并通过再建立一个新的jsp类加载来实现jsp的热更新功能(关于jsp可以参考JSP是如何编译成servlet并提供服务的)。

关于违反双亲委派

上面提到了,对于web容器来说,都需要解决的一个重要问题:对于不同的webapp,各自目录下的jar包可能需要相互隔离。一个简单的例子就是部署在同一个容器下的不同应用程序可能会依赖同一jar包的不同版本,所以必须要相互隔离。要做到隔离也很简单,每个应用程序都有一个对应自己目录的类加载器,而这些jar包存放在自己应用的目录下,由各自的类加载器加载,即可做到相互隔离。但是从这个描述来说,其实并没有违反双亲委派模型,主要看如何实现。

要达到这个目的,从实现上有两种方案:

  • 如果某个共同父类加载器和应用专属的类加载器都能加载这些jar包,那么就控制应用各自的类加载器不按照双亲委派的建议首先交给父类加载器加载,而是直接先由自己加载

  • 遵循双亲委派模型,应用专属的类加载器还是先委托父类加载器加载,但是要保证它们所有共同的父类加载器都不能加载这些jar包,这样按照双亲委派回溯,还是会由自己加载

二、OSGI

OSGI(Open Service Gateway Initiative)是OSGI联盟制定的一个基于Java语言的动态模块化规范。OSGI中的每个模块(称为Bundle)可以声明它所依赖的Java Package(通过Import-Package描述),也可以声明它允许导出发布的Java-Package(通过Export-Package描述),每个模块从外观上看来为平级之间的依赖。OSGI其中一个很大的优点就是基于OSGI的程序很可能可以实现模块级的热插拔功能,这就要归功于它灵活的类加载器架构。

OSGI的Bundle类加载器之前只有规则,没有固定的委派关系。比如,某个Bundle声明了一个它依赖的Package,如果有其他Bundle声明发布了这个Package,那么所有对这个Package的类加载动作都会委派给发布它的Bundle类加载器去完成。不涉及某个具体的Package时,各个Bundle加载器之间都是平级的关系,只有具体使用某个Package和Class时,才会根据Package导入导出定义来构造Bundle间的委派和依赖。

假设存在BundleA、BundleB、BundleC三个模块,并且他们定义的依赖关系如下:

  • BundleA:发布packageA,依赖java.*

  • BundleB:依赖packageA、packageC和java.*

  • BundleC:发布packageC,依赖packageA

那么这三个Bundle之间的类加载关系如下图所示:

《深入理解Java虚拟机》读书笔记(八)–类加载及执行子系统案例(Tomcat类加载、OSGI、动态代理)
OSGI的类加载器架构

 

可以看出OSGI里的加载器之间的关系不再是双亲委派模型的树形结构,而是发展成了一种更为复杂的、运行时才能确定的网状结构。在早期,这种非树状结构的类加载器架构出现相互依赖的情况就容易出现死锁,以OSGI为例,如果BundleA依赖BundleB的packageB,BundleB依赖BundleA的packageA:当BundleA加载PackageB时,要先锁定当前类加载器的实例对象(ClassLoader.loadClass()是一个synchronized方法),然后将请求委派给BundleB的类加载器处理,但如果这时BundleB也正好想要加载packageA的类,就需要先锁定自己的类加载器再去请求BundleA的加载器处理,这样两个加载器都在等待对方处理自己的请求,但有各自持有己方的锁,就造成了死锁状态。

在JDK1.7中,为非树状继承关系的类加载器架构进行了一次专门的升级,参考:关于类加载的并发控制锁(ClassLoadingLock)

三、字节码生成技术与动态代理

相对于动态代理,静态代理实现比较简单,直接让代理类代替被代理类执行方法即可,当然前提是实现了相同接口或继承有相同父类。下面是一个简单的例子:

public class StaticProxyTest {
    interface Runnable {
        void run();
    }

    class Dog implements Runnable {
        @Override
        public void run() {
            System.out.println("dog run.");
        }
    }

    static class StaticProxy implements Runnable {
        Runnable originObj;

        public Runnable bind(Runnable obj) {
            this.originObj = obj;
            return this;
        }

        @Override
        public void run() {
            System.out.println("before run.");
            originObj.run();
        }
    }

    public static void main(String[] args) {
        new StaticProxy().bind(new StaticProxyTest().new Dog()).run();
    }
}

只听到从山间传来架构君的声音:
翩跹舞态燕还鹭,绰约妆容花尽妒。有谁来对上联或下联?

可以看到,静态代理实现很简单,也没有对被代理对象有入侵,但是也只限于这种简单场景。静态代理要求原始类和接口编译期可知,不具备动态性,而且如果被代理类实现多个接口,也就不那么容易维护了。Java中的动态代理实现了可以在原始类和接口还未知的时候,就确定代理类的代理行为,代理类与原始类脱离了直接联系,可以灵活地重用于不同的应用场景中。对应实现也较为简单,主要依赖两个类:Proxy和InvocationHandler,下面是一个简单的例子:

此代码由Java架构师必看网-架构君整理
public class DynamicProxyTest { interface Speakable { void speak(); } interface Runnable { void run(); } class Dog implements Runnable, Speakable { @Override public void run() { System.out.println("dog run."); } @Override public void speak() { System.out.println("dog speak."); } } static class DynamicProxy implements InvocationHandler { //原始对象 Object originObj; public Object bind(Object obj) { this.originObj = obj; //生成代理对象 return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("before method:" + method.getName()); return method.invoke(originObj); } } public static void main(String[] args) { System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); Object proxy = (new DynamicProxy().bind(new DynamicProxyTest().new Dog())); ((Runnable) proxy).run(); ((Speakable) proxy).speak(); } }

上述动态代理的逻辑很清晰,唯一不明了的就是Proxy.newProxyInstance方法,这个方法是如何返回代理对象的呢?都知道动态代理的原理是根据我们提供的代理接口和InvocationHandler实例创建了一个新的代理类(字节码)。由于源码分析不是这系列文章的重点,这里只贴出部分核心逻辑:

Proxy.newProxyInstance方法最终会通过java.lang.reflect.Proxy类的一个静态内部类ProxyClassFactory.apply(classLoader,interfaces)方法生成代理类字节码并完成字节码解析工作(部分代码):

private static final class ProxyClassFactory
        implements BiFunction<ClassLoader, Class<?>[], Class<?>>
    {
        // prefix for all proxy class names
        private static final String proxyClassNamePrefix = "$Proxy";

        // next number to use for generation of unique proxy class names
        private static final AtomicLong nextUniqueNumber = new AtomicLong();

        @Override
        public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
            .......
           /*
             * Generate the specified proxy class.
             * 这里生成代理类字节码内容
             */
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, interfaces, accessFlags);
            try {
                return defineClass0(loader, proxyName,
                                    proxyClassFile, 0, proxyClassFile.length);
            } catch (ClassFormatError e) {
                /*
                 * A ClassFormatError here means that (barring bugs in the
                 * proxy class generation code) there was some other
                 * invalid aspect of the arguments supplied to the proxy
                 * class creation (such as virtual machine limitations
                 * exceeded).
                 */
                throw new IllegalArgumentException(e.toString());
            }
            ......
        }
    }

其间会调用ProxyGenerator.generateProxyClass生成代理类。在main方法中加入语句:

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true")是为了输出自动生成的代理类。运行之后发现目录多了一个$Proxy0类:

此代码由Java架构师必看网-架构君整理
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package com.demo.aop; import com.demo.aop.DynamicProxyTest.Runnable; import com.demo.aop.DynamicProxyTest.Speakable; 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 Runnable, Speakable { private static Method m1; private static Method m3; private static Method m2; private static Method m4; private static Method m0; public $Proxy0(InvocationHandler var1) throws { super(var1); } public final boolean equals(Object var1) throws { try { return (Boolean)super.h.invoke(this, m1, new Object[]{var1}); } catch (RuntimeException | Error var3) { throw var3; } catch (Throwable var4) { throw new UndeclaredThrowableException(var4); } } public final void run() throws { try { super.h.invoke(this, m3, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final String toString() throws { try { return (String)super.h.invoke(this, m2, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final void speak() throws { try { super.h.invoke(this, m4, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final int hashCode() throws { try { return (Integer)super.h.invoke(this, m0, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } static { try { m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); m3 = Class.forName("com.demo.aop.DynamicProxyTest$Runnable").getMethod("run"); m2 = Class.forName("java.lang.Object").getMethod("toString"); m4 = Class.forName("com.demo.aop.DynamicProxyTest$Speakable").getMethod("speak"); m0 = Class.forName("java.lang.Object").getMethod("hashCode"); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError(var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError(var3.getMessage()); } } }

可以看到,自动生成的代理类默认继承了java.lang.reflect.Proxy,实现了被代理类实现的接口,调用invoke方法最终调用我们自己实现的invoke方法。由于Java是单继承,所以Java提供的动态代理要求被代理类必须实现接口,所以在spring的aop中才提供了InvocationHandler和cglib两种代理方式。

本文来源黄智霖-blog,由架构君转载发布,观点不代表Java架构师必看的立场,转载请标明来源出处:https://javajgs.com/archives/31655
2

发表评论