Bootique Jetty - v3.0


1. Bootique Integration with Jetty

bootique-jetty module embeds Jetty web server in your application. It provides environment for running servlet specification objects (servlets, servlet filters, servlet listeners). Also, you will be able to serve static files that are either packaged in the application jar or located somewhere on the filesystem. As things go with Bootique, you will be able to centrally configure both Jetty (e.g. set connector ports) and your apps (e.g. map servlet URL patterns and pass servlet parameters).

bootique-jetty is "drag-and-drop" just like any other Bootique module. It is enabled by simply adding it to the pom.xml dependencies (assuming autoLoadModules() is in effect):

Bootique 3.x supports both the legacy JavaEE and the newer Jakarta versions of Jetty. Each Bootique Jetty module is shipped in two flavors (with and without -jakarta in the name). The examples below a based on the newer Jakarta modules. But It is your choice which one to use. The API of both is identical (except for the import package).
Maven
<dependency>
    <groupId>io.bootique.jetty</groupId>
    <artifactId>bootique-jetty-jakarta</artifactId>
</dependency>
Gradle
{
  implementation: 'io.bootique.jetty:bootique-jetty-jakarta'
}

Alternatively you may include an "instrumented" version of bootique-jetty that will enable a number of metrics for your running app. Either the regular or the instrumented Jetty modules provide --server command, which starts your web server on foreground:

java -jar my.jar --server
...
i.b.j.s.ServerFactory - Adding listener i.b.j.s.DefaultServletEnvironment
i.b.j.s.h.ContextHandler - Started o.e.j.s.ServletContextHandler@1e78c66e{/myapp,null,AVAILABLE}
i.b.j.s.ServerConnector - Started ServerConnector@41ccbaa{HTTP/1.1,[http/1.1]}{0.0.0.0:8080}
i.b.j.s.Server - Started @490ms

Various aspects of the Jetty container, such as listen port, thread pool size, etc. can be configured in a normal Bootique way via YAML, as detailed in the "Configuration Reference" chapter. A few additional Bootique modules that extend Jetty to support CORS, websockets, etc. are documented in the "Extensions" chapter.

2. Programming Jetty Applications

You can write servlet specification objects (servlets, filters, listeners) as you’d do it under JavaEE, except that there’s no .war and no web.xml. There’s only your application, and you need to let Bootique know about your objects and how they should be mapped to request URLs. Let’s start with servlets.

2.1. Servlets

The easiest way to add a servlet to a Bootique app is to annotate it with @WebServlet, providing name and url patterns:

