namalzi

← All posts

Digging into Tomcat's Source 2: The Big Picture — Server, Service, Connector and the Four Containers

Tomcat · Published

A map for anyone opening the Tomcat source for the first time. We look at which class each server.xml element (Server, Service, Connector, Engine, Host) turns into, how the four containers Engine, Host, Context and Wrapper are linked as parents and children, why the connector (Coyote) and the servlet container (Catalina) are kept apart and joined by a single Adapter, and where Jasper, EL, JULI and Naming live — all checked against the real source and examples I actually ran.

What this post covers

This is the second post in "Digging into Tomcat's Source". In Part 1 we fetched and built the source and unfolded a map of the folders and of the main road a single request travels. This post is the big-picture map you spread out before reading the code line by line.

If you have used Tomcat, you have probably opened conf/server.xml and changed the port in <Connector port="8080">. But what are the <Server>, <Service>, <Engine> and <Host> written in that file, as far as the source is concerned? Using that configuration file as a bridge, this post answers these questions:

Item Details
Target Apache Tomcat
Version 12.0.0-M1 (main)
Commit 17aa1769b0 (2026-10-23)
Main classes startup/Catalina, core/StandardServer·StandardService, Container, core/ContainerBase·StandardEngine, connector/Connector·CoyoteAdapter, coyote/Adapter·ProtocolHandler

All line numbers in this post refer to this commit. Part 1 shows how to get the same commit and build the Docker image used for the examples. Paths start at the repository root, like java/org/apache/catalina/; when the text uses a short form such as core/StandardEngine, it means under org/apache/catalina/.

Since this post draws a map, it follows only the main lines. Each section points ahead to the post that reads that concept in detail ("we'll see this in Part N").

Terms to know first

A servlet container is a program that holds servlets (the classes you write by extending HttpServlet and filling in doGet()) and, when an HTTP request arrives, picks the right servlet and calls it. Creating the servlet (init()), calling it (service()) and cleaning it up (destroy()) is the container's job. That is exactly what Tomcat is.

Catalina is the name of the servlet-container part inside Tomcat. It is the package org.apache.catalina, and the same name shows up in catalina.sh, catalina.out and CATALINA_HOME.

Coyote is the name of the connector part. It reads bytes from a socket, interprets them as an HTTP request, and writes the response back out as bytes. Its package is org.apache.coyote. It does not know about servlets (Section 4).

Container has two meanings in this post. Broadly, it is the whole "servlet container" above; narrowly, it is the interface org.apache.catalina.Container. Engine, Host, Context and Wrapper all implement that interface, which is why we call them "the four containers." When this post writes it in code style, Container, it means the narrow sense.

Lifecycle is an interface with four methods: init(), start(), stop() and destroy(). Every component in this post, from Server down to Wrapper, implements it, and when a parent is start()ed its children are started in turn. How the states change is the subject of Part 3.

A pipeline and its valves form the processing chain attached to each container. When a request enters a container it passes through that container's valves in order, and the basic valve at the very end hands it to the next container. A <Valve> in server.xml plugs in here. We look at this closely in Part 8.

1. One server.xml is one tree of objects

The default server.xml

Strip the comments from the repository's conf/server.xml and the skeleton looks like this.

<Server port="8005" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <!-- ... (a few listeners) -->
  <GlobalNamingResources>
    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
  </GlobalNamingResources>
  <!-- ... -->
  <Service name="Catalina">
    <!-- ... -->
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    <!-- ... -->
    <Engine name="Catalina" defaultHost="localhost">
      <!-- ... -->
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <!-- ... -->
      </Realm>

      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
        <!-- ... -->
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />

      </Host>
    </Engine>
  </Service>
</Server>

conf/server.xml lines 22–154

The elements are nested layer inside layer: one Server holds a Service, the Service holds a Connector and an Engine, the Engine holds a Host. That nesting becomes, one for one, the parent–child relationship between Java objects.

The rules that turn XML into objects: Catalina.createStartDigester()

When you run catalina.sh start, it is eventually the org.apache.catalina.startup.Catalina class that reads server.xml (how we get there is covered in Part 3). The reader is Digester, Tomcat's own XML-to-object converter, and the conversion rules are gathered in this one method.

        digester.addObjectCreate("Server", "org.apache.catalina.core.StandardServer", "className");
        digester.addSetProperties("Server");
        digester.addSetNext("Server", "setServer", "org.apache.catalina.Server");
        // ...
        digester.addObjectCreate("Server/Service", "org.apache.catalina.core.StandardService", "className");
        digester.addSetProperties("Server/Service");
        digester.addSetNext("Server/Service", "addService", "org.apache.catalina.Service");
        // ...
        digester.addRule("Server/Service/Connector", new ConnectorCreateRule());
        digester.addSetProperties("Server/Service/Connector",
                new String[] { "executor", "sslImplementationName", "protocol" });
        digester.addSetNext("Server/Service/Connector", "addConnector", "org.apache.catalina.connector.Connector");
        // ...
        // Add RuleSets for nested elements
        digester.addRuleSet(new NamingRuleSet("Server/GlobalNamingResources/"));
        digester.addRuleSet(new EngineRuleSet("Server/Service/"));
        digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"));
        digester.addRuleSet(new ContextRuleSet("Server/Service/Engine/Host/"));

java/org/apache/catalina/startup/Catalina.java lines 513–592

Each element gets three rules.

  1. addObjectCreate(path, default class, "className") — when the XML reader meets the start tag at that path (Server/Service), it creates an object. If the element has a className attribute it uses that class, otherwise the default class.
  2. addSetProperties(path) — each attribute of the element goes into the setter of the same name. <Server port="8005"> becomes setPort(8005).
  3. addSetNext(path, method name, type) — at the end tag, the object just created is passed to that method of the object of the enclosing element. The Service goes into the Server's addService(), the Connector into the Service's addConnector().

Engine, Host and Context get the same treatment through separate bundles of rules (RuleSets). Host's has the same three lines plus a few more.

        digester.addObjectCreate(prefix + "Host", "org.apache.catalina.core.StandardHost", "className");
        digester.addSetProperties(prefix + "Host");
        digester.addRule(prefix + "Host", new CopyParentClassLoaderRule());
        digester.addRule(prefix + "Host",
                new LifecycleListenerRule("org.apache.catalina.startup.HostConfig", "hostConfigClass"));
        digester.addSetNext(prefix + "Host", "addChild", "org.apache.catalina.Container");

