上一篇我們講了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,並註冊其子容器,依次註冊,之後在請求的處理過程中去調動註冊的容器,我們下一篇再講述。