1 概述
本文屬于閱讀代碼中的筆記,主要通過(guò)Servlet的加載介紹Tomcat中WebappClassLoader對(duì)于每個(gè)Web app的隔離加載機(jī)制。
2 一個(gè)自定義ClassLoader的例子
在介紹具體加載機(jī)制之前,我們先看一個(gè)關(guān)于類加載的例子:
package xyf.classloader.demo;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
public class ClassloaderDemo {
public static void main(String[] args) throws Exception {
MyClassLoader loader = new MyClassLoader();
//使用MyClassLoader加載cl.test.A
Class aClazz = loader.loadClass("cl.test.A");
Object a = aClazz.newInstance();
Method getB = aClazz.getMethod("getB", null);
Object b = getB.invoke(a, null);
System.out.println(a.getClass().getClassLoader());
System.out.println(b.getClass().getClassLoader());
}
}
class MyClassLoader extends URLClassLoader {
//該路徑是一個(gè)非classpath路徑
public static File file = new File("D:\\developer\\java\\cltest");
//MyClassLoader從上面的非classpath路徑加載類
public MyClassLoader() throws Exception {
super(new URL[] {file.toURL()}, null, null);
}
}
//下面類A和B編譯成.class文件后放到非classpath路徑
//D:\\developer\\java\\cltest下面
package cl.test;
public class A{
B b = new B();
public B getB() {
return b;
}
}
package cl.test;
public class B{}
/*
上面ClassloaderDemo.main運(yùn)行之后輸出結(jié)果為:
MyClassLoader@5c647e05
MyClassLoader@5c647e05
說(shuō)明:
可見如果一個(gè)類由某個(gè)類加載器加載,那么因?yàn)槭褂迷擃悾ū纠蠥)而加載的類
(本例中B)也會(huì)使用加載同一個(gè)類加載器
當(dāng)然,如果我們將類B放在classpath下,然后將Thread.currentThread.getContextClassLoader()
作為參數(shù)傳入U(xiǎn)RLClassLoader構(gòu)造函數(shù),作為其雙親加載器,那么類B將由其雙親加載器加載,因?yàn)?URLClassLoader也遵循雙親委派模型
*/
為什么先介紹這樣一個(gè)自定義類加載器的例子呢?其實(shí)在Tomcat中,基本上一個(gè)Web App所有的操作都是由一個(gè)Servlet啟動(dòng)的,我們?cè)诙x自己的Servlet時(shí)可能會(huì)使用其他的三方類庫(kù),比如MyBatis。結(jié)合著上面的例子可以知道,如果我們?cè)诩虞dServlet時(shí)使用的是一個(gè)自定義的ClassLoader類實(shí)例,那么該Servlet中引用的三方類庫(kù),如:MyBatis也會(huì)由該ClassLoader加載。再假設(shè)我們?yōu)槊總€(gè)Web App都專門實(shí)例化一個(gè)ClassLoader實(shí)例,那么這樣就做到了不同Web App的隔離。因?yàn)槲覀冎繨ava中一個(gè)類是由該類名稱以及該類的defining loader定義的。
具體什么是defining loader,可以看jvms8中關(guān)于類加載器的介紹:
A class loader L may create C by defining it directly or by delegating to another class loader. If L creates C directly, we say that L defines C or, equivalently, that L is the defining loader of C.
When one class loader delegates to another class loader, the loader that initiates the loading is not necessarily the same loader that completes the loading and defines the class. If L creates C, either by defining it directly or by delegation, we say that L initiates loading of C or, equivalently, that L is an initiating loader of C.
At run time, a class or interface is determined not by its name alone, but by a pair: its binary name (§4.2.1) and its defining class loader. Each such class or interface belongs to a single run-time package. The run-time package of a class or interface is determined by the package name and defining class loader of the class or interface.
3 StandardContext實(shí)例化
現(xiàn)在看Tomcat是如何加載web app目錄(一般是webapps目錄)里的web app的,在HostConfig.start會(huì)進(jìn)行應(yīng)用部署:
//HostConfig
public void start() {
...
if (host.getDeployOnStartup())
//進(jìn)行應(yīng)用部署
deployApps();
}
protected void deployApps() {
File appBase = host.getAppBaseFile();
File configBase = host.getConfigBaseFile();
String[] filteredAppPaths = filterAppPaths(appBase.list());
// Deploy XML descriptors from configBase
deployDescriptors(configBase, configBase.list());
// Deploy WARs
deployWARs(appBase, filteredAppPaths);
// Deploy expanded folders
//我們主要看如何部署webapps里的應(yīng)用
deployDirectories(appBase, filteredAppPaths);
}
protected void deployDirectories(File appBase, String[] files) {
if (files == null)
return;
ExecutorService es = host.getStartStopExecutor();
List<Future<?>> results = new ArrayList<>();
//對(duì)于webapps下面的所有目錄,進(jìn)行以次部署,一般一個(gè)目錄
//表示一個(gè)單獨(dú)的web app
for (int i = 0; i < files.length; i++) {
if (files[i].equalsIgnoreCase("META-INF"))
continue;
if (files[i].equalsIgnoreCase("WEB-INF"))
continue;
File dir = new File(appBase, files[i]);
if (dir.isDirectory()) {
ContextName cn = new ContextName(files[i], false);
if (isServiced(cn.getName()) || deploymentExists(cn.getName()))
continue;
//并行部署多個(gè)web app
//DeployDirectory構(gòu)造函數(shù)第一個(gè)參數(shù)為HostConfig,這里
//傳的是this
results.add(es.submit(new DeployDirectory(this, cn, dir)));
}
}
for (Future<?> result : results) {
try {
result.get();
} catch (Exception e) {
log.error(sm.getString(
"hostConfig.deployDir.threaded.error"), e);
}
}
}
下面看DeployDirectory的定義,DeployDirectory是HostConfig的一個(gè)內(nèi)部類:
//HostConfig.DeployDirectory
private static class DeployDirectory implements Runnable {
private HostConfig config;
private ContextName cn;
private File dir;
public DeployDirectory(HostConfig config, ContextName cn, File dir) {
this.config = config;
this.cn = cn;
this.dir = dir;
}
@Override
public void run() {
//這里調(diào)用HostConfig的deployDirectory進(jìn)行單個(gè)web app(或者說(shuō)單個(gè))
//目錄的部署
//config就是上面代碼構(gòu)造函數(shù)傳的this
config.deployDirectory(cn, dir);
}
}
HostConfig.deployDirectory的方法體比較長(zhǎng),下面是省略了非主要部分之后的代碼:
protected void deployDirectory(ContextName cn, File dir) {
long startTime = 0;
// Deploy the application in this directory
...
Context context = null;
File xml = new File(dir, Constants.ApplicationContextXml);
File xmlCopy =
new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml");
DeployedApplication deployedApp;
boolean copyThisXml = isCopyXML();
boolean deployThisXML = isDeployThisXML(dir, cn);
try {
if (deployThisXML && xml.exists()) {
synchronized (digesterLock) {
try {
//最主要的代碼就是這里,這里會(huì)創(chuàng)建一個(gè)StandardContext
//類實(shí)例,也就是每個(gè)web app都會(huì)對(duì)應(yīng)一個(gè)
//StandardContext實(shí)例
context = (Context) digester.parse(xml);
} catch (Exception e) {
log.error(sm.getString(
"hostConfig.deployDescriptor.error",
xml), e);
context = new FailedContext();
} finally {
digester.reset();
if (context == null) {
context = new FailedContext();
}
}
}
...
} else if (!deployThisXML && xml.exists()) {
// Block deployment as META-INF/context.xml may contain security
// configuration necessary for a secure deployment.
log.error(sm.getString("hostConfig.deployDescriptor.blocked",
cn.getPath(), xml, xmlCopy));
context = new FailedContext();
} else {
//contextClass = org.apache.catalina.core.StandardContext
//同樣是實(shí)例化一個(gè)StandardContext的實(shí)例
context = (Context) Class.forName(contextClass).getConstructor().newInstance();
}
Class<?> clazz = Class.forName(host.getConfigClass());
LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
context.addLifecycleListener(listener);
context.setName(cn.getName());
context.setPath(cn.getPath());
context.setWebappVersion(cn.getVersion());
context.setDocBase(cn.getBaseName());
host.addChild(context);
} catch (Throwable t) {
...
} finally {
...
}
deployed.put(cn.getName(), deployedApp);
...
}
上面介紹了每個(gè)web app都會(huì)對(duì)應(yīng)一個(gè)StandardContext類實(shí)例,閱讀過(guò)Tomcat源碼的讀者都知道Tomcat中許多類都實(shí)現(xiàn)了LifecycleListener接口,實(shí)現(xiàn)了生命周期管理。StandardContext同樣也實(shí)現(xiàn)了該接口,那么我們接下來(lái)就看StandardContext.startInternal方法的定義,該方法同樣較長(zhǎng),因?yàn)槲覀冞@里主要介紹類加載機(jī)制,所以我們省略了其他不相關(guān)的部分:
//StandardContext
@Override
protected synchronized void startInternal() throws LifecycleException {
...
//每個(gè)StandardContext對(duì)象都持有一個(gè)WebappLoader對(duì)象,也就是自己的
//類加載器,所有該StandardContext加載的三方類和其他StandardContext
//加載的三方類是隔離的
if (getLoader() == null) {
//getParentClassLoader返回的parentClassLoader是其父類加載器
//是由CopyParentClassLoaderRule.begin中配置的,通過(guò)digester
//注入的,實(shí)現(xiàn)share加載
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
...
}
WebappLoader內(nèi)部持有一個(gè)具體的ClassLoader實(shí)現(xiàn),其實(shí)例化在該類start時(shí),WebappLoader同樣實(shí)現(xiàn)了LifecycleListener接口,StandardContext在start時(shí)會(huì)調(diào)用WebappLoader的start方法,所以這里看下其startInternal方法的定義:
protected void startInternal() throws LifecycleException {
...
try {
//創(chuàng)建具體的ClassLoader實(shí)例
classLoader = createClassLoader();
classLoader.setResources(context.getResources());
classLoader.setDelegate(this.delegate);
// Configure our repositories
setClassPath();
setPermissions();
((Lifecycle) classLoader).start();
String contextName = context.getName();
if (!contextName.startsWith("/")) {
contextName = "/" + contextName;
}
ObjectName cloname = new ObjectName(context.getDomain() + ":type=" +
classLoader.getClass().getSimpleName() + ",host=" +
context.getParent().getName() + ",context=" + contextName);
Registry.getRegistry(null, null)
.registerComponent(classLoader, cloname, null);
} catch (Throwable t) {
...
}
setState(LifecycleState.STARTING);
}
private WebappClassLoaderBase createClassLoader()
throws Exception {
//loaderClass是其私有變量
//private String loaderClass =
//ParallelWebappClassLoader.class.getName();
Class<?> clazz = Class.forName(loaderClass);
WebappClassLoaderBase classLoader = null;
if (parentClassLoader == null) {
parentClassLoader = context.getParentClassLoader();
}
Class<?>[] argTypes = { ClassLoader.class };
Object[] args = { parentClassLoader };
Constructor<?> constr = clazz.getConstructor(argTypes);
classLoader = (WebappClassLoaderBase) constr.newInstance(args);
return classLoader;
}
下面我們看一下ParallelWebappClassLoader的類繼承關(guān)系圖:

可見ParallelWebappClassLoader類是URLClassLoader類的一個(gè)實(shí)現(xiàn),所有要從中加載類,必須設(shè)置其要加載的類所在的URL,ParallelWebappClassLoader父類WebappClassLoaderBase同樣實(shí)現(xiàn)了Lifecycle接口,所以看其start方法實(shí)現(xiàn):
//WebappClassLoaderBase
public void start() throws LifecycleException {
state = LifecycleState.STARTING_PREP;
//設(shè)置其加載類的URL為/WEB-INF/classes和/WEB-INF/lib
WebResource classes = resources.getResource("/WEB-INF/classes");
if (classes.isDirectory() && classes.canRead()) {
localRepositories.add(classes.getURL());
}
WebResource[] jars = resources.listResources("/WEB-INF/lib");
for (WebResource jar : jars) {
if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
localRepositories.add(jar.getURL());
jarModificationTimes.put(
jar.getName(), Long.valueOf(jar.getLastModified()));
}
}
state = LifecycleState.STARTED;
}
//上面start方法將URL放入localRepositories中,所以WebappClassLoaderBase
//重寫了getURLs方法如下
@Override
public URL[] getURLs() {
ArrayList<URL> result = new ArrayList<>();
result.addAll(localRepositories);
result.addAll(Arrays.asList(super.getURLs()));
return result.toArray(new URL[result.size()]);
}
到此已經(jīng)介紹了每個(gè)Web app特有的StandardContext及其持有的ClassLoader是如何實(shí)例化的,下面看Servlet是如何加載的。
4 Servlet加載
要介紹Servlet加載首先要看下StandardWrapper類,StandardWrapper是Servlet的封裝,在web.xml中配置的每個(gè)servlet-class都會(huì)對(duì)應(yīng)一個(gè)StandardWrapper,StandardWrapper.servletClass(String類型,還未加載)對(duì)應(yīng)其servlet-class具體配置。
不管是在啟動(dòng)時(shí)加載Servlet還是在第一個(gè)請(qǐng)求到來(lái)時(shí)加載Servlet都會(huì)調(diào)用StandardWrapper.load方法。
在介紹StandardWrapper.load方法之前,我們首先看下InstanceManager,每個(gè)StandardContext都會(huì)持有一個(gè)InstanceManager實(shí)例,StandardContext.InstanceManager會(huì)在StandardContext.startInternal中實(shí)例化,默認(rèn)的InstanceManager實(shí)現(xiàn)是DefaultInstanceManager,DefaultInstanceManager會(huì)持有一個(gè)ClassLoader實(shí)例,該ClassLoader實(shí)例其實(shí)就是StandardContext持有的WebappLoader.classLoader:
//StandardContext
protected synchronized void startInternal() throws LifecycleException {
...
if (ok ) {
if (getInstanceManager() == null) {
javax.naming.Context context = null;
if (isUseNaming() && getNamingContextListener() != null) {
context = getNamingContextListener().getEnvContext();
}
Map<String, Map<String, String>> injectionMap = buildInjectionMap(
getIgnoreAnnotations() ? new NamingResourcesImpl(): getNamingResources());
//實(shí)例化InstanceManager對(duì)象實(shí)例,默認(rèn)實(shí)現(xiàn)為
//DefaultInstanceManager
setInstanceManager(new DefaultInstanceManager(context,
injectionMap, this, this.getClass().getClassLoader()));
}
getServletContext().setAttribute(
InstanceManager.class.getName(), getInstanceManager());
InstanceManagerBindings.bind(getLoader().getClassLoader(), getInstanceManager());
}
...
}
//DefaultInstanceManager構(gòu)造函數(shù)
public DefaultInstanceManager(Context context,
Map<String, Map<String, String>> injectionMap,
org.apache.catalina.Context catalinaContext,
ClassLoader containerClassLoader) {
//獲取`StandardContext`持有的`WebappLoader.classLoader`
classLoader = catalinaContext.getLoader().getClassLoader();
privileged = catalinaContext.getPrivileged();
//containerClassLoader是加載StandardContext類的類加載器
this.containerClassLoader = containerClassLoader;
ignoreAnnotations = catalinaContext.getIgnoreAnnotations();
Log log = catalinaContext.getLogger();
Set<String> classNames = new HashSet<>();
...
}
介紹了InstanceManager之后,下面看StandardWrapper.load方法:
//StandardWrapper
@Override
public synchronized void load() throws ServletException {
//根據(jù)servletClass(String)加載Servlet實(shí)例
instance = loadServlet();
//初始化
if (!instanceInitialized) {
initServlet(instance);
}
if (isJspServlet) {
...
}
}
public synchronized Servlet loadServlet() throws ServletException {
...
//獲取StandardContext持有的InstanceManager對(duì)象實(shí)例
InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();
try {
//通過(guò)InstanceManager加載Servlet
servlet = (Servlet) instanceManager.newInstance(servletClass);
} catch (ClassCastException e) {
...
}
下面看DefaultInstanceManager.newInstance是如何實(shí)例化類的:
//DefaultInstanceManager
@Override
public Object newInstance(String className) throws IllegalAccessException,
InvocationTargetException, NamingException, InstantiationException,
ClassNotFoundException, IllegalArgumentException, NoSuchMethodException, SecurityException {
//首先根據(jù)類名加載Class對(duì)象
Class<?> clazz = loadClassMaybePrivileged(className, classLoader);
return newInstance(clazz.getConstructor().newInstance(), clazz);
}
protected Class<?> loadClassMaybePrivileged(final String className,
final ClassLoader classLoader) throws ClassNotFoundException {
Class<?> clazz;
if (SecurityUtil.isPackageProtectionEnabled()) {
try {
clazz = AccessController.doPrivileged(new PrivilegedExceptionAction<Class<?>>() {
@Override
public Class<?> run() throws Exception {
//實(shí)際調(diào)用loadClass加載類
return loadClass(className, classLoader);
}
});
} catch (PrivilegedActionException e) {
Throwable t = e.getCause();
if (t instanceof ClassNotFoundException) {
throw (ClassNotFoundException) t;
}
throw new RuntimeException(t);
}
} else {
//實(shí)際調(diào)用loadClass加載類
clazz = loadClass(className, classLoader);
}
checkAccess(clazz);
return clazz;
}
protected Class<?> loadClass(String className, ClassLoader classLoader)
throws ClassNotFoundException {
//如果是Tomcat內(nèi)部類,則只使用containerClassLoader嘗試加載
//containerClassLoader是構(gòu)造函數(shù)中傳入的加載StandardContext類的加載器
//這是和其他StandardContext共用的加載器
if (className.startsWith("org.apache.catalina")) {
return containerClassLoader.loadClass(className);
}
try {
//如果不是Tomcat內(nèi)部類,同樣先使用containerClassLoader進(jìn)行加載
//所以Servlet中引用的三方類會(huì)先使用share版本
Class<?> clazz = containerClassLoader.loadClass(className);
if (ContainerServlet.class.isAssignableFrom(clazz)) {
return clazz;
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
}
//如果不是上述情景,則使用該StandardContext自己的類加載器進(jìn)行加載
return classLoader.loadClass(className);
}
5 總結(jié)
結(jié)合第2節(jié)的例子,以及每個(gè)StandardContext都持有一個(gè)自己的ClassLoader實(shí)例,就可以知道Tomcat WebAppClassLoader隔離加載的機(jī)制