Tomcat 为什么要破坏Java双亲委派机制

1. 什么是类加载机制?

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

Java虚拟机把描述类的数据从Class文件加载进内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

虚拟机设计团队把类加载阶段中的”通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这动作的代码模块成为”类加载器”

1.1 类与类加载器的关系

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每个类的加载器,都拥有一个独立的类命名空间。

这句话通俗的讲:比较两个类是否”相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来自同一个class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就必定不相等。

2. 什么是双亲委派模型

  1. 从Java虚拟机的角度来说,只存在两种不同类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器是使用C++语言实现的(只限定HotSpot),是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都是由Java语言实现的,独立于虚拟机外部,并且都继承自抽象类java.lang.ClassLoader
  1. 从Java开发人员的角度来看,类加载还可以划分的更细致一些,比如:

    • 启动类加载器(Bootstrap ClassLoader):这个类加载器负责加载存放在JAVA_HOME/lib目录中的或者被-Xbootclasspath参数指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录下也不加载)
    • 扩展类加载器(Extension ClassLoader):这个类加载器由sum.misc.Launcher$ExtClassLoader实现,它负责加载JAVA_MOME/lib/ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
    • 应用程序加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$ApplicationLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader方法的返回值,所以也称为系统类加载器。它负责加载用户路径(ClassPath)上所指定的类库。开发者可以直接使用这个类加载器,如果应用中没有定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

这些类加载器之间的关系一般如下图所示:

图中各个类加载器之间的关系成为类加载器的双亲委派模型(Parents Dlegation Mode)。 双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应由自己的父类加载器加载,这里的类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器的代码。

类加载器的双亲 委派模型在JDK1.2期间被引入并广泛应用于所有Java程序中,但并不是强制性的约束模型,而是Java设计者推荐给开发者的一种类加载器实现方式。

2.1 工作流程

如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托为父类的加载器去执行,如果父加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

2.2 为什么要这么做?

如果没有使用双亲委派模型,由各个类加载器自行加载的话,如果用户自己编写一个称为java.lang.Object类,并放在程序的ClassPath中,那系统将会出现多个不同的Object类,Java类型体系中最基本的行为就无法保证,应用程序会变得混乱。

2.3 双亲委派模型是如何实现的?

所有的代码都在java.lang.ClassLoaderloadClass方法之中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//首先,检查请求的类是否已经被加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
//如果父类加载器抛出ClassNotException异常,说明父加载器无法完成加载请求
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
//如果在父加载器仍无法加载时,再调用本身的findClass方法来进行加载
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
//这里是定义类装入器;记录数据
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

逻辑清晰易懂:先检查是否已经被加载过了,如果没有加载则调用父加载器的loadClass方法,如父加载器为空则默认使用启动类加载器作为父加载器。如果父加载器失败,抛出ClassNotFoundException异常后,再调用自己的findClass方法进行加载

2.4 双亲委派优势

  • 采用双亲委派模式的好处是:Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载依次
  • 其次时考虑到安全因素,Java核心API中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委派模式传递到启动类加载器,而启动类加载器在核心Java API中发现了这个名字的类,发现该类已经被加载,并不会重新加载网络传递的Integer类,而直接返回已经加载过的Integer.class,这样可以防止核心API库篡改。
  • 如果我们在ClassPath路径下自定义个名为java.lang.SingleInteger类呢?该类并不存在java.lang中,经过双亲委派模式,传递到启动类加载器中,由于父加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做时不允许的,因为java.lang是核心API包,需要访问权限,强制加载会报异常

    1
    java.lang.SecurityException: Prohibited package name: java.lang

3. 如何破坏双亲委派模型?

前面提到,双亲委派模型不是一个强制性的约束模型,而是一个建议型的类加载器实现方式。在Java中大部份的类加载器都遵循这模型,但是也会存在特殊情况导致被双亲委派模型破坏。

  • 通过预加载的方式

  • 通过线程上下文类加载器Thread.getContextClassLoader();这个类加载器可以通过java.lang.Thread类的setContextClassLoader方法进行设置。如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过多的话,那这个类加载器默认即使应用程序类加载器

3.1 预加载

这里通过一个简单的例子,就拿sql连接来说:

  1. java.sql.DriverManager: rt.jar包中的类,通过Boostra类加载器加载。
  1. DriverTest:开发人员自定义的实现java.sql.Driver接口的类,通过Application 加载器加载

开发人员通过DriverManager.registerDriver方法把自己实现的获取连接的Driver实现类加载并注册到DriverManager中。然后DriverManager.getConnection方法会遍历所有注册的Driver,并触发Driver的connect接口来获取连接。(即绕过在DriverManager所在的Bootstrap加载器,因为Bootstrap加载器不能加载开发人员实现的Driver类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class DriverTest implements Driver {

static {
try {
java.sql.DriverManager.registerDriver(new DriverTest());
System.out.println("who load DriverTest:" + DriverTest.class.getClassLoader());
} catch (SQLException e) {
throw new RuntimeException("Can't register driver");
}
}

public static void main(String[] args) {
try {
Class.forName("org.example.DriverTest");
System.out.println("who load DriverManager:" + DriverManager.class.getClassLoader());
//通过rt.jar中的DriverManger去获取连接,DriverManager由BootstrapClassLoader加载
Connection connection = DriverManager.getConnection("jdbc://");
} catch (Exception ex) {
ex.printStackTrace();
}
}

@Override
public Connection connect(String url, Properties info) throws SQLException {
return new Connection() {
//......省略.....
};
}
}

运行main方法打印输出:

1
2
who load DriverTest:sun.misc.Launcher$AppClassLoader@18b4aac2
who load DriverManager:null

DriverManager是由Boostrap加载器加载的,因而获取不了Boostrap加载器,所以为null,从双亲委派模型的机制上看,因为rt.jar是由Boostrap加载器加载的,所以里面的类,都不能用到rt.jar以外的类。

那么DriverManager.getConnection是怎么调用DriverTest(App加载器)的getConnection方法呢?

因为双亲委派模型的限制,DriverManager不肯呢个自己去加载DriverTest,DriverTest的加载实际上是由AppClassLoader完成的,DriverTest里面会往DriverManager中注册一个驱动

1
java.sql.DriverManager.registerDriver(new DriverTest());

对于DriverManager而言,它不关注driver的加载,只需要遍历所有注册的Driver,然后检查驱动类是否能被”调用类的类加载器””识别,如果可以识别,则调用driver.connect方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class DriverManager{
// Worker method called by the public getConnection() methods.
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}

if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;

for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
//安全校验
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
//在这里调用DriverTest的connect方法
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}

} else {
println(" skipping: " + aDriver.getClass().getName());
}

}

