Jetty Component Architecture

Applications that use the Jetty libraries (both client and server) create objects from Jetty classes and compose them together to obtain the desired functionalities.

A client application creates a ClientConnector instance, a HttpClientTransport instance and an HttpClient instance and compose them to have a working HTTP client that uses to call third party services.

A server application creates a ThreadPool instance, a Server instance, a ServerConnector instance, a Handler instance and compose them together to expose an HTTP service.

Internally, the Jetty libraries create even more instances of other components that also are composed together with the main ones created by applications.

The end result is that an application based on the Jetty libraries is a tree of components. In server application the root of the component tree is a Server instance, while in client applications the root of the component tree is an HttpClient instance.

Having all the Jetty components in a tree is beneficial in a number of use cases. It makes possible to register the components in the tree as JMX MBeans so that a JMX console can look at the internal state of the components. It also makes possible to dump the component tree (and therefore each component’s internal state) to a log file or to the console for troubleshooting purposes.

Jetty Component Lifecycle

Jetty components typically have a life cycle: they can be started and stopped. The Jetty components that have a life cycle implement the org.eclipse.jetty.util.component.LifeCycle interface.

Jetty components that contain other components implement the org.eclipse.jetty.util.component.Container interface and typically extend the org.eclipse.jetty.util.component.ContainerLifeCycle class. ContainerLifeCycle can contain these type of components, also called beans:

  • managed beans, LifeCycle instances whose life cycle is tied to the life cycle of their container

  • unmanaged beans, LifeCycle instances whose life cycle is not tied to the life cycle of their container

  • POJO (Plain Old Java Object) beans, instances that do not implement LifeCycle

ContainerLifeCycle uses the following logic to determine if a bean should be managed, unmanaged or POJO:

  • the bean implements LifeCycle

    • the bean is not started, add it as managed

    • the bean is started, add it as unmanaged

  • the bean does not implement LifeCycle, add it as POJO

When a ContainerLifeCycle is started, it also starts recursively all its managed beans (if they implement LifeCycle); unmanaged beans are not started during the ContainerLifeCycle start cycle. Likewise, stopping a ContainerLifeCycle stops recursively and in reverse order all its managed beans; unmanaged beans are not stopped during the ContainerLifeCycle stop cycle.

Components can also be started and stopped individually, therefore activating or deactivating the functionalities that they offer.

Applications should first compose components in the desired structure, and then start the root component:

class Monitor extends AbstractLifeCycle
{
}

class Root extends ContainerLifeCycle
{
    // Monitor is an internal component.
    private final Monitor monitor = new Monitor();

    public Root()
    {
        // The Monitor life cycle is managed by Root.
        addManaged(monitor);
    }
}

class Service extends ContainerLifeCycle
{
    // An instance of the Java scheduler service.
    private ScheduledExecutorService scheduler;

    @Override
    protected void doStart() throws Exception
    {
        // Java's schedulers cannot be restarted, so they must
        // be created anew every time their container is started.
        scheduler = Executors.newSingleThreadScheduledExecutor();
        // Even if Java scheduler does not implement
        // LifeCycle, make it part of the component tree.
        addBean(scheduler);
        // Start all the children beans.
        super.doStart();
    }

    @Override
    protected void doStop() throws Exception
    {
        // Perform the opposite operations that were
        // performed in doStart(), in reverse order.
        super.doStop();
        removeBean(scheduler);
        scheduler.shutdown();
    }
}

// Create a Root instance.
Root root = new Root();

// Create a Service instance.
Service service = new Service();

// Link the components.
root.addBean(service);

// Start the root component to
// start the whole component tree.
root.start();

The component tree is the following:

Root
├── Monitor (MANAGED)
└── Service (MANAGED)
    └── ScheduledExecutorService (POJO)

When the Root instance is created, also the Monitor instance is created and added as bean, so Monitor is the first bean of Root. Monitor is a managed bean, because it has been explicitly added to Root via ContainerLifeCycle.addManaged(…​).

Then, the application creates a Service instance and adds it to Root via ContainerLifeCycle.addBean(…​), so Service is the second bean of Root. Service is a managed bean too, because it is a LifeCycle and at the time it was added to Root is was not started.

The ScheduledExecutorService within Service does not implement LifeCycle so it is added as a POJO to Service.

It is possible to stop and re-start any component in a tree, for example:

class Root extends ContainerLifeCycle
{
}

class Service extends ContainerLifeCycle
{
    // An instance of the Java scheduler service.
    private ScheduledExecutorService scheduler;

    @Override
    protected void doStart() throws Exception
    {
        // Java's schedulers cannot be restarted, so they must
        // be created anew every time their container is started.
        scheduler = Executors.newSingleThreadScheduledExecutor();
        // Even if Java scheduler does not implement
        // LifeCycle, make it part of the component tree.
        addBean(scheduler);
        // Start all the children beans.
        super.doStart();
    }

    @Override
    protected void doStop() throws Exception
    {
        // Perform the opposite operations that were
        // performed in doStart(), in reverse order.
        super.doStop();
        removeBean(scheduler);
        scheduler.shutdown();
    }
}

