首先我们来看看启动脚本catalina.sh中的一段代码。
eval $_NOHUP "\"$_RUNJAVA\"" "\"$CATALINA_LOGGING_CONFIG\"" $LOGGING_MANAGER "$JAVA_OPTS" "$CATALINA_OPTS" \
-D$ENDORSED_PROP="\"$JAVA_ENDORSED_DIRS\"" \
-classpath "\"$CLASSPATH\"" \
-Dcatalina.base="\"$CATALINA_BASE\"" \
-Dcatalina.home="\"$CATALINA_HOME\"" \
-Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
org.apache.catalina.startup.Bootstrap "$@" start \
>> "$CATALINA_OUT" 2>&1 "&"
可以开单哦最终tomcat通过shell启动时 手动设置了-classpath满足上面描述的加载位置和顺序,然后通过bootstrap.jar包中的org.apache.catalina.Bootstrap类启动,并且制定了启动参数为strat。随后再来看看catalina.properties属性文件中对于上面说到的
Common和server shared类加载器的默认定义
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=
common.loader置顶的加载目录和之前描述的顺序一模一样 且server shared类加载器并没有指定类的路径,默认使用了简化模型,App类加载器下只有一个common类加载器和多个webapp类加载器,现在来看bootstrap对于类加载器的源码实现。
public static void main(String args[]) {
//获取锁 然后创建bootstrap对象 调用init方法进行初始化
synchronized (daemonLock) {
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
//如果已经启动 那么只需要设置上下文类加载器接口
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
}
可以看到调用了bootstrap.init方法进行初始化 而我们的类加载器的初始化便在这个方法里面进行初始化,而对于main方法的奇遇部分 暂时不讨论 这里只关注类加载器的源码即可 bootstrap和其他类源码会在后面的tomcat启动 关闭流程中再详细讨论。
我们跟进去init方法
public void init() throws Exception {
//初始化类加载器
initClassLoaders();
//将catalinaloader设置为线程上下文加载器
Thread.currentThread().setContextClassLoader(catalinaLoader);
...
private void initClassLoaders() {
try {
//创建common类加载器
commonLoader = createClassLoader("common", null);
if (commonLoader == null) {
//如果没有设置 那么默认是当前类加载器
commonLoader = this.getClass().getClassLoader();
}
//创建server和shared类加载器 catalinaloader对应于server类加载器
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 且我们将创建的commonloader加载器设置为了server和shared类加载器的父类加载器
我们继续看createClassloader方法的实现,可以看到通过将CatalinaProperties.getProperty(name + ".loader"); 方法获取到的value字符串解成路径后 包装为Repository对象 供后面创建类加载器时的url做准备 同时这里的Repository对象包含了3个类型 多个jar包的通配符 单个jar包的路径 完成jar包的绝对路径
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
//获取相应传递的属性名对应的属性值 如果属性值为空 那么直接返回parent
String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals(""))) {
return parent;
}
//修改属性值字符串格式
value = replace(value);
//将指定的路径封装为Repository列表
List<Repository> repositories = new ArrayList<>();
//从字符串格式中分割全部路径
String[] repositoryPaths = getPaths(value);
//遍历所有路径并创建Repository对象 然后放入Repository列表中
for (String repository : repositoryPaths) {
//若指定类url路径 则现场时通过url来包装
try {
@SuppressWarnings("unused")
URL url = new URL(repository);
repositories.add(new Repository(repository, RepositoryType.URL));
continue;
} catch (MalformedURLException e) {
// Ignore
}
//否则为jar包资源路径
if (repository.endsWith("*.jar")) {
//加载多个jar包
repository = repository.substring
(0, repository.length() - "*.jar".length());
repositories.add(new Repository(repository, RepositoryType.GLOB));
} else if (repository.endsWith(".jar")) {
//加载单个jar包
repositories.add(new Repository(repository, RepositoryType.JAR));
} else {
//加载目录下所有的资源
repositories.add(new Repository(repository, RepositoryType.DIR));
}
}
//通过Repository列表中来创建类加载器
return ClassLoaderFactory.createClassLoader(repositories, parent);
}
最终是通过createClassLoader方法来创建类加载器,其中包含了解析的Repository列表和父类加载器 同时我们看到方法入口处的判断,由于没有设置shared server类加载器 所以这两个类加载器就等于common类加载器。从URLClassLoader的源码中看到, 它的构造器智能接收URL统一资源定位符,同时这里也是使用URLclassLoader 所以将Repository对转换为URL对象来处理
public static ClassLoader createClassLoader(List<Repository> repositories,
final ClassLoader parent)
throws Exception {
if (log.isDebugEnabled()) {
log.debug("Creating new class loader");
}
//保存所有从Repository创建的URL对象
Set<URL> set = new LinkedHashSet<>();
if (repositories != null) {
//for循环处理所有的Repository对象 根据不同类型的Repository来做相应的处理
for (Repository repository : repositories) {
//如果是url类型
if (repository.getType() == RepositoryType.URL) {
URL url = buildClassLoaderUrl(repository.getLocation());
if (log.isDebugEnabled()) {
log.debug(" Including URL " + url);
}
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);
if (log.isDebugEnabled()) {
log.debug(" Including directory " + url);
}
set.add(url);
//jar包类型
} 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);
if (log.isDebugEnabled()) {
log.debug(" Including jar file " + url);
}
set.add(url);
} else if (repository.getType() == RepositoryType.GLOB) {
File directory=new File(repository.getLocation());
directory = directory.getCanonicalFile();
if (!validateFile(directory, RepositoryType.GLOB)) {
continue;
}
if (log.isDebugEnabled()) {
log.debug(" Including directory glob "
+ directory.getAbsolutePath());
}
String filenames[] = directory.list();
if (filenames == null) {
continue;
}
//遍历所有文件识别的所有jar包
for (String s : filenames) {
String filename = s.toLowerCase(Locale.ENGLISH);
if (!filename.endsWith(".jar")) {
continue;
}
File file = new File(directory, s);
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);
}
}
}
}
//到这里set集合中就包含了所有类夹杂器可以加载的类路径url了
final URL[] array = set.toArray(new URL[0]);
if (log.isDebugEnabled()) {
for (int i = 0; i < array.length; i++) {
log.debug(" location " + i + " is " + array[i]);
}
}
//直接创建URLClassLoader进行返回
return AccessController.doPrivileged(
new PrivilegedAction<URLClassLoader>() {
@Override
public URLClassLoader run() {
if (parent == null) {
return new URLClassLoader(array);
} else {
return new URLClassLoader(array, parent);
}
}
});
}
从源码中可以看到 不同类型的Repository对象都需要转换为URL 然后最终根据我们之前介绍的jDK基础类加载器URLCLASSloader来创建名字为name 父类加载器为parent的类加载器
前面说过 servlet规范中定义Tomcat让每个web应用程序使用自己的类加载器。web应用类加载器使用Loader接口定义其行为,我们在Tomcat中看到,Context代表一个web应用,对于Context后面会说到,这里只关注应用类加载器原理即可,尽管Loader是嵌入在Context中 也是通过Context来创建并初始化的 实现了一个web应用一个Loader类加载器 以下是Loader接口的定义
public interface Loader {
//执行周期性事件,我们可以通过该方法实现监听类的变化 然后重新加载
public void backgroundProcess();
//获取当前Loader创建的ClassLoader对象
public ClassLoader getClassLoader();
//获取当前Loader相关联的Context容器对象 就是我们的web应用程序
public Context getContext();
//设置与当前Loader相关联的Context容器对象 就是我们的web应用对象
public void setContext(Context context);
//标识是否使用标准双亲委派加载模型 一般设置为false
public boolean getDelegate();
public void setDelegate(boolean delegate);
//标识当前web应用程序是否可以reload
@Deprecated
public boolean getReloadable();
@Deprecated
public void setReloadable(boolean reloadable);
//添加坚挺当前Loader类属性变换的监听器对象
public void addPropertyChangeListener(PropertyChangeListener listener);
//标识是否修改了与这个Loader关联的类库 决定是否reload
public boolean modified();
//移除坚挺当前Loader类属性变换的监听器对象
public void removePropertyChangeListener(PropertyChangeListener listener);
}
Loader接口定义了完整的Web应用类加载器的行为。可以设置delegate标志为true 让web应用类加载器满足标准双亲委派模型 同时可以设置reloadable变量为true 就可以结合modified方法来决定是否reload整个类信息,例如我们监听到类发生变化后 是否重新加载 同时包含了监听属性变化的监听器,接下来是Tomcat对于该结构的标准实现类WebappLoader的原理
查看WebappLoader类的定义 可以看到,WebappLoader类继承自LifecycleMBeanBase 说明满足Tomcat的声明周期而且介入了
JMX(LifecycleMBeanBase用于扩展JMX的声明周期模版类), 同时实现了Loader接口和PropertyChangeListener接口 这表明WebappLoader类本身就可以作为监听属性变换的监听器。
public class WebappLoader extends LifecycleMBeanBase
implements Loader, PropertyChangeListener {
}
既然WebappLoader类满足Tomcat的生命周期,那么就可以从声明周期方法来研究它的原理,WebappLoader类只实现了startInternal方法,我们知道Tomcat的生命周期定义中 是由父组件对子组件初始化和启动 所以这里的Webapploader类也是由Context接口的实现类来启动 因为Context接口属于web应用程序。而WebappLoader又和Context一一对应。所以自然由Context接口的实现类来完成 但是由于我们这里只研究类加载器的原理。我们直接看startInternal方法实现即可
通过源码得知 首先创建于WebappLoader对象关联的ClassLoader对象 该classLoader对象用于加载web应用程序所需资源,而且ClassLoader对象也满足Tomcat的生命周期 但是我们从这里看到的是 直接调用了生命周期Lifecycle接口的start方法 并没有使用模版方法
@Override
protected void startInternal() throws LifecycleException {
if (log.isDebugEnabled()) {
log.debug(sm.getString("webappLoader.starting"));
}
//如果context没有定义资源
if (context.getResources() == null) {
log.info("No resources for " + context);
setState(LifecycleState.STARTING);
return;
}
//创建与WebappLoader关联的Classloader对象 设定加载资源并且制定是否使用标准双亲委派模型
try {
classLoader = createClassLoader();
//将context即web应用上下文制定的资源路径传递给ClassLoader对象 用于加载类的信息
classLoader.setResources(context.getResources());
classLoader.setDelegate(this.delegate);
//配置加载类的路径信息
setClassPath();
setPermissions();
//启动类加载器
((Lifecycle) classLoader).start();
//通过contextName构建注册到JMX中classLoader对象的ObjectName
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);
//将与之关联的classLoader注册到JMX中
Registry.getRegistry(null, null)
.registerComponent(classLoader, cloname, null);
} catch (Throwable t) {
t = ExceptionUtils.unwrapInvocationTargetException(t);
ExceptionUtils.handleThrowable(t);
log.error( "LifecycleException ", t );
throw new LifecycleException("start: ", t);
}
setState(LifecycleState.STARTING);
}
1