双亲委派模型
从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
从Java开发人员的角度来看, 类加载器还可以划分得更细致一些, 绝大部分Java程序都会使用到以下3种系统提供的类加载器。
1)启动类加载器(Bootstrap ClassLoader):前面已经介绍过,这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。
2)扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher.ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
3)应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher.AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(Class Path)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
4)我们的应用程序都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。这些类加载器之间的关系一般如下图所示。
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//首先, 检查请求的类是否已经被加载过了
Class c=findLoadedClass(name);
if( c== null ){
try{
if( parent != null ){
c = parent.loadClass(name,false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//如果父类加载器抛出ClassNotFoundException
//说明父类加载器无法完成加载请求
}
if( c == null ) {
//在父类加载器无法加载的时候
//再调用本身的findClass方法来进行类加载
c = findClass(name);
}
}
if(resolve){
resolveClass(c);
}
return c;
}
打破双亲委派模型
上文提到过双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型,但也有例外。
Tomcat有自己定义的类加载器,因为一个功能健全的Web容器,要解决如下几个问题:
**1)部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以实现相互隔离。**这是最基本的需求,两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类库可以互相独立使用。
**2)部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以互相共享。**这个需求也很常见,例如,用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,如果把10份Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费——这主要倒不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到Web容器的内存,如果类库不能共享,虚拟机的方法区就会很容易出现过度膨胀的风险。
**3)Web容器需要尽可能地保证自身的安全不受部署的Web应用程序影响。**目前,有许多主流的Java Web容器自身也是使用Java语言来实现的。因此,Web容器本身也有类库依赖的问题,一般来说,基于安全考虑,容器所使用的类库应该与应用程序的类库互相独立。
**4)支持JSP应用的Web容器,大多数都需要支持HotSwap功能。**我们知道,JSP文件最终要编译成Java Class才能由虚拟机执行,但JSP文件由于其纯文本存储的特性,运行时修改的概率远远大于第三方类库或程序自身的Class文件。而且ASP、PHP和JSP这些网页应用也把修改后无须重启作为一个很大的“优势”来看待,因此“主流”的Web容器都会支持JSP生成类的热替换,当然也有“非主流”的,如运行在生产模式(Production Mode)下的WebLogic服务器默认就不会处理JSP文件的变化。
在Tomcat目录结构中,有3组目录(“/common/”、“/server/”和“/shared/” tomcat6以后移到lib目录下)可以存放Java类库,另外还可以加上Web应用程序自身的目录“/WEB-INF/”,一共4组,把Java类库放置在这些目录中的含义分别如下:
①放置在/common目录中:类库可被Tomcat和所有的Web应用程序共同使用。
②放置在/server目录中:类库可被Tomcat使用,对所有的Web应用程序都不可见。
③放置在/shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
④放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。
为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,其关系如下图所示。
前面3个类加载和默认的一致,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/、/server/、/shared/*(在tomcat 6之后已经合并到根目录下的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,JasperClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;
Springboot启动嵌入式tomcat过程源码(参考我上一篇博文:SprintBoot 1.4.3 启动tomcat源码分析,与Tomcat启动war包的不同)如下:
@Override
public EmbeddedServletContainer getEmbeddedServletContainer(
ServletContextInitializer... initializers) {
// 实例化一个tomcat
Tomcat tomcat = new Tomcat();
// 设置tomcat目录
File baseDir = (this.baseDirectory != null ? this.baseDirectory
: createTempDir("tomcat"));
tomcat.setBaseDir(baseDir.getAbsolutePath());
// 创建一个连接器
Connector connector = new Connector(this.protocol);
// service 添加连接器,一个service可以添加多个连接器
tomcat.getService().addConnector(connector);
// 设置连接器的相关参数
customizeConnector(connector);
// tomcat设置该连接器
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
// 配置tomcat的engine
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
// 准备context
prepareContext(tomcat.getHost(), initializers);
// 创建Container
return getTomcatEmbeddedServletContainer(tomcat);
}
今天我们进入prepareContext方法继续分析逻辑,就可以看出嵌入式tomcat的类加载方式,看源码:
protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
File docBase = getValidDocumentRoot();
docBase = (docBase != null ? docBase : createTempDir("tomcat-docbase"));
TomcatEmbeddedContext context = new TomcatEmbeddedContext();
context.setName(getContextPath());
context.setDisplayName(getDisplayName());
context.setPath(getContextPath());
context.setDocBase(docBase.getAbsolutePath());
context.addLifecycleListener(new FixContextListener());
// 设置父类加载器,可以为空
context.setParentClassLoader(
this.resourceLoader != null ? this.resourceLoader.getClassLoader()
: ClassUtils.getDefaultClassLoader());
resetDefaultLocaleMapping(context);
addLocaleMappings(context);
try {
context.setUseRelativeRedirects(false);
}
catch (NoSuchMethodError ex) {
// Tomcat is < 8.0.30. Continue
}
SkipPatternJarScanner.apply(context, this.tldSkip);
// 使用WebApp类加载器加载应用信息
WebappLoader loader = new WebappLoader(context.getParentClassLoader());
// 嵌入式tomcat的webapp类加载器TomcatEmbeddedWebappClassLoader
loader.setLoaderClass(TomcatEmbeddedWebappClassLoader.class.getName());
loader.setDelegate(true);
context.setLoader(loader);
if (isRegisterDefaultServlet()) {
addDefaultServlet(context);
}
if (shouldRegisterJspServlet()) {
addJspServlet(context);
addJasperInitializer(context);
context.addLifecycleListener(new StoreMergedWebXmlListener());
}
ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
configureContext(context, initializersToUse);
host.addChild(context);
postProcessContext(context);
}
我们进入TomcatEmbeddedWebappClassLoader类继续分析:
**
* Extension of Tomcat's {
@link WebappClassLoader} that does not consider the
* {
@link ClassLoader#getSystemClassLoader() system classloader}. This is required to to
* ensure that any custom context classloader is always used (as is the case with some
* executable archives).
*
* @author Phillip Webb
*/
public class TomcatEmbeddedWebappClassLoader extends WebappClassLoader {
private static final Log logger = LogFactory
.getLog(TomcatEmbeddedWebappClassLoader.class);
public TomcatEmbeddedWebappClassLoader() {
super();
}
public TomcatEmbeddedWebappClassLoader(ClassLoader parent) {
super(parent);
}
// 类加载过程
@Override
public synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Class<?> resultClass = null;
// 验证当前class缓存,即类是否被加载过
resultClass = (resultClass == null ? findLoadedClass0(name) : resultClass);
resultClass = (resultClass == null ? findLoadedClass(name) : resultClass);
if (resultClass != null) {
return resolveIfNecessary(resultClass, resolve);
}
// 校验权限
checkPackageAccess(name);
// 执行具体加载
boolean delegateLoad = (this.delegate || filter(name, true));
if (delegateLoad) {
resultClass = (resultClass == null ? loadFromParent(name) : resultClass);
}
resultClass = (resultClass == null ? findClassIgnoringNotFound(name)
: resultClass);
if (!delegateLoad) {
resultClass = (resultClass == null ? loadFromParent(name) : resultClass);
}
if (resultClass == null) {
throw new ClassNotFoundException(name);
}
return resolveIfNecessary(resultClass, resolve);
}
SpringBoot嵌入式的Tomcat的启动过程中的类加载只需要WebApp的类加载和Jsper类加载。
而tomcat启动war要经历所有的类加载过程。
本文分析若有不对的地方还请给位指正。