Tomcat源码学习笔记(日更)
2023-12-15 00:2:12 Author: 白帽子(查看原文) 阅读量:2 收藏

Tomcat顶层类加载器源码分析

首先我们来看看启动脚本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的类加载器

Tomcat应用层类加载器源码分析

前面说过 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类的定义 可以看到,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


文章来源: http://mp.weixin.qq.com/s?__biz=MzAwMDQwNTE5MA==&mid=2650247218&idx=1&sn=fda88a1a4028fca20e09b15cdafe550e&chksm=8326468dc61314aff680db719ccd48480fde940c046b236c646eca5cd4a8128db53e0c0ff642&scene=0&xtrack=1#rd
如有侵权请联系:admin#unsafe.sh