Java类加载机制,双亲委托模型相必大家已经熟的不能再熟了。本文就从Tomcat的源码级别上来探究一下类加载机制的秘密。
首先咱们还是老调重弹,看一下网上已经泛滥的一张Tomcat类加载关系图
属于JavaSE的BootstrapClassLoader、ExtClassLoader、AppClassLoader(这里主要加载Tomcat启动的Bootstrap类)在本文不再赘述;
CommonClassLoader加载common.loader属性下的jar;一般是CATALINA_HOME/lib目录下
CatalinaClassLoader加载server.loader属性下的jar;默认未配置路径,返回其父加载器即CommonClassLoader
SharedClassloader加载share.loader属性下的jar;默认未配置路径,返回其父加载器即CommonClassLoader
由于WebAppClassLoader需要等Tomcat的各个组件初始化完成之后才加载对应的server.xml配置文件,解析对应Host下的docBase目录寻找WEB-INF下的类文件,并且会将该根目录下的每个直接子目录当作一个web项目加载,为了确保各个项目之间的相互独立,每个项目都是单独的WebAppClassLoader加载的,咱们后文再讲。
CommonClassLoader、CatalinaClassLoader、SharedClassloader
先从源码角度来看看CommonClassLoader、CatalinaClassLoader、SharedClassloader这三者是如何绑定关系的?
org.apache.catalina.startup.Bootstrap#initClassLoaders
// 初始化类加载器
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);
}
}
调用了createClassLoader方法来创建对象,第二个参数是指定它的父级类加载器。可以看到catalinaLoader、sharedLoader均指明commonLoader为它的父级类加载器,这说明catalinaLoader、sharedLoader是同级类加载器,印证了上图。
再看看createClassLoader方法怎么做的:
// 创建基础类型类加载器
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
// 根据“name+.loader”从系统配置中读取需要加载的jar路径
String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals("")))
return parent;
value = replace(value);
List<Repository> repositories = new ArrayList<>();
String[] repositoryPaths = getPaths(value);
for (String repository : repositoryPaths) {
// Check for a JAR URL repository
try {
@SuppressWarnings("unused")
URL url = new URL(repository);
repositories.add(new Repository(repository, RepositoryType.URL));
continue;
} catch (MalformedURLException e) {
// Ignore
}
// 封装路径,指定其jar的类型:jar包、目录等
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));
}
}
// 通过类加载工厂创建,repositories的值为当前Tomcat所在目录下配置的jar子目录,比如 Tomcat_Home/lib/
return ClassLoaderFactory.createClassLoader(repositories, parent);
}
ClassLoaderFactory.createClassLoader是一个典型的工厂模式,屏蔽了类加载器对象初始化的细节:
public static ClassLoader createClassLoader(List<Repository> repositories,
final ClassLoader parent)
throws Exception {
if (log.isDebugEnabled())
log.debug("Creating new class loader");
// Construct the "class path" for this class loader
Set<URL> set = new LinkedHashSet<>();
if (repositories != null) {
// 根据jar包类型解析其Url,并添加到Set<URL> set
for (Repository repository : repositories) {
if (repository.getType() == RepositoryType.URL) {
URL url = buildClassLoaderUrl(repository.getLocation());
set.add(url);
} else if (repository.getType() == RepositoryType.DIR) {
File directory = new File(repository.getLocation());
directory = directory.getCanonicalFile();
if (!validateFile(directory, RepositoryType.DIR)) {
continue;
}
URL url = buildClassLoaderUrl(directory);
set.add(url);
} else if (repository.getType() == RepositoryType.JAR) {
File file=new File(repository.getLocation());
file = file.getCanonicalFile();
if (!validateFile(file, RepositoryType.JAR)) {
continue;
}
URL url = buildClassLoaderUrl(file);
set.add(url);
} else if (repository.getType() == RepositoryType.GLOB) {
File directory=new File(repository.getLocation());
directory = directory.getCanonicalFile();
if (!validateFile(directory, RepositoryType.GLOB)) {
continue;
}
String filenames[] = directory.list();
if (filenames == null) {
continue;
}
for (int j = 0; j < filenames.length; j++) {
String filename = filenames[j].toLowerCase(Locale.ENGLISH);
if (!filename.endsWith(".jar"))
continue;
File file = new File(directory, filenames[j]);
file = file.getCanonicalFile();
if (!validateFile(file, RepositoryType.JAR)) {
continue;
}
if (log.isDebugEnabled())
log.debug(" Including glob jar file "
+ file.getAbsolutePath());
URL url = buildClassLoaderUrl(file);
set.add(url);
}
}
}
}
// Construct the class loader itself
final URL[] array = set.toArray(new URL[set.size()]);
// 直接通过URLClassLoader创建ClassLoader
return AccessController.doPrivileged(
new PrivilegedAction<URLClassLoader>() {
@Override
public URLClassLoader run() {
if (parent == null)
return new URLClassLoader(array);
else
return new URLClassLoader(array, parent);
}
});
}
可以看到通过ClassLoaderFactory.createClassLoader(List
上文提到catalinaLoader会加载Tomcat本身的类,其主要目的是进行类加载隔离,与SharedClassloader区分开,这样咱们开发人员编写的web项目就不能直接访问到Tomcat的类,造成安全问题了。 那么又是怎么实现的呢?咱们接着看Tomcat的启动前的初始方法org.apache.catalina.startup.Bootstrap#init()
public void init() throws Exception {
// 初始化commonLoader、catalinaLoader、sharedLoader3个类加载器
initClassLoaders();
// 直接设置当前线程的类加载器为catalinaLoader
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
// 使用当前线程的catalinaLoader来加载Catalina类
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();
// 以下整段表示:Catalina初始化后得到的对象startupInstance调用setParentClassLoader方法,将sharedLoader设置为父加载器
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method = startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
catalinaDaemon = startupInstance;
}
从源码中可以看到步骤:
- 用catalinaLoader加载了”org.apache.catalina.startup.Catalina”类;
- 立即通过setParentClassLoader方法指定其父加载器为sharedLoader,这样Catalina对象也可访问sharedLoader下的类了;
如何为每一个webApp项目定义一个独立的WebAppClassLoader?
咱们从源码角度来分析一下。
Tomcat的各容器调用流程不是本文关注的重点,直接查看webApp的类加载器初始化的代码位置org.apache.catalina.loader.WebappLoader#startInternal()
// 每一个项目context均会执行此方法
protected void startInternal() throws LifecycleException {
// 省略部分代码
// Construct a class loader based on our current repositories list
try {
// 创建WebApp的类加载器
classLoader = createClassLoader();
// 获取一个webapp项目的类加载路径:WEB-INF/classes、WEB-INF/lib/
classLoader.setResources(context.getResources());
// delegate是否遵循类加载的双亲委派模型,默认为false
classLoader.setDelegate(this.delegate);
// 执行类加载
((Lifecycle) classLoader).start();
// 省略
} catch (Throwable t) {
// 省略
}
// 省略
}
private String loaderClass = ParallelWebappClassLoader.class.getName();
/**
* Create associated classLoader.
*/
private WebappClassLoaderBase createClassLoader()
throws Exception {
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;
}
当每一个HOST执行org.apache.catalina.startup.HostConfig#deployApps()方法发布所有的项目时,其工作目录下的每个webapp项目均会执行org.apache.catalina.loader.WebappLoader#startInternal()方法,并在其中使用createClassLoader()创建类加载器。可以从createClassLoader()方法看到 WebappClassLoaderBase的真正实现类为ParallelWebappClassLoader。
ParallelWebappClassLoader类继承于WebappClassLoaderBase,类结构如下:
WebappClassLoaderBase继承于URLClassLoader,由URLClassLoader可指定加载位置的Path来进行类加载。
还记得上面的 this.delegate 字段吗?默认为false,表示不使用双亲委派机制,我们再看看是如何破坏双亲委派机制的。方法位置:org.apache.catalina.loader.WebappClassLoaderBase#loadClass(java.lang.String, boolean)
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = null;
// 省略一些检查代码
// (0) 从本地字节码缓存中查找
clazz = findLoadedClass0(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
// (0.1) 从已经加载过的类中查找
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
// (0.2) 尝试使用系统类加载器加载该类,以防止webapp覆盖Java SE类。This implements SRV.10.7.2
String resourceName = binaryNameToPath(name, false);
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
//使用getResource,因为如果Java SE类加载器无法提供该资源,它将不会触发昂贵的ClassNotFoundException。
//但是,在极少数情况下在安全管理器下运行时(有关详细信息,请参见https://bz.apache.org/bugzilla/show_bug.cgi?id=58125 //),此调用可能触发ClassCircularityError。
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;
}
// 尝试通过JavaSE类加载器加载
if (tryLoadingFromJavaseLoader) {
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// (0.5) 使用SecurityManager时访问此类的权限
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;
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}
//默认为不使用双亲委派,调用filter进行过滤是否可以委派加载此方法,如果是JSP,EL表达式,Tomcat等一些类则可以委派
boolean delegateLoad = delegate || filter(name, true);
// (1) 如果需要,委托给我们的父加载器
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + 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
}
}
// (2) 从本地搜索查找类即自我加载
try {
clazz = findClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// (3) 实在找不到,无条件委托给父加载器
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
}
}
}
// 所有方法均加载失败,返回ClassNotFoundException
throw new ClassNotFoundException(name);
}
从源码中,可以提取为以下步骤:
- (0) 从本地字节码缓存中查找
- (0.1) 从已经加载过的类中查找
- (0.2) 尝试使用系统类加载器加载该类,以防止webapp覆盖Java SE类。
- (0.3) 系统类加载器不适合加载此类
- (0.4) 尝试通过JavaSE类加载器加载
- (0.5) 使用SecurityManager时访问此类的权限
- (1) 判断是否双亲委派(一般为false),如果需要,委托给我们的父加载器
- (2) 使用自己加载器进行类加载,注意此步骤如果执行则破坏了双亲委派原则,因为没有让父类加载器进行加载。
- (3) 实在找不到,无条件委托给父加载器
- 所有方法均加载失败,返回ClassNotFoundException
总结
Java的双亲委派机制无疑是一个非常优秀的设计,它有以下优点:
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
增加系统安全,java核心API中定义类型不会被随意替换。
但是在Tomcat这种需要多项目部署时,其反而可能会有一些弊端,比如刚好部署了A,B两个字节码完全相同的系统,如果是传统的双亲委派机制,则后加载的系统的类不会加载成功,如果有类静态变量则会被2个系统共享,引起系统异常。