Tomcat source code analysis 3: the source code analysis of Tomcat startup loading process

Posted by scrupul0us on Fri, 25 Oct 2019 07:03:57 +0200

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.

Thank you for watching, if you have any valuable comments, please put forward in time, thank you. Welcome to wechat public account: source bay ~

Topics: Programming Tomcat Java Apache Windows