java/org/apache/catalina/startup/HostRuleSet.java lines 61–66

Note the line that attaches a listener called HostConfig to every Host. Later, this listener looks at the webapps/ folder and deploys the web apps (see "Easy to confuse" below and Part 10).

Put the rules in a table and server.xml lines up with the source row by row.

server.xml element Object created (default class) Method on the enclosing object Where the rule is
<Server> core/StandardServer Catalina.setServer() Catalina.java lines 513–515
<Service> core/StandardService Server.addService() Catalina.java lines 526–528
<Connector> connector/Connector Service.addConnector() Catalina.java lines 542–545, ConnectorCreateRule
<Engine> core/StandardEngine Service.setContainer() EngineRuleSet.java lines 61–65
<Host> core/StandardHost Engine.addChild() HostRuleSet.java lines 61–66
<Context> (only if written) core/StandardContext Host.addChild() ContextRuleSet.java lines 84–94
<Valve> the class given in className that container's addValve() each RuleSet

Only <Connector> uses ConnectorCreateRule instead of addObjectCreate, because it has to read the protocol attribute first and create the object with new Connector(protocolName) (ConnectorCreateRule.java lines 63–64). We look inside that constructor in Section 4. How Digester itself (tomcat/util/digester) runs the rules is covered in Part 3.

How server.xml elements become a tree of objects. The Server (StandardServer) holds a Service (StandardService) through addService. The Service holds many Connectors through addConnector and exactly one Engine (StandardEngine) through setContainer. The Connector picks Http11NioProtocol from its protocol attribute. The Engine has a Host (StandardHost) as a child through addChild, the Host has Contexts (StandardContext), and a Context has Wrappers (StandardWrapper). Contexts and Wrappers are created at start time from webapps and web.xml, not from server.xml. Blue boxes are elements written in server.xml; gray boxes are created outside server.xml.

Example 1: the tree right after reading server.xml, and right after starting

To check that this tree really appears, I read the default server.xml with the same Catalina class that catalina.sh uses and printed the tree. load() reads server.xml and goes as far as init(); start() starts the server. Only two web apps, ROOT and docs, were deployed.

All examples in this post were run in a Docker container (Eclipse Temurin JDK 21) with Tomcat 12.0.0-M1-dev built unchanged from the reference commit (17aa1769b0). Everything is in a single script, ex/u02-architecture/run.sh, and the ### lines in the output are headings the script prints.

// Example written for this post (not part of Tomcat's source)
// Reads conf/server.xml with the same Catalina class catalina.sh uses to build the object tree,
// and prints the tree before start (right after load) and after start (right after start).
// ... (imports omitted)
public class ServerXmlTree {

    public static void main(String[] args) throws Exception {
        Catalina catalina = new Catalina();
        catalina.setAwait(false);          // don't wait on the shutdown port
        catalina.setUseShutdownHook(false);

        catalina.load();                   // server.xml -> objects, then init()
        System.out.println("=== after load() (server.xml parsed, not started) ===");
        print(catalina.getServer(), false);

        catalina.start();                  // start(): this is when webapps/ gets deployed
        System.out.println("=== after start() ===");
        print(catalina.getServer(), true);

        catalina.stop();
    }
    // ... print(): prints Server → Service → Connector·Engine → Host → Context → Wrapper in order
}
### 1. server.xml -> objects (Catalina.load / start)
=== after load() (server.xml parsed, not started) ===
Server   org.apache.catalina.core.StandardServer port=8005
  Service   org.apache.catalina.core.StandardService name=Catalina
    Connector   org.apache.catalina.connector.Connector port=8080
      protocolHandler = org.apache.coyote.http11.Http11NioProtocol
      adapter         = org.apache.catalina.connector.CoyoteAdapter
    Engine   org.apache.catalina.core.StandardEngine name=Catalina parent=null
      Host   org.apache.catalina.core.StandardHost name=localhost contexts=0
=== after start() ===
Server   org.apache.catalina.core.StandardServer port=8005
  Service   org.apache.catalina.core.StandardService name=Catalina
    Connector   org.apache.catalina.connector.Connector port=8080
      protocolHandler = org.apache.coyote.http11.Http11NioProtocol
      adapter         = org.apache.catalina.connector.CoyoteAdapter
    Engine   org.apache.catalina.core.StandardEngine name=Catalina parent=null
      Host   org.apache.catalina.core.StandardHost name=localhost contexts=2
        Context   org.apache.catalina.core.StandardContext path='' wrappers=2
          Wrapper   org.apache.catalina.core.StandardWrapper name=default
          Wrapper   org.apache.catalina.core.StandardWrapper name=jsp
        Context   org.apache.catalina.core.StandardContext path='/docs' wrappers=2

Easy to confuse: server.xml has neither Context nor Wrapper

The default server.xml has no <Context>. Contexts are created when the Host starts: the HostConfig listener attached to the Host scans the webapps/ folder. On a START_EVENT, HostConfig calls start() (HostConfig.java line 286), which calls deployApps() (line 1665). That is why Example 1 showed zero Contexts right after load(). You can write a <Context> in server.xml (that is why ContextRuleSet exists), but usually contexts come from folders like this or from conf/Catalina/localhost/*.xml.

Wrappers are created one level further in. When a Context starts, ContextConfig reads web.xml, creates one Wrapper per <servlet>, and attaches it to the Context.

        for (ServletDef servlet : webxml.getServlets().values()) {
            Wrapper wrapper = context.createWrapper();
            // ...
            context.addChild(wrapper);

java/org/apache/catalina/startup/ContextConfig.java lines 1544–1592

The ROOT web app's WEB-INF/web.xml defines no <servlet> at all, yet it has two Wrappers (default and jsp). That is because both are defined in conf/web.xml, which is merged into every web app. How web.xml files are merged is covered in Part 10.

Easy to confuse: className lets you swap the class

Thanks to the third argument of addObjectCreate, "className", writing <Host className="com.example.MyHost"> creates some other class instead of StandardHost. That is why the source always handles components through interfaces such as Host and Engine; the classes starting with Standard are just "the default implementation." <Realm className=…> and <Valve className=…> in server.xml must spell out a class name because, under the same rule, their default class is null ("must be specified") (EngineRuleSet.java lines 76–77).

2. Server and Service — two boxes that are not containers

The comment at the top of server.xml warns us first:

A "Server" is not itself a "Container", so you may not define subcomponents such as "Valves" at this level.

(conf/server.xml lines 18–19) The comment above Service (lines 51–52) says the same. Server and Service do not implement Container; they implement only Lifecycle (public interface Server extends Lifecycle, Server.java line 38). So they have no pipeline and no valves. They are not boxes that process requests; they are boxes that hold components together.

Server: one Tomcat

The description of the Server interface says it represents "the entire Catalina servlet container," holds one or more Services, and listens on the port given by the port attribute for a shutdown command (Server.java lines 25–37). The default of 8005 lives in a StandardServer field (private int port = 8005;, StandardServer.java line 119). The implementation, StandardServer, holds exactly that.

    /**
     * The set of Services associated with this Server.
     */
    private Service[] services = new Service[0];
    // ...
    /**
     * The shutdown command string we are looking for.
     */
    private String shutdown = "SHUTDOWN";