Root root = new Root();
Service service = new Service();
root.addBean(service);

// Start the Root component.
root.start();

// Stop temporarily Service without stopping the Root.
service.stop();

// Restart Service.
service.start();

Service can be stopped independently of Root, and re-started. Starting and stopping a non-root component does not alter the structure of the component tree, just the state of the subtree starting from the component that has been stopped and re-started.

Container provides an API to find beans in the component tree:

class Root extends ContainerLifeCycle
{
}

class Service extends ContainerLifeCycle
{
    private ScheduledExecutorService scheduler;

    @Override
    protected void doStart() throws Exception
    {
        scheduler = Executors.newSingleThreadScheduledExecutor();
        addBean(scheduler);
        super.doStart();
    }

    @Override
    protected void doStop() throws Exception
    {
        super.doStop();
        removeBean(scheduler);
        scheduler.shutdown();
    }
}

Root root = new Root();
Service service = new Service();
root.addBean(service);

// Start the Root component.
root.start();

// Find all the direct children of root.
Collection<Object> children = root.getBeans();
// children contains only service

// Find all descendants of root that are instance of a particular class.
Collection<ScheduledExecutorService> schedulers = root.getContainedBeans(ScheduledExecutorService.class);
// schedulers contains the service scheduler.

You can add your own beans to the component tree at application startup time, and later find them from your application code to access their services.

The component tree should be used for long-lived or medium-lived components such as thread pools, web application contexts, etc.

It is not recommended adding to, and removing from, the component tree short-lived objects such as HTTP requests or TCP connections, for performance reasons.

If you need component tree features such as automatic export to JMX or dump capabilities for short-lived objects, consider having a long-lived container in the component tree instead. You can make the long-lived container efficient at adding/removing the short-lived components using a data structure that is not part of the component tree, and make the long-lived container handle the JMX and dump features for the short-lived components.

Jetty Component Listeners

A component that extends AbstractLifeCycle inherits the possibility to add/remove event listeners for various events emitted by components.

A component that implements java.util.EventListener that is added to a ContainerLifeCycle is also registered as an event listener.

The following sections describe in details the various listeners available in the Jetty component architecture.

LifeCycle.Listener

A LifeCycle.Listener emits events for life cycle events such as starting, stopping and failures:

Server server = new Server();

// Add an event listener of type LifeCycle.Listener.
server.addEventListener(new LifeCycle.Listener()
{
    @Override
    public void lifeCycleStarted(LifeCycle lifeCycle)
    {
        System.getLogger("server").log(INFO, "Server {0} has been started", lifeCycle);
    }

    @Override
    public void lifeCycleFailure(LifeCycle lifeCycle, Throwable failure)
    {
        System.getLogger("server").log(INFO, "Server {0} failed to start", lifeCycle, failure);
    }

    @Override
    public void lifeCycleStopped(LifeCycle lifeCycle)
    {
        System.getLogger("server").log(INFO, "Server {0} has been stopped", lifeCycle);
    }
});

For example, a life cycle listener attached to a Server instance could be used to create (for the started event) and delete (for the stopped event) a file containing the process ID of the JVM that runs the Server.

Container.Listener

A component that implements Container is a container for other components and ContainerLifeCycle is the typical implementation.

A Container emits events when a component (also called bean) is added to or removed from the container:

Server server = new Server();

// Add an event listener of type LifeCycle.Listener.
server.addEventListener(new Container.Listener()
{
    @Override
    public void beanAdded(Container parent, Object child)
    {
        System.getLogger("server").log(INFO, "Added bean {1} to {0}", parent, child);
    }

    @Override
    public void beanRemoved(Container parent, Object child)
    {
        System.getLogger("server").log(INFO, "Removed bean {1} from {0}", parent, child);
    }
});

A Container.Listener added as a bean will also be registered as a listener:

class Parent extends ContainerLifeCycle
{
}

class Child
{
}

// The older child takes care of its siblings.
class OlderChild extends Child implements Container.Listener
{
    private Set<Object> siblings = new HashSet<>();

    @Override
    public void beanAdded(Container parent, Object child)
    {
        siblings.add(child);
    }

    @Override
    public void beanRemoved(Container parent, Object child)
    {
        siblings.remove(child);
    }
}

Parent parent = new Parent();

Child older = new OlderChild();
// The older child is a child bean _and_ a listener.
parent.addBean(older);

Child younger = new Child();
// Adding a younger child will notify the older child.
parent.addBean(younger);

Container.InheritedListener

A Container.InheritedListener is a listener that will be added to all descendants that are also Containers.

Listeners of this type may be added to the component tree root only, but will be notified of every descendant component that is added to or removed from the component tree (not only first level children).

The primary use of Container.InheritedListener within the Jetty Libraries is MBeanContainer from the Jetty JMX support.

MBeanContainer listens for every component added to the tree, converts it to an MBean and registers it to the MBeanServer; for every component removed from the tree, it unregisters the corresponding MBean from the MBeanServer.