Tomcat 5.5.26源代码分析——启动过程(二)

上一篇文章主要分析了 Bootstrap main 方法的总体流程,并讨论 了JDK兼容性和启动参数。本篇开始深入细节。

Tomcat 的 运行时视图,简单地看,其实就是一些相互关联的组件。这些组件相互协作,完成一定的任务(比如部署 Web 应用、处理到 HTTP 请求等)。 Tomcat 启动过程中所做的主要工作,也就是创建这些组件,并建立组件之间的关联。当 然,要创建哪些组件,组件之间怎么关联,这是根据配置文件来定制的。

服务器程序的启动过程一般都有“三段式”, Tomcat 也不例外,它的三段式分别是 initloadstart

init 方 法

我们先看看 Bootstrapinit 方法。
public void init() throws Exception

{



// Set Catalina path

setCatalinaHome();

setCatalinaBase();



initClassLoaders();



Thread.currentThread().setContextClassLoader(catalinaLoader);



SecurityClassLoad.securityClassLoad(catalinaLoader);



// Load our startup class and call its process() method

if (log.isDebugEnabled())

log.debug("Loading startup class");

Class startupClass =

catalinaLoader.loadClass

("org.apache.catalina.startup.Catalina");

Object startupInstance = startupClass.newInstance();



// Set the shared extensions class loader

if (log.isDebugEnabled())

log.debug("Setting startup class properties");

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;

}
 
该 方法的主要工作依次是:
  1. 设置CatalinaTomcat Servlet 容器的代号)的路径:CATALINA_HOMECATALINA_BASE
  2. 初 始化Tomcat 的类加载器体系
  3. 创建org.apache.catalina.startup.Catalina 对象(启动阶段剩余的 工作由Catalina类 完成)

Catalina_Home 和Catalina_Base

首先,我们看看这两个路径有何区别。Tomcat的启动脚本已经设置了 CATALINA_HOMECATALINA_BASE 的 值,而且两者的值是相同的,都是Tomcat的根目录。那么为什么还要设置这两个变量呢?