java/org/apache/catalina/core/StandardServer.java lines 136–148

This is the mechanism that shuts Tomcat down when catalina.sh stop sends the word SHUTDOWN to port 8005. How StandardServer.await() guards that port is covered in Part 3. The <GlobalNamingResources> (global JNDI resources) in server.xml also attach to the Server (Section 5).

Service: many connectors + one engine

The description of Service says in one sentence why this box exists.

A Service is a group of one or more Connectors that share a single Container to process their incoming requests. This arrangement allows, for example, a non-SSL and SSL connector to share the same population of web apps.

(Service.java lines 22–29) For two connectors, 8080 (HTTP) and 8443 (HTTPS), to serve the same web apps, they must share one engine. The Service is what bundles "two or more and one" together. StandardService's fields have exactly that shape.

    /**
     * The set of Connectors associated with this Service.
     */
    protected Connector[] connectors = new Connector[0];
    // ...
    /**
     * The list of executors held by the service.
     */
    protected final ArrayList<Executor> executors = new ArrayList<>();
    // ...
    private Engine engine = null;
    // ...
    /**
     * Mapper.
     */
    protected final Mapper mapper = new Mapper();


    /**
     * Mapper listener.
     */
    protected final MapperListener mapperListener = new MapperListener(this);

java/org/apache/catalina/core/StandardService.java lines 84–109

Field What it is Details
connectors an array — there can be many connectors Section 4, Part 5
executors <Executor> in server.xml (a thread pool connectors can share) Part 5
engine a single field — there is only one engine Section 3
mapper, mapperListener the table that picks Host, Context and Wrapper from the URL, and the listener that keeps that table in sync with the container tree Part 7

It helps to remember that the Mapper lives in the Service, not in the Engine. When a connector receives a request, it uses "my Service's Mapper" to pick the destination and hands the request to "my Service's Engine" (CoyoteAdapter in Section 4).

Easy to confuse: the Engine's parent is not the Service

The Service gets its Engine through setContainer(engine), and at that moment tells the Engine about itself with engine.setService(this) (StandardService.java lines 150–158). But this is not the parent–child relationship that getParent() follows. Service is not a Container, so Container.getParent() cannot return it. In fact, an Engine's getParent() is always null (parent=null in Example 1, and Section 3). To find the Service from a container, you climb to the Engine and call getService(). Container has a helper that does exactly that.

    static Service getService(Container container) {
        while (container != null && !(container instanceof Engine)) {
            container = container.getParent();
        }
        if (container == null) {
            return null;
        }
        return ((Engine) container).getService();
    }

java/org/apache/catalina/Container.java lines 307–315

What StandardServer and StandardService hold. Neither is a Container, so neither has a pipeline or valves. StandardServer holds the services array, shutdown port 8005 and the shutdown command SHUTDOWN. Inside StandardService are the connector array (for example 8080 HTTP and 8443 HTTPS), executors (thread pools the connectors share), and mapper plus mapperListener, which pick Host, Context and Wrapper from the URL. A connector picks the target with its own Service's Mapper and hands the request to its own Service's Engine. engine is a single field, so there is only one engine. Inside setContainer(engine), the Service introduces itself with engine.setService(this), but the Engine's getParent() is null; the only way back from the Engine to the Service is getService(). Container.getService() climbs getParent() up to the Engine and then calls it. The two field lists above and Container.getService() in one picture. Solid arrows are the path a request takes; dotted arrows are how the Service and the Engine know each other. 8080 and 8443 are the example from the Service javadoc.

3. The four containers: Engine, Host, Context, Wrapper

The four levels, as the Container interface describes them

The description of the Container interface lists the four levels itself.

  • Engine - Representation of the entire Catalina servlet engine, most likely containing one or more subcontainers that are either Host or Context implementations, or other custom groups.
  • Host - Representation of a virtual host containing a number of Contexts.
  • Context - Representation of a single ServletContext, which will typically contain one or more Wrappers for the supported servlets.
  • Wrapper - Representation of an individual servlet definition.

(Container.java lines 36–41) Linking them to the configuration files, here is a summary.

