Source code analysis of Tomcat start loading process (1)
Today, I will share the source code method to explain the loading process of Tomcat startup. For the architecture of tomcat, please refer to the article "Tomcat source code analysis II: first look at the overall architecture of Tomcat".
First look at the application
In the article "Servlet and Tomcat running example", I recorded in detail how Tomcat started a Servlet program. Among them, the sixth step is to start tomcat, that is, to execute startup.bat on the windows system and the startup.sh script on the linux operating system. So, let's start from this script, go to Tomcat and see how it starts? Here, we take startup.sh as an example. Startup.bat on windows is similar.
What is the content of startup.sh?
Let's first look at the content of tomcat's startup script, startup.sh, and its script content (some comments are omitted), as follows:
#!/bin/sh # ----------------------------------------------------------------------------- # Start Script for the CATALINA Server # ----------------------------------------------------------------------------- # Better OS/400 detection: see Bugzilla 31132 os400=false case "`uname`" in OS400*) os400=true;; esac # resolve links - $0 may be a softlink PRG="$0" while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`/"$link" fi done PRGDIR=`dirname "$PRG"` EXECUTABLE=catalina.sh # Check that target executable exists if $os400; then # -x will Only work on the os400 if the files are: # 1. owned by the user # 2. owned by the PRIMARY group of the user # this will not work if the user belongs in secondary groups eval else if [ ! -x "$PRGDIR"/"$EXECUTABLE" ]; then echo "Cannot find $PRGDIR/$EXECUTABLE" echo "The file is absent or does not have execute permission" echo "This file is needed to run this program" exit 1 fi fi exec "$PRGDIR"/"$EXECUTABLE" start "$@"
Extract the main sentences:
PRGDIR=`dirname "$PRG"` EXECUTABLE=catalina.sh exec "$PRGDIR"/"$EXECUTABLE" start "$@"
In short, the execution content of the script is to call the catalina.sh script. Now, let's continue to see the catalina.sh script.
catalina.sh script
Because the catalina.sh script has a lot of content, here we extract some important content, and then explain its purpose:
Then it briefly describes the functions in catalina.sh: to complete the environment check, environment initialization, parameter initialization, and startup steps. Notice the content shown in the green box in the figure above. You can see that the org.apache.catalina.startup.Bootstrap class is called and executed, and the command instruction transmitted in the past is start.
Return to Java code
What does the Bootstrap class do?
Next, let's explore the Bootstrap class with these questions:
- What does the Bootstrap class do after receiving the start instruction?
- What are the responsibilities of the Bootstrap class during startup?
Next, let's discuss the source code of Tomcat with the above questions. Let's first look at the main method of the Bootstrap class:
public static void main(String args[]) { synchronized (daemonLock) { if (daemon == null) { // Don't set daemon until init() has completed Bootstrap bootstrap = new Bootstrap(); try { bootstrap.init(); } catch (Throwable t) { handleThrowable(t); t.printStackTrace(); return; } daemon = bootstrap; } else { Thread.currentThread().setContextClassLoader(daemon.catalinaLoader); } } try { String command = "start"; if (args.length > 0) { command = args[args.length - 1]; } if (command.equals("startd")) { args[args.length - 1] = "start"; daemon.load(args); daemon.start(); } else if (command.equals("stopd")) { args[args.length - 1] = "stop"; daemon.stop(); } else if (command.equals("start")) { daemon.setAwait(true); daemon.load(args); daemon.start(); if (null == daemon.getServer()) { System.exit(1); } } else if (command.equals("stop")) { daemon.stopServer(args); } else if (command.equals("configtest")) { daemon.load(args); if (null == daemon.getServer()) { System.exit(1); } System.exit(0); } else { log.warn("Bootstrap: command \"" + command + "\" does not exist."); } } catch (Throwable t) { // Unwrap the Exception for clearer error reporting if (t instanceof InvocationTargetException && t.getCause() != null) { t = t.getCause(); } handleThrowable(t); t.printStackTrace(); System.exit(1); } }
From this code, we can see that it mainly implements two functions:
- Initializing a daemon variable
- Load the parameters passed by catalina.sh, analyze the instructions passed by catalina.sh, execute the program according to the instructions, and control the start and stop of the daemon.
bootstrap.init(); what's the operation?
For the above two functions, let's go to the init() method to see what operations are available. First, let's look at the code of the init() method:
public void init() throws Exception { initClassLoaders(); Thread.currentThread().setContextClassLoader(catalinaLoader); SecurityClassLoad.securityClassLoad(catalinaLoader); // Load our startup class and call its process() method if (log.isDebugEnabled()) log.debug("Loading startup class"); Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina"); Object startupInstance = startupClass.getConstructor().newInstance(); // Set the shared extensions class loader if (log.isDebugEnabled()) log.debug("Setting startup class properties"); String methodName = "setParentClassLoader"; Class<?> paramTypes[] = new Class[1]; paramTypes[0] = Class.forName("java.lang.ClassLoader"); Object paramValues[] = new Object[1]; paramValues[0] = sharedLoader; Method method = startupInstance.getClass().getMethod(methodName, paramTypes); method.invoke(startupInstance, paramValues); catalinaDaemon = startupInstance; }
In the init() method, the first executed method, initClassLoaders(), is used to initialize three class loaders. The code is as follows:
/** * Daemon reference. */ private Object catalinaDaemon = null; ClassLoader commonLoader = null; ClassLoader catalinaLoader = null; ClassLoader sharedLoader = null; private void initClassLoaders() { try { commonLoader = createClassLoader("common", null); if (commonLoader == null) { // no config file, default to this loader - we might be in a 'single' env. commonLoader = this.getClass().getClassLoader(); } catalinaLoader = createClassLoader("server", commonLoader); sharedLoader = createClassLoader("shared", commonLoader); } catch (Throwable t) { handleThrowable(t); log.error("Class loader creation threw exception", t); System.exit(1); } } private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception { String value = CatalinaProperties.getProperty(name + ".loader"); if ((value == null) || (value.equals(""))) return parent; value = replace(value); List<Repository> repositories = new ArrayList<>(); String[] repositoryPaths = getPaths(value); for (String repository : repositoryPaths) { // Check for a JAR URL repository try { @SuppressWarnings("unused") URL url = new URL(repository); repositories.add(new Repository(repository, RepositoryType.URL)); continue; } catch (MalformedURLException e) { // Ignore } // Local repository if (repository.endsWith("*.jar")) { repository = repository.substring (0, repository.length() - "*.jar".length()); repositories.add(new Repository(repository, RepositoryType.GLOB)); } else if (repository.endsWith(".jar")) { repositories.add(new Repository(repository, RepositoryType.JAR)); } else { repositories.add(new Repository(repository, RepositoryType.DIR)); } } return ClassLoaderFactory.createClassLoader(repositories, parent); }
// catalina.properties common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
- Common loader: according to the configuration of common.loader properties (via code CatalinaProperties.getProperty(name + ".loader"); read: catalina.properties), create a commonLoader class loader. By default, load ${catalina.base}/lib, ${catalina.base}/lib/.jar, ${catalina.home}/lib, ${catalina.home}/lib/.jar in four directories.
- catalinaLoader: create a catalinaLoader class loader according to the configuration of the server.loader property. Its parent class loads it as commonLoader. The default server.loader property is empty. Use commonLoader directly.
- Shareloader: create a shareloader class loader according to the configuration of the shared.loader property. Its parent class loads it as commonLoader. The default shared.loader property is empty. Use commonLoader directly.
After executing the initClassLoaders() method, call Thread.currentThread().setContextClassLoader(catalinaLoader); set the context class loader to catalinaLoader. From the above analysis, the context class loader is actually set to commonLoader, the parent of catalinaLoader.
The function of SecurityClassLoad.securityClassLoad(catalinaLoader) is to load some classes in advance if there is a SecurityManager.
After that, by using catalinaLoader to load the org.apache.catalina.startup.Catalina class, create the instance Catalina and use the reflection call method setParentClassLoader(), set the parentClassLoader property of Catalina instance to shareloader class loader (that is, commonLoader).
Finally, set the daemon to Bootstrap for the newly created instance. Next, look at the instruction handling under the main() method.
How are the command instructions delivered handled?
Let's look at the second half of the main() method, and paste the following code here:
try { String command = "start"; if (args.length > 0) { command = args[args.length - 1]; } if (command.equals("startd")) { args[args.length - 1] = "start"; daemon.load(args); daemon.start(); } else if (command.equals("stopd")) { args[args.length - 1] = "stop"; daemon.stop(); } else if (command.equals("start")) { daemon.setAwait(true); daemon.load(args); daemon.start(); if (null == daemon.getServer()) { System.exit(1); } } else if (command.equals("stop")) { daemon.stopServer(args); } else if (command.equals("configtest")) { daemon.load(args); if (null == daemon.getServer()) { System.exit(1); } System.exit(0); } else { log.warn("Bootstrap: command \"" + command + "\" does not exist."); } } catch (Throwable t) { // ... omitted }
As you can see, its default instruction is start, and then it can be divided into six instruction cases, namely, startd, stopd, start, stop, configtest and others, according to the received parameters. Here we mainly look at the execution logic of the start instruction.
- Day.setawait (true): what's the meaning of this code? Let's analyze it specifically:
/** * Set flag. * @param await <code>true</code> if the daemon should block * @throws Exception Reflection error */ public void setAwait(boolean await) throws Exception { Class<?> paramTypes[] = new Class[1]; paramTypes[0] = Boolean.TYPE; Object paramValues[] = new Object[1]; paramValues[0] = Boolean.valueOf(await); Method method = catalinaDaemon.getClass().getMethod("setAwait", paramTypes); method.invoke(catalinaDaemon, paramValues); }
The main function of this code is to call Catalina.setAwait(true) through reflection. The main purpose is to block the main thread after startup and wait for the stop command to arrive. If you do not set daemon.setAwait(true), the main thread exits directly after execution.
- **daemon.load(args) ** In the Catalina.load() method, the main function is to initialize the temp directory first, then initialize some system properties of naming, then obtain the server.xml configuration file, create the Digester instance, and start the operation of parsing the server.xml.
/** * Start a new server instance. */ public void load() { if (loaded) { return; } loaded = true; long t1 = System.nanoTime(); initDirs(); // Before digester - it may be needed initNaming(); // Set configuration source ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(Bootstrap.getCatalinaBaseFile(), getConfigFile())); File file = configFile(); // Create and execute our Digester Digester digester = createStartDigester(); try (ConfigurationSource.Resource resource = ConfigFileLoader.getSource().getServerXml()) { InputStream inputStream = resource.getInputStream(); InputSource inputSource = new InputSource(resource.getURI().toURL().toString()); inputSource.setByteStream(inputStream); digester.push(this); digester.parse(inputSource); } catch (Exception e) { log.warn(sm.getString("catalina.configFail", file.getAbsolutePath()), e); if (file.exists() && !file.canRead()) { log.warn(sm.getString("catalina.incorrectPermissions")); } return; } getServer().setCatalina(this); getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile()); getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile()); // Stream redirection initStreams(); // Start the new server try { getServer().init(); } catch (LifecycleException e) { if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) { throw new java.lang.Error(e); } else { log.error(sm.getString("catalina.initError"), e); } } long t2 = System.nanoTime(); if(log.isInfoEnabled()) { log.info(sm.getString("catalina.init", Long.valueOf((t2 - t1) / 1000000))); } }
- Day. Start(): start Tomcat
Start Tomcat by calling daemon.start(), as follows:
/** * Start the Catalina daemon. * @throws Exception Fatal start error */ public void start() throws Exception { if (catalinaDaemon == null) { init(); } Method method = catalinaDaemon.getClass().getMethod("start", (Class [])null); method.invoke(catalinaDaemon, (Object [])null); }
The program calls Catalina.start() to start Tomcat through reflection. Let's see the implementation logic of Catalina.start():
/** * Start a new server instance. */ public void start() { if (getServer() == null) { load(); } if (getServer() == null) { log.fatal(sm.getString("catalina.noServer")); return; } long t1 = System.nanoTime(); // Start the new server try { getServer().start(); } catch (LifecycleException e) { log.fatal(sm.getString("catalina.serverStartFail"), e); try { getServer().destroy(); } catch (LifecycleException e1) { log.debug("destroy() failed for failed Server ", e1); } return; } long t2 = System.nanoTime(); if(log.isInfoEnabled()) { log.info(sm.getString("catalina.startup", Long.valueOf((t2 - t1) / 1000000))); } // 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); } } if (await) { await(); stop(); } }
It can be seen that the program calls getServer().start() to start. The getServer() method returns a StandardServer class, and then it calls StandardServer.startInternal() method. In StandardServer, it calls StandardService.startInternal() method.
// StandardServer.java protected void startInternal() throws LifecycleException { fireLifecycleEvent(CONFIGURE_START_EVENT, null); setState(LifecycleState.STARTING); globalNamingResources.start(); // Start our defined Services synchronized (servicesLock) { for (int i = 0; i < services.length; i++) { services[i].start(); } } // ... omit some codes } 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 (engine != null) { synchronized (engine) { engine.start(); } } synchronized (executors) { for (Executor executor: executors) { executor.start(); } } mapperListener.start(); // Start our defined Connectors second synchronized (connectorsLock) { for (Connector connector: connectors) { // If it has already failed, don't try and start it if (connector.getState() != LifecycleState.FAILED) { connector.start(); } } } }
Notice why this is not the start() method, but the startInternal() method. The reason is that the StandardServer and StandService classes inherit the lifecycle mbeanbase class, while the lifecycle mbeanbase class inherits the lifecycle base class. Let's look at the start() method of the LifecycleBase class:
public final synchronized void start() throws LifecycleException { if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) || LifecycleState.STARTED.equals(state)) { if (log.isDebugEnabled()) { Exception e = new LifecycleException(); log.debug(sm.getString("lifecycleBase.alreadyStarted", toString()), e); } else if (log.isInfoEnabled()) { log.info(sm.getString("lifecycleBase.alreadyStarted", toString())); } return; } if (state.equals(LifecycleState.NEW)) { init(); } else if (state.equals(LifecycleState.FAILED)) { stop(); } else if (!state.equals(LifecycleState.INITIALIZED) && !state.equals(LifecycleState.STOPPED)) { invalidTransition(Lifecycle.BEFORE_START_EVENT); } try { setStateInternal(LifecycleState.STARTING_PREP, null, false); startInternal(); if (state.equals(LifecycleState.FAILED)) { // This is a 'controlled' failure. The component put itself into the // FAILED state so call stop() to complete the clean-up. stop(); } else if (!state.equals(LifecycleState.STARTING)) { // Shouldn't be necessary but acts as a check that sub-classes are // doing what they are supposed to. invalidTransition(Lifecycle.AFTER_START_EVENT); } else { setStateInternal(LifecycleState.STARTED, null, false); } } catch (Throwable t) { // This is an 'uncontrolled' failure so put the component into the // FAILED state and throw an exception. handleSubClassException(t, "lifecycleBase.startFail", toString()); } }
As you can see, when you call the start() method, you will eventually call the startInternal() method. In the next article, we will take a detailed look at what engine.start(), executor.start(), and connector.start() in StandardService.java have started respectively? Coming soon!
Wechat public account: source bay
Welcome to my wechat public account: source bay. This public account will share relevant source code and related development technology from time to time, grow together and make progress together.