Tomcat源码五部曲(四)、类加载器
在Tomcat中提供了三个基础的类加载器和Web应用类加载器,如下图,绿色是JDK本身类加载器,蓝色则为Tomcat定义的类加载器,这三个类加载器指向的路径和包列表可以由catalina.properties配置,此文件位于Tomcat_Home/conf下。
Common
他以System为父类加载器,是用于加载Tomcat应用服务器顶层的公用类加载器,其路径为common.loader,默认指向catalina.home/lib下的包。他加载的类对于Web应用均可见。
Catalina
以Common为父加载器,用于加载Tomcat应用服务器的类加载器,其路径为server.loader,默认为空。他加载的类对于Web应用不可见。
Shared
以Common为父加载器,是所有Web应用的父加载器,其路径为shared.loader,默认也为空。
Web应用
以Shared为父加载器,加载/WEB-INF/classes目录下的未压缩的Class和资源文件以及/WEB-INF/lib目录下的Jar包,这个类加载器只对当前Web应用可见,对其他Web应用不可见。
自定义加载器的初始化
Common 类加载器是在 Bootstrap 的 initClassLoaders 初始化的,代码如下。从中可以看到上述所说对应路径是什么。
ClassLoader commonLoader = null;
ClassLoader catalinaLoader = null;
ClassLoader sharedLoader = null;
private void initClassLoaders() {
try {
//创建commonLoader类加载器
commonLoader = createClassLoader("common", null);
if (commonLoader == null) {
commonLoader = this.getClass().getClassLoader();
}
//创建catalinaLoader类加载器,父加载器为commonLoader
catalinaLoader = createClassLoader("server", commonLoader);
//创建sharedLoader类加载器,父加载器为commonLoader
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
具体还是要看createClassLoader方法。
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
String value = CatalinaProperties.getProperty(name + ".loader");
//如果没有配置,则返回传入的类加载器
if ((value == null) || (value.equals("")))
return parent;
List<Repository> repositories = new ArrayList<>();
..............
return ClassLoaderFactory.createClassLoader(repositories, parent);
}
上述中省略了不少, CatalinaProperties.getProperty就是从catalina.properties(位于conf目录下)中获取对应值,如下图示例。获取到值后判断是否为空,为空则返回传入的类加载器,也就是说如果我们没有配置过他的话,Shared、Catalina最终结果都是Common。
不为空则解析出所有路径,添加到List中,最后通过类加载器工厂创建一个类加载并返回。类加载器初始化完毕后,使用catalinaLoader加载创建一个org.apache.catalina.startup.Catalina 对象,最终会调用它的 load 方法,解析 server.xml 初始化容器内部组件。并且Catalina 对象有一个 parentClassLoader 成员变量,它是所有组件的父加载器,默认是 AppClassLoader,Catalina创建完毕后,会反射调用它的 setParentClassLoader 方法,将父加载器设为 sharedLoader。
而 Tomcat 内部顶级容器 Engine 在初始化时,Digester 有一个 SetParentClassLoaderRule 规则,会将 Catalina 的 parentClassLoader 通过 Engine.setParentClassLoader 方法关联起来。
还有Web应用加载器,Web应用类加载器是Web应用私有的,每个Web应用就是一个Context,在Tomcat中,Context的默认实现为StandardContext。所以,创建Web应用类加载器也在StandardContext中。
也就是下面这段代码,每个StandardContext对象都持有一个WebappLoader对象,也就是自己的类加载器,创建后会启动WebappLoader。
protected synchronized void startInternal() throws LifecycleException {
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
}
public void setLoader(Loader loader) {
......
//最终会调用WebappLoader的startInternal方法
((Lifecycle) loader).start();
}
下面是WebappLoader的startInternal()方法,首先创建类加载器(创建的是ParallelWebappClassLoader,ParallelWebappClassLoader继承WebappClassLoaderBase,这里分析的是Tomcat-9),然后启动类加载器。
@Override
protected void startInternal() throws LifecycleException {
..........
//创建类加载器
classLoader = createClassLoader();
//启动类加载器
classLoader.start();
}
下面是WebappClassLoaderBase的start方法。
@Override
public void start() throws LifecycleException {
state = LifecycleState.STARTING_PREP;
//加载web应用的所有class文件
WebResource classes = resources.getResource("/WEB-INF/classes");
if (classes.isDirectory() && classes.canRead()) {
localRepositories.add(classes.getURL());
}
//加载web应用lib目录下的jar文件
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;
}
然后就是我们Servlet类加载过程,这就需要回到StandardWrapper中的loadServlet方法。在这里推荐在web.xml的web-app上设置属性metadata-complete为true,这是由于在ContextConfig的configureStart方法中判断metadata-complete值,如果为false,最终会取反为true,会在此时加载我们的类(但是也无所谓,但是我们想做的是,在第一次访问Servlet的时候去跟踪调试)。
public synchronized Servlet loadServlet() throws ServletException {
InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();
try {
//加载我们的Servlet
servlet = (Servlet) instanceManager.newInstance(servletClass);
} catch (ClassCastException e) {
}
}
上述代码首先从父容器StandardContext中获取InstanceManager,InstanceManager在StandardContext的startInternal()方法中创建,其实也就是createInstanceManager()方法。
实现类也就是DefaultInstanceManager,其中持有一个ClassLoader,也就是在上面说的ParallelWebappClassLoader实例。
然后在看DefaultInstanceManager的newInstance方法,核心也在loadClassMaybePrivileged中。
@Override
public Object newInstance(String className) throws IllegalAccessException,
InvocationTargetException, NamingException, InstantiationException,
ClassNotFoundException, IllegalArgumentException, NoSuchMethodException, SecurityException {
Class<?> clazz = loadClassMaybePrivileged(className, classLoader);
return newInstance(clazz.getConstructor().newInstance(), clazz);
}
loadClassMaybePrivileged方法中判断是否开启安全机制,其实还要看loadClass方法。
protected Class<?> loadClassMaybePrivileged(final String className,
final ClassLoader classLoader) throws ClassNotFoundException {
Class<?> clazz;
//判断是否启动了安全机制
if (SecurityUtil.isPackageProtectionEnabled()) {
try {
clazz = AccessController.doPrivileged(
new PrivilegedLoadClass(className, classLoader));
} catch (PrivilegedActionException e) {
Throwable t = e.getCause();
if (t instanceof ClassNotFoundException) {
throw (ClassNotFoundException) t;
}
throw new RuntimeException(t);
}
} else {
//加载类
clazz = loadClass(className, classLoader);
}
checkAccess(clazz);
return clazz;
}
首先判断要加载的类全名开头是不是org.apache.catalina,如果是,则使用containerClassLoader加载。否则使用containerClassLoader尝试再次加载,但是可能会出现ClassNotFoundException异常,如果加载成功,判断他的超类是不是ContainerServlet。如果是,则返回,否则使用classLoader加载,classLoader就是ParallelWebappClassLoader的实例。
protected Class<?> loadClass(String className, ClassLoader classLoader)
throws ClassNotFoundException {
if (className.startsWith("org.apache.catalina")) {
return containerClassLoader.loadClass(className);
}
try {
Class<?> clazz = containerClassLoader.loadClass(className);
if (ContainerServlet.class.isAssignableFrom(clazz)) {
return clazz;
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
}
Class<?> aClass = classLoader.loadClass(className);
return aClass;
}
最后就是WebappClassLoaderBase的loadClass方法。
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
String name1 = name;
synchronized (JreCompat.isGraalAvailable() ? this : getClassLoadingLock(name)) {
Class<?> clazz = null;
checkStateForClassLoading(name);
// 从当前ClassLoader的本地缓存中查找,如果不为空则返回
clazz = findLoadedClass0(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
// 查看jvm是否已经加载过此类,如果不为空则返回
clazz = JreCompat.isGraalAvailable() ? null : findLoadedClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
//通过系统的类加载器加载此类,这里防止应用写的类覆盖了J2SE的类
String resourceName = binaryNameToPath(name, false);
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
URL url;
if (securityManager != null) {
PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
url = AccessController.doPrivileged(dp);
} else {
url = javaseLoader.getResource(resourceName);
}
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
tryLoadingFromJavaseLoader = true;
}
if (tryLoadingFromJavaseLoader) {
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
//使用 SecurityManager 检查是否有此类的访问权限
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = sm.getString("webappClassLoader.restrictedPackage", name);
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}
//判断是否需要委托给父类加载器进行加载,delegateLoad一般结果为false
boolean delegateLoad = delegate || filter(name, true);
if (delegateLoad) {
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
}
}
try {
//调用findClass方法在webapp级别进行加载
clazz = findClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
//如果还是没有加载到类,并且不采用委托机制的话,则通过父类加载器去加载。
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);
}
可以看到这个类加载器和Java中的双亲委派模型有点不一样,Java默认的双亲委派加载过程如下:
1.从缓存中加载。
2.缓存中不存在则从父类加载器中加载。
3.父类加载器如果加载不了,则从当前类加载器加载。
4.如果还没有,则抛出异常。
而Tomcat提供的Web应用类加载器则不然,当类加载时,除JVM基础类库外,首先尝试当前类加载,然后才进行委派,他的过程大概如下:
1.从缓存中加载。
2.如果没有,则从从JVM的Bootstrap类加载器加载。
3.如果没有,则从当前类加载器加载,顺序先WEB-INF/classes后WEB-INF/lib。
4.如果没有,则从父类加载器加载,由于父类加载采用默认的委派模式,所以顺序为System、Common、Shared。
但上述代码中提到了delegate属性,用于控制是否启用Java委派模式,默认为false,当配置为true时,Tomcat将使用默认的委派模式,过程如下:
1.从缓存中加载。
2.如果没有,则从从JVM的Bootstrap类加载器加载。
3.如果没有,则从父类加载器加载,System、Common、Shared。
4.如果没有,则从当前类加载器加载。
原文始发于微信公众号(十四个字节):Tomcat源码五部曲(四)、类加载器