Level Default implementation What one of them represents Where it comes from Children
Engine StandardEngine the whole servlet engine; one per Service server.xml <Engine> Host
Host StandardHost one virtual host (localhost, www.example.com) server.xml <Host> Context
Context StandardContext one web app = one ServletContext (/, /docs) webapps/, conf/Catalina/localhost/*.xml, (rarely) server.xml Wrapper
Wrapper StandardWrapper one servlet definition (one <servlet> in web.xml) web.xml, @WebServlet none

A Host is the same idea as httpd's <VirtualHost>. In httpd, each virtual host got its own server_rec (httpd Part 3). In Tomcat it is one Host object.

The shared skeleton: parent, children, pipeline

Of the methods Container requires from all four levels, these are the ones we need for the map.

public interface Container extends Lifecycle {
    // ...
    Pipeline getPipeline();
    // ...
    String getName();
    // ...
    Container getParent();
    // ...
    void setParent(Container container);
    // ...
    void addChild(Container child);
    // ...
    Container findChild(String name);
    // ...
    Container[] findChildren();

java/org/apache/catalina/Container.java lines 64–377

One parent (getParent), many children found by name (findChild), and one pipeline (getPipeline). Because all four levels share this shape, the request-processing code also has a single shape: "run my pipeline, and at the end call my child's pipeline" (Section 6).

The class that actually implements this shared part is the abstract class ContainerBase. All four Standard… classes extend it. Here is addChild(), which attaches a child.

    @Override
    public void addChild(Container child) {
        // ...
        childrenLock.writeLock().lock();
        try {
            if (children.get(child.getName()) != null) {
                throw new IllegalArgumentException(sm.getString("containerBase.child.notUnique", child.getName()));
            }
            child.setParent(this); // May throw IAE
            children.put(child.getName(), child);
        } finally {
            childrenLock.writeLock().unlock();
        }

        fireContainerEvent(ADD_CHILD_EVENT, child);

        // Start child
        // Don't do this inside sync block - start can be a slow process and
        // locking the children object can cause problems elsewhere
        try {
            if ((getState().isAvailable() || LifecycleState.STARTING_PREP.equals(getState())) && startChildren) {
                child.start();
            }
        } catch (LifecycleException e) {
            throw new IllegalStateException(sm.getString("containerBase.child.start"), e);
        }
    }

java/org/apache/catalina/core/ContainerBase.java lines 559–588

  1. Children go into a HashMap keyed by name (children, line 135). A duplicate name is rejected. A Host's name is localhost, a Context's name is its path (/docs), a Wrapper's name is the servlet name.
  2. It calls the child's setParent(this), laying the road back up.
  3. If the parent is already running, the child is start()ed immediately. This line is why a WAR you drop in while Tomcat is running comes to life right away (Part 10).

The class hierarchy. StandardEngine, StandardHost, StandardContext and StandardWrapper extend ContainerBase, which implements Container. ContainerBase, StandardServer, StandardService and Connector all extend LifecycleMBeanBase, which extends LifecycleBase, which implements the Lifecycle interface. Blue boxes are the four containers and their common parent. Server, Service and Connector share the same lifecycle root but are not Containers.

The relationships in the picture are exactly the class declarations in the source: public abstract class ContainerBase extends LifecycleMBeanBase implements Container (ContainerBase.java line 119), public class StandardEngine extends ContainerBase implements Engine (StandardEngine.java line 49), public final class StandardServer extends LifecycleMBeanBase implements Server (StandardServer.java line 67), and public class Connector extends LifecycleMBeanBase (Connector.java line 56). The "MBean" in LifecycleMBeanBase means the object registers itself with JMX, Java's remote-management facility (Part 16).

Who enforces the order of the levels?

The Container interface itself does not enforce "only Hosts under an Engine." Each implementation's addChild() and setParent() do.

    public void addChild(Container child) {

        if (!(child instanceof Host)) {
            throw new IllegalArgumentException(sm.getString("standardEngine.notHost"));
        }
        super.addChild(child);

    }
    // ...
    @Override
    public void setParent(Container container) {

        throw new IllegalArgumentException(sm.getString("standardEngine.notParent"));

    }

java/org/apache/catalina/core/StandardEngine.java lines 159–180

Check Where If violated
An Engine's children must be Hosts StandardEngine.addChild() line 161 IllegalArgumentException
An Engine cannot have a parent StandardEngine.setParent() line 178 IllegalArgumentException
A Host's children must be Contexts StandardHost.addChild() line 670 IllegalArgumentException
A Context's children must be Wrappers StandardContext.addChild() line 2830 IllegalArgumentException
A Wrapper's parent must be a Context StandardWrapper.setParent() line 337 IllegalArgumentException
A Wrapper cannot have children StandardWrapper.addChild() line 525 IllegalStateException

Example 2: building the tree with embedded Tomcat and climbing getParent()

I built the tree with embedded Tomcat (org.apache.catalina.startup.Tomcat) — the way Spring Boot does it, starting Tomcat from code without a server.xml. The Tomcat class creates a StandardServer and a StandardService the first time getServer() is called (Tomcat.java lines 626–647), and getEngine() and getHost() create and attach a StandardEngine and StandardHost when needed (lines 591–619). Code does what Digester did with server.xml. We tour the whole embedded API in Part 16.

        Tomcat tomcat = new Tomcat();
        tomcat.setBaseDir("/tmp/emb");
        tomcat.setPort(8080);
        tomcat.getConnector();                           // makes it create the connector

        Context ctx = tomcat.addContext("/app", null);   // one Context under the Host
        Wrapper wrapper = Tomcat.addServlet(ctx, "stack", new StackServlet()); // one Wrapper under the Context
        ctx.addServletMapping("/stack", "stack");    // 11.0.x also had addServletMappingDecoded

        System.out.println("=== 1. getParent() chain from the Wrapper ===");
        Container c = wrapper;
        while (c != null) {
            System.out.println(c.getClass().getSimpleName() + "  name='" + c.getName() + "'");
            c = c.getParent();
        }
        Engine engine = tomcat.getEngine();
        System.out.println("Engine.getService() = " + engine.getService().getClass().getSimpleName()
                + "  name='" + engine.getService().getName() + "'");
        System.out.println("Service.getServer() = " + engine.getService().getServer().getClass().getSimpleName());

        System.out.println("=== 2. adding a child in the wrong place ===");
        try {
            engine.addChild(new StandardContext());
        } catch (RuntimeException e) {
            System.out.println("engine.addChild(Context)  -> " + e.getClass().getSimpleName() + ": " + e.getMessage());
        }
        // ... wrapper.addChild(new StandardContext()) and engine.setParent(tomcat.getHost()) look the same

This is the first half of main() in ex/u02-architecture/EmbeddedTree.java.

### 2. embedded Tomcat
=== 1. getParent() chain from the Wrapper ===
ExistingStandardWrapper  name='stack'
StandardContext  name='/app'
StandardHost  name='localhost'
StandardEngine  name='Tomcat'
Engine.getService() = StandardService  name='Tomcat'
Service.getServer() = StandardServer
=== 2. adding a child in the wrong place ===
engine.addChild(Context)  -> IllegalArgumentException: Child of an Engine must be a Host
wrapper.addChild(Context) -> IllegalStateException: Wrapper container may not have child containers
engine.setParent(Host)    -> IllegalArgumentException: Engine cannot have a parent Container

Easy to confuse

4. Catalina and Coyote — why they are split, and how the Adapter joins them

What each one does

They live in the same Tomcat, but their jobs differ.

The seam between them is server.xml's <Connector>. Connector is a Catalina class (org.apache.catalina.connector), but its constructor creates and holds one Coyote protocol handler (ProtocolHandler).

    public Connector(String protocol) {
        configuredProtocol = protocol;
        ProtocolHandler p = null;
        Exception ex = null;
        try {
            p = ProtocolHandler.create(protocol);
        } catch (Exception e) {
            ex = e;
        }
        protocolHandlerCreationException = ex;
        if (p != null) {
            protocolHandler = p;
            protocolHandlerClassName = protocolHandler.getClass().getName();
        } else {
            protocolHandler = null;
            protocolHandlerClassName = protocol;
        }
        // ...
    }

java/org/apache/catalina/connector/Connector.java lines 101–120

The place that turns the text of the protocol attribute into a class is ProtocolHandler.create(), on the Coyote side.

    static ProtocolHandler create(String protocol)
            throws ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException,
            InvocationTargetException, NoSuchMethodException, SecurityException {
        if (protocol == null || "HTTP/1.1".equals(protocol) ||
                org.apache.coyote.http11.Http11NioProtocol.class.getName().equals(protocol)) {
            return new org.apache.coyote.http11.Http11NioProtocol();
        } else if ("AJP/1.3".equals(protocol) ||
                org.apache.coyote.ajp.AjpNioProtocol.class.getName().equals(protocol)) {
            return new org.apache.coyote.ajp.AjpNioProtocol();
        } else {
            // Instantiate protocol handler
            Class<?> clazz = Class.forName(protocol);
            return (ProtocolHandler) clazz.getConstructor().newInstance();
        }
    }

java/org/apache/coyote/ProtocolHandler.java lines 250–264

"HTTP/1.1" (or nothing) gives the NIO-based Http11NioProtocol (NIO is Java's non-blocking I/O API), "AJP/1.3" gives AjpNioProtocol (AJP is a binary protocol a front-end web server such as httpd uses to pass requests to Tomcat), and anything else is treated as a class name and created by reflection (the Java feature that finds a class from its name as a string and creates an object of it). This method looks the same on the 11.0.x branch. Http11NioProtocol's constructor in turn creates the endpoint that handles sockets, with new NioEndpoint() (Http11NioProtocol.java lines 36–38). The endpoint's threads are the subject of Part 5.

One bridge: Adapter

Once Coyote has read a whole request, whom does it hand it to? Coyote does not know Catalina's classes by name. Instead, it defines one interface in its own package.

/**
 * Adapter. This represents the entry point in a coyote-based servlet container.
 *
 * @see ProtocolHandler
 */