@WebServlet(
        name = "myservlet",
        urlPatterns = "/b",
        initParams = {
                @WebInitParam(name = "p1", value = "v1"),
                @WebInitParam(name = "p2", value = "v2")
        }
)
public class AnnotatedServlet extends HttpServlet { // ...

The "name" annotation is kind of important as it would allow to override annotation values in the YAML, as described in the "Configuration Reference" chapter. A servlet created this way can inject any services it might need using normal Bootique DI injection.

Next step is adding it to Bootique via JettyModule contribution API called from your application Module:

@Override
public void configure(Binder binder) {
    JettyModule.extend(binder).addServlet(AnnotatedServlet.class);
}

But what if you are deploying a third-party servlet that is not annotated? Or annotation values are off in the context of your application? There are two choices. The first is to subclass such servlets and annotate the subclasses that you control. The second is to wrap your servlet in a special metadata object called MappedServlet, providing all the mapping information in that wrapper. This is a bit too verbose, but can be a good way to define the mapping that is not otherwise available:

@Override
public void configure(Binder binder) {
    MappedServlet mappedServlet = new MappedServlet(
            new MyServlet(),
            Collections.singleton("/c"),
            "myservlet");

    JettyModule.extend(binder).addMappedServlet(mappedServlet);
}

Finally if we need to use MappedServlet for mapping servlet URLs and parameters, but also need the ability to initialize the underlying servlet with environment dependencies, we can do something like this:

@Override
public void configure(Binder binder) {

    // must use TypeLiteral to identify which kind of MappedServlet<..> to add
    TypeLiteral<MappedServlet<MyServlet>> tl = new TypeLiteral<MappedServlet<MyServlet>>() {
    };
    JettyModule.extend(binder).addMappedServlet(tl);
}

@Singleton
@Provides
MappedServlet<MyServlet> provideMyServlet(MyService1 s1) {
    MyServlet servlet = new MyServlet(s1);
    return new MappedServlet<>(servlet, Collections.singleton("/c"), "myservlet");
}

2.2. Servlet Filters

Just like servlets, you can annotate and register your filters:

@WebFilter(
        filterName = "f1",
        urlPatterns = "/b/*",
        initParams = {
                @WebInitParam(name = "p1", value = "v1"),
                @WebInitParam(name = "p2", value = "v2")
        }
)
public class AnnotatedFilter implements Filter { // ...
@Override
public void configure(Binder binder) {
    JettyModule.extend(binder).addFilter(AnnotatedFilter.class);
}

And just like with servlets you can use MappedFilter and JettyModule.extend(..).addMappedFilter to wrap a filter in app-specific metadata.

2.3. Listeners

Listeners are simpler then servlets or filters. All you need is to create classes that implement one of servlet specification listener interfaces (ServletContextListener, HttpSessionListener, etc.) and bind them in your app:

@Override
public void configure(Binder binder) {
    JettyModule.extend(binder).addListener(MyListener.class);
}

Listeners can rely on injection to obtain dependencies, just like servlets and filters.

2.4. Serving Static Files

Jetty is not limited to just servlets. It can also act as a webserver for static files stored on the filesystem or even on the application classpath. By default this behavior is disabled to prevent unintended security risks. To enable the "webserver" feature use extender’s useDefaultServlet() method:

@Override
public void configure(Binder binder) {
    JettyModule.extend(binder).useDefaultServlet();
}

This enables a special internal servlet at "/" path, that will act as a static webserver. It requires an additional configuration parameter though - the base directory where the files are stored. This is configured in YAML, and the path can be either a directory on the filesystem or a classpath:

jetty:
  context: "/myapp"
  staticResourceBase: "classpath:com/example/docroot"

"Default" servlet always has a base URL of "/" (relative to the application context). If you want a different path (or need to serve more then one file directory under different base URLs), you can define more of those "webserver" servlets, each with its own parameters:

@Override
public void configure(Binder binder) {
    JettyModule.extend(binder)
            .addStaticServlet("abc", "/abc/*")
            .addStaticServlet("xyz", "/xyz/*");
}

By default each "static servlet" shares the common staticResourceBase, but it can also define its own base and URL-to-file mapping approach using a few servlet parameters, namely resourceBase and pathInfoOnly:

jetty:
  context: "/myapp"
  servlets:
    abc:
      params:
        # Note that "resourceBase" follows Bootique resource URL format
        # and hence can be a  "classpath:" URL or a filesystem path
        resourceBase: "classpath:com/example/abcdocroot"
        pathInfoOnly: true

resourceBase simply overrides staticResourceBase for a given servlet, while pathInfoOnly controls static resource URL resolution as follows:

URL pathInfoOnly File Path

/abc/dir1/f1.html

false (default)

<resource_base>/abc/dir1/f1.html

/abc/dir1/f1.html

true

<resource_base>/dir1/f1.html

2.5. ServletEnvironment

Some application classes have direct access to the current servlet request and context (e.g. servlets, filters, listeners are all passed the servlet environment objects in their methods). But other classes don’t. E.g. imagine you are writing an audit service that needs to know the request URL and the calling client IP address. For such services Bootique provides an injectable ServletEnvironment object:

@Inject
private ServletEnvironment servletEnv;

Now any method in this class can access ServletContext or HttpServletRequest:

String url = servletEnv.request().map(HttpServletRequest::getRequestURI).orElse("unknown");

Note that ServletEnvironment returns Optional for both, as there is no guarantee that it is invoked within a request or after the Jetty engine initialized its servlets. It is the responsibility of the caller to verify the state of the Optional and react accordingly, just like we did in this example.

3. Jetty Extensions

Bootique includes additional modules described in this chapter that provide metrics, health checks and other Jetty extensions.

3.1. Metrics and Healthchecks

You may use an "instrumented" version bootique-jetty, that will extend the server with a number of metrics and health checks for your running app:

Maven
<dependency>
    <groupId>io.bootique.jetty</groupId>
    <artifactId>bootique-jetty-jakarta-instrumented</artifactId>
</dependency>
Gradle
{
  implementation: 'io.bootique.jetty:bootique-jetty-jakarta-instrumented'
}

3.2. Support for CORS

If the services running on Jetty are accessed from other domains, you may need to explicitly configure CORS rules to to prevent the browsers from blocking access. To achieve that include the following module:

Maven
<dependency>
    <groupId>io.bootique.jetty</groupId>
    <artifactId>bootique-jetty-jakarta-cors</artifactId>
</dependency>
Gradle
{
  implementation: 'io.bootique.jetty:bootique-jetty-jakarta-cors'
}

and then configure rules in YAML under "jettycors" root element (see details here).

3.3. Support for Websockets

If in addition to HTTP requests, you’d like your server to provide access via websockets, you need to add the following module:

Maven
<dependency>
    <groupId>io.bootique.jetty</groupId>
    <artifactId>bootique-jetty-jakarta-websocket</artifactId>
</dependency>
Gradle
{
  implementation: 'io.bootique.jetty:bootique-jetty-jakarta-websocket'
}

Somewhat similar to servlets, the application will need to provide its own classes for websocket endpoints (that must follow JSR-356 API to be recognized as endpoints). Then you need to register them using JettyWebSocketModule extender:

@ServerEndpoint(value = "/ws1")
public class AnnotatedWebsocket {

