上一篇我们讲了Tomcat源码包的下载配置以及Tomcat组件的基本介绍,这一篇我们着重来讲述Tomcat的完整启动过程。
众所周知,Tomcat的启动和停止是有startup.sh/bat和shutdown.sh/bat来控制的,这里sh是linux的shellScript脚本,其运行的模式是调用catalina.sh,并传入参数startup或shutdown,具体就不做详述了,而catalina.sh最终会调用java类org.apache.catalina.startup.BootStrap的start或stop方法,我们来看下Bootstrap的start方法:
/**
* Start the Catalina daemon.
*/
public void start()
throws Exception {
if( catalinaDaemon==null ) init();
Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null);
method.invoke(catalinaDaemon, (Object [])null);
}
这里我们可以看到bootstrap的start方法其实是用反射形式调用真正的启动类的start方法,这里的catalinaDamon其实是同一个包下的Catalina类,其start方法如下:
/**
* Start a new server instance.
*/
public void start() {
if (getServer() == null) {
load();
}
if (getServer() == null) {
log.fatal("Cannot start server. Server instance is not configured.");
return;
}
long t1 = System.nanoTime();
// Start the new server
try {
getServer().start();
} catch (LifecycleException e) {
log.error("Catalina.start: ", e);
}
long t2 = System.nanoTime();
if(log.isInfoEnabled())
log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");
try {
// Register shutdown hook
if (useShutdownHook) {
if (shutdownHook == null) {
shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);
// If JULI is being used, disable JULI's shutdown hook since
// shutdown hooks run in parallel and log messages may be lost
// if JULI's hook completes before the CatalinaShutdownHook()
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager) logManager).setUseShutdownHook(
false);
}
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
// This will fail on JDK 1.2. Ignoring, as Tomcat can run
// fine without the shutdown hook.
}
if (await) {
await();
stop();
}
}
代码很长,我们可以看到他使用了getServer,获取到了对应的server组件,server组件的初始化是在init的过程中,对应到本类的load方法,其中核心的代码块是:
// Create and execute our Digester
Digester digester = createStartDigester();
// some code
digester.push(this);
digester.parse(inputSource);
我们可以看到这里使用Digester去初始化一些组件,Digester的实际作用是解析server.xml,用的是SAX包解析,这里本篇先不做详述。
总而言之在解析完server.xml之后,catalina中的server类算是初始化了,然后就调用了server类的startup方法,来看server接口的实现类org.apache.catalina.core.StandardServer:
@Override
protected void startInternal() throws LifecycleException {
fireLifecycleEvent(CONFIGURE_START_EVENT, null);
setState(LifecycleState.STARTING);
globalNamingResources.start();
// Start our defined Services
synchronized (services) {
for (int i = 0; i < services.length; i++) {
services[i].start();
}
}
}
我们可以看到server类的start就是将server.xml中解析出来的service全部启动,但是要注意一点的是,server类还有个非常大的作用,在上述catalina的启动中我们很清楚看到了其调用了await方法:
/**
* Await and shutdown.
*/
public void await() {
getServer().await();
}
而server类的await方法是这样的:
/**
* Wait until a proper shutdown command is received, then return.
* This keeps the main thread alive - the thread pool listening for http
* connections is daemon threads.
*/
@Override
public void 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 ) {
try {
awaitThread = Thread.currentThread();
while(!stopAwait) {
try {
Thread.sleep( 10000 );
} catch( InterruptedException ex ) {
// continue and check the flag
}
}
} finally {
awaitThread = null;
}
return;
}
// Set up a server socket to wait on
try {
awaitSocket = new ServerSocket(port, 1,
InetAddress.getByName(address));
} catch (IOException e) {
log.error("StandardServer.await: create[" + address
+ ":" + port
+ "]: ", e);
return;
}
try {
awaitThread = Thread.currentThread();
// Loop waiting for a connection and a valid command
while (!stopAwait) {
ServerSocket serverSocket = awaitSocket;
if (serverSocket == null) {
break;
}
// Wait for the next connection
Socket socket = null;
StringBuilder command = new StringBuilder();
try {
InputStream stream;
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) {
if (stopAwait) {
// Wait was aborted with socket.close()
break;
}
log.error("StandardServer.await: accept: ", e);
break;
}
// Read a set of characters from the socket
int expected = 1024; // Cut off to avoid DoS attack
while (expected < shutdown.length()) {
if (random == null)
random = new Random();
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--;
}
} finally {
// Close the socket now that we are done with it
try {
if (socket != null) {
socket.close();
}
} catch (IOException e) {
// Ignore
}
}
// Match against our command string
boolean match = command.toString().equals(shutdown);
if (match) {
log.info(sm.getString("standardServer.shutdownViaPort"));
break;
} else
log.warn("StandardServer.await: Invalid command '"
+ command.toString() + "' received");
}
} finally {
ServerSocket serverSocket = awaitSocket;
awaitThread = null;
awaitSocket = null;
// Close the server socket and return
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
// Ignore
}
}
}
}
这段代码曾经将我带入了一个彻底的误区,以为这就是Tomcat接收请求的serverSocket的启动,但是事实完全不是这样子,当我看到port的默认值的时候:
private int port = 8005;
以及在server.xml的配置的时候
<Server port="8005" shutdown="SHUTDOWN">
我才明白过来,众所周知Tomcat启动时候默认占用了三个端口:8080、8005、8009,8080和8009分别是HTTP和AJP协议的服务端口,那么8005是干嘛的呢,其实8005是tomcat停止监听线程的服务端口,看上面await方法就可以看出,这个serverSocket一直在监听shutdown指令,如果接受到了则开始停止过程。
这也说明了上面在catalina的start过程中为何要把awiat方法放在最后,因为我们看到这个停止监听是运行在当前线程(即启动线程)下的,必须确保所有需要开启的服务都在新线程中运行起来再进行await过程。
现在回过头来看service的start过程,我们看到org.apache.catalina.core.StandardService类
@Override
protected void startInternal() throws LifecycleException {
if(log.isInfoEnabled())
log.info(sm.getString("standardService.start.name", this.name));
setState(LifecycleState.STARTING);
// Start our defined Container first
if (container != null) {
synchronized (container) {
container.start();
}
}
synchronized (executors) {
for (Executor executor: executors) {
executor.start();
}
}
// Start our defined Connectors second
synchronized (connectors) {
for (Connector connector: connectors) {
try {
// If it has already failed, don't try and start it
if (connector.getState() != LifecycleState.FAILED) {
connector.start();
}
} catch (Exception e) {
log.error(sm.getString(
"standardService.connector.startFailed",
connector), e);
}
}
}
}
可以看到这个过程的启动有三件,其中目前我比较清楚的是头尾两件,一是容器container的启动,这里的container实则是engine容器;二是connector的启动,Connector是server.xml中注册的,我们先来看Connector的启动过程,看到org.apache.catalina.connector.Connector类:
/**
* Begin processing requests via this Connector.
*
* @exception LifecycleException if a fatal startup error occurs
*/
@Override
protected void startInternal() throws LifecycleException {
setState(LifecycleState.STARTING);
try {
protocolHandler.start();
} catch (Exception e) {
String errPrefix = "";
if(this.service != null) {
errPrefix += "service.getName(): \"" + this.service.getName() + "\"; ";
}
throw new LifecycleException
(errPrefix + " " + sm.getString
("coyoteConnector.protocolHandlerStartFailed"), e);
}
mapperListener.start();
}
这里启动了protocolHandler,根据Connector的实质作用的不同,Tomcat会给他分配不同的Handler,这里我们看到用于处理Http1.1的handler,org.apache.coyote.http11.Http11Protocol类,看到其对应的start,这里的start是在其父类AbstratcProtocolHandler中实现的:
@Override
public void start() throws Exception {
if (getLog().isInfoEnabled())
getLog().info(sm.getString("abstractProtocolHandler.start",
getName()));
try {
endpoint.start();
} catch (Exception ex) {
getLog().error(sm.getString("abstractProtocolHandler.startError",
getName()), ex);
throw ex;
}
}
这里启动了endpoint,看到其子类JIOEndpoint:
@Override
public void startInternal() throws Exception {
if (!running) {
running = true;
paused = false;
// Create worker collection
if (getExecutor() == null) {
createExecutor();
}
initializeConnectionLatch();
// Start acceptor threads
for (int i = 0; i < acceptorThreadCount; i++) {
Thread acceptorThread = new Thread(new Acceptor(),
getName() + "-Acceptor-" + i);
acceptorThread.setPriority(threadPriority);
acceptorThread.setDaemon(getDaemon());
acceptorThread.start();
}
// Start async timeout thread
Thread timeoutThread = new Thread(new AsyncTimeout(),
getName() + "-AsyncTimeout");
timeoutThread.setPriority(threadPriority);
timeoutThread.setDaemon(true);
timeoutThread.start();
}
}
在这里启动了新线程,新线程的操作定义在其内部类Acceptor类中:
/**
* Server socket acceptor thread.
*/
protected class Acceptor implements Runnable {
/**
* The background thread that listens for incoming TCP/IP connections and
* hands them off to an appropriate processor.
*/
@Override
public void run() {
int errorDelay = 0;
// Loop until we receive a shutdown command
while (running) {
// Loop if endpoint is paused
while (paused && running) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// Ignore
}
}
if (!running) {
break;
}
try {
//if we have reached max connections, wait
awaitConnection();
Socket socket = null;
try {
// Accept the next incoming connection from the server
// socket
socket = serverSocketFactory.acceptSocket(serverSocket);
} catch (IOException ioe) {
// Introduce delay if necessary
errorDelay = handleExceptionWithDelay(errorDelay);
// re-throw
throw ioe;
}
// Successful accept, reset the error delay
errorDelay = 0;
// Configure the socket
if (setSocketOptions(socket)) {
// Hand this socket off to an appropriate processor
if (!processSocket(socket)) {
// Close socket right away
try {
socket.close();
} catch (IOException e) {
// Ignore
}
} else {
countUpConnection();
}
} else {
// Close socket right away
try {
socket.close();
} catch (IOException e) {
// Ignore
}
}
} catch (IOException x) {
if (running) {
log.error(sm.getString("endpoint.accept.fail"), x);
}
} catch (NullPointerException npe) {
if (running) {
log.error(sm.getString("endpoint.accept.fail"), npe);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("endpoint.accept.fail"), t);
}
// The processor will recycle itself when it finishes
}
}
}
看到这里,我们长舒了一口气,因为终于看到了处理http请求的监听在8080端口serverSocket运行起来,从代码看这里把接收到的socket通过processSocket方法进行处理,这在之后的请求处理的篇章再做详述,到此为止Tomcat自身类的组件算是启动完成了,但是对于我们开发人员添加的web应用的处理还尚未讲解,我们回到service的start方法中,看到这里container的启动:
@Override
protected void startInternal() throws LifecycleException {
if(log.isInfoEnabled())
log.info(sm.getString("standardService.start.name", this.name));
setState(LifecycleState.STARTING);
// Start our defined Container first
if (container != null) {
synchronized (container) {
container.start();
}
}
}
上一篇我们说过,tomcat的容器有engine、host、context、wrapper四种,其中前两者都是在server.xml中解析出来的,而后两者则需要在运行时候再去读取,我们来看container的基本实现类org.apache.catalina.core.ContainerBase类的start过程:
/**
* Start this component and implement the requirements
* of {@link org.apache.catalina.util.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 {
// Start our subordinate components, if any
if ((loader != null) && (loader instanceof Lifecycle))
((Lifecycle) loader).start();
logger = null;
getLogger();
if ((logger != null) && (logger instanceof Lifecycle))
((Lifecycle) logger).start();
if ((manager != null) && (manager instanceof Lifecycle))
((Lifecycle) manager).start();
if ((cluster != null) && (cluster instanceof Lifecycle))
((Lifecycle) cluster).start();
if ((realm != null) && (realm instanceof Lifecycle))
((Lifecycle) realm).start();
if ((resources != null) && (resources instanceof Lifecycle))
((Lifecycle) resources).start();
// Start our child containers, if any
Container children[] = findChildren();
for (int i = 0; i < children.length; i++) {
children[i].start();
}
// Start the Valves in our pipeline (including the basic), if any
if (pipeline instanceof Lifecycle)
((Lifecycle) pipeline).start();
setState(LifecycleState.STARTING);
// Start our thread
threadStart();
}
可以看到所有的容器都有一个启动子组件的过程,但是重点在于刚开始父组件是如何拿到子组件的呢,其实我个人觉得这里的for循环只是保险起见,万一子组件已经被纳入了父组件中的操作,真正实现启动子组件的是下面的threadStart过程:
/**
* Start the background thread that will periodically check for
* session timeouts.
*/
protected void threadStart() {
if (thread != null)
return;
if (backgroundProcessorDelay <= 0)
return;
threadDone = false;
String threadName = "ContainerBackgroundProcessor[" + toString() + "]";
thread = new Thread(new ContainerBackgroundProcessor(), threadName);
thread.setDaemon(true);
thread.start();
}
这个过程是个非常长的链式调用,大家自己去一个个ctrl点击进去,到最后会发现四个组件都有对应的初始化控制类,在startup包下的HostConfig、ContextConfig等,其实现了lifecycleEvent方法,我们专门来看一下Host组件对应的Config的过程:
@Override
public void lifecycleEvent(LifecycleEvent event) {
if (event.getType().equals(Lifecycle.PERIODIC_EVENT))
check();
// Identify the host we are associated with
try {
host = (Host) event.getLifecycle();
if (host instanceof StandardHost) {
setCopyXML(((StandardHost) host).isCopyXML());
setDeployXML(((StandardHost) host).isDeployXML());
setUnpackWARs(((StandardHost) host).isUnpackWARs());
}
} catch (ClassCastException e) {
log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
return;
}
// Process the event that has occurred
if (event.getType().equals(Lifecycle.START_EVENT))
start();
else if (event.getType().equals(Lifecycle.STOP_EVENT))
stop();
}
这里我们看到,如果tomcat处于启动之初,会调用check方法,check则会调用deployApps方法:
**
* Deploy applications for any directories or WAR files that are found
* in our "application root" directory.
*/
protected void deployApps() {
File appBase = appBase();
File configBase = configBase();
String[] filteredAppPaths = filterAppPaths(appBase.list());
// Deploy XML descriptors from configBase
deployDescriptors(configBase, configBase.list());
// Deploy WARs, and loop if additional descriptors are found
deployWARs(appBase, filteredAppPaths);
// Deploy expanded folders
deployDirectories(appBase, filteredAppPaths);
}
这里就分流处理了,取得webapp文件夹的路径之后,会分别处理war、文件夹等多种web应用的发布,根据经验我们知道war的发布实质是tomcat将其解压成为文件夹应用,所以我们关键看下deployDirectories方法:
/**
* Deploy directories.
*/
protected void deployDirectories(File appBase, String[] files) {
if (files == null)
return;
for (int i = 0; i < files.length; i++) {
if (files[i].equalsIgnoreCase("META-INF"))
continue;
if (files[i].equalsIgnoreCase("WEB-INF"))
continue;
File dir = new File(appBase, files[i]);
if (dir.isDirectory()) {
ContextName cn = new ContextName(files[i]);
if (isServiced(cn.getName()))
continue;
deployDirectory(cn, dir, files[i]);
}
}
}
获取所有文件夹,并进入deployDirectory:
/**
* @param cn
* @param dir
* @param file
*/
protected void deployDirectory(ContextName cn, File dir, String file) {
if (deploymentExists(cn.getName()))
return;
DeployedApplication deployedApp = new DeployedApplication(cn.getName());
// Deploy the application in this directory
if( log.isInfoEnabled() )
log.info(sm.getString("hostConfig.deployDir", file));
try {
Context context = null;
File xml = new File(dir, Constants.ApplicationContextXml);
File xmlCopy = null;
if (deployXML && xml.exists()) {
synchronized (digester) {
try {
context = (Context) digester.parse(xml);
if (context == null) {
log.error(sm.getString(
"hostConfig.deployDescriptor.error",
xml));
return;
}
} finally {
digester.reset();
}
}
if (copyXML) {
xmlCopy = new File(configBase(), file + ".xml");
InputStream is = null;
OutputStream os = null;
try {
is = new FileInputStream(xml);
os = new FileOutputStream(xmlCopy);
IOTools.flow(is, os);
// Don't catch IOE - let the outer try/catch handle it
} finally {
try {
if (is != null) is.close();
} catch (IOException e){
// Ignore
}
try {
if (os != null) os.close();
} catch (IOException e){
// Ignore
}
}
context.setConfigFile(xmlCopy.toURI().toURL());
} else {
context.setConfigFile(xml.toURI().toURL());
}
} else {
context = (Context) Class.forName(contextClass).newInstance();
}
Class<?> clazz = Class.forName(host.getConfigClass());
LifecycleListener listener =
(LifecycleListener) clazz.newInstance();
context.addLifecycleListener(listener);
context.setName(cn.getName());
context.setPath(cn.getPath());
context.setWebappVersion(cn.getVersion());
context.setDocBase(file);
host.addChild(context);
deployedApp.redeployResources.put(dir.getAbsolutePath(),
Long.valueOf(dir.lastModified()));
if (xmlCopy != null) {
deployedApp.redeployResources.put(
xmlCopy.getAbsolutePath(),
Long.valueOf(xmlCopy.lastModified()));
}
addWatchedResources(deployedApp, dir.getAbsolutePath(), context);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("hostConfig.deployDir.error", file), t);
}
deployed.put(cn.getName(), deployedApp);
}
当初我看到这段代码真的是一身爽快,因为终于找到容器的初始化方式了,我们看到这里初始化了StandardContext类对象,初始化完了以后将web应用的根路径等信息全部填入context,并让context去初始化wrapper的内容,中间过程我们省去了,大家自己看,我们直接找到ContextConfig解析过程的核心代码:
protected void parseWebXml(InputSource source, WebXml dest,
boolean fragment) {
if (source == null) return;
XmlErrorHandler handler = new XmlErrorHandler();
// Web digesters and rulesets are shared between contexts but are not
// thread safe. Whilst there should only be one thread at a time
// processing a config, play safe and sync.
Digester digester;
if (fragment) {
digester = webFragmentDigester;
} else {
digester = webDigester;
}
synchronized(digester) {
digester.push(dest);
digester.setErrorHandler(handler);
if(log.isDebugEnabled()) {
log.debug(sm.getString("contextConfig.applicationStart",
source.getSystemId()));
}
try {
digester.parse(source);
if (handler.getWarnings().size() > 0 ||
handler.getErrors().size() > 0) {
ok = false;
handler.logFindings(log, source.getSystemId());
}
} catch (SAXParseException e) {
log.error(sm.getString("contextConfig.applicationParse",
source.getSystemId()), e);
log.error(sm.getString("contextConfig.applicationPosition",
"" + e.getLineNumber(),
"" + e.getColumnNumber()));
ok = false;
} catch (Exception e) {
log.error(sm.getString("contextConfig.applicationParse",
source.getSystemId()), e);
ok = false;
} finally {
digester.reset();
if (fragment) {
webFragmentRuleSet.recycle();
} else {
webRuleSet.recycle();
}
}
}
}
这里还是用degister,只不过这里的degister是用来解析web.xml的,解析完了以后,将每个Servlet类的信息装载在一个Wrapper中,到此为止容器的基本启动算是完成了。
最后还记得connector的start过程中有一个mappingListner的init吗?
@Override
public void startInternal() throws LifecycleException {
setState(LifecycleState.STARTING);
// Find any components that have already been initialized since the
// MBean listener won't be notified as those components will have
// already registered their MBeans
findDefaultHost();
Engine engine = (Engine) connector.getService().getContainer();
addListeners(engine);
Container[] conHosts = engine.findChildren();
for (Container conHost : conHosts) {
Host host = (Host) conHost;
if (!LifecycleState.NEW.equals(host.getState())) {
// Registering the host will register the context and wrappers
registerHost(host);
}
}
}
这个过程就是从connector对应的service中取出容器engine,并注册其子容器,依次注册,之后在请求的处理过程中去调动注册的容器,我们下一篇再讲述。