public interface Adapter {

    /**
     * Call the service method, and notify all listeners.
     *
     * @param req The request object
     * @param res The response object
     // ...
     */
    void service(Request req, Response res) throws Exception;
    // ...

java/org/apache/coyote/Adapter.java lines 21–44

The description calls it "the entry point in a coyote-based servlet container." The Request and Response that service() receives are the ones from org.apache.coyote — Coyote's own request objects, unrelated to the servlet specification. There are five more methods, such as asyncDispatch() for async processing and log() for logging (up to line 98).

The class that implements this interface is CoyoteAdapter on the Catalina side (public class CoyoteAdapter implements Adapter, CoyoteAdapter.java line 63), and it is plugged in by Connector.initInternal().

        // Initialize adapter
        adapter = new CoyoteAdapter(this);
        protocolHandler.setAdapter(adapter);

java/org/apache/catalina/connector/Connector.java lines 1300–1302

That is why Example 1 already showed adapter = …CoyoteAdapter right after load(): load() goes as far as init(). From then on, once the HTTP/1.1 processor has read the request headers, it calls getAdapter().service(request, response) (Http11Processor.java line 404). All Coyote knows is one interface, Adapter; what actually runs is Catalina's CoyoteAdapter.

The other side of the bridge: CoyoteAdapter.service()

Let's look at just the beginning of what happens after the request crosses the bridge.