    @OnMessage
    public void onMessageText(String message) {
        System.out.println("message via websocket:" + message );
    }
}
@Override
public void configure(Binder binder) {
    JettyWebSocketModule.extend(binder).addEndpoint(AnnotatedWebsocket.class);
}

Just like with servlets, endpoints can be managed by the DI container and can inject any services available in the application. Websockets server parameters are configured in YAML under "jettywebsocket" root element (see details here).

4. Testing Jetty Services

"bootique-jetty" provides integration for JUnit 5. You can still test Jetty apps using JUnit 4 with generic Bootique test tools (BQTestFactory), however this would require a bit more setup, and doesn’t provide all the cool features like dynamic ports and response assertions.

To use "bootique-jetty" test extensions, import the following module in the "test" scope:

Maven
<dependency>
    <groupId>io.bootique.jetty</groupId>
    <artifactId>bootique-jetty-jakarta-junit5</artifactId>
    <scope>test</scope>
</dependency>
Gradle
{
  testImplementation: 'io.bootique.jetty:bootique-jetty-jakarta-junit5'
}

Most Jetty tests start like this:

@BQTest
public class JettyIT {

    static final JettyTester jetty = JettyTester.create(); (1)

    @BQApp
    static final BQRuntime app = Bootique.app("-s")
            .autoLoadModules()
            .module(jetty.moduleReplacingConnectors()) (2)
            .createRuntime();
1 JettyTester doesn’t require lifecycle annotations. All you need is one instance per BQRuntime. Declaring it as a test class variable, so that both the runtime and the test code can access it per examples below.
2 JettyTester helps to bootstrap a test BQRuntime. It resets all connectors configured in the app, and creates a connector on a randomly selected unoccupied port.
Not hardcoding the connector port improves your chances of tests working regardless of what else is running on the same machine. Also it may reduce the amount of test configuration (previously it was common to create a test .yml file that would define a specific Jetty port). Bootique uses the dynamic port approach in other test helpers too.

An actual test looks as follows:

@Test
public void test() {
    Response ok = jetty.getTarget() (1)
            .path("helloworld").request().get();

    JettyTester.assertOk(ok).assertContent("Hello, world!"); (2)
}
1 Web requests are sent using JAX-RS HTTP client API. JettyHelper provides access to the client "target", so we don’t need to know about the dynamic port or service host name.
2 JettyHelper provides a simple DSL for response assertions.

5. Configuration Reference

5.1. jetty

"jetty" is a root element of the Jetty configuration and is bound to a ServerFactory object. Example:

jetty:
  context: "/myapp"
  maxThreads: 100
  params:
    a: a1
    b: b2

It supports the following properties:

Table 1. "jetty" Element Property Reference
Property Default Description

compression

true

A boolean specifying whether gzip compression should be supported. When enabled (default), responses will be compressed if a client indicates it supports compression via "Accept-Encoding: gzip" header.

connectors

a single HTTP connector on port 8080

A list of connectors. Each connector listens on a single port. There can be HTTP or HTTPS connectors. See jetty.connectors below.

context

/

Web application context path.

idleThreadTimeout

60000

A period in milliseconds specifying how long until an idle thread is terminated.

filters

empty map

A map of servlet filter configurations by filter name. See jetty.filters below.

maxThreads

200

Maximum number of request processing threads in the pool.

minThreads

4

Initial number of request processing threads in the pool.

maxQueuedRequests

1024

Maximum number of requests to queue if the thread pool is busy.

params

empty map

A map of arbitrary key/value parameters that are used as "init" parameters of the ServletContext.

servlets

empty map

A map of servlet configurations by servlet name. See jetty.servlets below.

sessions

true

A boolean specifying whether servlet sessions should be supported by Jetty.

staticResourceBase

none

This setting is used in conjunction with "default" servlet that serves "static" resources. It defines a base location for static resources of the Jetty context. It can be a filesystem path, a URL or a special “classpath:” URL. The last option gives the ability to bundle resources in the app, not unlike a JavaEE .war file. For security reasons this property has to be set explicitly. There’s no default.

compactPath

false

True if URLs are compacted to replace multiple '/'s with a single '/'

5.1.1. jetty.connectors

"jetty.connectors" element configures one or more web connectors. Each connector listens on a specified port and has an associated protocol (http or https). If no connectors are provided, Bootique will create a single HTTP connector on port 8080. Example:

jetty:
  connectors:
    - port: 9999
    - port: 9998
      type: https

Each HTTPS connector requires an SSL certificate (real or self-signed), stored in a keystore. Jetty documentation on the subject should help with generating a certificate. In its simplest form it may look like this:

keytool -keystore src/main/resources/mykeystore \
       -alias mycert -genkey -keyalg RSA -sigalg SHA256withRSA -validity 365
Table 2. HTTP connector property reference
Property Default Description

type

N/A

Connector type. To use HTTP connector, this value must be set to "http", or omitted all together ("http" is the default).

port

8080

A port the connector listens on.

host

*

A host the connector listens on. * to listen on all, 127.0.0.1 to listen on IPv4 localhost.

requestHeaderSize

8192

A max size in bytes of Jetty request headers and GET URLs.

responseHeaderSize

8192

A max size in bytes of Jetty response headers.

Table 3. HTTPS connector property reference
Property Default Description

type

N/A

Connector type. To use HTTPS connector, this value must be set to "https".

port

8080

A port the connector listens on.

requestHeaderSize

8192

A max size in bytes of Jetty request headers and GET URLs.

responseHeaderSize

8192

A max size in bytes of Jetty response headers.

keyStore

Required. A resource pointing to the keystore that has server SSL certificate. Can be a "classpath:" resource, etc.

keyStorePassword

changeit

A password to access the keystore.

certificateAlias

An optional name of the certificate in the keystore, if there’s more than one certificate.

5.1.2. jetty.filters

jetty:
  filters:
    f1:
      urlPatterns:
        - '/a/*/'
        - '/b/*'
      params:
        p1: v1
        p2: v2
    f2:
      params:
        p3: v3
        p4: v4

TODO

5.1.3. jetty.servlets

jetty:
  servlets:
    s1:
      urlPatterns:
        - '/myservlet'
        - '/someotherpath'
      params:
        p1: v1
        p2: v2
    s2:
      params:
        p3: v3
        p4: v4
    default:
      params:
        resourceBase: /var/www/html

TODO

5.2. jettycors

"jettycors" is a root element of the Jetty CORS module configuration and is bound to a CrossOriginFilterFactory object. Example:

jettycors:
  urlPatterns:
    - "/*"
  allowedOrigins: "https://www1.example.org,https://www2.example.org"
  allowedMethods: "GET,OPTIONS,HEAD"
  allowedHeaders: "*"

5.3. jettywebsocket

"jettywebsocket" is a root element of the Jetty Websocket module configuration and is bound to a WebSocketPolicyFactory object. Example:

jettywebsocket:
    asyncSendTimeout: "5s"
    maxSessionIdleTimeout: "30min"
    maxBinaryMessageBufferSize: "30kb"
    maxTextMessageBufferSize: "45kb"