首先我们来看看启动脚本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方法 并没有使用模版方法
@Overrideprotected 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