// if we got here nobody could connect.
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}

println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}
//........省略......
}

整体的流程:

3.2 Thread.getContextClassLoader()

通过调用Thread.getContextClassLoader()相当于直接拿类加载器,自然不用通过向上递归走双亲委派类。

举个例子:rt.jar中的javax.xml.parsers.FactoryFinder中的newInstance方法:

  1. 在newInstance中会用到getProviderClass方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    /**
    * Create an instance of a class. Delegates to method
    * <code>getProviderClass()</code> in order to load the class.
    *
    * 定义的JNDI接口
    * @param type Base class / Service interface of the factory to instantiate
    * JNDI的实现类名
    * @param className Name of the concrete class corresponding to the service provider
    * 加载器:如果为null,则通过线程的上下文加载器进行加载
    * @param cl <code>ClassLoader</code> used to load the factory class. If <code>null</code>
    * current <code>Thread</code>'s context classLoader is used to load the factory class.
    * 如果为true,则使用bootstrap加载器。
    * @param useBSClsLoader True if cl=null actually meant bootstrap classLoader. This parameter
    * is needed since DocumentBuilderFactory/SAXParserFactory defined null as context classLoader.
    */
    static <T> T newInstance(Class<T> type, String className, ClassLoader cl,
    boolean doFallback, boolean useBSClsLoader)
    throws FactoryConfigurationError
    {
    //省略一堆代码
    try {
    //在这个方法里面,可以通过线程上下文加载器进行加载className对应的类
    Class<?> providerClass = getProviderClass(className, cl, doFallback, useBSClsLoader);
    //省略一堆代码
    }
    }
  2. getProviderClass中会用到SecuritySupport.getContextClassLoader方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    static private Class<?> getProviderClass(String className, ClassLoader cl,
    boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
    {
    try {
    if (cl == null) {
    if (useBSClsLoader) {
    return Class.forName(className, false, FactoryFinder.class.getClassLoader());
    } else {
    //在这里,会获得线程的上下文加载器去加载类
    //其中 ss是 SecuritySupport.java
    cl = ss.getContextClassLoader();
    if (cl == null) {
    throw new ClassNotFoundException();
    }
    else {
    return Class.forName(className, false, cl);
    }
    }
    }
    //省略一堆代码
    }

  3. 在SecuritySupport.getContextClassLoader中会用到Thread.currentThread().getContextClassLoader()拿到线程上下文类加载器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class SecuritySupport  {
    ClassLoader getContextClassLoader() throws SecurityException{
    return (ClassLoader)
    AccessController.doPrivileged(new PrivilegedAction() {
    public Object run() {
    ClassLoader cl = null;
    //try {
    //获得线程的上下文加载器
    cl = Thread.currentThread().getContextClassLoader();
    //} catch (SecurityException ex) { }

    if (cl == null)
    cl = ClassLoader.getSystemClassLoader();

    return cl;
    }
    });
    }

4. Tomcat的类加载器是怎么设计的?

  • 问题一:Tomcat如果使用默认的类加载机制行不行?

    可以思考一下:Tomcat是个web容器,那么它要解决什么问题?

    • 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,相互隔离
    • 部署在同一个web容器中相同的类库相同的版本可以共享。
    • web容器也有自己依赖的类库,不能和应用程序的类库混淆,基于安全考虑,应该让容器的类库和程序的类库隔离开来。
    • web容器要支持jsp的修改,我们直到,jsp文件最终也要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,所以,web容器需要支持jsp修改后不用重启

如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。

  • 问题二:Tomcat如果使用默认的类加载机制行不行?

    答案是不行的。默认的类加载器是能够实现的,因为它的职责就是保证唯一性。我们想我们要怎么实现jsp文件的热修改,jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。

    那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件

5. Tomcat 如何实现自己的类加载机制

从图中的委派关系可以看出:

  1. CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用

  2. CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。

  1. WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
  1. JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。

前面3个类加载器和默认的一致,CommonClassLoaderCatalinaClassLoaderSharedClassLoaderWebappClassLoader则是Tomcat自己定义的类加载器。它们分别加载/common/*/server/*/shared/*(在tomcat6之后合并到根路径下的lib目录下)、和/WebApp/WEB-INF/*
中的Java类库。其中WebApp类加载器和JSP类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个JSP类加载器

  • commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
  • catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
  • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;

5.1 Tomcat 违背了双亲委派模型吗?

Tomcat 违背了双亲委派模型。因为双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当由自己的父类加载器加载。
Tomcat 不是这样实现,Tomcat 为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。