Tomcat 是如何加载webapps中的类的?
【问题】
Tomcat
作为一个Java Web
容器,他在启动时会加载其他用户的代码,而其他用户又可能依赖了其他的jar
包,因此Tomcat
是如何将所有的class
文件加载到容器的呢?
【思路】
建议复习:JVM双亲委派机制与Tomcat
关于类加载器,其实在前面的双亲委派模型中已经简单讲过,但是具体的细节没有多讲,我们大体知道Tomcat
有4种类加载器:
Common
类加载器,用于加载Tomcat
和各个Web
应用共享的类Share
类加载器,用于加载各个Web
共享的类Server
类加载器,用于加载Tomcat
各个类WebApp
类加载器,用于加载各个WebApp
中的类
而对于如何具体的加载各个class
,对于class
文件,直接load
即可,对于文件夹,应该递归查找子文件夹和文件,对于jar
包,同理应该解压并加载里面的class
文件和jar
包。
【Tomcat】
对于上面的理论,其实在JDK
中都有现成的工具类:URLClassLoader
,因此在Tomcat
的加载器中,很大一部分都是利用的这个类再加上一些业务规则。
对于Tomcat
来说,各个类加载器的父子关系如下:
在Tomcat
中,还可以通过配置是否委托来打破双亲委派机制:
对于WebApp
加载器,如果按照双亲委派,那么在需要加载某个类的时候,会首先委派给JVM
的加载器,如果没有加载成功则再委派给Common
类加载器,接下来依次是Shared
类加载器,WebApp
类加载器。
但是在Tomcat
实现中默认并不是这样,而是首先交给JVM
类加载器加载,如果JVM
加载器没有加载成功,再从WebApp
类中加载,如果还是没有加载成功,再委托给Shared
和Common
类加载器加载。这样便是违背了双亲委派机制。
为什么要这么实现呢?个人认为是
Tomcat
认为一般用户不会提供多余的类,并且在实际开发中Share
和Common
目录用的非常少,因此默认直接取WebApp
先查找,这样速度更快。
【代码】
首先是三个父加载器:Common
,Server
,Shared
加载器:
**Bootstrap###initClassLoaders() **
这里就是启动Tomcat
之前的初始化类加载器。
private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader=this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
可以看到,首先是初始化
commonLoader
,它的父加载器为null
然后是
catalinaLoader
,它的父加载器是commonLoader
最后是
sharedLoader
,它的父加载器同样是commonLoader
Bootstrap###createClassLoader()
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
//读取配置文件,获得对应的值
//例如:
//common.loader="{catalina.base}/lib","{catalina.base}/lib/*.jar"...
String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals("")))
return parent;
//替换本地变量的值,比如${catalina_home}换为真正的路径
value = replace(value);
List repositories = new ArrayList<>();
//拆分为单个的路径
//这里的getPaths()方法是通过正则表达式匹配""或,拆分的,
//为什么不使用String.split()?
String[] repositoryPaths = getPaths(value);
for (String repository : repositoryPaths) {
// Check for a JAR URL repository
//URL包
try {
@SuppressWarnings("unused")
URL url = new URL(repository);
repositories.add(new Repository(repository, RepositoryType.URL));
continue;
} catch (MalformedURLException e) {
// Ignore
}
//*.jar包
// Local repository
if (repository.endsWith("*.jar")) {
repository = repository.substring
(0, repository.length() - "*.jar".length());
repositories.add(new Repository(repository, RepositoryType.GLOB));
}
//具体的jar包
else if (repository.endsWith(".jar")) {
repositories.add(new Repository(repository, RepositoryType.JAR));
}
//文件夹
else {
repositories.add(new Repository(repository, RepositoryType.DIR));
}
}
return ClassLoaderFactory.createClassLoader(repositories, parent);
}
ClassLoaderFactory###createClassLoader()
//。。。
return AccessController.doPrivileged(
new PrivilegedAction() {
@Override
public URLClassLoader run() {
if (parent == null)
return new URLClassLoader(array);
else
return new URLClassLoader(array, parent);
}
});
这个方法比较长,前面还有挺多代码,大概是验证
URL
的有效性,这里先省略
这里可以看到,这三个类加载其实最底层都是URLClassLoader
,关于URLClassLoader
,我们以后再说,这里我们重点关注下WebAppClassLoader
WebAppClassLoader
- 初始化
StandardContext##startInternal()
if (getLoader() == null) {
//Webap
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
这个是StandardContext##startInternal()
方法的代码片段,可以看到,Context
包含一个属性:private Loader loader = null;
,而loader
初始化就在startInternal
这个loader
有什么用呢?
在DefaultInstanceManager
赋值classLoader
的时候,便是使用的这个Context
的classLoader
classLoader = catalinaContext.getLoader().getClassLoader();
而DefaultInstanceManager
正是用来加载Servlet
的工具类。
说远了,这里一句话就是WebAppClassLoader
便是用来加载Servlet
的ClassLoader
下面来看看WebAppClassLoader
//WebappClassLoader
public class WebappClassLoader extends WebappClassLoaderBase
//WebappClassLoaderBase
public abstract class WebappClassLoaderBase extends URLClassLoader
implements Lifecycle, InstrumentableClassLoader, WebappProperties, PermissionCheck
可以发现,WebappClassLoader
继承自WebappClassLoaderBase
,而WebappClassLoaderBase
继承自URLClassLoader
也就是说,WebappClassLoader
本质上还是URLClassLoader
WebappClassLoader
中方法并不多,主要代码都在WebappClassLoaderBase
中。
WebappClassLoaderBase##WebappClassLoaderBase()
protected WebappClassLoaderBase(ClassLoader parent) {
super(new URL[0], parent);
//初始化父加载器属性
ClassLoader p = getParent();
if (p == null) {
//如果父加载器为空,则设置为ApplicationClassLoader
p = getSystemClassLoader();
}
this.parent = p;
//初始化JavaSe加载器
//String类的加载器应该是最顶层的类加载器:
//默认应该是C++实现,因此这里一般都会返回null
ClassLoader j = String.class.getClassLoader();
if (j == null) {
j = getSystemClassLoader();
//如果String.class.getClassLoader返回null
//则找到能引用的最高级别的类加载器
while (j.getParent() != null) {
j = j.getParent();
}
}
this.javaseClassLoader = j;
securityManager = System.getSecurityManager();
if (securityManager != null) {
refreshPolicy();
}
}
可以看到,这里主要就是初始化了两个类加载器属性
接下来比那时整个类的核心:loadClass()
WebappClassBase##loadClass()
@Override
public Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class> clazz = null;
// (0) Check our previously loaded local class cache
//首先查找本地缓存是否已经加载过了
clazz = findLoadedClass0(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
//如果没有加载成功,则看看JVM是否加载过
clazz = findLoadedClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
//然后尝试使用JVM ClassLoader加载此类
//这一步是必须的
String resourceName = binaryNameToPath(name, false);
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
tryLoadingFromJavaseLoader = (javaseLoader.getResource(resourceName) != null);
//如果能够加载,则加载此类
if (tryLoadingFromJavaseLoader) {
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
//否则
//1. 检查用户是否配置需要委托,默认fasle
boolean delegateLoad = delegate || filter(name, true);
// 如果需要委托的话
if (delegateLoad) {
try {
//使用父加载器加载
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// 如果父加载器未加载成功或者不需要首先委托
// 则使用WebappClassLoader加载器加载
try {
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 如果不需要委托,且本地加载器未加载成功
// 则再给父加载器加载
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
}
throw new ClassNotFoundException(name);
}
到这里,基本就能明确了,默认情况下
- 首先,让
JVM
加载器加载类- 如果不成功,则
WebAppClassLoader
先尝试加载- 如果本地加载不成功,则再委托给父类加载器加载
- 如果依然不成功,则抛出异常
委托的情况下:
- 首先,让
JVM
加载器加载类- 如果
JVM
加载不成功,则委托给父类加载器加载- 如果父类加载器加载不成功,则
WebAppClassLoader
再尝试加载- 如果依然不成功,则抛出异常
这里加载类的逻辑基本明白,但是我们还可以发现,WebAppClassLoader
还重写了findClass()
WebappClassBase##findClass()
@Override
public Class> findClass(String name) throws ClassNotFoundException {
checkStateForClassLoading(name);
// Ask our superclass to locate this class, if possible
// (throws ClassNotFoundException if it is not found)
Class> clazz = null;
clazz = findClassInternal(name);
if ((clazz == null) && hasExternalRepositories) {
clazz = super.findClass(name);
}
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
原本的代码很多,,这里删除了许多无关紧要的代码
这里可以看到大体的逻辑
- 首先调用
findInternal()
方法查找并加载类 - 如果
findInternal()
加载失败,则调用父类findClass()
加载 - 如果还是加载失败,则抛出异常
WebappClassBase##findClassInternal()
protected Class> findClassInternal(String name) {
checkStateForResourceLoading(name);
if (name == null) {
return null;
}
//转换为实际类名
String path = binaryNameToPath(name, true);
//检查本地是否有缓存
ResourceEntry entry = resourceEntries.get(path);
WebResource resource = null;
if (entry == null) {
//没有,则去实际路径加载
//这里路径便是: return getResource("/WEB-INF/classes" + path, true, true);
//到这里我们就很熟悉了,这便是`Servlet`的规定路径
resource = resources.getClassLoaderResource(path);
if (!resource.exists()) {
return null;
}
entry = new ResourceEntry();
//记录修改时间,避免重复加载,同时也可以方便热加载
entry.lastModified = resource.getLastModified();
// Add the entry in the local resource repository
synchronized (resourceEntries) {
//这里再次确认其他线程是否已经加载了这个类
//这里考虑的确实比较细
ResourceEntry entry2 = resourceEntries.get(path);
if (entry2 == null) {
resourceEntries.put(path, entry);
} else {
entry = entry2;
}
}
}
//检查其他线程是否已经加载了这个类
Class> clazz = entry.loadedClass;
//如果已经加载了,则直接返回
if (clazz != null)
return clazz;
//等待获取classLoading锁
synchronized (getClassLoadingLock(name)) {
//获取到锁后,再次检查是否被其他线程加载
clazz = entry.loadedClass;
if (clazz != null)
return clazz;
//这里防止entry不为null,但是`resource`为null的情况
//为什么要分开锁?不分开锁就不会存在此情况
if (resource == null) {
resource = resources.getClassLoaderResource(path);
}
if (!resource.exists()) {
return null;
}
//后续便是读取Mainfest文件以及其他文件,
//读取完成后,再使用URLClassLoader加载整个jar包
byte[] binaryContent = resource.getContent();
Manifest manifest = resource.getManifest();
URL codeBase = resource.getCodeBase();
Certificate[] certificates = resource.getCertificates();
if (transformers.size() > 0) {
// If the resource is a class just being loaded, decorate it
// with any attached transformers
// Ignore leading '/' and trailing CLASS_FILE_SUFFIX
// Should be cheaper than replacing '.' by '/' in class name.
String internalName = path.substring(1, path.length() - CLASS_FILE_SUFFIX.length());
for (ClassFileTransformer transformer : this.transformers) {
try {
byte[] transformed = transformer.transform(
this, internalName, null, null, binaryContent);
if (transformed != null) {
binaryContent = transformed;
}
} catch (IllegalClassFormatException e) {
log.error(sm.getString("webappClassLoader.transformError", name), e);
return null;
}
}
}
// Looking up the package
String packageName = null;
int pos = name.lastIndexOf('.');
if (pos != -1)
packageName = name.substring(0, pos);
Package pkg = null;
if (packageName != null) {
pkg = getPackage(packageName);
// Define the package (if null)
if (pkg == null) {
try {
if (manifest == null) {
definePackage(packageName, null, null, null, null, null, null, null);
} else {
definePackage(packageName, manifest, codeBase);
}
} catch (IllegalArgumentException e) {
// Ignore: normal error due to dual definition of package
}
pkg = getPackage(packageName);
}
}
try {
clazz = defineClass(name, binaryContent, 0,
binaryContent.length, new CodeSource(codeBase, certificates));
} catch (UnsupportedClassVersionError ucve) {
throw new UnsupportedClassVersionError(
ucve.getLocalizedMessage() + " " +
sm.getString("webappClassLoader.wrongVersion",
name));
}
entry.loadedClass = clazz;
}
return clazz;
}
可以看见,加载器最复杂的部分在于findClassInternal
,涉及到许多多线程的问题。
不过,借此多了解下URLClassLoader
也是很好的。