一、类加载器的创建
在之前的Tomcat启动源码中,简单的介绍了Tomcat的四种类加载器,再复习一遍。
类加载器 | 作用 | 父加载器 |
commonLoader(共同类加载器) | 加载$CATALINA_HOME/lib下的类加载器 | 应用类加载器 |
catalinaLoader(容器类加载器) | 加载Tomcat应用服务器的类加载器,可以理解为加载Tomcat源码中的类 | 共同类加载器 |
sharedLoader(共享类加载器) | 加载应用类加载器的共享的类加载器,例如相同版本的mysql驱动等 | 共同类加载器 |
webappLoader(Web应用类加载) | 加载Web应用下的类类加载,每个web应用之间是相互隔离的 | 共享类加载器 |
类加载器的结构层次:
commonLoader、catalinaLoader、sharedLoader可以在tomcat下的conf/catalina.properties文件中修改。
在Tomcat的启动中,一开始就创建了commonLoader、catalinaLoader、sharedLoader类加载器并且加载对应设置的资源。
Bootstrap:
private void initClassLoaders() {
try {
//读取catalina.properties下的common.loader资源加载
//commonLoader的父类加载器是应用类加载器(不设置都默认为应用类加载器)
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
commonLoader=this.getClass().getClassLoader();
}
//读取catalina.properties下的server.loader资源加载
//catalinaLoader的父类加载器是commonLoader
catalinaLoader = createClassLoader("server", commonLoader);
//读取catalina.properties下的shard.loader资源加载
//sharedLoader的父类加载器是commonLoader
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
//读取catalina.properties中对应的key值
String value = CatalinaProperties.getProperty(name + ".loader");
//如果没有设值对应的属性值,则返回父类加载器
if ((value == null) || (value.equals("")))
return parent;
//替换掉Tomact的表达式变量值,如catalina.home、catalina.base等为具体的路径
value = replace(value);
List<Repository> repositories = new ArrayList<Repository>();
StringTokenizer tokenizer = new StringTokenizer(value, ",");
while (tokenizer.hasMoreElements()) {
String repository = tokenizer.nextToken().trim();
if (repository.length() == 0) {
continue;
}
try {
//创建一个URL
@SuppressWarnings("unused")
URL url = new URL(repository);
repositories.add(
new Repository(repository, RepositoryType.URL));
continue;
} catch (MalformedURLException e) {
// Ignore
}
if (repository.endsWith("*.jar")) {
repository = repository.substring
(0, repository.length() - "*.jar".length());
repositories.add(
new Repository(repository, RepositoryType.GLOB));
} else if (repository.endsWith(".jar")) {
repositories.add(
new Repository(repository, RepositoryType.JAR));
} else {
repositories.add(
new Repository(repository, RepositoryType.DIR));
}
}
//创建一个URL的类加载器,并把要加载的路径传递过去
return ClassLoaderFactory.createClassLoader(repositories, parent);
}
严格来说,commonLoader和sharedLoader都算是可以共享的类加载器,只是作用域不同,所以在catalina.properties中设置了common.loader和shared.loader加载路径时,则在应用的maven中需要排除掉这些共享的依赖(不排除的话,会在web类加载器中重复加载,common.loader和server.loader设置就无意义)。
commonLoader是catalinaLoader和sharedLoader的父类加载器,如果server.loader和shared.loader没有设值时,那么这3个类加载器的值都是一样的。
Web应用类加载器的创建是在Host容器启动之后,Host容器会发送事件到HostConfig中,然后启动Host下的应用。
HostConfig:
public void lifecycleEvent(LifecycleEvent event) {
try {
//Host的生命周期事件
host = (Host) event.getLifecycle();
if (host instanceof StandardHost) {
setCopyXML(((StandardHost) host).isCopyXML());
setDeployXML(((StandardHost) host).isDeployXML());
setUnpackWARs(((StandardHost) host).isUnpackWARs());
setContextClass(((StandardHost) host).getContextClass());
}
} catch (ClassCastException e) {
log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
return;
}
if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
check();
} else if (event.getType().equals(Lifecycle.START_EVENT)) {
//Host启动之后,部署启动Host下的应用
start();
} else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
stop();
}
}
public void start() {
//...省略
if (host.getDeployOnStartup())
//部署应用,即启动应用
deployApps();
}
protected void deployApps() {
File appBase = appBase();
File configBase = configBase();
String[] filteredAppPaths = filterAppPaths(appBase.list());
deployDescriptors(configBase, configBase.list());
//热部署War包
deployWARs(appBase, filteredAppPaths);
//部署扩展文件夹,即War包的解压
deployDirectories(appBase, filteredAppPaths);
}
不管是如何部署,最终都会调用Context的start方法启动应用,在Context启动时,会创建对应的Web应用类加载器进行绑定,即一个Context对应一个Web应用类加载器,从而实现应用之间Jar包的隔离。
WebappLoader:
private String loaderClass =
"org.apache.catalina.loader.WebappClassLoader";
protected void startInternal() throws LifecycleException {
//...省略
try {
//创建应用对应的Web应用类加载器
classLoader = createClassLoader();
classLoader.setResources(container.getResources());
classLoader.setDelegate(this.delegate);
classLoader.setSearchExternalFirst(searchExternalFirst);
if (container instanceof StandardContext) {
classLoader.setAntiJARLocking(
((StandardContext) container).getAntiJARLocking());
classLoader.setClearReferencesStatic(
((StandardContext) container).getClearReferencesStatic());
classLoader.setClearReferencesStopThreads(
((StandardContext) container).getClearReferencesStopThreads());
classLoader.setClearReferencesStopTimerThreads(
((StandardContext) container).getClearReferencesStopTimerThreads());
classLoader.setClearReferencesHttpClientKeepAliveThread(
((StandardContext) container).getClearReferencesHttpClientKeepAliveThread());
}
//...省略
}
private WebappClassLoaderBase createClassLoader()
throws Exception {
Class<?> clazz = Class.forName(loaderClass);
WebappClassLoaderBase classLoader = null;
if (parentClassLoader == null) {
parentClassLoader = container.getParentClassLoader();
}
Class<?>[] argTypes = { ClassLoader.class };
Object[] args = { parentClassLoader };
Constructor<?> constr = clazz.getConstructor(argTypes);
classLoader = (WebappClassLoaderBase) constr.newInstance(args);
return classLoader;
}
可以看到,默认创建的Web应用类加载器是WebappClassLoader,同时会设置父类加载。
通过打断点的方式,可以看到Web应用类加载器的父类加载器是共享类加载器。
二、类加载器的使用
在上文中,Tomcat的四大类加载器已经创建完毕,那么Tomcat是如何实现应用之间的隔离?
在上一篇Tomcat的请求流程中,我们知道当一个请求到达时,会经过容器通过Valve传递。
StandardHostValve:
public final void invoke(Request request, Response response)
throws IOException, ServletException {
//...省略
//为当前线程设置应用的类加载器,从而实现应用之间隔离
Thread.currentThread().setContextClassLoader
(context.getLoader().getClassLoader());
//...省略
//传递到下一个容器的Valve中
context.getPipeline().getFirst().invoke(request, response)
}
当请求到达HostValve中,会为当前线程设置类加载器为对应的Web应用类加载器,那么JVM在后面加载类时,会使用该类加载器。(JVM的机制)
WebappClassLoader是默认的Web应用类加载器,重写了loadClass方法。
WebappClassLoader:
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLockInternal(name)) {
Class<?> clazz = null;
//先查看本地缓存是否加载了该类
clazz = findLoadedClass0(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return (clazz);
}
//查询当前的类加载器是否已经加载了
clazz = findLoadedClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return (clazz);
}
//尝试用扩展类加载器加载该类(依旧遵循双亲委派机制),防止Java的核心类被破坏
try {
clazz = j2seClassLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
}
//检查访问权限
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = "Security Violation, attempt to use " +
"Restricted Class: " + name;
throw new ClassNotFoundException(error, se);
}
}
}
boolean delegateLoad = delegate || filter(name);
//当delegate为true时,会继续使用双亲委派加载方式,不过默认都是false
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
}
}
try {
//查找加载应用目录的class(/WEB-INF/classes和/WEB-INF/lib,既war包中的所有类)
clazz = findClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
}
//如果本地的class还没加载没有找到,则使用父加载器加载继续尝试(此处会走双亲委派)
if (!delegateLoad) {
try {
//使用父加载器加载,此处会走双亲委派的机制
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
}
}
}
throw new ClassNotFoundException(name);
}
应用类加载器的加载流程:
1.查询缓存是否已经加载,加载不成功则进行下一步
2.使用扩展类加载器加载(防止Java核心类被破坏),加载不成功则进行下一步
3.使用Web应用类加载器加载(加载/WEB-INF/classes和/WEB-INF/lib),加载不成功则进行下一步
4.交给父类加载器走双亲委派加载,加载路径则为:共享类加载器——>共同类加载器——>应用类加载器——>扩展类加载器——>系统类加载器
每个应用都使用自己的Web应用类加载器加载/WEB-INF/classes和/WEB-INF/lib,从而实现了Tomcat之间的应用隔离。
三、为什么要打破双亲委派机制?
如果不打破双亲委派机制,能实现应用之间的隔离吗?
答案是可以的,如果每个应用都创建一个自己的类加载器,走双亲委派加载时,最终还是在该类加载器实现最终的加载。
既然如此,为什么Tomcat要打破双亲委派机制呢?
因为Tomcat需要节约资源,如果走了双亲委派机制,那么一些共同的类库将无法实现共享,每个应用的类加载器都需要把所有的类库全部加载到自己的类加载器中,会浪费很多的内存资源,打破双亲委派机制,不仅可以让共同使用的类库实现共享,还能实现应用之间的隔离,不造成内存资源的浪费。