    public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception {

        Request request = (Request) req.getNote(ADAPTER_NOTES);
        Response response = (Response) res.getNote(ADAPTER_NOTES);

        if (request == null) {
            // Create objects
            request = connector.createRequest(req);
            response = connector.createResponse(res);

            // Link objects
            request.setResponse(response);
            response.setRequest(request);

            // Set as notes
            req.setNote(ADAPTER_NOTES, request);
            res.setNote(ADAPTER_NOTES, response);

        }
        // ...
        try {
            // Parse and set Catalina and configuration specific
            // request parameters
            postParseSuccess = postParseRequest(req, request, res, response);
            if (postParseSuccess) {
                // check valves if we support async
                request.setAsyncSupported(connector.getService().getContainer().getPipeline().isAsyncSupported());
                // Calling the container
                connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
            }

java/org/apache/catalina/connector/CoyoteAdapter.java lines 305–348

  1. It takes Catalina's request object from a note attached to Coyote's request (req). If there is none, it creates one with connector.createRequest() and attaches it as a note; the next request reuses the same object (recycling is covered in Part 7).
  2. postParseRequest() decodes and normalizes the URI, then uses the Service's Mapper from Section 2 to pick the Host, Context and Wrapper and write them on the request (connector.getService().getMapper().map(…) at CoyoteAdapter.java line 700). This is the subject of Part 7.
  3. It calls the first valve in the pipeline of connector.getService().getContainer() — my Service's Engine. From here on we are in the world of the four containers (Section 6).

The boundary between Coyote and Catalina. Coyote's NioEndpoint accepts a socket and passes it to Http11Processor, which turns bytes into a coyote Request and calls the Adapter interface with getAdapter().service(). That is all Coyote knows; what actually runs is Catalina's CoyoteAdapter. CoyoteAdapter wraps the coyote Request in a connector.Request that implements HttpServletRequest and calls the first valve of the Engine pipeline. The two gray boxes and the first blue box at the top are Coyote; the bottom three are Catalina. They meet at a single method, Adapter.service().

Example 3: counting the direction of dependencies

To check whether "Coyote doesn't know Catalina" is really true, I counted, in the reference commit's source inside the example container (/usr/src/tomcat/java), how many files in each package import another package.

for pair in coyote:catalina tomcat:catalina jasper:catalina el:catalina juli:catalina naming:catalina \
            catalina:coyote catalina:jasper catalina:naming; do
  from=${pair%%:*}; to=${pair##*:}
  n=$(grep -rl "^import org\.apache\.$to\." org/apache/$from --include=*.java | wc -l)
  printf '  org.apache.%-9s -> org.apache.%-9s %4d\n' "$from" "$to" "$n"
done
n=$(grep -rl "^import jakarta\.servlet\." org/apache/coyote --include=*.java | wc -l)
printf '  org.apache.%-9s -> %-20s %4d\n' coyote jakarta.servlet "$n"
### 0b. who imports whom (number of .java files with such an import, in /usr/src/tomcat/java)
  org.apache.coyote    -> org.apache.catalina     0
  org.apache.tomcat    -> org.apache.catalina     0
  org.apache.jasper    -> org.apache.catalina     0
  org.apache.el        -> org.apache.catalina     0
  org.apache.juli      -> org.apache.catalina     0
  org.apache.naming    -> org.apache.catalina     0
  org.apache.catalina  -> org.apache.coyote      24
  org.apache.catalina  -> org.apache.jasper       0
  org.apache.catalina  -> org.apache.naming       6
  org.apache.coyote    -> jakarta.servlet        20

The arrows point one way only. Catalina uses Coyote (24 files), but neither Coyote nor the shared toolbox org.apache.tomcat imports Catalina. The only road from Coyote to Catalina is the one object plugged in backwards through the Adapter interface. On the 11.0.x branch, too, no file under coyote or jasper imports org.apache.catalina.

You can see the payoff of this split in the source. A new protocol only needs one more ProtocolHandler implementation on the Coyote side (the ajp/, http11/ and http2/ folders sit side by side), and Catalina receives a request with one call to Adapter.service() no matter which protocol it came in on. Conversely, changing Catalina's valves or sessions never requires another look at Coyote. The historical reasons Tomcat was originally split this way cannot be confirmed from the source, so I leave them out.

We saw the same idea in Spring. WebFlux's HttpHandler does not know any particular server; an adapter, one per server, wraps the server's request in Spring types and passes it on (Spring Part 24). That is the same spot CoyoteAdapter occupies when it wraps a Coyote request as a servlet request. In httpd, the MPM that accepts connections and the core and modules that process requests were joined by hooks (httpd Part 3).

Easy to confuse: there are two Requests

When you meet a class called Request in the source, check the package first.

Class Declaration What it is
org.apache.coyote.Request public final class Request (coyote/Request.java line 55) the raw request Coyote read from the bytes; it does not implement any servlet interface
org.apache.catalina.connector.Request public class Request implements HttpServletRequest (connector/Request.java line 129) the servlet request that wraps the one above; the Host, Context and Wrapper chosen by the Mapper are written here too

In CoyoteAdapter.service(), the parameter req is the first and the local variable request is the second. What actually reaches the servlet is a RequestFacade that wraps this one more time; why it is wrapped again is covered in Part 7. Response comes in the same two flavors.

Easy to confuse: Coyote does use a bit of the servlet API

As the last line of the output shows, 20 files in Coyote do import jakarta.servlet. What they use is a handful of interfaces such as ReadListener and WriteListener (non-blocking I/O), HttpUpgradeHandler and WebConnection (protocol upgrade), and ServletConnection. Async I/O and upgrades in the servlet specification have to reach deep into the connector (Part 12, Part 15). "Coyote doesn't know Catalina" is true, but "Coyote doesn't know the servlet API" is not quite.

Easy to confuse: the Mapper, not the Engine, picks the Host

The comment above <Engine> in server.xml says the Engine "analyzes the HTTP headers included with the request, and passes them on to the appropriate Host" (lines 106–109). As a description of behavior that is fine, but in the source the job of choosing the Host was already done by the Mapper in step 2. The Engine's basic valve just takes the Host written on the request.

    public void invoke(Request request, Response response) throws IOException, ServletException {

        // Select the Host to be used for this Request
        Host host = request.getHost();
        if (host == null) {
            // No Host found for this request (unknown server name or no default host defined)
            // Don't overwrite an existing error
            if (!response.isError()) {
                response.sendError(404);
            }
            return;
        }
        if (request.isAsyncSupported()) {
            request.setAsyncSupported(host.getPipeline().isAsyncSupported());
        }

        // Ask this Host to process this request
        host.getPipeline().getFirst().invoke(request, response);
    }

java/org/apache/catalina/core/StandardEngineValve.java lines 54–72

request.getHost() is a one-line method that returns mappingData.host, filled in by the Mapper (connector/Request.java lines 675–677). The Host valve taking the Context, and the Context valve taking the Wrapper, work the same way (Section 6).

5. The jakarta API and its implementation, and where Jasper, EL, JULI and Naming sit

The repository's java/ folder

java/ splits into two big parts. java/jakarta/ is the specification (the interfaces and base classes of Jakarta EE specifications such as Servlet, EL and WebSocket), and java/org/apache/ is Tomcat's implementation. I counted the .java files at the reference commit myself.

Folder .java files What Details
jakarta/servlet 117 Servlet specification (HttpServlet, Filter, ServletContext …)
jakarta/el 41 EL specification (ExpressionFactory …)
jakarta/websocket 33 WebSocket specification Part 15
org/apache/catalina 611 the servlet container most of this series
org/apache/coyote 92 the connector Parts 5 and 6
org/apache/tomcat 577 shared tools (util/net, util/buf, util/threads, util/digester …) and the WebSocket implementation several parts
org/apache/jasper 109 the JSP engine Part 13
org/apache/el 82 the EL implementation Part 13
org/apache/naming 32 the JNDI implementation this post
org/apache/juli 14 logging Part 16

Under jakarta/ there are also smaller folders such as annotation, security, transaction and mail. The full folder map is in Part 1.

Example 4: what's inside the built jars?

In an installation's lib/ and bin/, these packages are split across jars. In the example container's $CATALINA_HOME, I printed the packages inside each jar (cut at three levels so the output stays short).

### 0. jars in $CATALINA_HOME/lib and bin: top-level packages inside each
catalina.jar       org/apache/catalina org/apache/naming 
tomcat-coyote.jar  org/apache/coyote org/apache/tomcat 
tomcat-util.jar    org/apache/tomcat 
servlet-api.jar    jakarta/servlet jakarta/servlet/annotation jakarta/servlet/descriptor jakarta/servlet/http 
jasper.jar         org/apache/jasper 
jasper-el.jar      org/apache/el 
el-api.jar         jakarta/el 
tomcat-juli.jar    org/apache/juli 
org/apache/tomcat/util/* is split across jars:
  tomcat-coyote.jar  bcel http log modeler net 
  tomcat-util.jar    buf collections compat concurrent file json res security threads 
  tomcat-util-scan.jar descriptor digester scan 
where is org/apache/naming ?
  catalina.jar: 34 classes

A map of packages and jars. A web app calls only the specification, such as jakarta.servlet and jakarta.el (servlet-api.jar, el-api.jar). Catalina (catalina.jar), Jasper (jasper.jar) and EL (jasper-el.jar) implement that specification. Jasper finds the EL implementation through jakarta.el. Catalina uses Coyote (tomcat-coyote.jar) and Naming (inside catalina.jar), and both Catalina and Coyote use org.apache.tomcat.util. JULI (bin/tomcat-juli.jar) is the logging used by everything except EL. Arrows mean "uses" or "implements." No arrow points into Catalina (Example 3).

Jasper: "just a servlet" that turns JSP into servlets

Jasper is the JSP engine: it turns JSP files into Java servlet source, compiles them and runs them. Yet it is not wired into Catalina in any special way. It is simply registered as one servlet in conf/web.xml, which is merged into every web app.

    <servlet>
        <servlet-name>jsp</servlet-name>
        <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
        <!-- ... -->
        <load-on-startup>3</load-on-startup>
    </servlet>

conf/web.xml lines 268–280

Further down the same file, *.jsp and *.jspx are mapped to this servlet (lines 441–445). The Wrapper named jsp in the ROOT web app in Example 1 is this servlet. As Example 3's output shows, Catalina does not import org.apache.jasper; it knows the class name only as a string (JSP_SERVLET_CLASS = "org.apache.jasper.servlet.JspServlet", core/Constants.java line 43). So from the container's point of view, Jasper is just "a servlet inside one Wrapper," and Catalina's source never references Jasper's classes directly. How a JSP becomes a servlet is covered in Part 13.

EL: an implementation found by the specification's ExpressionFactory

EL (Expression Language) is the language that evaluates expressions like ${user.name} in a JSP. Jasper does not call the EL implementation class directly; it calls the specification's jakarta.el.ExpressionFactory.newInstance() (jasper/compiler/PageInfo.java line 77). That method's description lists the order in which it looks for an implementation.

  1. services API (META-INF/services/jakarta.el.ExpressionFactory)
  2. $JRE_HOME/lib/el.properties - key jakarta.el.ExpressionFactory
  3. jakarta.el.ExpressionFactory
  4. Platform default implementation - org.apache.el.ExpressionFactoryImpl

(java/jakarta/el/ExpressionFactory.java lines 61–68) Unless you configure something else, the last one — org.apache.el.ExpressionFactoryImpl, Tomcat's implementation in jasper-el.jar — is used. We get only a taste of EL in Part 13. It sounds like Spring's SpEL, but it is a different language (Spring Part 15).

JULI: the logging everyone uses

JULI is Tomcat's logging package (org.apache.juli). The repository's logging documentation describes JULI as a renamed fork of Apache Commons Logging that is hard-coded to use the JDK's java.util.logging (webapps/docs/logging.xml lines 38–40). Its key component is ClassLoaderLogManager, which extends the JDK's LogManager so that each class loader — that is, each web app — can have its own logging configuration (ClassLoaderLogManager.java lines 39–44). The private static final Log log = LogFactory.getLog(…) lines all over the source are JULI's. Container.getLogger() also returns a JULI Log (import org.apache.juli.logging.Log;, Container.java line 26 and line 104). At the reference commit, the number of files importing org.apache.juli was 205 in catalina, 73 in tomcat, 27 in coyote, 19 in jasper and 5 in naming, and none in el. We look at it in Part 16.

Naming: the JNDI implementation that provides java:comp/env

Naming (org.apache.naming) is Tomcat's implementation of JNDI, Java's "look up resources by name" API. It is what lets a web app find a database connection pool by name, as in new InitialContext().lookup("java:comp/env/jdbc/MyDB"). As Example 4 shows, it has no jar of its own; it lives inside catalina.jar.

server.xml's <GlobalNamingResources> becomes a NamingResourcesImpl object attached to the Server (Catalina.java lines 517–520), and StandardServer's constructor attaches a NamingContextListener to itself when naming is enabled.

        if (isUseNaming()) {
            namingContextListener = new NamingContextListener();
            addLifecycleListener(namingContextListener);
        } else {
            namingContextListener = null;
        }

java/org/apache/catalina/core/StandardServer.java lines 85–90

isUseNaming() looks at the system property catalina.useNaming (lines 851–859). Embedded Tomcat sets that property to "false" in getServer() (Tomcat.java line 632), which is why JNDI is off by default in embedded Tomcat. This series does not cover Naming separately.

Easy to confuse: org.apache.tomcat.util is spread over three jars

As Example 4 shows, util/net, which handles sockets, is in tomcat-coyote.jar; the thread pool in util/threads is in tomcat-util.jar; and util/digester, which reads server.xml, is in tomcat-util-scan.jar. There is one source folder, but the jars are split by purpose. When Part 1's picture or this post's picture draws "tomcat.util" as one box, it is going by the source folder.

6. One big picture: a request moving into the containers, outside to inside

Now let's string the pieces together. Having crossed the bridge in Section 4, a request moves into ever-inner boxes: Engine → Host → Context → Wrapper.

The order in which a request passes through the four containers. In CoyoteAdapter.service(), the Mapper picks Host, Context and Wrapper in advance and writes them on the request, then the first valve of the Engine pipeline is called. StandardEngineValve, at the end of the Engine pipeline, calls the Host's pipeline via request.getHost(); StandardHostValve calls the Context's pipeline via request.getContext(); StandardContextValve calls the Wrapper's pipeline via request.getWrapper(); and StandardWrapperValve calls the filter chain and the servlet. Each blue box is one container's pipeline, with its basic valve at the end. The method on each arrow is how that basic valve takes out the next container.

Example 5: printing the call stack inside a servlet

To see whether requests really arrive in this order, I put one servlet on the embedded Tomcat from Example 2 and printed the call stack inside doGet().

    // A servlet that prints its call stack
    public static class StackServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
            StringBuilder sb = new StringBuilder();
            StackTraceElement[] frames = new Throwable().getStackTrace();
            for (int i = 0; i < frames.length; i++) {
                StackTraceElement f = frames[i];
                sb.append(String.format("#%-2d %s.%s%n", i, f.getClassName(), f.getMethodName()));
            }
            sb.append("thread = ").append(Thread.currentThread().getName()).append('\n');
            resp.setContentType("text/plain");
            resp.getWriter().print(sb);
        }
    }
=== 3. call stack inside the servlet (GET /app/stack) ===
#0  EmbeddedTree$StackServlet.doGet
#1  jakarta.servlet.http.HttpServlet.service
#2  jakarta.servlet.http.HttpServlet.service
#3  org.apache.catalina.core.ApplicationFilterChain.doFilter
#4  org.apache.catalina.core.StandardWrapperValve.invoke
#5  org.apache.catalina.core.StandardContextValve.invoke
#6  org.apache.catalina.authenticator.AuthenticatorBase.invoke
#7  org.apache.catalina.core.StandardHostValve.invoke
#8  org.apache.catalina.valves.ErrorReportValve.invoke
#9  org.apache.catalina.core.StandardEngineValve.invoke
#10 org.apache.catalina.connector.CoyoteAdapter.service
#11 org.apache.coyote.http11.Http11Processor.service
#12 org.apache.coyote.AbstractProcessorLight.process
#13 org.apache.coyote.AbstractProtocol$ConnectionHandler.process
#14 org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun
#15 org.apache.tomcat.util.net.SocketProcessorBase.run
#16 org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker
#17 org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run
#18 org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run
#19 java.lang.Thread.run
thread = http-nio-8080-exec-1

The number at the end of the thread name (exec-1) varies from run to run (another run printed exec-2). Read from the bottom up, and this post's map lines up exactly.

Frames Layer This post Details
#14–#19 tomcat.util endpoint and thread pool (http-nio-8080-exec-N) Section 4 Part 5
#11–#13 Coyote: protocol handler and HTTP/1.1 processor Section 4 Part 6
#10 the bridge: CoyoteAdapter.service() Section 4 Part 7
#9 the Engine's basic valve Sections 3 and 6 Part 8
#7–#8 the Host's pipeline: ErrorReportValve → basic valve Section 6 Part 8
#5–#6 the Context's pipeline: authentication valve → basic valve Section 6 Part 8, Part 14
#4 the Wrapper's basic valve Section 6 Part 9
#0–#3 the filter chain and my servlet Part 9

Easy to confuse: valves show up even though I added none

The example adds no <Valve> at all, yet ErrorReportValve (#8) and AuthenticatorBase (#6) appear in the stack. The first is the error-page valve that StandardHost attaches by default when it starts (startInternal()). The class it attaches is the errorReportValveClass property, whose default is "org.apache.catalina.valves.ErrorReportValve" (StandardHost.java line 146; it is attached at lines 786–800). The second comes from embedded Tomcat: it hangs a FixContextListener on every Context made by addContext() (Tomcat.java line 680), and that listener attaches a NonLoginAuthenticator to a Context that has no login configuration (lines 1133–1135). The stack prints the class name AuthenticatorBase because invoke() is defined in that superclass. When each valve gets attached is covered in Part 8 and Part 14.

Easy to confuse: example code for 11.0.x may not compile on main

When I first wrote Example 2, I mapped the servlet with ctx.addServletMappingDecoded("/stack", "stack") and got a cannot find symbol compile error. Context on the 11.0.x branch has addServletMappingDecoded(), but at the reference commit (main) only addServletMapping() remains (Context.java lines 914 and 928). You will run into this difference when moving embedded-Tomcat code written for Tomcat 11.0.x — like the example in this blog's Spring Part 25 — onto main.

What this post leaves out

Things left out to keep the map on one page:

Summary

Concept One-line summary
server.xml → objects Following the rules in Catalina.createStartDigester(), each element becomes an object (addObjectCreate), gets its attributes, and is attached to the enclosing object with addService, addConnector, setContainer or addChild
Server One Tomcat (StandardServer). Holds an array of Services and the shutdown port (8005). Not a Container
Service Many connectors + one Engine + a Mapper (StandardService). Not a Container
The four containers Engine (whole engine) ⊃ Host (virtual host) ⊃ Context (web app) ⊃ Wrapper (servlet definition). All extend ContainerBase
Parent and child ContainerBase.addChild() stores children by name and calls setParent(). Each addChild()/setParent() checks the order of levels. The Engine's parent is null
Where Contexts and Wrappers come from Contexts from webapps/ via HostConfig; Wrappers from web.xml via ContextConfig. Both at start time
Coyote The connector. Bytes ↔ org.apache.coyote.Request. The protocol attribute picks the ProtocolHandler. Does not import Catalina
Adapter An interface defined by Coyote. Connector.initInternal() plugs in a CoyoteAdapter, and the processor calls it with getAdapter().service()
CoyoteAdapter Wraps the coyote request as an HttpServletRequest, picks the destination with the Mapper, and calls the first valve of the Engine pipeline
jakarta vs. implementation Specification in java/jakarta (servlet-api.jar etc.), implementation in java/org/apache (catalina.jar etc.)
Jasper and EL One JspServlet registered in conf/web.xml. The EL implementation is found by ExpressionFactory.newInstance()
JULI and Naming JULI is the logging in bin/tomcat-juli.jar. Naming is the JNDI implementation inside catalina.jar, off by default in embedded Tomcat

What's next

Let's fold the map and start reading from the root that every box on it shares. The next post is about what happens after you type catalina.sh start: the road by which Bootstrap builds class loaders and calls Catalina, how Digester — whose rules we only glanced at here — actually reads server.xml, and the Lifecycle state machine through which init() and start() spread from Server down to Wrapper, parent to child, checked with an example that prints the order of events through a listener.

Digging into Tomcat's Source 3: Lifecycle and Startup — What Happens After catalina.sh start

The two series I finished earlier also spread out a big-picture map at this point. In httpd Part 3, httpd was a core that defines hooks with the MPM and modules hanging functions on them, and the three structs server_rec, conn_rec and request_rec pointed at each other. Tomcat does the same job with a parent–child tree of Java objects and interfaces: a virtual host is a Host object rather than a server_rec, and Coyote stands where the MPM accepted connections. Compare it with the map in Spring Part 2 and one more spot comes into view: Spring MVC's DispatcherServlet is, in this post's tree, the servlet inside one Wrapper. DispatcherServlet takes the #0 position in Example 5, and Spring's world unfolds from there (Spring Part 21). In Spring Part 20, we started embedded Tomcat ourselves and watched Spring's root context being stored in Tomcat's ServletContext — that is, in one Context.

More posts