Tomcat类加载方式和SpringBoot嵌入式tomcat的类加载方式分析

一、Tomcat类加载方式

1、jdk定义的类加载

双亲委派模型
从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种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。这些类加载器之间的关系一般如下图所示。

Tomcat类加载方式和SpringBoot嵌入式tomcat的类加载方式分析_第1张图片
双亲委托类加载源码参考如下:

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

2、Tomcat类加载方式

打破双亲委派模型
上文提到过双亲委派模型并不是一个强制性的约束模型,而是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自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,其关系如下图所示。
Tomcat类加载方式和SpringBoot嵌入式tomcat的类加载方式分析_第2张图片

前面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容器不可见;
WebappClassLoaderJasperClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;

二、Springboot启动的嵌入式tomcat类加载

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要经历所有的类加载过程。

本文分析若有不对的地方还请给位指正。

你可能感兴趣的:(java,tomcat,java,jvm)