我们可以从 Tomcat 5.5 的配置文档( http://tomcat.apache.org/tomcat-5.5-doc/config/host.html ) 中找到答案:

The description below uses the variable name $CATALINA_HOME to refer to the directory into which you have installed Tomcat 5, and is the base directory against which most relative paths are resolved. However, if you have configured Tomcat 5 for multiple instances by setting a CATALINA_BASE directory, you should use $CATALINA_BASE instead of $CATALINA_HOME for each of these references.

从这段描述可以看出 CATALINA_HOMECATALINA_BASE 的区别。简单的说, CATALINA_HOMETomcat 的安装目 录, CATALINA_BASETomcat 的工作目录。如果我们想要运行 Tomcat 的 多个实例,但是不想安装多个 Tomcat 软件副本。那么我们可以配置多个工作 目录,每个运行实例独占一个工作目录,但是共享同一个安装目录。


Tomcat 每个运行实例需要使用自己的 conflogstempwebappsworkshared 目录,因此 CATALINA_BASE 就 指向这些目录。 而其他目录主要包括了 Tomcat 的二进制文件和脚本, CATALINA_HOME 就指向这些目录。

如果我们希望再运行另一个 Tomcat 实例,那么我们可以建立一个目录,把 conflogstempwebappsworkshared 拷贝 到该目录下,然后让 CATALINA_BASE 指向该目录即可。

下 面,我们看看 Bootstrap 是如何设置 CATALINA_HOME和CATALINA_BASE。
private void setCatalinaHome() {

if (System.getProperty("catalina.home") != null)

return;

File bootstrapJar =

new File(System.getProperty("user.dir"), "bootstrap.jar");

if (bootstrapJar.exists()) {

try {

System.setProperty

("catalina.home",

(new File(System.getProperty("user.dir"), ".."))

.getCanonicalPath());

} catch (Exception e) {

// Ignore

System.setProperty("catalina.home",

System.getProperty("user.dir"));

}

} else {

System.setProperty("catalina.home",

System.getProperty("user.dir"));

}

}
 
CATALINA_HOME 保 存在系统变量 catalina.home 中。 setCatalinaHome 方法首先检查 catalina.home 系 统变量是否设置。如果已经设置,则直接返回;否则,就检查 Tomcat 的启动 目录(系统变量user.dir)。如果启动目录是bin,那么启动目录下就存在 bootstrap.jar故CATALINA_HOME 就是 bin 的 上级目录;如果启动目录下没有 bootstrap.jar ,那么就假定启动目录就是 CATALINA_HOME
    private void setCatalinaBase() {

if (System.getProperty("catalina.base") != null)

return;

if (System.getProperty("catalina.home") != null)

System.setProperty("catalina.base",

System.getProperty("catalina.home"));

else

System.setProperty("catalina.base",

System.getProperty("user.dir"));

}
 
CATALINA_BASE 保存在系统变量 catalina.base 中。 setCatalinaBase 方 法首先检查 catalina.base 系统变量是否设置,如果已经设置,就直接返回。 否则,就检查 catalina.home 系统变量是否设置。如果已经设置,则以 CATALINA_HOME 作为 CATALINA_BASE 。 否则,就以 Tomcat 的启动目录(系统变量 user.dir )作为 CATALINA_BASE

catalina.bat 中已经设置了 catalin.homecatalina.base 的值,详见下面代码:
%_EXECJAVA% %JAVA_OPTS% %CATALINA_OPTS% %DEBUG_OPTS% -Djava.endorsed.dirs="%JAVA_ENDORSED_DIRS%" -classpath "%CLASSPATH%" -Dcatalina.base="%CATALINA_BASE%" -Dcatalina.home="%CATALINA_HOME%" -Djava.io.tmpdir="%CATALINA_TMPDIR%" %MAINCLASS% %CMD_LINE_ARGS% %ACTION%
 
我们可以通过修改 catalina.batCATALINA_HOMECATALINA_BASE 的值 ,来设置 catalina.homecatalina.base 这两个系统变量。

初始化类加载器体系

initClassLoaders() 很 显然是初始化类加载器。在看代码之前,我们先看看 Tomcat 的类加载器体 系。
Tomcat 的类加载器体系
很多服务器程序( TomcatJBossGlassFishGeronimo 等), 都会有自己的类加载器体系。这主要是为了要把开发者编写的各种应用( WAREAR 等)部署到容器中,并实现应用之间的隔离。

Tomcat 也实现了自己的类加载器体系。这个在 Tomcat 的官方文档中有详细介绍,详见 http://tomcat.apache.org/tomcat-5.5-doc/class-loader-howto.html 。 这里做点简单介绍。

Tomcat 的类加载器体系如下图所示:

     Bootstrap
         |
       System
         |
       Common
        / /
    Catalina Shared
              / /
        Webapp1 Webapp2 ...


BootStrap 就是 JVM 的启动类 加载器,负责加载 Java 核心类库和系统扩展类库( %JAVA_HOME%/jre/lib/ext 下的 jar 文件)。有的JVM实现提供了两个类加载器,分别加载核心类库和系统扩展类库。我们这里仍用一个 Bootstrap 类加载器表示,不影响理解。

System 就是JVM的系统类加载器,负责加载 CLASSPATH 下的 jar 文件,这些文件包 括:
  1. %CATALINA_HOME%/bin/bootstrap.jar
  2. %JAVA_HOME%/lib/tools.jar
  3. %CATALINA_HOME%/bin/commons-logging-api-x.y.z.jar
  4. %CATALINA_HOME%/bin/tomcat-juli.jar
  5. %CATALINA_HOME%/bin/tomcat-daemon.jar
  6. %CATALINA_HOME%/bin/jmx.jar (即之前提到的JDK 1.4 兼容包)

其中, 12 是在 catalina.bat 中指定的, 3-6 是 在 bootstrap.jarMETA-INF/MANIFEST.MF 文 件中指定的。

Common 就是公共类加载器,负责加载 Tomcat 内部和 Web 应 用程序都可以看到的类。 %CATALINA_HOME%/conf/catalina.properties 文 件中指定了这些类:

common.loader=${catalina.home}/common/classes,${catalina.home}/common/i18n/*.jar,${catalina.home}/common/endorsed/*.jar,${catalina.home}/common/lib/*.jar

可 见, Common 加载的是 %CATALINA_HOME%/common 目 录下的 jar 文件。

Catalina 负责加载Tomcat 内 部使用的类,这些类对于Web 应用程序不可见。同样,%CATALINA_HOME%/conf/catalina.properties 文件中指定了 这些类:

server.loader=${catalina.home}/server/classes,${catalina.home}/server/lib/*.jar

可 见,Catalina 加载的是%CATALINA_HOME%/server 目录下的jar 文 件。

Shared 负责加载 在Web应用程序 之间共享的类,这些类对于Tomcat 内部是不可见的。同样,%CATALINA_HOME%/conf/catalina.properties 文件中指定了 这些类:

shared.loader=${catalina.base}/shared/classes,${catalina.base}/shared/lib/*.jar

可 见,Catalina 加载的是% CATALINA_BASE %/shared 目录下的jar 文 件。注意,这里是CATALINA_BASE shared 属于Tomcat 的 工作目录。

Webapp 负责加载Web 应用程序的类,这些类只对本Web 应用程序可见。很显然,每个Web 应用程 序都有一个独立的Webapp 类加载器。它们加载的类包括WAR 包中/WEB-INF/classes/WEB-INF/lib 目录下的类。

需要注意的是,Webapp 并没有遵循类加载器委派模型。Webapp 优先从自己的搜索路径中加载类,而不是委派给父亲Shared 。 这样做的原因应该是保证Web 应用程序优先使用自己的类。但是,System 负责加载的类不应该被覆盖,因此,Webapp 会首先委派给System , 然后自己加载,接着才会委派给父亲Shared 。这个可以参考org.apache.catalina.loader.WebappClassLoaderloadClass 方法。

了解Tomcat 类加载器体系之后,我们来看看initClassLoaders 方 法的代码。

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) {

log.error("Class loader creation threw exception", t);

System.exit(1);

}

}

 

可见,该方法以通过createClassLoader 依 次创建了commonservershared 类加载器。这三者就 是前面提到的CommonCatalinaShared 类加载器。如果%CATALINA_HOME%/conf/catalina.properties 中没有指定Common 的搜索路径,那么就是用当前类的类加载器——系统类加载器作为Common

下面,我们看看createClassLoader 的代码:

private ClassLoader createClassLoader(String name, ClassLoader parent)

throws Exception {

// CatalinaProperties类读取并封装了catalina.properties中的配置信息

String value = CatalinaProperties.getProperty(name + ".loader");

if ((value == null) || (value.equals("")))

return parent;



// 解析catalina.properties中配置的类搜索路径

// 将类路径中的${catalina.home}替换成CATALINA_HOME的值,将${catalina.base}替换成CATALINA_BASE的值

ArrayList repositoryLocations = new ArrayList();

ArrayList repositoryTypes = new ArrayList();

int i;



StringTokenizer tokenizer = new StringTokenizer(value, ",");

while (tokenizer.hasMoreElements()) {

String repository = tokenizer.nextToken();



// Local repository

boolean replace = false;

String before = repository;

while ((i=repository.indexOf(CATALINA_HOME_TOKEN))>=0) {

replace=true;

if (i>0) {

repository = repository.substring(0,i) + getCatalinaHome()

+ repository.substring(i+CATALINA_HOME_TOKEN.length());

} else {

repository = getCatalinaHome()

+ repository.substring(CATALINA_HOME_TOKEN.length());

}

}

while ((i=repository.indexOf(CATALINA_BASE_TOKEN))>=0) {

replace=true;

if (i>0) {

repository = repository.substring(0,i) + getCatalinaBase()

+ repository.substring(i+CATALINA_BASE_TOKEN.length());

} else {

repository = getCatalinaBase()

+ repository.substring(CATALINA_BASE_TOKEN.length());

}

}

if (replace && log.isDebugEnabled())

log.debug("Expanded " + before + " to " + replace);



// 区分四种类型的路径

// Check for a JAR URL repository

try {

URL url=new URL(repository);

repositoryLocations.add(repository);

repositoryTypes.add(ClassLoaderFactory.IS_URL);

continue;

} catch (MalformedURLException e) {

// Ignore

}



if (repository.endsWith("*.jar")) {

repository = repository.substring

(0, repository.length() - "*.jar".length());

repositoryLocations.add(repository);

repositoryTypes.add(ClassLoaderFactory.IS_GLOB);

} else if (repository.endsWith(".jar")) {

repositoryLocations.add(repository);

repositoryTypes.add(ClassLoaderFactory.IS_JAR);

} else {

repositoryLocations.add(repository);

repositoryTypes.add(ClassLoaderFactory.IS_DIR);

}

}



String[] locations = (String[]) repositoryLocations.toArray(new String[0]);

Integer[] types = (Integer[]) repositoryTypes.toArray(new Integer[0]);



// 创建类加载器对象

ClassLoader classLoader = ClassLoaderFactory.createClassLoader

(locations, types, parent);



// 类加载器被注册成MBean

// Retrieving MBean server

MBeanServer mBeanServer = null;

if (MBeanServerFactory.findMBeanServer(null).size() > 0) {

mBeanServer =

(MBeanServer) MBeanServerFactory.findMBeanServer(null).get(0);

} else {

mBeanServer = MBeanServerFactory.createMBeanServer();

}



// Register the server classloader

ObjectName objectName =

new ObjectName("Catalina:type=ServerClassLoader,name=" + name);

mBeanServer.registerMBean(classLoader, objectName);



return classLoader;

}

 

创建类加载器,必须要知道类搜索路径是什么。前面提到,Tomcat 各类加载器的类搜索路径都定义在%CATALINA_HOME%/conf/catalina.properties 中。该配置文件的主要内容如下:

common.loader=${catalina.home}/common/classes,${catalina.home}/common/i18n/*.jar,${catalina.home}/common/endorsed/*.jar,${catalina.home}/common/lib/*.jar

server.loader=${catalina.home}/server/classes,${catalina.home}/server/lib/*.jar

shared.loader=${catalina.base}/shared/classes,${catalina.base}/shared/lib/*.jar

 

配 置文件内容的读取是由CatalinaProperties类 完成的。另外,CatalinaProperties 类还提供了额外的功能:

  1. 配置文件 的位置可以通过系统变量catalina.config 进行自定义
  2. 如 果没有定义配置文件,或配置文件不存在,那么将使用bootstrap.jar 中的org/apache/catalina/startup/catalina.properties

CatalinaProperties 在最大程度上保证了配置文件总是存在的。

上 述配置的${catalina.home}${catalina.base} 还需要解析,替换成真实的值。例如,将${catalina.home}/common/classes 替 换成D:/ProgramFiles/apache-tomcat-5.5.26/common/classes

类 搜搜路径被分为四种:URL*.jarjar 和目录。ClassLoaderFactory 类在创建类加载器时,会分别处理这四种类型,具体就不细看 了。

类加载器创建之后,将被注册成MBeanObjectNameCatalina:type=ServerClassLoader,name= XXX ,其中XXX 表示类加 载器的名称(即commoncatalinashared )。这样,我们 就可以JMX 观察每个类加载器的信息,比如加载了哪些类。

两 个疑问

至此,类加载器体系基本建立。但是,init 方法又做了两件事件:

  1. Catalina 类加载器被设置成线程的上下文类加载器。
  2. SecurityClassLoad 类,使用Catalian 类 加载器预先加载了一些类


1 件事情不足 为奇,SPI 机制经常依赖上下文类加载器。但是,Tomcat 哪里使用了上下文类加载器,还没有找到。

2 件事情就想不通了,虽然代码注释告诉我们,这是为了在启用安全管理器的情况下避免defineClassInPackage 权限错误。但是,我没有理解。

望各位 不吝赐教!

Catalina 对象

init 方法接下来的工作就很简单了:

  1. 通过反射机制,创建一个org.apache.catalina.startup.Catalina 对象
  2. 再 通过反射机制,调用该Catalina 对象的setParentClassLoader 方法,将Shared 类 加载器设置成其parentClassLoader


也 许你会奇怪,parentClassLoader 是什么用呢?其实这还是和前面提到的Webapp 类加载器有关。Catalina 对象的parentClassLoader 其实是Webapp 的 父亲,即Shared 类加载器。

我们知道,initClassLoader 方法指定了CommonCatalinaShared 之间的父子关系,那么谁来指定SharedWebapp 之间的父子关系呢? 显然应该是Webapp 的创建者。Webapp 的创建者是org.apache.catalina.loader.Webapploader 对 象,它从Catalina 对象获取Webapp 的父亲类加载器。

另外,关于Catalina 对 象的另一个话题就是:BootstrapCatalina 之间的关系。

仔细阅读Bootstrap 类 的代码,发现init 之后的工作(loadstartstop 等), 都是委派给Catalina 类的同名方法。也就是说,后续的启动和停止,都是Catalina 类完成的。

那么,Tomcat 的启动过程为什么要放在BootstrapCatalina 两个类中呢?而且,Bootstrap 还 是通过反射机制调用Catalina ,感觉上是在走弯路。

《How Tomcat Works》17 章的开头提到这个问题。

This chapter focuses on Tomcat startup using two classes in the org.apache.catalina.startup package, Catalina and Bootstrap. The Catalina class is used to start and stop a Server object as well as parse the Tomcat configuration file, server.xml. The Bootstrap class is the entry point that creates an instance of Catalina and calls its process method. In theory, these two classes could have been merged. However, to support more than one mode of running Tomcat, a number of bootstrap classes are provided. For example, the aforementioned Bootstrap class is used for running Tomcat as a stand-alone application. Another class, org.apache.catalina.startup.BootstrapService, is used to run Tomcat as a Windows NT service.

理论上,这两个类是可以合并的。但是,为了支持以多种方式 启动,Tomcat 将启动的核心逻辑(即Catalina类)和不同启动方式 (比如 Bootstrap类)分开,并且通过提供多种Bootstrap来实现不同的启动方式。

目前,Tomcat 共支持三种启动方式:

  1. 作为独立的程序,从命令行启动
  2. 作为嵌入式程序,从其他进程中启动
  3. 作为Windows 服务,自动启动


Tomcat 5.5 中:

  1. 第一种启动方式就是由Bootstrap类和Catalina类实现
  2. 第二种启动 方式是由org.apache.catalina.startup.Embedded 类 实现的(其实Catalina 类就是Embedded的子类)
  3. 第三 种启动方式仍然由Bootstrap类和Catalina类实现


不 过在Tomcat 4.x 中,第三种启动方式由专门的Bootstrap Service类和Catalina Service 类来实现的。

可见,Tomcat 的发展历史中,还是体现了“BootstrapCatalina 分 离,Bootstrap 封装不同启动方式”的策略。但是,现在的代码看,这个分离已经 不是很清楚。

OKinit 方法的讨论到此结束。下面我们看看loadstart 方法。Bootstrap 的 这两个方法,其实都是直接调用Catalina 类的同名方法,因此,我们主要分析的其 实是Catalina 类的相关代码。

load 方法

 private void load(String[] arguments)

throws Exception {

// Call the load() method

String methodName = "load";

Object param[];

Class paramTypes[];

if (arguments==null || arguments.length==0) {

paramTypes = null;

param = null;

} else {

paramTypes = new Class[1];

paramTypes[0] = arguments.getClass();

param = new Object[1];

param[0] = arguments;

}

Method method =

catalinaDaemon.getClass().getMethod(methodName, paramTypes);

if (log.isDebugEnabled())

log.debug("Calling startup class " + method);

method.invoke(catalinaDaemon, param);

}

 

可以看出,Catalina的load方法有两个版本:有参数和无参 数。有参数版本,首先调用了arguments方法来处理参数。如果处理成功,则再调用无参数版本。因此,load的核心逻辑在无参数版本中。

    public void load(String args[]) {



try {

if (arguments(args))

load();

} catch (Exception e) {

e.printStackTrace(System.out);

}

}

 

无参数版本的load方法做的工作,主要是根据Tomcat的配置文件,创建各个组件,并建立组件之间的关联。比如,创建核心的Server 组件、Service 组 件、Manager 组件、 Loader 组 件等。 这个细节我们等会讨论,首先简单看看arguments 方法。arguments 方法对理解启动过程不是很关键,如果不感兴趣可以跳过。

Catalina 类的命令行参数

Catalina.bat 脚本的参数,其实是直接传递到Catalina 类的。arguments 方 法正是处理这些参数的。

Catalina 类可以识别的参数包括:

  1. -config {pathname} 设置配置文件的路径。如果是相对路径,则是相对于CATALINA_HOME 。默认值是conf/server.xml
  2. -noaming 不启用命名服务
  3. -help 打印帮助信息
  4. -start 启动当前的Catalina 实 例
  5. -stop 停止当前的Catalina 实例


arguments 的代码比较简单。

 protected boolean arguments(String args[]) {



boolean isConfig = false;



if (args.length < 1) {

usage();

return (false);

}



for (int i = 0; i < args.length; i++) {

if (isConfig) {

configFile = args[i];

isConfig = false;

} else if (args[i].equals("-config")) {

isConfig = true;

} else if (args[i].equals("-nonaming")) {

setUseNaming( false );

} else if (args[i].equals("-help")) {

usage();

return (false);

} else if (args[i].equals("start")) {

starting = true;

stopping = false;

} else if (args[i].equals("stop")) {

starting = false;

stopping = true;

} else {

usage();

return (false);

}

}



return (true);

}
 


Catalina 类的成员变量startingstopping 分别表示要启动和停止Tomcat

加 载过程

下面我们看看load方法的代码。 

 public void load() {



// 初始化CATALINA_HOME、CATALINA_BASE和临时目录,该方法在Embedded类中。

initDirs();



// 初始化命名服务的基本配置,包括java.naming.factory.url.pkgs和java.naming.factory.initial

// java.naming.factory.url.pkgs的默认值为 org.apache.naming

// java.naming.factory.initial 的默认值为org.apache.naming.java.javaURLContextFactory



// Before digester - it may be needed

initNaming();



// 创建Digester对象。该对象被用来解析配置文件(默认为conf/server.xml)

// Create and execute our Digester

Digester digester = createStartDigester();

long t1 = System.currentTimeMillis();



Exception ex = null;

InputSource inputSource = null;

InputStream inputStream = null;

File file = null;

try {

// 配置文件,由命令行参数-config指定,否则取默认值conf/server.xml

file = configFile();

inputStream = new FileInputStream(file);

inputSource = new InputSource("file://" + file.getAbsolutePath());

} catch (Exception e) {

;

}



// 如果配置文件不存在,则在类路径中加载

if (inputStream == null) {

try {

inputStream = getClass().getClassLoader()

.getResourceAsStream(getConfigFile());

inputSource = new InputSource

(getClass().getClassLoader()

.getResource(getConfigFile()).toString());

} catch (Exception e) {

;

}

}



// 如果类路径中也找不到,则加载server-embed.xml

// This should be included in catalina.jar

// Alternative: don't bother with xml, just create it manually.

if( inputStream==null ) {

try {

inputStream = getClass().getClassLoader()

.getResourceAsStream("server-embed.xml");

inputSource = new InputSource

(getClass().getClassLoader()

.getResource("server-embed.xml").toString());

} catch (Exception e) {

;

}

}





// 如果没能加载配置文件,则报错,中断启动

if ((inputStream == null) && (file != null)) {

log.warn("Can't load server.xml from " + file.getAbsolutePath());

return;

}



// 使用Digester对象解析配置文件,解析的过程中会创建各种组件,包括Server组件。

try {

inputSource.setByteStream(inputStream);

digester.push(this);

digester.parse(inputSource);

inputStream.close();

} catch (Exception e) {

log.warn("Catalina.start using "

+ getConfigFile() + ": " , e);

return;

}



// 将系统标准输出(System.out)和系统错误输出(System.err)重定向到定制的SystemLogHandler。

// SystemLogHandler可以将每个线程的输出隔离到不同的输出流中。

// Stream redirection

initStreams();



// 初始化Server组件。成员变量server就代表Server组件

// Start the new server

if (server instanceof Lifecycle) {

try {

server.initialize();

} catch (LifecycleException e) {

log.error("Catalina.start", e);

}

}



long t2 = System.currentTimeMillis();

if(log.isInfoEnabled())

log.info("Initialization processed in " + (t2 - t1) + " ms");



}

 

Catalina 加载的主要流程参见上述代码中的中文注释,应该比较清楚,这里就不一一赘述了。

需 要注意的是Digester ,它是Apache 基金会的另一个项目,主要负责解析XML 并 执行一定的操作。其主要原理是,为XML 的每个元素配置特定的规则,规则描述 了Digester 遇到该元素时需要执行的操作。

Tomcat 使用Digester 来 解析配置文件(默认是conf/server.xml ),并根据配置创建各种组件,并 建立组件之间的关联。创建和关联,都是通过自定义的规则来实现的。我们以conf/server.xml的部分内容和部分规则为例,解释一下 Digester的原理。

配置文件的部分内容:

<?xml version="1.0" encoding="UTF-8"?>

<Server port="8005" shutdown="SHUTDOWN">



<Listener className="org.apache.catalina.mbeans.ServerLifecycleListener" />

<Service name="Catalina">

<Connector port="8080" maxHttpHeaderSize="8192"

maxThreads="150" minSpareThreads="25" maxSpareThreads="75"

enableLookups="false" redirectPort="8443" acceptCount="100"

connectionTimeout="20000" disableUploadTimeout="true" />

<Engine name="Catalina" defaultHost="localhost">

<Host name="localhost" appBase="webapps"

unpackWARs="true" autoDeploy="true"

xmlValidation="false" xmlNamespaceAware="false">

...

</Host>

</Engine>

</Service>

</Server>
 


其中, 每个XML元素代表一个组件对象,元素中属性对应了组件的成员变量。例如,<Server>就代表Server组件,该组件对象有两个成员变 量port和shutdown。不难猜到,它们其实是之前提到的SHUTDOWN端口和SHUTDOWN命令。

Server组件包含了 Listener对象和Service组件。Service组件又包含了Connetor组件和Engine组件。Enging组件又包含了Host组 件。Host组件也包含了...这就是组件之间的关联。

至于这些组件的作用是什么,关联关系为什么是这样的,我们会在后面的文章中看到。

下 面我们在看看Digester规则。规则以方法调用的定义。Tomcat启动相关的规则定义在createStartDigester方法中,部分代码如 下:

 protected Digester createStartDigester() {



// ...



// Configure the actions we will be using



// 创建Server组件,即StandardServer对象

digester.addObjectCreate("Server",

"org.apache.catalina.core.StandardServer",

"className");

// 设置StandardServer的成员变量port和shutdown

digester.addSetProperties("Server");

// 建立Catalina对象和StandardServer对象之间的关联,前者包含后者

digester.addSetNext("Server",

"setServer",

"org.apache.catalina.Server");



//...

// 创建Listener对象,类由className属性决定

digester.addObjectCreate("Server/Listener",

null, // MUST be specified in the element

"className");

// 设置Listener对象的成员变量

digester.addSetProperties("Server/Listener");

// 建立StandardServer对象和Listener对象之间的关联,前者包含后者

digester.addSetNext("Server/Listener",

"addLifecycleListener",

"org.apache.catalina.LifecycleListener");



// 创建Service组件,即StandardService对象

digester.addObjectCreate("Server/Service",

"org.apache.catalina.core.StandardService",

"className");

// 设置StandardService的成员变量 name

digester.addSetProperties("Server/Service");

// 建立StandardServer对象和StandardService对象之间的关联,前者包含后者

digester.addSetNext("Server/Service",

"addService",

"org.apache.catalina.Service");



// ...

return (digester);



}

 

概括地 说,addObjectCreate 表示创建对象,addSetProperties 表 示设置对象的属性,addSetNext 表示设置对象的包含对象。这都是 Digester的常用规则。当然,Tomcat 也定制了一些规则,以执行更 加复杂的操作。

OK,load方法就算看完了。总之,该方法结束后,Tomcat的运行时视图已经被建立,各大组件及关联关系均以建立。 下一步,就是启动Catalina了。

start 方法

public void start()

throws Exception {

if( catalinaDaemon==null ) init();



Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null);

method.invoke(catalinaDaemon, (Object [])null);

}

 

该方法其实仅仅是调用了Catalina 类的start 方法,因此我们重点看Catalina 类的start 方法。

 public void start() {



//确保load方法已经被调用。load方法会创建StandardServer 实例,并赋值给成员变量server。

if (server == null) {

load();

}



long t1 = System.currentTimeMillis();



//调用server的生命周期方法start

// Start the new server

if (server instanceof Lifecycle) {

try {

((Lifecycle) server).start();

} catch (LifecycleException e) {

log.error("Catalina.start: ", e);

}

}



long t2 = System.currentTimeMillis();

if(log.isInfoEnabled())

log.info("Server startup in " + (t2 - t1) + " ms");



//注册JVM的shutdown钩子

try {

// Register shutdown hook

if (useShutdownHook) {

if (shutdownHook == null) {

shutdownHook = new CatalinaShutdownHook();

}

Runtime.getRuntime().addShutdownHook(shutdownHook);

}

} catch (Throwable t) {

// This will fail on JDK 1.2. Ignoring, as Tomcat can run

// fine without the shutdown hook.

}



//如果await设置成true,则进入await状态

if (await) {

await();

//退出await状态后,就停止Tomcat

stop();

}

}

 

start 方法执行之前,需要确保load 方 法已经执行。如果load 方法已经执行,那么成员变量server 肯定被赋值。因此,start 方 法首先判断成员变量 server 是 否为null ,如果是,则调用load 方 法。

接着,调用server 的start方法,启动 Catalina。启动阶段的工作和加载阶段还是不同的。例如,Connector组件负责网络通信,加载阶段只是创建组件对象,启动阶段才会监听端口。

然 后,Catalina 的主线程会进入await状态,如果成员变量await被设置成 true的话。我在《Tomcat 5.5.26 源代码分析——启动过程(一)》中也提到,如果await被设置成true,那么Tomcat的主线程将监听SHUTDOWN 端口,等待SHUTDOWN 命 令,从而我们可以在Tomcat进程外部通过网络停止Tomcat的运行。Tomcat收到SHUTDOWN命令之后,主线程就会退出await状 态,await方法也执行结束, 从而stop方法被调用,Tomcat停止运行。

这里我们需要注意一下server instanceof Lifecycle 的代码。Lifecycle是一个生命周期接口,定义了各种生命周期方法start和 stop。只有实现了该接口的组件才能拥有生命周期方法。生命周期方法的调用是嵌套的,父组件的生命周期方法会调用子组件的同名方法。这样,只需调用顶层 组件的start方法,就可以启动所有子孙组件。stop也一样。因此,生命周期方法和组件关联关系,使得Tomcat 很容易管理各个组件 的启动和停止。

OK,start方法也介绍完了。各组件的start方法被调用之后,Tomcat已经处于就绪状态,等待请求的到来。

本 文的最后,详细讨论一下await状态的实现细节。

await 状态

setAwait方法

Bootstrap类的main方法中,处理start启动参数时,会调用 setAwait方法。

 public void setAwait(boolean await)

throws Exception {

Class paramTypes[] = new Class[1];

paramTypes[0] = Boolean.TYPE;

Object paramValues[] = new Object[1];

paramValues[0] = new Boolean(await);

Method method =

catalinaDaemon.getClass().getMethod("setAwait", paramTypes);

method.invoke(catalinaDaemon, paramValues);

}


上 述代码其实调用了Catalina 类的setAwait 方法。CatalinasetAwait 方法定义在于其父类Embedded主要就是设置成员变量await的值

    public void setAwait(boolean b) {

await = b;

}

 

await变成true之后,Tomcat就进入await状态,这一点在start方法中已经分析 过。

await方法

如果成员变量await为true,那么Catalina类的await方法就会被调用。

    public void await() {



//直接调用StandardServer的await方法

server.await();

}

 

下面我们看看StandardServer类的await方法。

 public void await() {

// port是SHUTDOWN端口。如果值为-2,则表示不进入await状态,直接返回;如果值为-1,则表示通过简单循环的方式来实现await状态。

// 如果port为-2,则表示不进入await状态,直接返回;如果值为-1,则表示通过简单循环的方式来实现 await状态。

// 如果port为-1,则表示通过简单循环的方式来实现 await状态。此方法适合嵌入式Tomcat。

// 如果port为其他值,则表示通过监听网络端口的方式来实现 await状态。



// Negative values - don't wait on port - tomcat is embedded or we just don't like ports

if( port == -2 ) {

// undocumented yet - for embedding apps that are around, alive.

return;

}

if( port==-1 ) {

while( true ) {

try {

Thread.sleep( 100000 );

} catch( InterruptedException ex ) {

}

if( stopAwait ) return;

}

}



// 创建SHUTDOWN端口的ServerSocket

// Set up a server socket to wait on

ServerSocket serverSocket = null;

try {

serverSocket =

new ServerSocket(port, 1,

InetAddress.getByName("127.0.0.1"));

} catch (IOException e) {

log.error("StandardServer.await: create[" + port

+ "]: ", e);

System.exit(1);

}



// 在SHUTDOWN端口上等待SHUTDOWN命令

// Loop waiting for a connection and a valid command

while (true) {



// Wait for the next connection

Socket socket = null;

InputStream stream = null;

try {

// 接收到一个网络连接请求

socket = serverSocket.accept();

socket.setSoTimeout(10 * 1000); // Ten seconds

stream = socket.getInputStream();

} catch (AccessControlException ace) {

log.warn("StandardServer.accept security exception: "

+ ace.getMessage(), ace);

continue;

} catch (IOException e) {

log.error("StandardServer.await: accept: ", e);



// 直接退出JVM,是不是有点过分?

System.exit(1);

}



// 读取客户端的请求

// 这里为了避免Dos攻击,对读取的数据长度做了限制。不过这个限制还是很宽松。

// Read a set of characters from the socket

StringBuffer command = new StringBuffer();

int expected = 1024; // Cut off to avoid DoS attack

while (expected < shutdown.length()) {

if (random == null)

random = new Random(System.currentTimeMillis());

expected += (random.nextInt() % 1024);

}

while (expected > 0) {

int ch = -1;

try {

ch = stream.read();

} catch (IOException e) {

log.warn("StandardServer.await: read: ", e);

ch = -1;

}

if (ch < 32) // Control character or EOF terminates loop

break;

command.append((char) ch);

expected--;

}



// Close the socket now that we are done with it

try {

socket.close();

} catch (IOException e) {

;

}



// 判断客户端的请求是不是 SHUTDOWN命令,如果是就跳出循环

// Match against our command string

boolean match = command.toString().equals(shutdown);

if (match) {

break;

} else

log.warn("StandardServer.await: Invalid command '" +

command.toString() + "' received");



}



// 关闭ServerSocket,退出await状态

// Close the server socket and return

try {

serverSocket.close();

} catch (IOException e) {

;

}

}


该方法的逻辑比较简单,参考中文注释即可。有两点需要注意:

  1. 防 Dos攻击的简单机制。主要是防止客户端缓慢地、一秒一个字节地发送大量数据。
  2. SHUTDOWN端口和SHUTDOWN命令可以在 conf/server.xml中配置

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值