Tomcat7启动分析(一)启动脚本
在之前的环境搭建那篇文章里是通过直接运行BootStarp的main函数来启动的,只是加了一个catalina.home的系统属性。而正常情况下启动Tomcat是通过运行脚本的方式,这个就涉及到建立工程时拷贝过来的script目录下的一堆脚本文件了。
以我的windows系统举例,实际上最终是执行startup.bat这个批处理文件来启动tomcat的。
那么启动分析就冲这个文件开始吧:
一、分析startup.bat文件
1@echooff 2rem Licensed to theApache Software Foundation (ASF) under one or more 3rem contributor license agreements. See the NOTICE file distributed with 4rem this work foradditional information regarding copyright ownership. 5rem The ASF licenses this file to You underthe Apache License, Version 2.0 6rem (the "License"); you may not use this file except incompliance with 7rem the License. You may obtain acopy of the License at 8rem 9rem http://www.apache.org/licenses/LICENSE-2.010rem11rem Unless required by applicable law or agreed to in writing, software12rem distributed underthe License is distributed on an "AS IS" BASIS,13rem WITHOUTWARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.14rem See the Licensefor the specific language governing permissions and15rem limitations under the License.1617if "%OS%" == "Windows_NT"setlocal18rem ---------------------------------------------------------------------------19rem Start script forthe CATALINA Server20rem21rem $Id: startup.bat 895392 2010-01-03 14:02:31Z kkolinko $22rem---------------------------------------------------------------------------2324rem Guess CATALINA_HOME if not defined25set "CURRENT_DIR=%cd%"26ifnot "%CATALINA_HOME%" =="" goto gotHome27set "CATALINA_HOME=%CURRENT_DIR%"28ifexist"%CATALINA_HOME%/bin/catalina.bat"gotookHome29cd ..30set "CATALINA_HOME=%cd%"31cd "%CURRENT_DIR%"32:gotHome33ifexist"%CATALINA_HOME%/bin/catalina.bat"gotookHome34echo The CATALINA_HOME environment variable isnot defined correctly35echo This environment variable is needed torun this program36gotoend37:okHome3839set "EXECUTABLE=%CATALINA_HOME%/bin/catalina.bat"4041rem Check that target executable exists42ifexist "%EXECUTABLE%" goto okExec43echo Cannotfind"%EXECUTABLE%"44echo This file is needed torun this program45gotoend46:okExec4748rem Get remaining unshifted command line arguments and save them in the49set CMD_LINE_ARGS=50:setArgs51if ""%1""==""""goto doneSetArgs52set CMD_LINE_ARGS=%CMD_LINE_ARGS%%153shift54goto setArgs55:doneSetArgs5657call "%EXECUTABLE%"start %CMD_LINE_ARGS%5859:end
第1行@echo off的是关闭显示命令行自身的意思,接下来一大堆rem是注释,第17行if"%OS%" == "Windows_NT" setlocal是要临时修改系统变量,方便批处理运行。第26到33行的作用是设置CATALINA_HOME环境变量,如果操作系统中没设置,则将当前运行脚本的目录作为该环境变量的值,最终都会跳转到37行。39到46行验证CATALINA_HOME变量所示目录下的catalina.bat文件是否存在,不存在则批处理直接结束。48到55行将运行脚本前如果设置了其他参数,将参数保存到CMD_LINE_ARGS变量中。最后第57行执行catalina.bat批处理文件,注意,该行后面跟着两个参数,第1个参数是start。
二、分析catalina.bat文件
1@echooff 2rem Licensed to the Apache Software Foundation (ASF) under one or more 3rem contributor license agreements. See the NOTICE file distributed with 4rem this work for additional information regarding copyright ownership. 5rem The ASF licenses this file to You under the Apache License, Version2.0 6rem (the "License"); you may not use this file except in compliancewith 7rem the License. You may obtain acopy of the License at 8rem 9rem http://www.apache.org/licenses/LICENSE-2.0 10rem11rem Unless required by applicable law or agreed to inwriting, software 12rem distributed under the License is distributed on an "AS IS"BASIS, 13rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express orimplied. 14rem See the License for the specific language governing permissions and 15rem limitations underthe License. 16 17if "%OS%" == "Windows_NT"setlocal 18rem--------------------------------------------------------------------------- 19rem Start/Stop Scriptfor the CATALINA Server 20rem 21rem Environment Variable Prerequisites 22rem23rem Do not setthe variables in this script. Instead put them into a script 24rem setenv.bat in CATALINA_BASE/bin to keep yourcustomizations separate. 25rem 26rem CATALINA_HOME May point at your Catalina "build"directory. 27rem 28rem CATALINA_BASE (Optional) Base directory for resolvingdynamic portions 29rem of a Catalinainstallation. If not present, resolvesto 30rem the same directory thatCATALINA_HOME points to. 31rem 32rem CATALINA_OPTS (Optional) Java runtime options used whenthe "start", 33rem "run" or "debug" command is executed. 34rem Include here and not inJAVA_OPTS all options, that should35rem only be used by Tomcat itself, not by the stop process, 36rem the version command etc. 37rem Examples are heap size, GClogging, JMX ports etc. 38rem 39rem CATALINA_TMPDIR (Optional)Directory path location of temporary directory 40rem the JVM should use(java.io.tmpdir). Defaults to 41rem %CATALINA_BASE%/temp. 42rem 43rem JAVA_HOME Must point at your Java Development Kitinstallation. 44rem Required torun the with the "debug" argument. 45rem46rem JRE_HOME Must point at your Java Runtimeinstallation. 47rem Defaults toJAVA_HOME if empty. If JRE_HOME and JAVA_HOME 48rem are both set, JRE_HOME isused. 49rem 50rem JAVA_OPTS (Optional) Java runtime options usedwhen any command 51rem is executed. 52rem Include here and not inCATALINA_OPTS all options, that 53rem should be usedby Tomcat and also by the stop process, 54rem the version command etc. 55rem Most options should go intoCATALINA_OPTS. 56rem 57rem JAVA_ENDORSED_DIRS (Optional)Lists of of semi-colon separated directories 58rem containing some jars inorder to allow replacement of APIs59rem created outside of the JCP (i.e. DOM and SAX from W3C). 60rem It can also be used toupdate the XML parser implementation.61rem Defaults to $CATALINA_HOME/endorsed. 62rem63rem JPDA_TRANSPORT (Optional) JPDAtransport used when the "jpda start" 64rem command is executed. Thedefault is "dt_socket". 65rem 66rem JPDA_ADDRESS (Optional) Java runtime options used whenthe "jpda start" 67rem command isexecuted. The default is 8000. 68rem 69rem JPDA_SUSPEND (Optional) Java runtime options used whenthe "jpda start" 70rem command isexecuted. Specifies whether JVM should suspend 71rem execution immediately after startup.Default is "n". 72rem 73rem JPDA_OPTS (Optional) Java runtime options usedwhen the "jpda start" 74rem command isexecuted. If used, JPDA_TRANSPORT, JPDA_ADDRESS, 75rem and JPDA_SUSPEND are ignored. Thus, allrequired jpda 76rem options MUSTbe specified. The default is: 77rem 78rem -agentlib:jdwp=transport=%JPDA_TRANSPORT%, 79rem address=%JPDA_ADDRESS%,server=y,suspend=%JPDA_SUSPEND% 80rem 81rem LOGGING_CONFIG (Optional) Override Tomcat's logging configfile 82rem Example (all one line) 83rem setLOGGING_CONFIG="-Djava.util.logging.config.file=%CATALINA_BASE%/conf/logging.properties" 84rem 85rem LOGGING_MANAGER (Optional) Override Tomcat'slogging manager 86rem Example (allone line) 87rem setLOGGING_MANAGER="-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager" 88rem 89rem TITLE (Optional) Specify the title ofTomcat window. The default 90rem TITLE isTomcat if it's not specified. 91rem Example (allone line) 92rem setTITLE=Tomcat.Cluster#1.Server#1 [%DATE% %TIME%] 93rem94rem 95rem 96rem $Id: catalina.bat 1344732 2012-05-31 14:08:02Z kkolinko $ 97rem--------------------------------------------------------------------------- 98 99rem SuppressTerminate batch job on CTRL+C100ifnot ""%1""== ""run"" gotomainEntry101if "%TEMP%" == ""goto mainEntry102ifexist "%TEMP%/%~nx0.run"goto mainEntry103echo Y>"%TEMP%/%~nx0.run"104ifnotexist "%TEMP%/%~nx0.run" goto mainEntry105echo Y>"%TEMP%/%~nx0.Y"106call "%~f0" %*<"%TEMP%/%~nx0.Y"107rem Use provided errorlevel108set RETVAL=%ERRORLEVEL%109del /Q "%TEMP%/%~nx0.Y" >NUL2>&1110exit /B %RETVAL%111:mainEntry112del /Q "%TEMP%/%~nx0.run" >NUL2>&1113114rem Guess CATALINA_HOME if not defined115set "CURRENT_DIR=%cd%"116ifnot "%CATALINA_HOME%" ==""goto gotHome117set "CATALINA_HOME=%CURRENT_DIR%"118ifexist"%CATALINA_HOME%/bin/catalina.bat"gotookHome119cd ..120set "CATALINA_HOME=%cd%"121cd "%CURRENT_DIR%"122:gotHome123124ifexist"%CATALINA_HOME%/bin/catalina.bat"gotookHome125echo The CATALINA_HOME environment variable isnot defined correctly126echo This environment variable is needed torun this program127gotoend128:okHome129130rem Copy CATALINA_BASE from CATALINA_HOME if not defined131ifnot "%CATALINA_BASE%" ==""goto gotBase132set "CATALINA_BASE=%CATALINA_HOME%"133:gotBase134135rem Ensure that anyuser defined CLASSPATH variables are not used on startup,136rem but allow them tobe specified in setenv.bat, in rare case when it is needed.137set CLASSPATH=138139rem Get standardenvironment variables140ifnotexist "%CATALINA_BASE%/bin/setenv.bat"goto checkSetenvHome141call "%CATALINA_BASE%/bin/setenv.bat"142goto setenvDone143:checkSetenvHome144ifexist "%CATALINA_HOME%/bin/setenv.bat"call "%CATALINA_HOME%/bin/setenv.bat"145:setenvDone146147rem Get standard Java environment variables148ifexist"%CATALINA_HOME%/bin/setclasspath.bat"gotookSetclasspath149echo Cannotfind"%CATALINA_HOME%/bin/setclasspath.bat"150echo This file is needed torun this program151gotoend152:okSetclasspath153call"%CATALINA_HOME%/bin/setclasspath.bat"%1154iferrorlevel 1gotoend155156rem Add on extra jar file to CLASSPATH157rem Note that there are no quotes as we donot want to introduce random158rem quotes into the CLASSPATH159if "%CLASSPATH%" == "" goto emptyClasspath160set "CLASSPATH=%CLASSPATH%;"161:emptyClasspath162set"CLASSPATH=%CLASSPATH%%CATALINA_HOME%/bin/bootstrap.jar"163164ifnot "%CATALINA_TMPDIR%" =="" goto gotTmpdir165set "CATALINA_TMPDIR=%CATALINA_BASE%/temp"166:gotTmpdir167168rem Addtomcat-juli.jar to classpath169rem tomcat-juli.jar can be over-ridden per instance170ifnotexist"%CATALINA_BASE%/bin/tomcat-juli.jar"gotojuliClasspathHome171set "CLASSPATH=%CLASSPATH%;%CATALINA_BASE%/bin/tomcat-juli.jar"172goto juliClasspathDone173:juliClasspathHome174set "CLASSPATH=%CLASSPATH%;%CATALINA_HOME%/bin/tomcat-juli.jar"175:juliClasspathDone176177ifnot "%LOGGING_CONFIG%" =="" goto noJuliConfig178set LOGGING_CONFIG=-Dnop179ifnotexist"%CATALINA_BASE%/conf/logging.properties"gotonoJuliConfig180setLOGGING_CONFIG=-Djava.util.logging.config.file="%CATALINA_BASE%/conf/logging.properties"181:noJuliConfig182set JAVA_OPTS=%JAVA_OPTS% %LOGGING_CONFIG%183184ifnot "%LOGGING_MANAGER%" ==""goto noJuliManager185set LOGGING_MANAGER=-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager186:noJuliManager187set JAVA_OPTS=%JAVA_OPTS%%LOGGING_MANAGER%188189rem ----- Execute The Requested Command ---------------------------------------190191echo Using CATALINA_BASE: "%CATALINA_BASE%"192echo Using CATALINA_HOME: "%CATALINA_HOME%"193echo Using CATALINA_TMPDIR:"%CATALINA_TMPDIR%"194if ""%1""== ""debug""gotouse_jdk195echo Using JRE_HOME: "%JRE_HOME%"196goto java_dir_displayed197:use_jdk198echo Using JAVA_HOME: "%JAVA_HOME%"199:java_dir_displayed200echo Using CLASSPATH: "%CLASSPATH%"201202set _EXECJAVA=%_RUNJAVA%203set MAINCLASS=org.apache.catalina.startup.Bootstrap204set ACTION=start205set SECURITY_POLICY_FILE=206set DEBUG_OPTS=207set JPDA=208209ifnot ""%1""== ""jpda""goto noJpda210set JPDA=jpda211ifnot "%JPDA_TRANSPORT%" =="" goto gotJpdaTransport212set JPDA_TRANSPORT=dt_socket213:gotJpdaTransport214ifnot "%JPDA_ADDRESS%" == ""goto gotJpdaAddress215set JPDA_ADDRESS=8000216:gotJpdaAddress217ifnot "%JPDA_SUSPEND%" == ""goto gotJpdaSuspend218set JPDA_SUSPEND=n219:gotJpdaSuspend220ifnot "%JPDA_OPTS%" == "" goto gotJpdaOpts221set JPDA_OPTS=-agentlib:jdwp=transport=%JPDA_TRANSPORT%,address=%JPDA_ADDRESS%,server=y,suspend=%JPDA_SUSPEND%222:gotJpdaOpts223shift224:noJpda225226if ""%1""== ""debug""gotodoDebug227if ""%1""== ""run""goto doRun228if ""%1""== ""start""gotodoStart229if ""%1""== ""stop""goto doStop230if ""%1"" ==""configtest""goto doConfigTest231if ""%1"" ==""version""goto doVersion232233echo Usage: catalina ( commands ... )234echo commands:235echo debug StartCatalina in a debugger236echo debug -security Debug Catalina with a security manager237echo jpdastart Start Catalina under JPDA debugger238echo run Start Catalina in the current window239echo run -security Start in the current window with security manager240echo start StartCatalina in a separate window241echo start -security Start in a separate window with security manager242echo stop Stop Catalina243echo configtest Run a basic syntax checkon server.xml244echo version What version oftomcat are you running?245gotoend246247:doDebug248shift249set _EXECJAVA=%_RUNJDB%250set DEBUG_OPTS=-sourcepath"%CATALINA_HOME%/../../java"251ifnot ""%1""== ""-security""goto execCmd252shift253echo Using Security Manager254set"SECURITY_POLICY_FILE=%CATALINA_BASE%/conf/catalina.policy"255goto execCmd256257:doRun258shift259ifnot ""%1""== ""-security""goto execCmd260shift261echo Using Security Manager262set "SECURITY_POLICY_FILE=%CATALINA_BASE%/conf/catalina.policy"263goto execCmd264265:doStart266shift267ifnot "%OS%" == "Windows_NT"goto noTitle268if "%TITLE%" == ""set TITLE=Tomcat269set _EXECJAVA=start"%TITLE%" %_RUNJAVA%270goto gotTitle271:noTitle272set _EXECJAVA=start %_RUNJAVA%273:gotTitle274ifnot ""%1""== ""-security""goto execCmd275shift276echo Using Security Manager277set"SECURITY_POLICY_FILE=%CATALINA_BASE%/conf/catalina.policy"278goto execCmd279280:doStop281shift282set ACTION=stop283set CATALINA_OPTS=284goto execCmd285286:doConfigTest287shift288set ACTION=configtest289set CATALINA_OPTS=290goto execCmd291292:doVersion293 %_EXECJAVA% -classpath"%CATALINA_HOME%/lib/catalina.jar" org.apache.catalina.util.ServerInfo294gotoend295296297:execCmd298rem Get remaining unshifted command line arguments and save them in the299set CMD_LINE_ARGS=300:setArgs301if ""%1""==""""goto doneSetArgs302set CMD_LINE_ARGS=%CMD_LINE_ARGS%%1303shift304goto setArgs305:doneSetArgs306307rem Execute Java withthe applicable properties308ifnot "%JPDA%" == ""goto doJpda309ifnot "%SECURITY_POLICY_FILE%" ==""goto doSecurity310%_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%311gotoend312:doSecurity313 %_EXECJAVA% %JAVA_OPTS% %CATALINA_OPTS% %DEBUG_OPTS% -Djava.endorsed.dirs="%JAVA_ENDORSED_DIRS%"-classpath "%CLASSPATH%" -Djava.security.manager-Djava.security.policy=="%SECURITY_POLICY_FILE%"-Dcatalina.base="%CATALINA_BASE%"-Dcatalina.home="%CATALINA_HOME%"-Djava.io.tmpdir="%CATALINA_TMPDIR%" %MAINCLASS% %CMD_LINE_ARGS% %ACTION%314gotoend315:doJpda316ifnot "%SECURITY_POLICY_FILE%" =="" goto doSecurityJpda317 %_EXECJAVA%%JAVA_OPTS% %CATALINA_OPTS% %JPDA_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%318gotoend319:doSecurityJpda320 %_EXECJAVA% %JAVA_OPTS% %CATALINA_OPTS% %JPDA_OPTS% %DEBUG_OPTS%-Djava.endorsed.dirs="%JAVA_ENDORSED_DIRS%" -classpath"%CLASSPATH%" -Djava.security.manager-Djava.security.policy=="%SECURITY_POLICY_FILE%"-Dcatalina.base="%CATALINA_BASE%"-Dcatalina.home="%CATALINA_HOME%"-Djava.io.tmpdir="%CATALINA_TMPDIR%" %MAINCLASS% %CMD_LINE_ARGS% %ACTION%321gotoend322323:end
1到99行一大堆注释说明就不看了,100到110行一堆run文件校验之类的一般用不到,直接跳到111行,接下来先删除上面可能产生的中间文件,接着按照正常流程,在上面的startup.bat文件中已经设置过CATALINA_HOME变量,并且校验了改变量下bin/catalina.bat文件已经存在(我猜作者写这一段是防止用户直接运行catalina.bat文件)。经过这些判断后跳转到128行,130到133行设置CATALINA_BASE变量,中间几行注释略过,139到144行如果有setenv.bat文件则执行一下该文件。147到153行执行setclasspath.bat文件(该文件在同级目录下已经提供,具体脚本下面已经贴出来了,可以读一下,这里不再做分析。主要功能校验是否设置JAVA_HOME变量,没有则在控制台显示出提示信息。注意该文件中第79行将会设置_RUNJAVA变量,该变量的值即JAVA_HOME或JRE_HOME的bin目录下的java.exe文件)。从156到224行校验并设置一堆变量(注意第202、203和204行,最后启动分析时将会用到)。226到231行将根据调用catalina.bat时设置的参数决定跳转,按照上一段的最后的说明,此时将会跳转到doStart标记行。从265行开始,校验和设置一些变量之后(注意第269行)将会跳转到execCmd标记行,即297行。297到305行作用是将系统参数保存到CMD_LINE_ARGS变量中。
接下来便是谜底解开的时候,正常情况下将会执行到310行,这里单独贴出来分析一下:
%_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%
按照202行的提示_EXECJAVA一开始的值等于_RUNJAVA,但在269行将该值改成了start "%TITLE%" %_RUNJAVA%,这里的作用是另启动一个命令行窗口,并给窗口起名,最终还是执行java.exe文件。从%JAVA_OPTS%开始到%MAINCLASS%之前都是在设置运行时参数和系统属性,%MAINCLASS%即203行的org.apache.catalina.startup.Bootstrap,后面是其它的命令行参数,最后一个参数%ACTION%在204行时被设置成start。归结起来一句话,即执行Bootstrap类的main方法。
setclasspath.bat:
1@echooff 2rem Licensed to theApache Software Foundation (ASF) under one or more 3rem contributor license agreements. See the NOTICE file distributed with 4rem this work foradditional information regarding copyright ownership. 5rem The ASF licenses this file to You underthe Apache License, Version 2.0 6rem (the "License"); you may not use this file except incompliance with 7rem the License. You may obtain acopy of the License at 8rem 9rem http://www.apache.org/licenses/LICENSE-2.010rem11rem Unless required by applicable law or agreed to in writing, software12rem distributed underthe License is distributed on an "AS IS" BASIS,13rem WITHOUTWARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.14rem See the Licensefor the specific language governing permissions and15rem limitations under the License.1617rem ---------------------------------------------------------------------------18rem Set JAVA_HOME orJRE_HOME if not already set, ensure any provided settings19rem are valid andconsistent with the selected start-up options and set up the20rem endorsed directory.21rem22rem $Id:setclasspath.bat 1202062 2011-11-15 06:50:02Z mturk $23rem---------------------------------------------------------------------------2425rem Make sure prerequisite environment variables are set2627rem In debug mode we need a real JDK (JAVA_HOME)28if ""%1"" == ""debug""goto needJavaHome2930rem Otherwise eitherJRE or JDK are fine31ifnot "%JRE_HOME%" == ""goto gotJreHome32ifnot "%JAVA_HOME%" == ""goto gotJavaHome33echo Neither the JAVA_HOME nor the JRE_HOMEenvironment variable is defined34echo At least one of these environment variableis needed torun this program35gotoexit3637:needJavaHome38rem Check if we have a usable JDK39if "%JAVA_HOME%" == ""goto noJavaHome40ifnotexist "%JAVA_HOME%/bin/java.exe"goto noJavaHome41ifnotexist "%JAVA_HOME%/bin/javaw.exe"goto noJavaHome42ifnotexist "%JAVA_HOME%/bin/jdb.exe"goto noJavaHome43ifnotexist "%JAVA_HOME%/bin/javac.exe"goto noJavaHome44set "JRE_HOME=%JAVA_HOME%"45goto okJava4647:noJavaHome48echo The JAVA_HOME environment variable isnot defined correctly.49echo It is needed torunthis program indebugmode.50echo NB: JAVA_HOME should point to a JDKnot a JRE.51gotoexit5253:gotJavaHome54rem No JRE given, use JAVA_HOME as JRE_HOME55set "JRE_HOME=%JAVA_HOME%"5657:gotJreHome58rem Check if we have a usable JRE59ifnotexist "%JRE_HOME%/bin/java.exe"goto noJreHome60ifnotexist "%JRE_HOME%/bin/javaw.exe"goto noJreHome61goto okJava6263:noJreHome64rem Needed at least aJRE65echo The JRE_HOME environment variable isnot defined correctly66echo This environment variable is needed torun this program67gotoexit6869:okJava70rem Don't override the endorsed dir if the user has set it previously71ifnot "%JAVA_ENDORSED_DIRS%" ==""goto gotEndorseddir72rem Set the default -Djava.endorsed.dirsargument73set"JAVA_ENDORSED_DIRS=%CATALINA_HOME%/endorsed"74:gotEndorseddir7576rem Set standardcommand for invoking Java.77rem Note that NT requires a window name argument when using start.78rem Also note thequoting as JAVA_HOME may contain spaces.79set _RUNJAVA="%JRE_HOME%/bin/java"80set _RUNJDB="%JAVA_HOME%/bin/jdb"8182gotoend8384:exit85exit /b 18687:end88exit /b 0
三、总结
以上即tomcat的启动脚本中的总体流程,只是讲了默认的脚本运行方式,但在脚本阅读中可以看到能根据catalina.bat里可以根据不同参数以不同方式运行tomcat:
debug Start Catalina in a debugger
debug -security Debug Catalina with asecurity manager
jpda start Start Catalina under JPDA debugger
run Start Catalina in the current window
run -security Start in thecurrent window with security manager
start Start Catalina in a separate window
start -security Start in a separate windowwith security manager
stop Stop Catalina
configtest Runa basic syntax check on server.xml
只要能看懂基本dos命令,这几个脚本的阅读其实很简单,其他bat文件可以大体浏览一下,会发现转了半天,最终都会执行catalina.bat文件,而catalina.bat文件里最终会执行Bootstrap文件的main方法,不同的是调用main方法时会各自添加不同的入参而已。bat文件用于windows操作系统下启动tomcat,而sh文件则用于unix环境下的启动,原理是一致的。
Tomcat7启动分析(二)Bootstrap类中的main方法
之前分析了Tomcat的启动脚本,如果从startup.bat开始启动Tomcat的话会发现最后会调用org.apache.catalina.startup.Bootstrap里的main方法,并且传过来的最后一个命令行参数是start,接下来的启动代码分析就从这里开始。
先看下这个main方法的代码:
1/** 2 * Main method and entry point when starting Tomcat via the provided 3 * scripts. 4 * 5 * @param args Command line arguments to be processed 6 */ 7 publicstaticvoid main(String args[]) { 8 9 if (daemon == null) {10 // Don't set daemon until init() has completed11 Bootstrap bootstrap = new Bootstrap();12 try {13 bootstrap.init();14 } catch (Throwable t) {15 handleThrowable(t);16 t.printStackTrace();17 return;18 }19 daemon = bootstrap;20 } else {21 // When running as a service the call to stop will be on a new22 // thread so make sure the correct class loader is used to prevent23 // a range of class not found exceptions.24 Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);25 }2627 try {28 String command = "start";29 if (args.length > 0) {30 command = args[args.length - 1];31 }3233 if (command.equals("startd")) {34 args[args.length - 1] = "start";35 daemon.load(args);36 daemon.start();37 } elseif (command.equals("stopd")) {38 args[args.length - 1] = "stop";39 daemon.stop();40 } elseif (command.equals("start")) {41 daemon.setAwait(true);42 daemon.load(args);43 daemon.start();44 } elseif (command.equals("stop")) {45 daemon.stopServer(args);46 } elseif (command.equals("configtest")) {47 daemon.load(args);48 if (null==daemon.getServer()) {49 System.exit(1);50 }51 System.exit(0);52 } else {53 log.warn("Bootstrap: command /"" + command + "/" does not exist.");54 }55 } catch (Throwable t) {56 // Unwrap the Exception for clearer error reporting57 if (t instanceof InvocationTargetException &&58 t.getCause() != null) {59 t = t.getCause();60 }61 handleThrowable(t);62 t.printStackTrace();63 System.exit(1);64 }6566 }
这里的daemon是Bootstrap类中的一个静态成员变量,类型就是Bootstrap,第10行的注释已经说明在调用过init方法之后才会给该变量赋值,初始时将是null,所以首先将实例化一个Bootstrap对象,接着调用init方法,该方法代码如下:
1/** 2 * Initialize daemon. 3 */ 4 publicvoid init() 5 throws Exception 6 { 7 8 // Set Catalina path 9 setCatalinaHome();10 setCatalinaBase();1112 initClassLoaders();1314 Thread.currentThread().setContextClassLoader(catalinaLoader);1516 SecurityClassLoad.securityClassLoad(catalinaLoader);1718 // Load our startup class and call its process() method19 if (log.isDebugEnabled())20 log.debug("Loading startup class");21 Class<?> startupClass =22 catalinaLoader.loadClass23 ("org.apache.catalina.startup.Catalina");24 Object startupInstance = startupClass.newInstance();2526 // Set the shared extensions class loader27 if (log.isDebugEnabled())28 log.debug("Setting startup class properties");29 String methodName = "setParentClassLoader";30 Class<?> paramTypes[] = new Class[1];31 paramTypes[0] = Class.forName("java.lang.ClassLoader");32 Object paramValues[] = new Object[1];33 paramValues[0] = sharedLoader;34 Method method =35 startupInstance.getClass().getMethod(methodName, paramTypes);36 method.invoke(startupInstance, paramValues);3738 catalinaDaemon = startupInstance;3940 }
这里不再逐句解释代码的作用,总的来说这个方法主要做了一下几件事:1.设置catalina.home、catalina.base系统属性,2.创建commonLoader、catalinaLoader、sharedLoader三个类加载器(建议看看createClassLoader方法,里面做的事情还挺多,比如装载catalina.properties里配置的目录下的文件和jar包,后两个加载器的父加载器都是第一个,最后注册了MBean,可以用于JVM监控该对象),3.实例化一个org.apache.catalina.startup.Catalina对象,并赋值给静态成员catalinaDaemon,以sharedLoader作为入参通过反射调用该对象的setParentClassLoader方法。
接下来去命令行最后一个参数,按文章开头所说是start,所以将执行34行到36行的代码,将会执行Bootstrap类中的load、start方法。
load方法代码如下:
1 /** 2 * Load daemon. 3 */ 4 privatevoid load(String[] arguments) 5 throws Exception { 6 7 // Call the load() method 8 String methodName = "load"; 9 Object param[];10 Class<?> paramTypes[];11 if (arguments==null || arguments.length==0) {12 paramTypes = null;13 param = null;14 } else {15 paramTypes = new Class[1];16 paramTypes[0] = arguments.getClass();17 param = new Object[1];18 param[0] = arguments;19 }20 Method method =21 catalinaDaemon.getClass().getMethod(methodName, paramTypes);22 if (log.isDebugEnabled())23 log.debug("Calling startup class " + method);24 method.invoke(catalinaDaemon, param);2526 }
就是通过反射调用catalinaDaemon对象的load方法,catalinaDaemon对象在上面的init方法中已经实例化过了。
start方法与load方法相似,也是通过反射调用catalinaDaemon对象上的start方法:
1 /** 2 * Start the Catalina daemon. 3 */ 4 publicvoid start() 5 throws Exception { 6 if( catalinaDaemon==null ) init(); 7 8 Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null); 9 method.invoke(catalinaDaemon, (Object [])null);1011 }
下面一篇文章将分析catalinaDaemon对象中的load、start两个方法,里面会涉及一个有趣的话题——Digester的使用。
Tomcat7启动分析(三)Digester的使用
前一篇文章里最后看到Bootstrap的main方法最后会调用org.apache.catalina.startup.Catalina对象的load和start两个方法,那么就来看看这两个方法里面到底做了些什么。
load方法:
1 /** 2 * Start a new server instance. 3 */ 4 publicvoid load() { 5 6 long t1= System.nanoTime(); 7 8 initDirs(); 9 10 // Before digester - it may be needed 11 12 initNaming(); 13 14 // Create and executeour Digester 15 Digester digester =createStartDigester(); 16 17 InputSource inputSource = null; 18 InputStream inputStream =null;19 File file =null;20 try {21 file = configFile();22 inputStream = new FileInputStream(file); 23 inputSource =newInputSource(file.toURI().toURL().toString()); 24 }catch(Exception e) { 25 if (log.isDebugEnabled()) { 26 log.debug(sm.getString("catalina.configFail", file), e); 27 } 28 } 29 if(inputStream ==null) {30 try { 31 inputStream = getClass().getClassLoader() 32 .getResourceAsStream(getConfigFile()); 33 inputSource =new InputSource 34 (getClass().getClassLoader() 35 .getResource(getConfigFile()).toString()); 36 } catch (Exception e) { 37 if(log.isDebugEnabled()) { 38 log.debug(sm.getString("catalina.configFail",39 getConfigFile()),e); 40 } 41 } 42 }43 44 // This should beincluded in catalina.jar 45 // Alternative: don'tbother with xml, just create it manually. 46 if(inputStream==null ) {47 try { 48 inputStream = getClass().getClassLoader() 49 .getResourceAsStream("server-embed.xml");50 inputSource =new InputSource 51 (getClass().getClassLoader() 52 .getResource("server-embed.xml").toString());53 } catch(Exception e) { 54 if(log.isDebugEnabled()) { 55 log.debug(sm.getString("catalina.configFail",56 "server-embed.xml"), e); 57 } 58 } 59 } 60 61 62 if(inputStream ==null || inputSource ==null) { 63 if (file == null) { 64 log.warn(sm.getString("catalina.configFail",65 getConfigFile() +"] or [server-embed.xml]")); 66 }else {67 log.warn(sm.getString("catalina.configFail",68 file.getAbsolutePath())); 69 if (file.exists() && !file.canRead()){ 70 log.warn("Permissions incorrect, read permission is not allowed onthe file."); 71 } 72 } 73 return; 74 } 75 76 try {77 inputSource.setByteStream(inputStream); 78 digester.push(this); 79 digester.parse(inputSource); 80 } catch(SAXParseException spe) { 81 log.warn("Catalina.start using " + getConfigFile() + ":" + 82 spe.getMessage()); 83 return; 84 }catch (Exception e) { 85 log.warn("Catalina.startusing " + getConfigFile() + ": " , e); 86 return;87 }finally {88 try { 89 inputStream.close(); 90 }catch (IOException e) { 91 // Ignore 92 } 93 } 94 95 getServer().setCatalina(this);96 97 // Stream redirection 98 initStreams(); 99100 // Start the new server101 try {102 getServer().init();103 }catch(LifecycleException e) {104 if(Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {105 thrownew java.lang.Error(e);106 }else {107 log.error("Catalina.start", e);108 }109110 }111112 long t2= System.nanoTime();113 if(log.isInfoEnabled()){114 log.info("Initialization processed in " + ((t2 - t1) /1000000) + " ms");115 }116117 }
这个110行的代码看起来东西挺多,把注释、异常抛出、记录日志、流关闭、非空判断这些放在一边就会发现实际上真正做事的就这么几行代码:
1 Digester digester = createStartDigester();2 inputSource.setByteStream(inputStream);3 digester.push(this);4 digester.parse(inputSource);5 getServer().setCatalina(this);6 getServer().init();
做的事情就两个,一是创建一个Digester对象,将当前对象压入Digester里的对象栈顶,根据inputSource里设置的文件xml路径及所创建的Digester对象所包含的解析规则生成相应对象,并调用相应方法将对象之间关联起来。二是调用Server接口对象的init方法。
这里碰到了Digester,有必要介绍一下Digester的一些基础知识。一般来说Java里解析xml文件有两种方式:一种是Dom4J之类将文件全部读取到内存中,在内存里构造一棵Dom树的方式来解析。一种是SAX的读取文件流,在流中碰到相应的xml节点触发相应的节点事件回调相应方法,基于事件的解析方式,优点是不需要先将文件全部读取到内存中。
Digester本身是采用SAX的解析方式,在其上提供了一层包装,对于使用者更简便友好罢了。最早是在struts1里面用的,后来独立出来成为apache的Commons下面的一个单独的子项目。Tomcat里又把它又封装了一层,为了描述方便,直接拿Tomcat里的Digester建一个单独的Digester的例子来介绍。
1package org.study.digester; 2 3import java.io.IOException; 4import java.io.InputStream; 5import java.util.ArrayList; 6import java.util.HashMap; 7import java.util.List; 8 9import junit.framework.Assert; 10 11import org.apache.tomcat.util.digester.Digester; 12import org.xml.sax.InputSource; 13 14publicclass MyDigester { 15 16 private MyServer myServer; 17 18 public MyServer getMyServer() { 19 returnmyServer; 20 } 21 22 publicvoid setMyServer(MyServer myServer) { 23 this.myServer= myServer; 24 } 25 26 privateDigester createStartDigester() { 27 Digester digester =new Digester(); 28 29 //是否进行DTD校验 30 digester.setValidating(false); 31 32 //是否进行节点设置规则校验 33 digester.setRulesValidation(true);34 35 //className作为假属性,不需要设置到相应对象的setter方法中 36 HashMap<Class<?>, List<String>> fakeAttributes =new HashMap<Class<?>, List<String>>(); 37 ArrayList<String> attrs =newArrayList<String>(); 38 attrs.add("className"); 39 fakeAttributes.put(Object.class, attrs); 40 digester.setFakeAttributes(fakeAttributes); 41 42 digester.addObjectCreate("Server", 43 "org.study.digester.MyStandardServer", "className");44 digester.addSetProperties("Server"); 45 digester.addSetNext("Server", "setMyServer",46 "org.study.digester.MyServer"); 47 48 digester.addObjectCreate("Server/Listener",null, "className"); 49 digester.addSetProperties("Server/Listener");50 digester.addSetNext("Server/Listener","addLifecycleListener", 51 "org.apache.catalina.LifecycleListener");52 53 digester.addObjectCreate("Server/Service",54 "org.study.digester.MyStandardService", "className");55 digester.addSetProperties("Server/Service");56 digester.addSetNext("Server/Service", "addMyService",57 "org.study.digester.MyService"); 58 59 digester.addObjectCreate("Server/Service/Listener",null, 60 "className"); 61 digester.addSetProperties("Server/Service/Listener");62 digester.addSetNext("Server/Service/Listener",63 "addLifecycleListener", 64 "org.apache.catalina.LifecycleListener");65 returndigester; 66 } 67 68 public MyDigester() {69 Digester digester =createStartDigester(); 70 71 InputSource inputSource = null; 72 InputStream inputStream =null;73 try {74 String configFile ="myServer.xml"; 75 inputStream =getClass().getClassLoader().getResourceAsStream( 76 configFile); 77 inputSource =new InputSource(getClass().getClassLoader() 78 .getResource(configFile).toString()); 79 80 inputSource.setByteStream(inputStream); 81 digester.push(this); 82 digester.parse(inputSource); 83 } catch(Exception e) { 84 e.printStackTrace(); 85 }finally { 86 try {87 inputStream.close(); 88 }catch (IOException e) { 89 // Ignore 90 } 91 } 92 93 getMyServer().setMyDigester(this);94 } 95 96 publicstaticvoid main(String[] agrs) { 97 MyDigester md =new MyDigester(); 98 Assert.assertNotNull(md.getMyServer()); 99 }100 }
上面是我自己写的一个拿Tomcat里的Digester的工具类解析xml文件的例子,解析的是项目源文件发布的根目录下的myServer.xml文件。
1<?xml version='1.0' encoding='utf-8'?>2 3<Serverport="8005" shutdown="SHUTDOWN"> 4 5 <Listener 6 className="org.apache.catalina.core.JasperListener"/> 7 8 <Listener 9 className="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/>1011 <Servicename="Catalina">1213 <Listener14 className="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/>15 </Service>16</Server>
Digester的使用一般来说有4步:1.实例化一个Digester对象,并在对象里设置相应的节点解析规则。2.设置要解析的文件作为输入源(InputSource),这里InputSource与SAX里的一样,用以表示一个xml实体。3.将当前对象压入栈,4.调用Digester的parse方法解析xml,生成相应的对象。第3步中将当前对象压入栈中的作用是可以保存一个到Digester生成的一系列对象直接的引用,方便后续使用而已,所以不必是当前对象,只要有一个地方存放这个引用即可。
再说下上面例子中的createStartDigester方法,第27行实例化一个Digester对象,第30行设置为false表示解析xml时不需要进行DTD的规则校验,第33行的意思是如果xml中相应节点没有设置解析规则会在控制台显示提示信息,第36到40行是将xml节点中的className作为假属性,不必调用默认的setter方法(一般的节点属性在解析时将会以属性值作为入参调用该节点相应对象的setter方法,而className属性的作用是提示解析器用该属性的值来实例化对象),第42行addObjectCreate方法的意思是碰到xml文件中的Server节点则创建一个org.study.digester.MyStandardServer对象,第44行是根据Server节点中的属性信息调用相应属性的setter方法,以上面的xml文件为例则会调用setPort、setShutdown方法,入参分别是8005、SHUTDOWN,第45行的意思是将Server节点对应的对象作为入参调用栈顶对象的setMyServer方法,这里的栈顶对象即第81行所设置的当前类的对象this,就是说调用MyDigester类的setMyServer方法。下面几段代码的作用与42到46类似,只是在碰到xml文件的不同节点时产生不同的对象,并调用相应的方法,如第48行在碰到xml的Server节点下的Listener节点时取className属性的值作为实例化类实例化一个对象等等,不再赘述。
这里有必要说明的是很多文章里按照代码顺序来描述Digester的所谓栈模型来讲述addSetNext方法时的调用对象,实际上我的理解addSetNext方法具体哪个调用对象与XML文件里的节点树形结构相关,当前节点的父节点是哪个对象该对象就是调用对象。可以试验一下把这里的代码顺序打乱仍然可以按规则解析出来,createStartDigester方法只是在定义解析规则,具体解析与addObjectCreate、addSetProperties、addSetNext这些方法的调用顺序无关。Digester自己内部在解析xml的节点元素时增加了一个rule的概念,addObjectCreate、addSetProperties、addSetNext这些方法内部实际上是在添加rule,在最后解析xml时将会根据读取到的节点匹配相应节点路径下的rule,调用内部的方法。关于Tomcat内的Digester的解析原理以后可以单独写篇文章分析一下。
该示例的代码在附件中,将其放入tomcat7的源代码环境中即可直接运行。
看懂了上面的例子Catalina的createStartDigester方法应该就可以看懂了,它只是比示例要处理的节点类型更多,并且增加几个自定义的解析规则,如384行在碰到Server/GlobalNamingResources/节点时将会调用org.apache.catalina.startup.NamingRuleSet类中的addRuleInstances方法添加解析规则。
要解析的XML文件默认会先找conf/server.xml,如果当前项目找不到则通过其他路径找xml文件来解析,这里以默认情况为例将会解析server.xml
1<?xml version='1.0' encoding='utf-8'?> 2<!-- 3 Licensed to the Apache SoftwareFoundation (ASF) under one or more 4 contributor license agreements. See the NOTICE file distributed with 5 this work for additionalinformation regarding copyright ownership. 6 The ASF licenses this file to Youunder the Apache License, Version 2.0 7 (the "License"); you may not usethis file except in compliance with 8 the License. You may obtain a copy of the License at 9 10 http://www.apache.org/licenses/LICENSE-2.0 11 12 Unless required by applicable law or agreedto in writing, software 13 distributed under the License isdistributed on an "AS IS" BASIS, 14 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,either express or implied. 15 See the License for the specificlanguage governing permissions and16 limitations underthe License. 17--> 18<!-- Note: A "Server"is not itself a "Container", so you may not 19 define subcomponents such as"Valves" at this level. 20 Documentation at/docs/config/server.html 21 --> 22<Serverport="8005" shutdown="SHUTDOWN"> 23 <!-- Security listener. Documentation at/docs/config/listeners.html 24 <ListenerclassName="org.apache.catalina.security.SecurityListener" /> 25 --> 26 <!--APR library loader. Documentation at/docs/apr.html --> 27 <ListenerclassName="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on"/> 28 <!--Initialize Jasper prior to webapps are loaded.Documentation at /docs/jasper-howto.html --> 29 <ListenerclassName="org.apache.catalina.core.JasperListener"/> 30 <!-- Prevent memory leaks due to use of particularjava/javax APIs--> 31 <ListenerclassName="org.apache.catalina.core.JreMemoryLeakPreventionListener"/> 32 <ListenerclassName="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener"/> 33 <ListenerclassName="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/> 34 35 <!-- Global JNDI resources 36 Documentation at/docs/jndi-resources-howto.html 37 --> 38 <GlobalNamingResources>39 <!-- Editable userdatabase that can also be used by 40 UserDatabaseRealm toauthenticate users 41 --> 42 <Resourcename="UserDatabase" auth="Container" 43 type="org.apache.catalina.UserDatabase" 44 description="User database that can be updated andsaved" 45 factory="org.apache.catalina.users.MemoryUserDatabaseFactory" 46 pathname="conf/tomcat-users.xml"/> 47 </GlobalNamingResources>48 49 <!-- A "Service" is a collectionof one or more "Connectors" that share 50 a single "Container"Note: A "Service" is notitself a "Container", 51 so you may not definesubcomponents such as "Valves" at this level. 52 Documentation at/docs/config/service.html 53 --> 54 <Servicename="Catalina"> 55 56 <!--The connectors can use a shared executor, you candefine one or more named thread pools--> 57 <!-- 58 <Executorname="tomcatThreadPool" namePrefix="catalina-exec-" 59 maxThreads="150"minSpareThreads="4"/>60 --> 61 62 63 <!-- A "Connector" represents an endpoint bywhich requests are received 64 and responses are returned.Documentation at : 65 Java HTTP Connector:/docs/config/http.html (blocking & non-blocking) 66 Java AJP Connector: /docs/config/ajp.html 67 APR (HTTP/AJP) Connector:/docs/apr.html 68 Define a non-SSL HTTP/1.1Connector on port 8080 69 --> 70 <Connectorport="8080" protocol="HTTP/1.1" 71 connectionTimeout="20000" 72 redirectPort="8443"/> 73 <!-- A "Connector" using the shared threadpool--> 74 <!-- 75 <Connectorexecutor="tomcatThreadPool"76 port="8080"protocol="HTTP/1.1" 77 connectionTimeout="20000" 78 redirectPort="8443"/> 79 --> 80 <!-- Define a SSL HTTP/1.1 Connector on port 8443 81 This connector uses the JSSEconfiguration, when using APR, the82 connectorshould be using the OpenSSL style configuration 83 described in the APR documentation--> 84 <!-- 85 <Connector port="8443"protocol="HTTP/1.1" SSLEnabled="true" 86 maxThreads="150"scheme="https" secure="true" 87 clientAuth="false"sslProtocol="TLS" /> 88 --> 89 90 <!-- Define an AJP 1.3 Connector on port8009 --> 91 <Connectorport="8009" protocol="AJP/1.3" redirectPort="8443"/> 92 93 94 <!-- An Engine represents the entry point (withinCatalina) that processes 95 every request. The Engine implementation for Tomcat standalone 96 analyzes the HTTP headers includedwith the request, and passes them 97 on to the appropriate Host(virtual host). 98 Documentation at/docs/config/engine.html --> 99100 <!-- You should set jvmRoute to supportload-balancing via AJP ie :101 <Enginename="Catalina" defaultHost="localhost"jvmRoute="jvm1">102 -->103 <Enginename="Catalina" defaultHost="localhost">104105 <!--For clustering, please take a look at documentationat:106 /docs/cluster-howto.html (simple how to)107 /docs/config/cluster.html (referencedocumentation) -->108 <!--109 <ClusterclassName="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>110 -->111112 <!-- Use theLockOutRealm to prevent attempts to guess user passwords113 via a brute-force attack -->114 <RealmclassName="org.apache.catalina.realm.LockOutRealm">115 <!-- This Realm uses the UserDatabase configured inthe global JNDI116 resources under the key"UserDatabase". Any edits117 that are performed against thisUserDatabase are immediately118 available for use bythe Realm. -->119 <RealmclassName="org.apache.catalina.realm.UserDatabaseRealm"120 resourceName="UserDatabase"/>121 </Realm>122123 <Hostname="localhost" appBase="webapps"124 unpackWARs="true" autoDeploy="true">125126 <!-- SingleSignOnvalve, share authentication between web applications127 Documentation at:/docs/config/valve.html -->128 <!--129 <ValveclassName="org.apache.catalina.authenticator.SingleSignOn" />130 -->131132 <!-- Access logprocesses all example.133 Documentation at:/docs/config/valve.html134 Note: The pattern usedis equivalent to using pattern="common" -->135 <ValveclassName="org.apache.catalina.valves.AccessLogValve" directory="logs"136 prefix="localhost_access_log." suffix=".txt"137 pattern="%h %l %u %t "%r" %s %b"/>138139 </Host>140 </Engine>141 </Service>142</Server>
这样经过对xml文件的解析将会产生org.apache.catalina.core.StandardServer、org.apache.catalina.core.StandardService、org.apache.catalina.connector.Connector、org.apache.catalina.core.StandardEngine、org.apache.catalina.core.StandardHost、org.apache.catalina.core.StandardContext等等一系列对象,这些对象从前到后前一个包含后一个对象的引用(一对一或一对多的关系)。
解析完xml之后关闭文件流(第627行),接着设置StandardServer对象(该对象在上面解析xml时)的catalina的引用为当前对象,这种对象间的双向引用在Tomcat的很多地方都会碰到。
接下来将调用StandardServer对象的init方法。
上面分析的是Catalina的load方法,上一篇文章里看到Bootstrap类启动时还会调用Catalina对象的start方法,代码如下:
1 /**2 * Start a newserver instance. 3 */ 4 publicvoid start() { 5 6 if (getServer() == null) { 7 load(); 8 } 910 if(getServer() ==null) {11 log.fatal("Cannot start server. Server instance is notconfigured.");12 return;13 }1415 long t1= System.nanoTime();1617 // Start the newserver18 try {19 getServer().start();20 }catch(LifecycleException e) {21 log.fatal(sm.getString("catalina.serverStartFail"), e);22 try {23 getServer().destroy();24 }catch (LifecycleException e1) {25 log.debug("destroy()failed for failed Server ", e1);26 }27 return;28 }2930 long t2= System.nanoTime();31 if(log.isInfoEnabled()){32 log.info("Serverstartup in " + ((t2 - t1) / 1000000) + " ms");33 }3435 // Register shutdown hook36 if(useShutdownHook) {37 if (shutdownHook ==null) {38 shutdownHook =new CatalinaShutdownHook();39 }40 Runtime.getRuntime().addShutdownHook(shutdownHook);4142 // If JULI is beingused, disable JULI's shutdown hook since43 // shutdown hooks run in parallel and log messages may belost44 // if JULI's hook completes before theCatalinaShutdownHook()45 LogManager logManager =LogManager.getLogManager();46 if (logManager instanceofClassLoaderLogManager) {47 ((ClassLoaderLogManager)logManager).setUseShutdownHook(48 false);49 }50 }5152 if(await) {53 await();54 stop();55 }56 }
这里最主要的是调用StandardServer对象的start方法。
经过以上分析发现,在解析xml产生相应一系列对象后会顺序调用StandardServer对象的init、start方法,这里将会涉及到Tomcat的容器生命周期(Lifecycle),关于这点留到下一篇文章中分析。
Tomcat7启动分析(四)各组件init、start方法调用
在正常启动Tomcat7的情况下,上篇文章分析到了执行org.apache.catalina.core.StandardServer的init和start方法这儿,那么就来看看这两个方法里面到底干了些什么。
但是在StandardServer类里面并没有发现这两个方法:
由此推知这两方法必定是在该类的父类中已实现了,在StandardServer类的父类LifecycleMBeanBase类的父类LifecycleBase类里面终于找到了这两个方法的实现,下面先来看下init方法:
1 @Override 2 publicfinalsynchronizedvoid init() throws LifecycleException { 3 if (!state.equals(LifecycleState.NEW)) { 4 invalidTransition(Lifecycle.BEFORE_INIT_EVENT); 5 } 6 setStateInternal(LifecycleState.INITIALIZING, null, false); 7 8 try { 9 initInternal();10 } catch (Throwable t) {11 ExceptionUtils.handleThrowable(t);12 setStateInternal(LifecycleState.FAILED, null, false);13 thrownew LifecycleException(14 sm.getString("lifecycleBase.initFail",toString()), t);15 }1617 setStateInternal(LifecycleState.INITIALIZED, null, false);18 }19 20 21 protectedabstractvoid initInternal() throws LifecycleException;
先将干扰程序阅读视线的setStateInternal方法调用忽略掉(下一篇文章会详细讲解该方法),发现这里面就做了一件事情,调用了一下接下来定义的抽象方法initInternal()(第21行)。实际上看下LifecycleBase的实现类就会发现,所有的组件类几乎都继承了LifecycleBase类,所以这些组件类一般只会有initInternal方法的定义。(这里所说的组件类就是前面我们分析Digester解析时发现的StandardServer、StandardService、StandardEngine、StandardHost、StandardContext等类)
这里所说的组件可以将其理解为我们最开始分析server.xml时xml文件里的各个节点,父子关系也即xml文件里的父子节点。浏览下LifecycleBase的子类就会发现节点的实现类都是这个类的子类(记住这点,后面会提到)。
同样分析start方法:
1 /** 2 * {@inheritDoc} 3 */ 4 @Override 5 publicfinalsynchronizedvoid start() throws LifecycleException { 6 7 if (LifecycleState.STARTING_PREP.equals(state) || 8 LifecycleState.STARTING.equals(state) || 9 LifecycleState.STARTED.equals(state)) {10 11 if (log.isDebugEnabled()) {12 Exception e = new LifecycleException();13 log.debug(sm.getString("lifecycleBase.alreadyStarted",14 toString()), e);15 } elseif (log.isInfoEnabled()) {16 log.info(sm.getString("lifecycleBase.alreadyStarted",17 toString()));18 }19 20 return;21 }22 23 if (state.equals(LifecycleState.NEW)) {24 init();25 } elseif (state.equals(LifecycleState.FAILED)){26 stop();27 } elseif (!state.equals(LifecycleState.INITIALIZED) &&28 !state.equals(LifecycleState.STOPPED)) {29 invalidTransition(Lifecycle.BEFORE_START_EVENT);30 }3132 setStateInternal(LifecycleState.STARTING_PREP, null, false);3334 try {35 startInternal();36 } catch (Throwable t) {37 ExceptionUtils.handleThrowable(t);38 setStateInternal(LifecycleState.FAILED, null, false);39 thrownew LifecycleException(40 sm.getString("lifecycleBase.startFail",toString()), t);41 }4243 if (state.equals(LifecycleState.FAILED) ||44 state.equals(LifecycleState.MUST_STOP)) {45 stop();46 } else {47 // Shouldn't be necessary but acts as a check that sub-classes are48 // doing what they are supposed to.49 if (!state.equals(LifecycleState.STARTING)) {50 invalidTransition(Lifecycle.AFTER_START_EVENT);51 }52 53 setStateInternal(LifecycleState.STARTED, null, false);54 }55 }565758 /**59 * Sub-classes must ensure that the state is changed to60 * {@link LifecycleState#STARTING} during the execution of this method.61 * Changing state will trigger the {@link Lifecycle#START_EVENT} event.62 * 63 * If a component fails to start it may either throw a64 * {@link LifecycleException} which will cause it's parent to fail to start65 * or it can place itself in the error state in which case {@link #stop()}66 * will be called on the failed component but the parent component will67 * continue to start normally.68 * 69 * @throws LifecycleException70 */71 protectedabstractvoid startInternal() throws LifecycleException;
第7到21行是start功能的前置校验,这里如果发现start方法已经调用过了,将会记录日志并直接返回。第23到30行如果发现start放的需要做的前置方法没有调用完,或者调用出错,将会先调用这些前置方法。第32行暂时先不管,不影响程序阅读,第35行是该方法的实质,将会调用本类中定义的抽象方法startInternal()(第71行)。下面的代码同上述一样,都是一些start方法调用过程中可能出现的错误的错误处理。
从以上init和start方法的定义可以看到这两个方法最终将会调用子类中定义的initInternal和startInternal。
接回本文开头,一开始在找StandardServer类中init和start方法的定义,看完了上面的分析发现最终会调用StandardServer类的initInternal和startInternal两个方法。那这两个方法里面干了些什么?
initInternal方法:
1 /** 2 * Invoke a pre-startup initialization. This is used to allow connectors 3 * to bind to restricted ports under Unix operating environments. 4 */ 5 @Override 6 protectedvoid initInternal() throws LifecycleException { 7 8 super.initInternal(); 910 // Register global String cache11 // Note although the cache is global, if there are multiple Servers12 // present in the JVM (may happen when embedding) then the same cache13 // will be registered under multiple names14 onameStringCache = register(new StringCache(), "type=StringCache");1516 // Register the MBeanFactory17 MBeanFactory factory = new MBeanFactory();18 factory.setContainer(this);19 onameMBeanFactory = register(factory, "type=MBeanFactory");20 21 // Register the naming resources22 globalNamingResources.init();23 24 // Populate the extension validator with JARs from common and shared25 // class loaders26 if (getCatalina() != null) {27 ClassLoader cl = getCatalina().getParentClassLoader();28 // Walk the class loader hierarchy. Stop at the system class loader.29 // This will add the shared (if present) and common class loaders30 while (cl != null && cl != ClassLoader.getSystemClassLoader()) {31 if (cl instanceof URLClassLoader) {32 URL[] urls = ((URLClassLoader) cl).getURLs();33 for (URL url : urls) {34 if (url.getProtocol().equals("file")) {35 try {36 File f = new File (url.toURI());37 if (f.isFile() &&38 f.getName().endsWith(".jar")) {39 ExtensionValidator.addSystemResource(f);40 }41 } catch (URISyntaxException e) {42 // Ignore43 } catch (IOException e) {44 // Ignore45 }46 }47 }48 }49 cl = cl.getParent();50 }51 }52 // Initialize our defined Services53 for (int i = 0; i < services.length; i++) {54 services[i].init();55 }56 }
init方法里面做了好几件事情,牵涉的话题比较多,这里重点关注最后第53到55行的代码,这里将循环调用Server类里内置的Service数组的init方法。
startInternal方法:
1 /** 2 * Start nested components ({@link Service}s) and implement the requirements 3 * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}. 4 * 5 * @exception LifecycleException if this component detects a fatal error 6 * that prevents this component from being used 7 */ 8 @Override 9 protectedvoid startInternal() throws LifecycleException {1011 fireLifecycleEvent(CONFIGURE_START_EVENT, null);12 setState(LifecycleState.STARTING);1314 globalNamingResources.start();15 16 // Start our defined Services17 synchronized (services) {18 for (int i = 0; i < services.length; i++) {19 services[i].start();20 }21 }22 }
重点关注第17到21行,同上一段所分析的代码类似,将循环调用Sever类里内置的Service数组的start方法。
那么这里提到的Service的对象到底是什么?
上篇文章分析Digester时提到“经过对xml文件的解析将会产生org.apache.catalina.core.StandardServer、org.apache.catalina.core.StandardService、org.apache.catalina.connector.Connector、org.apache.catalina.core.StandardEngine、org.apache.catalina.core.StandardHost、org.apache.catalina.core.StandardContext等等一系列对象,这些对象从前到后前一个包含后一个对象的引用(一对一或一对多的关系)。”
所以正常情况下这里的Service将会是org.apache.catalina.core.StandardService的对象(该代码见org.apache.catalina.startup.Catalina类的339到341行)。
所以按上面的分析,接下来将会调用StandardService类的init和start方法,实际上这个类也是LifecycleBase类的子类,所以最终的也会调用本类中的initInternal和startInternal方法。
initInternal方法:
1 /** 2 * Invoke a pre-startup initialization. This is used to allow connectors 3 * to bind to restricted ports under Unix operating environments. 4 */ 5 @Override 6 protectedvoid initInternal() throws LifecycleException { 7 8 super.initInternal(); 9 10 if (container != null) {11 container.init();12 }1314 // Initialize any Executors15 for (Executor executor : findExecutors()) {16 if (executor instanceof LifecycleMBeanBase) {17 ((LifecycleMBeanBase) executor).setDomain(getDomain());18 }19 executor.init();20 }2122 // Initialize our defined Connectors23 synchronized (connectors) {24 for (Connector connector : connectors) {25 try {26 connector.init();27 } catch (Exception e) {28 String message = sm.getString(29 "standardService.connector.initFailed", connector);30 log.error(message, e);3132 if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"))33 thrownew LifecycleException(message);34 }35 }36 }37 }
这里将会调用Service下的各类子组件中的init方法。
startInternal方法:
1 /** 2 * Start nested components ({@link Executor}s, {@link Connector}s and 3 * {@link Container}s) and implement the requirements of 4 * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. 5 * 6 * @exception LifecycleException if this component detects a fatal error 7 * that prevents this component from being used 8 */ 9 @Override10 protectedvoid startInternal() throws LifecycleException {1112 if(log.isInfoEnabled())13 log.info(sm.getString("standardService.start.name", this.name));14 setState(LifecycleState.STARTING);1516 // Start our defined Container first17 if (container != null) {18 synchronized (container) {19 container.start();20 }21 }2223 synchronized (executors) {24 for (Executor executor: executors) {25 executor.start();26 }27 }2829 // Start our defined Connectors second30 synchronized (connectors) {31 for (Connector connector: connectors) {32 try {33 // If it has already failed, don't try and start it34 if (connector.getState() != LifecycleState.FAILED) {35 connector.start();36 }37 } catch (Exception e) {38 log.error(sm.getString(39 "standardService.connector.startFailed",40 connector), e);41 }42 }43 }44 }
同理,将会调用service下各类子组件中的start方法。
这里不再继续向下分析,建议有兴趣的同学逐一分析Review一遍,碰到组件里面嵌套的变量不知道具体实现类的就从上篇文章里面提到的createStartDigester那边开始找起,这里不能直接找到的就在里面提到的new*RuleSet的addRuleInstances方法里面找。通过这种调用将会最终执行完所有在server.xml里配置的节点的实现类中initInternal和startInternal方法。
最后上面提到的org.apache.catalina.core.StandardServer、org.apache.catalina.core.StandardService、org.apache.catalina.connector.Connector、org.apache.catalina.core.StandardEngine、org.apache.catalina.core.StandardHost、org.apache.catalina.core.StandardContext等等组件的这两个方法都会调用到。
就这样,Tomcat7在内存中为这一连串组件产生对象,建立对象调用关系,调用它们各自的初始化和启动方法,启动的总体过程就介绍完了,这些对象start之后将会响应客户端的请求,为用户服务了。当然,这里还没有涉及到对于具体的发布到tomcat里面的没有应用的载入过程,web应用中配置的servlet、filter、listener等的载入、启动服务过程,浏览器发起的一个请求如何经过Tomcat内各组件的流转调用到具体应用里去的,这一系列问题都还没谈到。因为Tomcat本身庞大繁杂,需要找一个视角切入进去,为了叙述的简单,先从整体上对Tomcat内包含的各组件产生机制有一个大体轮廓的了解,这样为后面的介绍提供一个统一的背景。
下一篇文章将分析本文开头遗留的一个问题——setStateInternal方法的作用,以及Tomcat中的Lifecycle实现原理。
Tomcat7启动分析(五)Lifecycle机制和实现原理
在上篇文章分析Tomcat7的各组件的init、start方法时经常会看到有一个setStateInternal方法的调用,在查看LifecycleBase类及其它各组件的源码时会在多处看到这个方法的调用,这篇文章就来说说这方法,以及与这个方法相关的Tomcat的Lifecycle机制和实现原理。
上篇文章里谈到Tomcat7的各组件的父类LifecycleBase类,该类实现了接口org.apache.catalina.Lifecycle,下面是这个接口里定义的常量和方法:
细心的读者会发现,上篇文章里提到的init和start方法实际上是在这个接口里面定义好的,也正因为有各组件最终都会实现这个接口作为前提条件,所以才能支持组件内部的initInternal、startInternal方法内对于子组件(组件里面嵌套的子组件都是以接口的形式定义的,但这些接口都会以Lifecycle作为父接口)的init和start方法的调用。通过这种方式,只要调用了最外层的Server组件的init和start方法,就可以将Tomcat内部的各级子组件初始化和启动起来。我叫这种方式为链式调用。实际上关于Tomcat的关闭机制也是通过这种方式一步步调用各层组件的stop方法的。这里不再展开叙述,留待读者自己研究研究吧。
Lifecycle接口中的这些字符串常量定义主要用于事件类型的定义,先按下不表,文章后面会提到。
重点看下面三个方法:
1 /** 2 * Add a LifecycleEvent listener to this component. 3 * 4 * @param listener The listener to add 5 */ 6 publicvoid addLifecycleListener(LifecycleListener listener);//给该组将添加一个监听器 7 8 9 /**10 * Get the life cycle listeners associated with this life cycle. If this11 * component has no listeners registered, a zero-length array is returned.12 */13 public LifecycleListener[] findLifecycleListeners();//获取该组件所有已注册的监听器141516 /**17 * Remove a LifecycleEvent listener from this component.18 *19 * @param listener The listener to remove20 */21 publicvoid removeLifecycleListener(LifecycleListener listener);//删除该组件中的一个监听器
这三个方法的作用在代码的注释里简要说明了一下。这三个方法涉及org.apache.catalina.LifecycleListener接口,那么就看下这个接口的定义:
1publicinterface LifecycleListener { 2 3 4 /** 5 * Acknowledge the occurrence of the specified event. 6 * 7 * @param event LifecycleEvent that has occurred 8 */ 9 publicvoid lifecycleEvent(LifecycleEvent event);101112 }
如此简单,只有一个方法,这个方法用作某个事件(org.apache.catalina.LifecycleEvent)产生时通知当前监听器的实现类,具体针对该事件如何处理由监听器实现类自己决定。
看下LifecycleEvent的实现:
1publicfinalclass LifecycleEvent extends EventObject { 2 3 privatestaticfinallong serialVersionUID = 1L; 4 5 6 // ----------------------------------------------------------- Constructors 7 8 /** 9 * Construct a new LifecycleEvent with the specified parameters.10 *11 * @param lifecycle Component on which this event occurred12 * @param type Event type (required)13 * @param data Event data (if any)14 */15 public LifecycleEvent(Lifecycle lifecycle, String type, Object data) {1617 super(lifecycle);18 this.type = type;19 this.data = data;20 }212223 // ----------------------------------------------------- Instance Variables242526 /**27 * The event data associated with this event.28 */29 private Object data = null;303132 /**33 * The event type this instance represents.34 */35 private String type = null;363738 // ------------------------------------------------------------- Properties394041 /**42 * Return the event data of this event.43 */44 public Object getData() {4546 return (this.data);4748 }495051 /**52 * Return the Lifecycle on which this event occurred.53 */54 public Lifecycle getLifecycle() {5556 return (Lifecycle) getSource();5758 }596061 /**62 * Return the event type of this event.63 */64 public String getType() {6566 return (this.type);6768 }697071 }
这个类也很简单,data和type作为类的内置实例变量,唯一特别是使用了jdk内置的java.util.EventObject作为父类来支持事件定义,这里在事件构造函数中将org.apache.catalina.Lifecycle类的实例lifecycle作为事件源,保存lifecycle对象的引用,并提供了getLifecycle方法返回这个引用。
那么Tomcat中是如何实现关于这些事件的监听以及通知的呢?
在本文开头提到的LifecycleBase类中第47行定义了一个实例变量lifecycle,正是通过该变量来注册组件上定义的各类监听器的。留心一下lifecycle这个实例变量,它并不是org.apache.catalina.Lifecycle类的实例,而是org.apache.catalina.util.LifecycleSupport类的实例。正是这个工具类提供了事件监听和事件通知的功能。
先看下实际代码中是如何给组件发布时间通知的,看下前面文章中曾经提到过的org.apache.catalina.core.StandardServer类的startInternal方法:
1 protectedvoid startInternal() throws LifecycleException { 2 3 fireLifecycleEvent(CONFIGURE_START_EVENT, null); 4 setState(LifecycleState.STARTING); 5 6 globalNamingResources.start(); 7 8 // Start our defined Services 9 synchronized (services) {10 for (int i = 0; i < services.length; i++) {11 services[i].start();12 }13 }14 }
我们前面已经分析过第9到13行代码,这里看下第3行,它调用了父类org.apache.catalina.util.LifecycleBase里的fireLifecycleEvent方法,这里的CONFIGURE_START_EVENT就是本文最开始Lifecycle接口中定义的常量,这里表示发布了一个start配置事件。
org.apache.catalina.util.LifecycleBase类中的fireLifecycleEvent方法里调用的是org.apache.catalina.util.LifecycleSupport类fireLifecycleEvent方法,该方法代码如下:
1 publicvoid fireLifecycleEvent(String type, Object data) {23 LifecycleEvent event = new LifecycleEvent(lifecycle, type, data);4 LifecycleListener interested[] = listeners;5 for (int i = 0; i < interested.length; i++)6 interested[i].lifecycleEvent(event);78 }
这里通过传进来的两个参数构造一个LifecycleEvent对象,然后向注册到组件中的所有监听器发布这个新构造的事件对象。
这里有个疑问,到底什么时候向组件里注册监听器的呢?
还是以StandardServer举例,在前面讲Digester的使用时,org.apache.catalina.startup.Catalina类的createStartDigester方法有这么一段代码:
1 // Configure the actions we will be using 2 digester.addObjectCreate("Server", 3 "org.apache.catalina.core.StandardServer", 4 "className"); 5 digester.addSetProperties("Server"); 6 digester.addSetNext("Server", 7 "setServer", 8 "org.apache.catalina.Server"); 910 digester.addObjectCreate("Server/GlobalNamingResources",11 "org.apache.catalina.deploy.NamingResources");12 digester.addSetProperties("Server/GlobalNamingResources");13 digester.addSetNext("Server/GlobalNamingResources",14 "setGlobalNamingResources",15 "org.apache.catalina.deploy.NamingResources");1617 digester.addObjectCreate("Server/Listener",18 null, // MUST be specified in the element19 "className");20 digester.addSetProperties("Server/Listener");21 digester.addSetNext("Server/Listener",22 "addLifecycleListener",23 "org.apache.catalina.LifecycleListener");
第17到24行,将调用org.apache.catalina.core.StandardServer类的addLifecycleListener方法,将根据server.xml中配置的Server节点下的Listener节点所定义的className属性构造对象实例,并作为addLifecycleListener方法的入参。所有的监听器都会实现上面提到的org.apache.catalina.LifecycleListener接口。Server节点下的Listener节点有好几个,这里以org.apache.catalina.core.JasperListener举例。
在构造完org.apache.catalina.core.JasperListener类的对象之后,调用addLifecycleListener方法,这个方法并没有直接在org.apache.catalina.core.StandardServer类中定义,而是在它的父类org.apache.catalina.util.LifecycleBase中:
1 @Override2 publicvoid addLifecycleListener(LifecycleListener listener) {3 lifecycle.addLifecycleListener(listener);4 }
这里调用的是前述的org.apache.catalina.util.LifecycleSupport类的addLifecycleListener方法:
1 /** 2 * Add a lifecycle event listener to this component. 3 * 4 * @param listener The listener to add 5 */ 6 publicvoid addLifecycleListener(LifecycleListener listener) { 7 8 synchronized (listenersLock) { 9 LifecycleListener results[] =10 new LifecycleListener[listeners.length + 1];11 for (int i = 0; i < listeners.length; i++)12 results[i] = listeners[i];13 results[listeners.length] = listener;14 listeners = results;15 }1617 }
LifecycleSupport作为一个工具类,内部保存了一个监听器对象实例数组,见该类的第68行:
1 /**2 * The set of registered LifecycleListeners for event notifications.3 */4 private LifecycleListener listeners[] = new LifecycleListener[0];
上面的addLifecycleListener方法内部实现的是同步给该数组增加一个监听器对象。
看到这里应该大体明白Tomcat中的Lifecycle是怎么回事了,总的来说就是通过一个工具类LifecycleSupport,调用该类的addLifecycleListener方法增加监听器,需要发布事件时还是调用该工具类的fireLifecycleEvent方法,将事件发布给组件上注册的所有监听器,由监听器内部实现来决定是否处理该事件。
以前面看到的一个监听器org.apache.catalina.core.JasperListener举例:
1publicclass JasperListener 2 implements LifecycleListener { 3 4 privatestaticfinal Log log = LogFactory.getLog(JasperListener.class); 5 6 /** 7 * The string manager for this package. 8 */ 9 protectedstaticfinal StringManager sm =10 StringManager.getManager(Constants.Package);111213 // ---------------------------------------------- LifecycleListener Methods141516 /**17 * Primary entry point for startup and shutdown events.18 *19 * @param event The event that has occurred20 */21 @Override22 publicvoid lifecycleEvent(LifecycleEvent event) {2324 if (Lifecycle.BEFORE_INIT_EVENT.equals(event.getType())) {25 try {26 // Set JSP factory27 Class.forName("org.apache.jasper.compiler.JspRuntimeContext",28 true,29 this.getClass().getClassLoader());30 } catch (Throwable t) {31 ExceptionUtils.handleThrowable(t);32 // Should not occur, obviously33 log.warn("Couldn't initialize Jasper", t);34 }35 // Another possibility is to do directly:36 // JspFactory.setDefaultFactory(new JspFactoryImpl());37 }3839 }404142 }
重点关注来自接口的lifecycleEvent方法的实现,可以看到这个监听器只关心事件类型为BEFORE_INIT_EVENT的事件,如果发布了该事件,才会做后续处理(这里会产生一个org.apache.jasper.compiler.JspRuntimeContext对象)。
Lifecycle相关类UML关系图:
如果对设计模式比较熟悉的话会发现Tomcat的Lifecycle使用的是观察者模式:LifecycleListener代表的是抽象观察者,它定义一个lifecycleEvent方法,而实现该接口的监听器是作为具体的观察者。Lifecycle 接口代表的是抽象主题,它定义了管理观察者的方法和它要所做的其它方法。而各组件代表的是具体主题,它实现了抽象主题的所有方法。通常会由具体主题保存对具体观察者对象有用的内部状态;在这种内部状态改变时给其观察者发出一个通知。Tomcat对这种模式做了改进,增加了另外两个工具类:LifecycleSupport、LifecycleEvent,它们作为辅助类扩展了观察者的功能。LifecycleEvent中定义了事件类别,不同的事件在具体观察者中可区别处理,更加灵活。LifecycleSupport 类代理了所有具体主题对观察者的管理,将这个管理抽出来统一实现,以后如果修改只要修改 LifecycleSupport 类就可以了,不需要去修改所有具体主题,因为所有具体主题的对观察者的操作都被代理给 LifecycleSupport 类了。
事件的发布使用的是推模式,即每发布一个事件都会通知主题的所有具体观察者,由各观察者再来决定是否需要对该事件进行后续处理。
下面再来看看本文一开头所说的setStateInternal方法,以org.apache.catalina.core.StandardServer类为例,上面看到的startInternal方法中第4行:setState(LifecycleState.STARTING);
它调用了父类org.apache.catalina.util.LifecycleBase中的setState方法:
1 /** 2 * Provides a mechanism for sub-classes to update the component state. 3 * Calling this method will automatically fire any associated 4 * {@link Lifecycle} event. It will also check that any attempted state 5 * transition is valid for a sub-class. 6 * 7 * @param state The new state for this component 8 */ 9 protectedsynchronizedvoid setState(LifecycleState state)10 throws LifecycleException {11 setStateInternal(state, null, true);12 }
在这个类里面调用本类的一个同步方法setStateInternal:
1 privatesynchronizedvoid setStateInternal(LifecycleState state, 2 Object data, boolean check) throws LifecycleException { 3 4 if (log.isDebugEnabled()) { 5 log.debug(sm.getString("lifecycleBase.setState", this, state)); 6 } 7 8 if (check) { 9 // Must have been triggered by one of the abstract methods (assume10 // code in this class is correct)11 // null is never a valid state12 if (state == null) {13 invalidTransition("null");14 // Unreachable code - here to stop eclipse complaining about15 // a possible NPE further down the method16 return;17 }18 19 // Any method can transition to failed20 // startInternal() permits STARTING_PREP to STARTING21 // stopInternal() permits STOPPING_PREP to STOPPING and FAILED to22 // STOPPING23 if (!(state == LifecycleState.FAILED ||24 (this.state == LifecycleState.STARTING_PREP &&25 state == LifecycleState.STARTING) ||26 (this.state == LifecycleState.STOPPING_PREP &&27 state == LifecycleState.STOPPING) ||28 (this.state == LifecycleState.FAILED &&29 state == LifecycleState.STOPPING))) {30 // No other transition permitted31 invalidTransition(state.name());32 }33 }34 35 this.state = state;36 String lifecycleEvent = state.getLifecycleEvent();37 if (lifecycleEvent != null) {38 fireLifecycleEvent(lifecycleEvent, data);39 }40 }
重点关注第35到39行,第35行将入参LifecycleState实例赋值给本类中的实例变量保存起来,第36行取出LifecycleState实例的LifecycleEvent事件,如果该事件非空,则调用fireLifecycleEvent方法发布该事件。
既然看到了LifecycleState类,就看下LifecycleState类的定义:
1publicenum LifecycleState { 2 NEW(false, null), 3 INITIALIZING(false, Lifecycle.BEFORE_INIT_EVENT), 4 INITIALIZED(false, Lifecycle.AFTER_INIT_EVENT), 5 STARTING_PREP(false, Lifecycle.BEFORE_START_EVENT), 6 STARTING(true, Lifecycle.START_EVENT), 7 STARTED(true, Lifecycle.AFTER_START_EVENT), 8 STOPPING_PREP(true, Lifecycle.BEFORE_STOP_EVENT), 9 STOPPING(false, Lifecycle.STOP_EVENT),10 STOPPED(false, Lifecycle.AFTER_STOP_EVENT),11 DESTROYING(false, Lifecycle.BEFORE_DESTROY_EVENT),12 DESTROYED(false, Lifecycle.AFTER_DESTROY_EVENT),13 FAILED(false, null),14 MUST_STOP(true, null),15 MUST_DESTROY(false, null);1617 privatefinalboolean available;18 privatefinal String lifecycleEvent;1920 private LifecycleState(boolean available, String lifecycleEvent) {21 this.available = available;22 this.lifecycleEvent = lifecycleEvent;23 }2425 /**26 * May the public methods other than property getters/setters and lifecycle27 * methods be called for a component in this state? It returns28 * <code>true</code> for any component in any of the following states:29 * <ul>30 * <li>{@link #STARTING}</li>31 * <li>{@link #STARTED}</li>32 * <li>{@link #STOPPING_PREP}</li>33 * <li>{@link #MUST_STOP}</li>34 * </ul>35 */36 publicboolean isAvailable() {37 return available;38 }3940 /**41 *42 */43 public String getLifecycleEvent() {44 return lifecycleEvent;45 }46 }
这个类在之前的Tomcat4和Tomcat5中都没有看到,可能是Tomcat7里面新定义的吧,就是一个枚举,内嵌了两个实例变量,一个布尔值表示是否可用,一个字符串表示是事件类型,看已经定义的枚举值里面发现这个字符串要么不设值,要么就是Lifecycle类中定义好的字符串常量。这个类实际上就是对Lifecycle类中定义好的字符串常量做了另外一层封装。
再说回开头在各组件代码中经常会看到的setStateInternal方法的调用,实际上就是向该组件中已注册的监听器发布一个事件。