07.Tomcat源码分析——类加载体系

简介: 由于在生产环境中,Tomcat一般部署在Linux系统下,所以本文将以 startup.sh shell脚本为准,对Tomcat的启动进行分析。

我们启动Tomcat的命令如下:

sh startup.sh

startup.sh的脚本代码如下:

# Better OS/400 detection: see Bugzilla 31132
os400=false
case "`uname`" in
OS400*) os400=true;;
esac

# resolve links - $0 may be a softlink
PRG="$0"

while [ -h "$PRG" ] ; do
  ls=`ls -ld "$PRG"`
  link=`expr "$ls" : '.*-> \(.*\)$'`
  if expr "$link" : '/.*' > /dev/null; then
    PRG="$link"
  else
    PRG=`dirname "{
   
   mathJaxContainer[1]}link"
  fi
done

PRGDIR=`dirname "$PRG"`
EXECUTABLE=catalina.sh

# Check that target executable exists
if $os400; then
  # -x will Only work on the os400 if the files are:
  # 1. owned by the user
  # 2. owned by the PRIMARY group of the user
  # this will not work if the user belongs in secondary groups
  eval
else
  if [ ! -x "{
   
   mathJaxContainer[2]}EXECUTABLE" ]; then
    echo "Cannot find {
   
   mathJaxContainer[3]}EXECUTABLE"
    echo "The file is absent or does not have execute permission"
    echo "This file is needed to run this program"
    exit 1
  fi
fi

exec "{
   
   mathJaxContainer[4]}EXECUTABLE" start "$@"

重点看最后一行代码:exec "$PRGDIR"/"$EXECUTABLE" start "$@"

这里有两个变量:

  • PRGDIR:当前shell脚本所在的路径;
  • EXECUTABLE:脚本catalina.sh。

通过以上变量的解释,我们知道了其实执行startup.sh就是在执行catalina.sh,并且传递了参数start。

catalina.sh中接收到start参数后的执行的脚本分支见代码如下:

elif [ "$1" = "start" ] ; then

# 此处省略参数校验的脚本

  shift
  touch "$CATALINA_OUT"
  if [ "$1" = "-security" ] ; then
    if [ $have_tty -eq 1 ]; then
      echo "Using Security Manager"
    fi
    shift
    eval "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" {
   
   mathJaxContainer[7]}JAVA_OPTS $CATALINA_OPTS \
      -Djava.endorsed.dirs="\"$JAVA_ENDORSED_DIRS\"" -classpath "\"$CLASSPATH\"" \
      -Djava.security.manager \
      -Djava.security.policy=="\"$CATALINA_BASE/conf/catalina.policy\"" \
      -Dcatalina.base="\"$CATALINA_BASE\"" \
      -Dcatalina.home="\"$CATALINA_HOME\"" \
      -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
      org.apache.catalina.startup.Bootstrap "$@" start \
      >> "$CATALINA_OUT" 2>&1 "&"

  else
    eval "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" {
   
   mathJaxContainer[10]}JAVA_OPTS $CATALINA_OPTS \
      -Djava.endorsed.dirs="\"$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 "&"

  fi

  if [ ! -z "$CATALINA_PID" ]; then
    echo {
   
   mathJaxContainer[12]}CATALINA_PID"
  fi

  echo "Tomcat started."

从以上代码片段可以看出,最终使用java命令执行了org.apache.catalina.startup.Bootstrap类中的main方法,参数也是start。Bootstrap的main方法的实现见代码如下:

public static void main(String[] args) {
   
   
        synchronized(daemonLock) {
   
   
            if (daemon == null) {
   
   
                Bootstrap bootstrap = new Bootstrap();

                try {
   
   
                    bootstrap.init();
                } catch (Throwable var5) {
   
   
                    handleThrowable(var5);
                    var5.printStackTrace();
                    return;
                }

                daemon = bootstrap;
            } else {
   
   
                Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
            }
        }

        try {
   
   
            String command = "start";
            if (args.length > 0) {
   
   
                command = args[args.length - 1];
            }

            if (command.equals("startd")) {
   
   
                args[args.length - 1] = "start";
                daemon.load(args);
                daemon.start();
            } else if (command.equals("stopd")) {
   
   
                args[args.length - 1] = "stop";
                daemon.stop();
            } else if (command.equals("start")) {
   
   
                daemon.setAwait(true);
                daemon.load(args);
                daemon.start();
                if (null == daemon.getServer()) {
   
   
                    System.exit(1);
                }
            } else if (command.equals("stop")) {
   
   
                daemon.stopServer(args);
            } else if (command.equals("configtest")) {
   
   
                daemon.load(args);
                if (null == daemon.getServer()) {
   
   
                    System.exit(1);
                }

                System.exit(0);
            } else {
   
   
                log.warn("Bootstrap: command \"" + command + "\" does not exist.");
            }
        } catch (Throwable var7) {
   
   
            Throwable t = var7;
            if (var7 instanceof InvocationTargetException && var7.getCause() != null) {
   
   
                t = var7.getCause();
            }

            handleThrowable(t);
            t.printStackTrace();
            System.exit(1);
        }

    }

可以很清晰的看到,一上来就调用了 bootstrap.init();方法进行初始化的操作,代码如下:

    public void init() throws Exception {
   
   
        this.initClassLoaders();
        Thread.currentThread().setContextClassLoader(this.catalinaLoader);
        SecurityClassLoad.securityClassLoad(this.catalinaLoader);
        if (log.isDebugEnabled()) {
   
   
            log.debug("Loading startup class");
        }

        Class<?> startupClass = this.catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
        Object startupInstance = startupClass.getConstructor().newInstance();
        if (log.isDebugEnabled()) {
   
   
            log.debug("Setting startup class properties");
        }

        String methodName = "setParentClassLoader";
        Class<?>[] paramTypes = new Class[]{
   
   Class.forName("java.lang.ClassLoader")};
        Object[] paramValues = new Object[]{
   
   this.sharedLoader};
        Method method = startupInstance.getClass().getMethod(methodName, paramTypes);
        method.invoke(startupInstance, paramValues);
        this.catalinaDaemon = startupInstance;
    }

而init方法一上来调用了这三行关键代码:

  • this.initClassLoaders();

  • Thread.currentThread().setContextClassLoader(this.catalinaLoader);

​ catalinaLoader会被设置为Tomcat主线程的线程上下文类加载器,并且使用catalinaLoader加载Tomcat容器自身容器下的class

  • SecurityClassLoad.securityClassLoad(this.catalinaLoader);


securityClassLoad方法主要加载Tomcat容器所需的class,包括:

  • Tomcat核心class,即org.apache.catalina.core路径下的class;
  • org.apache.catalina.loader.WebappClassLoader$PrivilegedFindResourceByName;
  • Tomcat有关session的class,即org.apache.catalina.session路径下的class;
  • Tomcat工具类的class,即org.apache.catalina.util路径下的class;
  • javax.servlet.http.Cookie;
  • Tomcat处理请求的class,即org.apache.catalina.connector路径下的class;
  • Tomcat其它工具类的class,也是org.apache.catalina.util路径下的class;

ok,Common/Catalina/Shared ClassLoader 已经创建好了,那么肯定是要被使用的,是在哪里使用的呢?它们之间同Webapp ClassLoader又是怎么联系起来的?我们继续看init方法:

    public void init() throws Exception {
   
   
        this.initClassLoaders();
        Thread.currentThread().setContextClassLoader(this.catalinaLoader);
        SecurityClassLoad.securityClassLoad(this.catalinaLoader);
        if (log.isDebugEnabled()) {
   
   
            log.debug("Loading startup class");
        }
        //1.先加载Catalina类
        Class<?> startupClass = this.catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
        //2.通过反射实例化对象
        Object startupInstance = startupClass.getConstructor().newInstance();
        if (log.isDebugEnabled()) {
   
   
            log.debug("Setting startup class properties");
        }
        //3.catalina的setParentClassLoader方法
        String methodName = "setParentClassLoader";
        Class<?>[] paramTypes = new Class[]{
   
   Class.forName("java.lang.ClassLoader")};
        Object[] paramValues = new Object[]{
   
   this.sharedLoader};\
        //4.将catalina设置为sharedLoader的上级加载器
        Method method = startupInstance.getClass().getMethod(methodName, paramTypes);
        method.invoke(startupInstance, paramValues);
        this.catalinaDaemon = startupInstance;
    }

在我们上面的securityClassLoad()方法中会执行 loadCorePackage方法,该方法源码如下:

    private static final void loadCorePackage(ClassLoader loader) throws Exception {
   
   
        String basePackage = "org.apache.catalina.core.";
        loader.loadClass("org.apache.catalina.core.AccessLogAdapter");
        loader.loadClass("org.apache.catalina.core.ApplicationContextFacade$PrivilegedExecuteMethod");
        loader.loadClass("org.apache.catalina.core.ApplicationDispatcher$PrivilegedForward");
        loader.loadClass("org.apache.catalina.core.ApplicationDispatcher$PrivilegedInclude");
        loader.loadClass("org.apache.catalina.core.ApplicationPushBuilder");
        loader.loadClass("org.apache.catalina.core.AsyncContextImpl");
        loader.loadClass("org.apache.catalina.core.AsyncContextImpl$AsyncRunnable");
        loader.loadClass("org.apache.catalina.core.AsyncContextImpl$DebugException");
        loader.loadClass("org.apache.catalina.core.AsyncListenerWrapper");
        loader.loadClass("org.apache.catalina.core.ContainerBase$PrivilegedAddChild");
        loader.loadClass("org.apache.catalina.core.DefaultInstanceManager$AnnotationCacheEntry");
        loader.loadClass("org.apache.catalina.core.DefaultInstanceManager$AnnotationCacheEntryType");
        loader.loadClass("org.apache.catalina.core.DefaultInstanceManager$PrivilegedGetField");
        loader.loadClass("org.apache.catalina.core.DefaultInstanceManager$PrivilegedGetMethod");
        loader.loadClass("org.apache.catalina.core.DefaultInstanceManager$PrivilegedLoadClass");
        loader.loadClass("org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator");
    }

其中 loader.loadClass("org.apache.catalina.core.ContainerBase$PrivilegedAddChild"); 会加载到 ContainerBase以及其子类,通过查看发现其子类有如下:


而StandardContext类就是一个核心的子类,因为在其 startInternal()方法中:

    /**
     * Start this component and implement the requirements
     * of {@link LifecycleBase#startInternal()}.
     *
     * @exception LifecycleException if this component detects a fatal error
     *  that prevents this component from being used
     */
    @Override
    protected synchronized void startInternal() throws LifecycleException {
   
   

        // 省略前边的代码 

        if (getLoader() == null) {
   
   
            WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
            webappLoader.setDelegate(getDelegate());
            setLoader(webappLoader);
        }
       // 省略中间的代码 
       // Start our subordinate components, if any
       if ((loader != null) && (loader instanceof Lifecycle))
            ((Lifecycle) loader).start(); 
       // 省略后边的代码 
    }

我们发现了关键的 WebappLoader 的创建,并且将WebappLoader设置为了当前的类加载器。

同时创建WebappLoader 的时候,传递了一个参数 getParentClassLoader() ,我们可以看下 WebappLoader的构造函数:

    public WebappLoader(ClassLoader parent) {
   
   
        this.classLoader = null;
        this.context = null;
        this.delegate = false;
        this.loaderClass = ParallelWebappClassLoader.class.getName();
        this.parentClassLoader = null;
        this.reloadable = false;
        this.support = new PropertyChangeSupport(this);
        this.classpath = null;
        this.parentClassLoader = parent;
    }

ok,最后一行代码给webappLoader设置了对应的父级类加载器,getParentClassLoader() 这个方法会获取父容器parentClassLoader的属性,也就是找到ContainerBase中的setParentClassLoader方法被谁调用了就知道附的啥值了:


可以看到这个设置方法被CopyParentClassLoaderRule的begin方法中赋值了:

    public void begin(String namespace, String name, Attributes attributes)
        throws Exception {
   
   

        if (digester.getLogger().isDebugEnabled())
            digester.getLogger().debug("Copying parent class loader");
        Container child = (Container) digester.peek(0);
        Object parent = digester.peek(1);
        Method method =
            parent.getClass().getMethod("getParentClassLoader", new Class[0]);
        ClassLoader classLoader =
            (ClassLoader) method.invoke(parent, new Object[0]);
        child.setParentClassLoader(classLoader);

    }

这里的classLoader其实就是通过反射取出来的 SharedLoader,这里刚好就与我们的BootStrap中init刚开始的赋值匹配上了:

因此,我们也能彻底知道,WebAppLoader的父级类加载器就是我们的ShareClassLoader了。

代码阅读到这里,已经基本清楚了Tomcat中ClassLoader的总体结构,总结如下:

Tomcat存在common、catalina、shared三个公共的classloader,默认情况下,这三个classloader其实是同一个,都是common classloader,而针对每个webapp,也就是context(对应代码中的StandardContext类),都有自己的WebappClassLoader实例来加载每个应用自己的类,该类加载实例的parent即是Shared ClassLoader。

OK,在理解了整个类加载器结构后,我们再来看 WebAppLoader 是如何加载我们的类的,重点查看 loadClass 方法,由于代码过长,直接截取核心代码:

①从本地缓存中查找:

    protected Class<?> findLoadedClass0(String name) {
   
   
        String path = this.binaryNameToPath(name, true);
        ResourceEntry entry = (ResourceEntry)this.resourceEntries.get(path);
        return entry != null ? entry.loadedClass : null;
    }

②缓存中没有,则从JVM的引导类Bootstrap类加载器加载:

该类加载器包含 Java 虚拟机提供的基本运行时类,以及系统扩展目录 ( $JAVA_HOME/jre/lib/ext) 中存在的 JAR 文件中的任何类。

③再从系统类加载器进行查找:

System这个类加载器通常从CLASSPATH环境变量的内容中初始化。所有这些类对 Tomcat 内部类和 Web 应用程序都是可见的。但是,标准的 Tomcat 启动脚本($CATALINA_HOME/bin/catalina.sh%CATALINA_HOME%\bin\catalina.bat)完全忽略了CLASSPATH环境变量本身的内容,而是从以下存储库构建 System 类加载器:

  • $CATALINA_HOME/bin/bootstrap.jar — 包含用于初始化 Tomcat 服务器的 main() 方法,以及它所依赖的类加载器实现类。

  • $CATALINA_BASE/bin/tomcat-juli.jar*或 *$CATALINA_HOME/bin/tomcat-juli.jar — 日志实现类。其中包括java.util.loggingAPI 的增强类 ,称为 Tomcat JULI,以及 Tomcat 内部使用的 Apache Commons Logging 库的包重命名副本。有关更多详细信息,请参阅日志记录文档

    如果tomcat-juli.jar存在于 $CATALINA_BASE/bin 中*,则使用它代替*$CATALINA_HOME/bin 中的那个 。它在某些日志记录配置中很有用

  • $CATALINA_HOME/bin/commons-daemon.jar — 来自Apache Commons Daemon项目的类。这个 JAR 文件不存在于CLASSPATH构建者 catalina.bat| .sh脚本,但从bootstrap.jar的清单文件中引用。

④由当前类加载器进行加载:

⑤最后由父类加载器加载

总结:

Web应用类加载器默认的加载顺序是:

(1).先从缓存中加载;
(2).如果没有,则从JVM的Bootstrap类加载器查找;
(3).如果没有,则从当前类加载器加载查找(按照WEB-INF/classes、WEB-INF/lib的顺序);
(4).如果没有,则从父类加载器加载,父类加载器采用默认的委派模式

tomcat提供了delegate属性用于控制是否启用java委派模式,默认false(不启用),当设置为true时,tomcat将使用java的默认委派模式,这时加载顺序如下:

(1).先从缓存中加载;
(2).如果没有,则从JVM的Bootstrap类加载器加载;
(3).如果没有,则从父类加载器加载,加载顺序是AppClassLoader、Common、Shared。
(4).如果没有,则从当前类加载器加载(按照WEB-INF/classes、WEB-INF/lib的顺序);

思考题:

我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写

Class.forName("com.mysql.jdbc.Driver")

也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?

这个问题大家可以下来自己尝试阅读源码得到解决。

目录
相关文章
|
3天前
|
监控 Java 应用服务中间件
Spring Boot整合Tomcat底层源码分析
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置和起步依赖等特性,大大简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是其与Tomcat的整合。
19 1
|
2月前
|
监控 网络协议 应用服务中间件
【Tomcat源码分析】从零开始理解 HTTP 请求处理 (第一篇)
本文详细解析了Tomcat架构中复杂的`Connector`组件。作为客户端与服务器间沟通的桥梁,`Connector`负责接收请求、封装为`Request`和`Response`对象,并传递给`Container`处理。文章通过四个关键问题逐步剖析了`Connector`的工作原理,并深入探讨了其构造方法、`init()`与`start()`方法。通过分析`ProtocolHandler`、`Endpoint`等核心组件,揭示了`Connector`初始化及启动的全过程。本文适合希望深入了解Tomcat内部机制的读者。欢迎关注并点赞,持续更新中。如有问题,可搜索【码上遇见你】交流。
【Tomcat源码分析】从零开始理解 HTTP 请求处理 (第一篇)
|
2月前
|
人工智能 前端开发 Java
【Tomcat源码分析】启动过程深度解析 (二)
本文深入探讨了Tomcat启动Web应用的过程,重点解析了其加载ServletContextListener及Servlet的机制。文章从Bootstrap反射调用Catalina的start方法开始,逐步介绍了StandardServer、StandardService、StandardEngine、StandardHost、StandardContext和StandardWrapper的启动流程。每个组件通过Lifecycle接口协调启动,子容器逐层启动,直至整个服务器完全启动。此外,还详细分析了Pipeline及其Valve组件的作用,展示了Tomcat内部组件间的协作机制。
【Tomcat源码分析】启动过程深度解析 (二)
|
2月前
|
前端开发 Java 应用服务中间件
【Tomcat源码分析 】"深入探索:Tomcat 类加载机制揭秘"
本文详细介绍了Java类加载机制及其在Tomcat中的应用。首先回顾了Java默认的类加载器,包括启动类加载器、扩展类加载器和应用程序类加载器,并解释了双亲委派模型的工作原理及其重要性。接着,文章分析了Tomcat为何不能使用默认类加载机制,因为它需要解决多个应用程序共存时的类库版本冲突、资源共享、类库隔离及JSP文件热更新等问题。最后,详细展示了Tomcat独特的类加载器设计,包括Common、Catalina、Shared、WebApp和Jsp类加载器,确保了系统的稳定性和安全性。通过这种设计,Tomcat实现了不同应用程序间的类库隔离与共享,同时支持JSP文件的热插拔。
【Tomcat源码分析 】"深入探索:Tomcat 类加载机制揭秘"
|
2月前
|
设计模式 应用服务中间件 容器
【Tomcat源码分析】Pipeline 与 Valve 的秘密花园
本文深入剖析了Tomcat中的Pipeline和Valve组件。Valve作为请求处理链中的核心组件,通过接口定义了关键方法;ValveBase为其基类,提供了通用实现。Pipeline则作为Valve容器,通过首尾相连的Valve链完成业务处理。StandardPipeline实现了Pipeline接口,提供了详细的Valve管理逻辑。通过对代码的详细分析,揭示了模板方法模式和责任链模式的应用,展示了系统的扩展性和模块化设计。
【Tomcat源码分析】Pipeline 与 Valve 的秘密花园
|
2月前
|
设计模式 人工智能 安全
【Tomcat源码分析】生命周期机制 Lifecycle
Tomcat内部通过各种组件协同工作,构建了一个复杂的Web服务器架构。其中,`Lifecycle`机制作为核心,管理组件从创建到销毁的整个生命周期。本文详细解析了Lifecycle的工作原理及其方法,如初始化、启动、停止和销毁等关键步骤,并展示了LifecycleBase类如何通过状态机和模板模式实现这一过程。通过深入理解Lifecycle,我们可以更好地掌握组件生命周期管理,提升系统设计能力。欢迎关注【码上遇见你】获取更多信息,或搜索【AI贝塔】体验免费的Chat GPT。希望本章内容对你有所帮助。
|
3月前
|
网络协议 Java 应用服务中间件
Tomcat源码分析 (一)----- 手撕Java Web服务器需要准备哪些工作
本文探讨了后端开发中Web服务器的重要性,特别是Tomcat框架的地位与作用。通过解析Tomcat的内部机制,文章引导读者理解其复杂性,并提出了一种实践方式——手工构建简易Web服务器,以此加深对Web服务器运作原理的认识。文章还详细介绍了HTTP协议的工作流程,包括请求与响应的具体格式,并通过Socket编程在Java中的应用实例,展示了客户端与服务器间的数据交换过程。最后,通过一个简单的Java Web服务器实现案例,说明了如何处理HTTP请求及响应,强调虽然构建基本的Web服务器相对直接,但诸如Tomcat这样的成熟框架提供了更为丰富和必要的功能。
|
6月前
|
前端开发 Java 应用服务中间件
|
6月前
|
XML Java 应用服务中间件
SpringBoot配置外部Tomcat项目启动流程源码分析(长文)
SpringBoot配置外部Tomcat项目启动流程源码分析(长文)
530 0