1 概述
本文属于阅读代码中的笔记,主要通过Servlet的加载介绍Tomcat中WebappClassLoader
对于每个Web app的隔离加载机制。
2 一个自定义ClassLoader
的例子
在介绍具体加载机制之前,我们先看一个关于类加载的例子:
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 {
//该路径是一个非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运行之后输出结果为:
MyClassLoader@5c647e05
MyClassLoader@5c647e05
说明:
可见如果一个类由某个类加载器加载,那么因为使用该类(本例中A)而加载的类
(本例中B)也会使用加载同一个类加载器
当然,如果我们将类B放在classpath下,然后将Thread.currentThread.getContextClassLoader()
作为参数传入URLClassLoader构造函数,作为其双亲加载器,那么类B将由其双亲加载器加载,因为
URLClassLoader也遵循双亲委派模型
*/
为什么先介绍这样一个自定义类加载器的例子呢?其实在Tomcat中,基本上一个Web App所有的操作都是由一个Servlet
启动的,我们在定义自己的Servlet
时可能会使用其他的三方类库,比如MyBatis。结合着上面的例子可以知道,如果我们在加载Servlet
时使用的是一个自定义的ClassLoader
类实例,那么该Servlet
中引用的三方类库,如:MyBatis也会由该ClassLoader
加载。再假设我们为每个Web App都专门实例化一个ClassLoader
实例,那么这样就做到了不同Web App的隔离。因为我们知道Java中一个类是由该类名称以及该类的defining loader定义的。
具体什么是defining loader,可以看jvms8中关于类加载器的介绍:
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
实例化
现在看Tomcat是如何加载web app目录(一般是webapps目录)里的web app的,在HostConfig.start
会进行应用部署:
//HostConfig
public void start() {
...
if (host.getDeployOnStartup())
//进行应用部署
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里的应用
deployDirectories(appBase, filteredAppPaths);
}
protected void deployDirectories(File appBase, String[] files) {
if (files == null)
return;
ExecutorService es = host.getStartStopExecutor();
List<Future<?>> results = new ArrayList<>();
//对于webapps下面的所有目录,进行以次部署,一般一个目录
//表示一个单独的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;
//并行部署多个web app
//DeployDirectory构造函数第一个参数为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
的一个内部类:
//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() {
//这里调用HostConfig的deployDirectory进行单个web app(或者说单个)
//目录的部署
//config就是上面代码构造函数传的this
config.deployDirectory(cn, dir);
}
}
HostConfig.deployDirectory
的方法体比较长,下面是省略了非主要部分之后的代码:
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 {
//最主要的代码就是这里,这里会创建一个StandardContext
//类实例,也就是每个web app都会对应一个
//StandardContext实例
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
//同样是实例化一个StandardContext的实例
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);
...
}
上面介绍了每个web app都会对应一个StandardContext
类实例,阅读过Tomcat源码的读者都知道Tomcat中许多类都实现了LifecycleListener
接口,实现了生命周期管理。StandardContext
同样也实现了该接口,那么我们接下来就看StandardContext.startInternal
方法的定义,该方法同样较长,因为我们这里主要介绍类加载机制,所以我们省略了其他不相关的部分:
//StandardContext
@Override
protected synchronized void startInternal() throws LifecycleException {
...
//每个StandardContext对象都持有一个WebappLoader对象,也就是自己的
//类加载器,所有该StandardContext加载的三方类和其他StandardContext
//加载的三方类是隔离的
if (getLoader() == null) {
//getParentClassLoader返回的parentClassLoader是其父类加载器
//是由CopyParentClassLoaderRule.begin中配置的,通过digester
//注入的,实现share加载
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
...
}
WebappLoader
内部持有一个具体的ClassLoader
实现,其实例化在该类start时,WebappLoader
同样实现了LifecycleListener
接口,StandardContext
在start时会调用WebappLoader
的start方法,所以这里看下其startInternal
方法的定义:
protected void startInternal() throws LifecycleException {
...
try {
//创建具体的ClassLoader实例
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的类继承关系图:
可见ParallelWebappClassLoader类是URLClassLoader
类的一个实现,所有要从中加载类,必须设置其要加载的类所在的URL,ParallelWebappClassLoader
父类WebappClassLoaderBase
同样实现了Lifecycle
接口,所以看其start方法实现:
//WebappClassLoaderBase
public void start() throws LifecycleException {
state = LifecycleState.STARTING_PREP;
//设置其加载类的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()]);
}
到此已经介绍了每个Web app特有的StandardContext
及其持有的ClassLoader
是如何实例化的,下面看Servlet是如何加载的。
4 Servlet
加载
要介绍Servlet
加载首先要看下StandardWrapper
类,StandardWrapper
是Servlet的封装,在web.xml中配置的每个servlet-class都会对应一个StandardWrapper
,StandardWrapper.servletClass
(String类型,还未加载)对应其servlet-class
具体配置。
不管是在启动时加载Servlet
还是在第一个请求到来时加载Servlet
都会调用StandardWrapper.load
方法。
在介绍StandardWrapper.load
方法之前,我们首先看下InstanceManager
,每个StandardContext
都会持有一个InstanceManager
实例,StandardContext.InstanceManager
会在StandardContext.startInternal
中实例化,默认的InstanceManager
实现是DefaultInstanceManager
,DefaultInstanceManager
会持有一个ClassLoader
实例,该ClassLoader
实例其实就是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());
//实例化InstanceManager对象实例,默认实现为
//DefaultInstanceManager
setInstanceManager(new DefaultInstanceManager(context,
injectionMap, this, this.getClass().getClassLoader()));
}
getServletContext().setAttribute(
InstanceManager.class.getName(), getInstanceManager());
InstanceManagerBindings.bind(getLoader().getClassLoader(), getInstanceManager());
}
...
}
//DefaultInstanceManager构造函数
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 {
//根据servletClass(String)加载Servlet实例
instance = loadServlet();
//初始化
if (!instanceInitialized) {
initServlet(instance);
}
if (isJspServlet) {
...
}
}
public synchronized Servlet loadServlet() throws ServletException {
...
//获取StandardContext持有的InstanceManager对象实例
InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();
try {
//通过InstanceManager加载Servlet
servlet = (Servlet) instanceManager.newInstance(servletClass);
} catch (ClassCastException e) {
...
}
下面看DefaultInstanceManager.newInstance
是如何实例化类的:
//DefaultInstanceManager
@Override
public Object newInstance(String className) throws IllegalAccessException,
InvocationTargetException, NamingException, InstantiationException,
ClassNotFoundException, IllegalArgumentException, NoSuchMethodException, SecurityException {
//首先根据类名加载Class对象
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 {
//实际调用loadClass加载类
return loadClass(className, classLoader);
}
});
} catch (PrivilegedActionException e) {
Throwable t = e.getCause();
if (t instanceof ClassNotFoundException) {
throw (ClassNotFoundException) t;
}
throw new RuntimeException(t);
}
} else {
//实际调用loadClass加载类
clazz = loadClass(className, classLoader);
}
checkAccess(clazz);
return clazz;
}
protected Class<?> loadClass(String className, ClassLoader classLoader)
throws ClassNotFoundException {
//如果是Tomcat内部类,则只使用containerClassLoader尝试加载
//containerClassLoader是构造函数中传入的加载StandardContext类的加载器
//这是和其他StandardContext共用的加载器
if (className.startsWith("org.apache.catalina")) {
return containerClassLoader.loadClass(className);
}
try {
//如果不是Tomcat内部类,同样先使用containerClassLoader进行加载
//所以Servlet中引用的三方类会先使用share版本
Class<?> clazz = containerClassLoader.loadClass(className);
if (ContainerServlet.class.isAssignableFrom(clazz)) {
return clazz;
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
}
//如果不是上述情景,则使用该StandardContext自己的类加载器进行加载
return classLoader.loadClass(className);
}
5 总结
结合第2节的例子,以及每个StandardContext
都持有一个自己的ClassLoader
实例,就可以知道Tomcat WebAppClassLoader隔离加载的机制