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):
Maven
<dependency>
<groupId>io.bootique.jetty</groupId>
<artifactId>bootique-jetty</artifactId>
</dependency>
Gradle
{
implementation: 'io.bootique.jetty:bootique-jetty'
}
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-instrumented</artifactId>
</dependency>
Gradle
{
implementation: 'io.bootique.jetty:bootique-jetty-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-cors</artifactId>
</dependency>
Gradle
{
implementation: 'io.bootique.jetty:bootique-jetty-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-websocket</artifactId>
</dependency>
Gradle
{
implementation: 'io.bootique.jetty:bootique-jetty-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 endpoonts). 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-junit5</artifactId>
<scope>test</scope>
</dependency>
Gradle
{
testImplementation: 'io.bootique.jetty:bootique-jetty-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:
Property | Default | Description |
---|---|---|
|
|
A boolean specifying whether gzip compression should be supported. When enabled (default), responses will be compressed if a client indicates it supports compression via |
|
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 |
|
|
Web application context path. |
|
|
A period in milliseconds specifying how long until an idle thread is terminated. |
|
empty map |
A map of servlet filter configurations by filter name. See |
|
|
Maximum number of request processing threads in the pool. |
|
|
Initial number of request processing threads in the pool. |
|
|
Maximum number of requests to queue if the thread pool is busy. |
|
empty map |
A map of arbitrary key/value parameters that are used as "init" parameters of the ServletContext. |
|
empty map |
A map of servlet configurations by servlet name. See |
|
|
A boolean specifying whether servlet sessions should be supported by Jetty. |
|
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. |
|
|
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
Property | Default | Description |
---|---|---|
|
N/A |
Connector type. To use HTTP connector, this value must be set to "http", or omitted all together ("http" is the default). |
|
|
A port the connector listens on. |
|
|
A host the connector listens on. * to listen on all, 127.0.0.1 to listen on IPv4 localhost. |
|
|
A max size in bytes of Jetty request headers and GET URLs. |
|
|
A max size in bytes of Jetty response headers. |
Property | Default | Description |
---|---|---|
|
N/A |
Connector type. To use HTTPS connector, this value must be set to "https". |
|
|
A port the connector listens on. |
|
|
A max size in bytes of Jetty request headers and GET URLs. |
|
|
A max size in bytes of Jetty response headers. |
|
Required. A resource pointing to the keystore that has server SSL certificate. Can be a "classpath:" resource, etc. |
|
|
|
A password to access the keystore. |
|
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"