@Get("/hello")
void process(
HttpServletRequest request, (1)
HttpServletResponse response) (2)
throws IOException {
response.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN);
response.setStatus(HttpStatus.ACCEPTED.getCode());
try (final PrintWriter writer = response.getWriter()) {
writer.append("Hello ").append(request.getParameter("name"));
writer.flush();
}
}
Table of Contents
Micronaut Servlet
Provides integration between Micronaut and the Servlet API
Version: 6.2.0-SNAPSHOT
1 Introduction
This project implements a Micronaut HTTP server backed onto the Servlet API and includes various subprojects that allow running popular Servlet containers as servers.
This project is for users who fall into one of the following categories:
-
Users who want to use Micronaut but the target deployment environment is based on Servlets
-
Users who prefer the thread per connection model of the Servlet API over the Event Loop model provided by the default Netty-based HTTP server
-
Users who have existing Servlets and/or Filters that they wish to combine with Micronaut.
2 Release History
For this project, you can find a list of releases (with release notes) here:
3 Working with the Servlet API
In general, you can follow the documentation for the HTTP server when building applications. All non-Netty specific features of the default HTTP server should work the same for Servlet containers (Report an issue if you find a difference).
There are a couple of additional extensions within Micronaut Servlet that make it easier to work with the Servlet API which are detailed in the following sections.
Threading Model
On a container that supports asynchronous servlets (Jetty, Tomcat and Undertow) a request is handled as follows:
-
A request without a body, or with a body that declares a
Content-Lengthno larger thanmicronaut.server.max-request-buffer-size(10MB by default) and is not a form submission, is handled entirely on the container thread that received it. The body, if any, is read on that thread before the route runs, as a blocking servlet application would, and the response is written from the same thread unless the route returns a reactive or asynchronous type. -
A request with a larger body or a chunked body is dispatched through
AsyncContext.startand its body is read through aReadListeneras it arrives, so no container thread waits for it. -
A form or multipart body is dispatched the same way, but its fields are not streamed: the container parses them itself when the route asks for its parameters or parts. A URL-encoded form without a declared length is the one exception, which the servlet runtime reads through
micronaut.server.max-request-sizeand decodes itself, since the container would otherwise parse it unbounded.
Reactive and asynchronous return types complete the response from whichever thread produces the result, and @ExecuteOn moves the route to the named executor as on the Netty server. When micronaut.servlet.async-supported is false, or on the built-in JDK server, every request is handled on the container thread and its body is read from a blocking stream.
Injecting the Servlet Request and Response
You can receive the HttpServletRequest and HttpServletResponse objects directly as parameters:
| 1 | The request object |
| 2 | The response object |
Simplified I/O code with Readable and Writable
Writing to the response and reading from the request can be simplified with Micronaut’s Readable and Writable interfaces:
import io.micronaut.core.io.Readable;
import io.micronaut.core.io.Writable;
@Post(value = "/writable", processes = "text/plain")
Writable readAndWrite(@Body Readable readable) throws IOException {
return out -> {
try (BufferedReader reader = new BufferedReader(readable.asReader())) {
out.append("Hello ").append(reader.readLine());
}
};
}
Multipart support with @Part
Multipart support is improved with the ability to inject parts using the annotation io.micronaut.http.annotation.Part. For example:
@Part@Post(value = "/multipart", consumes = MediaType.MULTIPART_FORM_DATA, produces = "text/plain")
String multipart(
String attribute, (1)
@Part("one") Person person, (2)
@Part("two") String text, (3)
@Part("three") byte[] bytes, (4)
@Part("four") jakarta.servlet.http.Part raw, (5)
@Part("five") CompletedPart part) { (6)
return "Ok";
}
| 1 | You can receive attributes with just parameter names that match the attribute name |
| 2 | Parts that have a content type of application/json can be bound to POJOs |
| 3 | You can read parts as text |
| 4 | You can read parts as byte[] |
| 5 | You can receive the raw jakarta.servlet.http.Part |
| 6 | You can receive Micronaut’s CompletedPart interface which works with Netty too |
4 Servlet Annotation Support
To use the Servlet APIs annotations to register servlets, filters and listeners you first need to add the following annotation processor dependency:
annotationProcessor("io.micronaut.servlet:micronaut-servlet-processor")
<annotationProcessorPaths>
<path>
<groupId>io.micronaut.servlet</groupId>
<artifactId>micronaut-servlet-processor</artifactId>
</path>
</annotationProcessorPaths>
The following annotations can then be used from the Servlet API to add additional servlets, filters and listeners as beans:
-
@WebFilter - Applicable to types that implement the
jakarta.servlet.Filterinterface. -
@WebServlet - Applicable to types that implement the
jakarta.servlet.Servletinterface. -
@WebListener - See the annotation javadoc for applicable types.
The @Order annotation can be used to control registration order and hence filter order. For example a value of io.micronaut.core.order.Ordered.HIGHEST_PRECEDENCE will run the filter first.
Using HIGHEST_PRECEDENCE will prevent any other filter running before your filter. The default position is 0 and HIGHEST_PRECEDENCE == Integer.MIN_VALUE hence you should consider using constants to the represent the position of your filter that exist somewhere between HIGHEST_PRECEDENCE and LOWEST_PRECEDENCE.
|
In addition, you can use the following annotations on methods of @Factory beans to instantiate servlets and filters and register them:
-
@ServletBean - Equivalent of @WebServlet but can be applied to a method of a factory to Register a new servlet.
-
@ServletFilterBean - Equivalent of @WebFilter but can be applied to a method of a factory to Register a new filter.
The following example adds a new Servlet filter with the highest precedence:
import io.micronaut.context.annotation.Factory;
import io.micronaut.core.annotation.Order;
import io.micronaut.core.order.Ordered;
import io.micronaut.servlet.api.annotation.ServletFilterBean;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.GenericFilter;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import java.io.IOException;
@Factory // (1)
public class MyFilterFactory {
@ServletFilterBean(
filterName = "another", // (2)
value = {"/extra-filter/*", "${my.filter.mapping}"}) // (3)
@Order(Ordered.HIGHEST_PRECEDENCE) // (4)
Filter myOtherFilter() {
return new GenericFilter() {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
request.setAttribute("runFirst", true);
chain.doFilter(request, response);
}
};
}
}
| 1 | A @Factory bean is defined |
| 2 | The @ServletFilterBean annotation is used and a filter name defined |
| 3 | 1 or more mappings are defined. Note these can be resolved from property placeholder configuration if necessary. |
| 4 | The order of the filter is defined. |
| Servlet Filters are not to be confused with Micronaut filters. Servlet Filters always run before the Micronaut Servlet which in turn runs the Micronaut Filters hence it is not possible to place a Servlet Filter after Micronaut Filters. |
The same processor maps the Jakarta WebSocket annotations - @ServerEndpoint, @ClientEndpoint, @OnOpen,
@OnMessage, @OnClose, @OnError and @PathParam - onto Micronaut’s WebSocket support.
See the WebSocket section.
5 WebSocket Support
Micronaut’s WebSocket programming model works on Jetty, Tomcat and Undertow. The
@ServerWebSocket beans, the WebSocketSession, the WebSocketBroadcaster and the
argument binding of @OnOpen, @OnMessage, @OnClose and @OnError behave the same as
they do on the Netty server.
To enable it, add the WebSocket module:
implementation("io.micronaut.servlet:micronaut-servlet-websocket")
<dependency>
<groupId>io.micronaut.servlet</groupId>
<artifactId>micronaut-servlet-websocket</artifactId>
</dependency>
and the Jakarta WebSocket implementation of the container you are using, which is a separate artifact in all three cases:
runtimeOnly("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server")
<dependency>
<groupId>org.eclipse.jetty.ee10.websocket</groupId>
<artifactId>jetty-ee10-websocket-jakarta-server</artifactId>
<scope>runtime</scope>
</dependency>
runtimeOnly("org.apache.tomcat.embed:tomcat-embed-websocket")
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<scope>runtime</scope>
</dependency>
runtimeOnly("io.undertow:undertow-websockets-jsr")
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-websockets-jsr</artifactId>
<scope>runtime</scope>
</dependency>
|
The Jetty artifact pulls in
|
An endpoint is written exactly as it is on the Netty server:
@ServerWebSocket("/ws/chat/{topic}/{username}") // (1)
public class ChatServerWebSocket {
private final WebSocketBroadcaster broadcaster;
public ChatServerWebSocket(WebSocketBroadcaster broadcaster) {
this.broadcaster = broadcaster;
}
@OnOpen // (2)
public void onOpen(String topic, String username, WebSocketSession session) {
broadcaster.broadcastSync("[" + username + "] Joined!", isValid(topic, session));
}
@OnMessage // (3)
public void onMessage(String topic, String username, String message, WebSocketSession session) {
broadcaster.broadcastSync("[" + username + "] " + message, isValid(topic, session)); // (4)
}
@OnClose // (5)
public void onClose(String topic, String username, WebSocketSession session) {
broadcaster.broadcastSync("[" + username + "] Disconnected!", isValid(topic, session));
}
private Predicate<WebSocketSession> isValid(String topic, WebSocketSession session) {
return s -> s != session && topic.equalsIgnoreCase(s.getUriVariables().get("topic", String.class, null));
}
}
@ServerWebSocket("/ws/chat/{topic}/{username}") // (1)
class ChatServerWebSocket(private val broadcaster: WebSocketBroadcaster) {
@OnOpen // (2)
fun onOpen(topic: String, username: String, session: WebSocketSession) {
broadcaster.broadcastSync("[$username] Joined!", isValid(topic, session))
}
@OnMessage // (3)
fun onMessage(topic: String, username: String, message: String, session: WebSocketSession) {
broadcaster.broadcastSync("[$username] $message", isValid(topic, session)) // (4)
}
@OnClose // (5)
fun onClose(topic: String, username: String, session: WebSocketSession) {
broadcaster.broadcastSync("[$username] Disconnected!", isValid(topic, session))
}
private fun isValid(topic: String, session: WebSocketSession) = Predicate<WebSocketSession> {
it !== session && topic.equals(it.uriVariables.get("topic", String::class.java).orElse(null), ignoreCase = true)
}
}
@ServerWebSocket("/ws/chat/{topic}/{username}") // (1)
class ChatServerWebSocket {
private final WebSocketBroadcaster broadcaster
ChatServerWebSocket(WebSocketBroadcaster broadcaster) {
this.broadcaster = broadcaster
}
@OnOpen // (2)
void onOpen(String topic, String username, WebSocketSession session) {
broadcaster.broadcastSync("[" + username + "] Joined!", isValid(topic, session))
}
@OnMessage // (3)
void onMessage(String topic, String username, String message, WebSocketSession session) {
broadcaster.broadcastSync("[" + username + "] " + message, isValid(topic, session)) // (4)
}
@OnClose // (5)
void onClose(String topic, String username, WebSocketSession session) {
broadcaster.broadcastSync("[" + username + "] Disconnected!", isValid(topic, session))
}
private Predicate<WebSocketSession> isValid(String topic, WebSocketSession session) {
return { WebSocketSession s ->
s !== session && topic.equalsIgnoreCase(s.getUriVariables().get("topic", String, null))
}
}
}
| 1 | The @ServerWebSocket annotation defines the URI the endpoint is mapped to, including
any path variables. |
| 2 | @OnOpen runs once the handshake has completed. Its arguments are bound from the path
variables, the originating request and the session. |
| 3 | @OnMessage receives each message. The single argument that cannot be bound from
anywhere else is the message body. |
| 4 | WebSocketBroadcaster sends a message to every open session that matches the given
predicate. |
| 5 | @OnClose runs when the connection is closed, whichever side closed it. |
Clients are written with @ClientWebSocket and are unchanged from the Netty server:
@ClientWebSocket("/ws/chat/{topic}/{username}") // (1)
public abstract class ChatClientWebSocket implements AutoCloseable { // (2)
private final Collection<String> replies = new ConcurrentLinkedQueue<>();
@OnMessage
public void onMessage(String message) {
replies.add(message); // (3)
}
public abstract void send(String message); // (4)
public Collection<String> getReplies() {
return replies;
}
}
@ClientWebSocket("/ws/chat/{topic}/{username}") // (1)
abstract class ChatClientWebSocket : AutoCloseable { // (2)
val replies: MutableCollection<String> = ConcurrentLinkedQueue()
@OnMessage
fun onMessage(message: String) {
replies.add(message) // (3)
}
abstract fun send(message: String) // (4)
}
@ClientWebSocket("/ws/chat/{topic}/{username}") // (1)
abstract class ChatClientWebSocket implements AutoCloseable { // (2)
private final Collection<String> replies = new ConcurrentLinkedQueue<>()
@OnMessage
void onMessage(String message) {
replies.add(message) // (3)
}
abstract void send(String message) // (4)
Collection<String> getReplies() {
return replies
}
}
| 1 | The client endpoint is mapped to the same URI template. |
| 2 | The class is abstract so that Micronaut can implement the send methods. |
| 3 | @OnMessage receives messages sent by the server. |
| 4 | Any abstract method whose name begins with send or broadcast is implemented to send
a message over the session. |
Jakarta WebSocket annotations
An endpoint can also be written with the
Jakarta WebSocket annotations,
@ServerEndpoint, @OnOpen, @OnMessage, @OnClose, @OnError and @PathParam, in the
same way that @WebServlet and @WebFilter can be used to register servlets and filters.
This needs the annotation processor:
annotationProcessor("io.micronaut.servlet:micronaut-servlet-processor")
<annotationProcessorPaths>
<path>
<groupId>io.micronaut.servlet</groupId>
<artifactId>micronaut-servlet-processor</artifactId>
</path>
</annotationProcessorPaths>
The processor maps the Jakarta annotations onto their Micronaut equivalents at compilation
time, so the endpoint is a @ServerWebSocket bean and takes the same path as one: the
handshake goes through the Micronaut router and filter chain, and the handlers are invoked
through the compiled bean metadata. The container never scans the class and nothing is
reflected on, so a Jakarta endpoint works in a native image as any Micronaut bean does.
import jakarta.websocket.CloseReason;
import jakarta.websocket.OnClose;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import java.io.IOException;
@ServerEndpoint("/ws/echo/{room}") // (1)
public class EchoEndpoint {
private String room;
@OnOpen
public void onOpen(Session session, @PathParam("room") String room) throws IOException { // (2)
this.room = room;
session.getBasicRemote().sendText("Welcome to " + room); // (3)
}
@OnMessage
public String onMessage(String message) { // (4)
return "[" + room + "] " + message;
}
@OnClose
public void onClose(CloseReason reason) { // (5)
}
}
import jakarta.websocket.CloseReason
import jakarta.websocket.OnClose
import jakarta.websocket.OnMessage
import jakarta.websocket.OnOpen
import jakarta.websocket.Session
import jakarta.websocket.server.PathParam
import jakarta.websocket.server.ServerEndpoint
@ServerEndpoint("/ws/echo/{room}") // (1)
class EchoEndpoint {
private lateinit var room: String
@OnOpen
fun onOpen(session: Session, @PathParam("room") room: String) { // (2)
this.room = room
session.basicRemote.sendText("Welcome to $room") // (3)
}
@OnMessage
fun onMessage(message: String): String { // (4)
return "[$room] $message"
}
@OnClose
fun onClose(reason: CloseReason) { // (5)
}
}
import jakarta.websocket.CloseReason
import jakarta.websocket.OnClose
import jakarta.websocket.OnMessage
import jakarta.websocket.OnOpen
import jakarta.websocket.Session
import jakarta.websocket.server.PathParam
import jakarta.websocket.server.ServerEndpoint
@ServerEndpoint("/ws/echo/{room}") // (1)
class EchoEndpoint {
private String room
@OnOpen
void onOpen(Session session, @PathParam("room") String room) { // (2)
this.room = room
session.basicRemote.sendText("Welcome to " + room) // (3)
}
@OnMessage
String onMessage(String message) { // (4)
"[" + room + "] " + message
}
@OnClose
void onClose(CloseReason reason) { // (5)
}
}
| 1 | @ServerEndpoint maps the endpoint. The URI template syntax is the same as
@ServerWebSocket’s, and `subprotocols is honoured. |
| 2 | A handler can take the container’s jakarta.websocket.Session and EndpointConfig by
type, @PathParam binds a path variable under the name it declares, and everything a
Micronaut handler can take - the WebSocketSession, @Header, @QueryValue, the
HttpRequest - can be taken too. |
| 3 | The Session is the container’s own, so getBasicRemote(), getAsyncRemote(),
getOpenSessions() and getUserProperties() all work as they do under the container. |
| 4 | A value returned by @OnMessage is sent to the peer, as the specification asks. An
endpoint may declare one text, one binary and one pong @OnMessage method, chosen by the
type of the message parameter. |
| 5 | @OnClose may take the Jakarta CloseReason, @OnError the Throwable. |
Unless the class declares a scope of its own, one endpoint instance is created per
connection, which is the Jakarta contract and lets the endpoint keep the session and its
state in fields. An explicit @Singleton shares one instance between all connections.
The processor reports at compilation time what could not work at runtime: a server endpoint
without an @OnMessage method, more than one handler for a message category, a partial
message handler (the boolean last-part parameter, since messages are always delivered
whole), and a maxMessageSize beyond Integer.MAX_VALUE.
Decoders, encoders and the configurator
decoders, encoders and configurator name classes the container would instantiate with
Class.newInstance(). Here the annotation processor generates an introspection for every
class the endpoint names, so a class with a public no-argument constructor - the Jakarta
contract - is instantiated through that, without reflection, and needs nothing else. A class
is resolved through the first of these that can produce it:
-
As a bean, when the class carries a scope such as
@Singletonor@Prototype. The bean can inject anObjectMapperor anything else, and its scope decides whether it is shared. -
Through its generated introspection, when it has a no-argument constructor.
-
Reflectively, when
io.micronaut:micronaut-reflectionis on the classpath and the type matches amicronaut.introspection.allow-reflectionpattern. That is the same switch that lets the shared introspector describe a type reflectively, so there is one place to opt a package into reflection. This is only needed for a class whose constructor takes arguments without being a bean; they are then injected.micronaut.introspection.allow-reflection[0]=com.example.legacy.codecs.*micronaut: introspection: allow-reflection: - com.example.legacy.codecs.*micronaut = {introspection = {allow-reflection = ["com.example.legacy.codecs.*"]}}micronaut { introspection { allowReflection = ["com.example.legacy.codecs.*"] } }{ micronaut { introspection { allow-reflection = ["com.example.legacy.codecs.*"] } } }{ "micronaut": { "introspection": { "allow-reflection": ["com.example.legacy.codecs.*"] } } }
A class none of these can produce - one whose constructor takes arguments, when
micronaut-reflection is not on the compile classpath - is reported at compilation time.
Decoders run before Micronaut’s own conversion and message body readers, and encoders apply
to the value an @OnMessage method returns, before Micronaut’s message body writers. Either
can be left out: a message type no decoder produces is read by the MessageBodyReader for the
media type the handler @Consumes (JSON by default), and a return value no encoder accepts is
written by the MessageBodyWriter for the media type it @Produces (JSON by default), exactly
as for a Micronaut endpoint. The type an encoder handles is read from its bean definition, or
from its generic signature when micronaut-reflection is present; an encoder whose type is
unknown is offered every value and stands aside for one it cannot encode.
RemoteEndpoint#sendObject on the raw session is served by the container, which instantiates
the encoders itself, reflectively; the encoders are therefore only passed to the container for
classes the reflection policy allows. Without that, return a value from the handler or send
through the Micronaut WebSocketSession instead.
A declared configurator has its modifyHandshake, checkOrigin, getNegotiatedSubprotocol
and getNegotiatedExtensions honoured; an origin must pass both its check and the
Micronaut policy. Its getEndpointInstance is not called: the endpoint
is always the bean.
Client endpoints
A @ClientEndpoint is supported the same way. The class is a plain bean whose handlers the
processor makes executable, so unlike under a Jakarta container - which needs a public
no-argument constructor - it can inject its dependencies through its constructor. It is opened
through the jakarta.websocket.WebSocketContainer bean rather than
ContainerProvider.getWebSocketContainer(), whose result depends on which implementation
ServiceLoader happens to find first.
import jakarta.websocket.ClientEndpoint;
import jakarta.websocket.CloseReason;
import jakarta.websocket.OnClose;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import java.io.IOException;
import java.util.Collection;
import java.util.concurrent.ConcurrentLinkedQueue;
@ClientEndpoint // (1)
public class EchoClientEndpoint {
private final Collection<String> replies = new ConcurrentLinkedQueue<>();
private Session session;
@OnOpen
public void onOpen(Session session) { // (2)
this.session = session;
}
@OnMessage
public void onMessage(String message) { // (3)
replies.add(message);
}
@OnClose
public void onClose(CloseReason reason) {
// the session is gone; nothing to release
}
public void send(String message) throws IOException {
session.getBasicRemote().sendText(message); // (4)
}
public Collection<String> getReplies() {
return replies;
}
}
import jakarta.websocket.ClientEndpoint
import jakarta.websocket.CloseReason
import jakarta.websocket.OnClose
import jakarta.websocket.OnMessage
import jakarta.websocket.OnOpen
import jakarta.websocket.Session
import java.util.concurrent.ConcurrentLinkedQueue
@ClientEndpoint // (1)
class EchoClientEndpoint {
val replies: MutableCollection<String> = ConcurrentLinkedQueue()
private lateinit var session: Session
@OnOpen
fun onOpen(session: Session) { // (2)
this.session = session
}
@OnMessage
fun onMessage(message: String) { // (3)
replies.add(message)
}
@OnClose
fun onClose(reason: CloseReason) {
// the session is gone; nothing to release
}
fun send(message: String) {
session.basicRemote.sendText(message) // (4)
}
}
import jakarta.websocket.ClientEndpoint
import jakarta.websocket.CloseReason
import jakarta.websocket.OnClose
import jakarta.websocket.OnMessage
import jakarta.websocket.OnOpen
import jakarta.websocket.Session
import java.util.concurrent.ConcurrentLinkedQueue
@ClientEndpoint // (1)
class EchoClientEndpoint {
final Collection<String> replies = new ConcurrentLinkedQueue<>()
private Session session
@OnOpen
void onOpen(Session session) { // (2)
this.session = session
}
@OnMessage
void onMessage(String message) { // (3)
replies.add(message)
}
@OnClose
void onClose(CloseReason reason) {
// the session is gone; nothing to release
}
void send(String message) {
session.basicRemote.sendText(message) // (4)
}
}
| 1 | @ClientEndpoint takes subprotocols, decoders, encoders and configurator, which are
honoured as for a @ServerEndpoint; the URI comes from connectToServer. |
| 2 | The handlers take what a server endpoint’s do: the container’s Session, the
EndpointConfig, the Jakarta CloseReason, the Throwable, and anything a Micronaut handler
binds, such as a @Header of the handshake request. |
| 3 | One text, one binary and one pong @OnMessage may be declared, and a value one returns is
sent to the server. Unlike a server endpoint a client need not declare any: a client that only
sends is valid. |
| 4 | Sending goes through the container’s session. The class’s own methods are its own, whatever their names. |
WebSocketContainer container = embeddedServer.applicationContext.getBean(WebSocketContainer) // (1)
EchoClientEndpoint client = new EchoClientEndpoint()
Session session = container.connectToServer(client, URI.create("ws://localhost:${embeddedServer.port}/ws/echo/lobby")) // (2)
| 1 | The container is a bean; inject it where the connection is opened. |
| 2 | connectToServer takes the endpoint class, which creates one instance per connection, or an
instance. It returns once @OnOpen has completed, and throws a DeploymentException carrying
the cause when it failed or did not complete within micronaut.servlet.websocket.connect-timeout
(30 seconds by default), in which case the session is closed. |
The connection itself is opened by the servlet container’s Jakarta client - the same
ServerContainer the server side upgrades through is also a client container on Jetty, Tomcat
and Undertow - so the Session a handler receives is the container’s own, and the handlers are
invoked through the compiled metadata: the container never scans or instantiates the class.
A @ClientEndpoint class compiled without the processor is handed to the container as it is,
which then does both reflectively. @PathParam is rejected on a client endpoint, which has no
URI template to bind it from, and Micronaut’s WebSocketClient does not open a
@ClientEndpoint; a client for it is a @ClientWebSocket.
Filters and security run on the handshake
The upgrade request reaches the Micronaut servlet like any other request, so the Micronaut
filter chain, including micronaut-security, runs against it before the connection is
handed to the container. A filter that produces its own response cancels the upgrade and
that response is written as an ordinary HTTP response, so @Secured on a @ServerWebSocket
class rejects the handshake with a 401 rather than opening a socket.
Servlet filters registered with @ServletFilterBean still run before the Micronaut servlet, and therefore before Micronaut filters, exactly as they do for HTTP requests.
Origin checking
When the endpoint declares @CrossOrigin, or when
micronaut.server.cors.enabled is set, the handshake is refused unless the Origin header
is same-origin or matches an allowed origin. A @CrossOrigin on the endpoint states the
policy for that endpoint and is used on its own; otherwise the global CORS configurations
apply. This check happens
during the handshake rather than on the response, because a browser applies its usual CORS
response check to ordinary requests but not to a WebSocket handshake, so nothing else would
stop a hostile page from opening an authenticated socket.
With no @CrossOrigin and CORS disabled every origin is accepted, matching the Netty
server.
@PreMatching filters do not run on the handshake. The upgrade is
matched to its route before the filter chain runs, so only filters resolved after route
matching apply. The Netty server behaves the same way.
|
Configuration
🔗| Property | Type | Description | Default value |
|---|---|---|---|
|
boolean |
Whether WebSocket support is enabled. Defaults to {@code true}. |
|
|
int |
The maximum size in bytes of an inbound text message. Default value (65536). |
|
|
int |
The maximum size in bytes of an inbound binary message. Default value (65536). |
|
|
java.time.Duration |
How long a session may stay idle before it is closed, or {@code null} to inherit {@code micronaut.server.idle-timeout} |
|
|
java.time.Duration |
How long an asynchronous send may take before it fails, or {@code null} to use the container default |
|
|
java.time.Duration |
How long {@code WebSocketContainer#connectToServer} waits for a client endpoint’s {@code @OnOpen} to complete before failing the connection. Default value 30 seconds. |
|
|
int |
The number of queued sends beyond which a session reports that it is not writable. Default value (64). |
Message sizes and the idle timeout are always applied explicitly, because the three
containers ship different defaults. The values above match those of the Netty server. When
micronaut.servlet.websocket.idle-timeout is not set, micronaut.server.idle-timeout is
used, and an idle session is closed with close code 1001.
An explicit maxPayloadLength on @OnMessage takes precedence over the configured message
sizes for that endpoint.
Differences from the Netty server
-
maxPayloadLengthbounds a whole message here, whereas on Netty it bounds a single frame. Handlers always receive whole messages on both servers, and a message over the limit closes the session with close code1009. -
WebSocketSession#getId()is the identifier the container assigns, not theSec-WebSocket-Keyheader. -
Outbound writes are queued per session with one write outstanding on the container at a time, because Jakarta containers reject a second asynchronous send while one is in flight.
WebSocketSession#isWritable()reportsfalseonce more thanmicronaut.servlet.websocket.max-pending-sendswrites are queued. -
The HTTP request that produced the handshake is copied before the protocol switch, since a servlet request is only valid for the duration of its dispatch. Handler arguments bound from the request, such as
@Headerand@QueryValue, therefore see the values as they were at handshake time. The authenticated principal is carried over with it, including one supplied by container-managed authentication in a WAR. -
The handshake enforces the configured CORS origins, which the Netty server does not.
-
Handlers for one session are dispatched as each message arrives rather than being serialised, so with a multi-threaded
@ExecuteOnexecutor two messages on the same session can run concurrently and complete out of order, and a message handler can overlap@OnOpen. This matches the Netty server. An endpoint that needs ordering should either keep its handlers non-blocking, so they run on the container thread that delivered the message, or synchronize on the session itself.
Unsupported runtimes
WebSocket is not available on the built-in Java HTTP server runtime or on POJA.
com.sun.net.httpserver.HttpServer has no protocol upgrade API and no ServletContext, and
a POJA application answers one request at a time over a single stream, so neither can hold a
long lived full duplex connection.
WAR deployment is supported on any container implementing Jakarta WebSocket 2.1. On an older
container the upgrade is answered with a 501, and a warning is logged the first time an
upgrade is attempted.
6 WAR Deployment
To deploy as a WAR file you need to make some adjustments to your dependencies.
First make the server you are using a developmentOnly dependency (or provided in Maven):
developmentOnly("io.micronaut.servlet:micronaut-http-server-jetty")
<dependency>
<groupId>io.micronaut.servlet</groupId>
<artifactId>micronaut-http-server-jetty</artifactId>
<scope>provided</scope>
</dependency>
Then make sure you include micronaut-servlet-engine dependency in your build configuration:
implementation("io.micronaut.servlet:micronaut-servlet-engine")
<dependency>
<groupId>io.micronaut.servlet</groupId>
<artifactId>micronaut-servlet-engine</artifactId>
</dependency>
Then alter your build configuration to build a WAR file. In Gradle this can be done by applying the WAR plugin:
plugins {
id "war"
id "application"
}
You can then build the WAR file and deploy it to the Servlet container as per the instructions provided by the container.
| Micronaut will load using MicronautServletInitializer which registers the DefaultMicronautServlet instance. |
6.1 Container version considerations
Micronaut 4.0.0 switched to using the new jakarta.servlet package for Servlet API classes.
This means that Micronaut 4.0.0+ WAR files will not run on Servlet containers that do not support the jakarta.servlet package.
For example, Tomcat 10 switched to the jakarta.servlet package, so Micronaut 4.0.0 is required to run as a WAR on Tomcat 10.
And Micronaut 4.0.0 WAR files cannot be run on Tomcat 9 or earlier.
If you have a Micronaut 3 based WAR file that you wish to deploy to Tomcat 10, you need to deploy it to the $CATALINA_BASE/webapps-javaee directory instead of the usual $CATALINA_BASE/webapps, and Tomcat will perform a conversion to jakarta.servlet.
6.2 Payara deployment
Payara scans application classes and libraries for Jakarta lifecycle annotations during deployment.
Micronaut applications packaged as WAR files include internal lifecycle beans from Micronaut Context, for example
io.micronaut.scheduling.io.watch.DefaultWatchThread, and Payara can reject those classes before Micronaut starts.
For Micronaut 4 and later, only Payara 6 or later is supported because Micronaut uses the jakarta.servlet API.
When deploying to Payara, add an application web.xml with metadata-complete="true" so the container does not try
to discover lifecycle annotations from Micronaut libraries:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
version="6.0"
metadata-complete="true">
</web-app>
This deployment descriptor complements the web-fragment.xml packaged in micronaut-servlet-engine and prevents
Payara from treating Micronaut internal lifecycle methods as container lifecycle callbacks.
6.3 External Configuration
In a standalone Micronaut Framework application, external property sources for configuration can be configured via the Java system property micronaut.config.files or the environment variable MICRONAUT_CONFIG_FILES.
When running as a WAR file, this can be problematic if you wish to run multiple Micronaut Framework WARs in the same container, as all applications will share the same location.
To allow an external location per application, it is necessary to write our own replacement for MicronautServletInitializer.
The following example will look for configuration in the /tmp directory and on the classpath in the /some/path directory.
package example;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.ApplicationContextBuilder;
import io.micronaut.servlet.engine.initializer.MicronautServletInitializer;
import jakarta.servlet.ServletContext;
public class CustomInitializer extends MicronautServletInitializer {
@Override
protected ApplicationContextBuilder buildApplicationContext(ServletContext ctx) {
return ApplicationContext
.builder()
.overrideConfigLocations(
"file:/tmp",
"classpath:/some/path"
)
.classLoader(ctx.getClassLoader())
.singletons(ctx);
}
}
We can then use Java’s Service Provider Interface to register this class as the initializer by pointing to it in the META-INF/services/jakarta.servlet.ServletContainerInitializer file.
example.CustomInitializer
| Some servlet containers may limit the locations that are accessible from applications for security reasons. |
6.4 WAR Context path
If you are deploying the WAR to the root context — for example by renaming the WAR to ROOT.war prior to deployment — then the context path may be overridden by configuring micronaut.server.context-path in your application configuration.
If you are deploying a WAR called myproject-1.0.war to Tomcat, Jetty, etc. the context path will be set to /myproject-1.0, and cannot be overridden via application configuration.
7 Jetty Server
To use Jetty as a server add the following dependency:
implementation("io.micronaut.servlet:micronaut-http-server-jetty")
<dependency>
<groupId>io.micronaut.servlet</groupId>
<artifactId>micronaut-http-server-jetty</artifactId>
</dependency>
| Jetty is supported with GraalVM native image |
If you plan to produce a WAR file then the dependency should be developmentOnly.
|
To customize the Jetty server you can use the following configuration properties:
| Property | Type | Description | Default value |
|---|---|---|---|
|
|||
|
|||
|
boolean |
||
|
java.nio.charset.Charset |
||
|
int |
||
|
java.lang.String |
||
|
java.lang.Integer |
||
|
long |
||
|
long |
||
|
java.time.Duration |
||
|
java.time.Duration |
||
|
java.time.Duration |
||
|
java.lang.String |
||
|
boolean |
||
|
boolean |
||
|
|||
|
java.lang.String |
||
|
java.lang.String |
||
|
boolean |
||
|
boolean |
||
|
boolean |
||
|
boolean |
||
|
boolean |
||
|
boolean |
||
|
boolean |
||
|
int |
||
|
boolean |
Sets whether the Jetty runtime is enabled. |
|
|
boolean |
Serve static resources with Jetty’s own {@code ResourceHandler} instead of the Micronaut static resource resolver. Jetty’s handler supports range requests and conditional caching natively, but sits outside the Micronaut filter chain, so Micronaut filters (including CORS) do not see those requests and the {@code cache-control} setting of each static resource configuration applies instead of {@code micronaut.server.responses.file}. Default value (false). |
|
|
java.util.Map |
Sets the servlet init parameters. |
To disable the Jetty runtime when it is present on the classpath:
micronaut.server.jetty.enabled=false
micronaut.server.jetty.enabled: false
"micronaut.server.jetty.enabled" = false
micronaut.server.jetty.enabled = false
{
"micronaut.server.jetty.enabled" = false
}
{
"micronaut.server.jetty.enabled": false
}
This is useful when multiple servlet runtimes are available and you want Micronaut to ignore Jetty during startup.
Or you can register a BeanCreatedEventListener:
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import org.eclipse.jetty.server.Server;
import jakarta.inject.Singleton;
@Singleton
public class JettyServerCustomizer implements BeanCreatedEventListener<Server> {
@Override
public Server onCreated(BeanCreatedEvent<Server> event) {
Server jettyServer = event.getBean();
// perform customizations...
return jettyServer;
}
}
Static Resources
Static resources configured with micronaut.router.static-resources are served by the Micronaut static resource resolver, as on the other runtimes, so filters, CORS and the micronaut.server.responses.file settings apply to them. Because the resolver runs inside the Micronaut servlet, static resource paths must fall under micronaut.servlet.mapping (the default / covers everything); a narrowed mapping such as /api/ leaves paths outside it unreachable, as on Tomcat and Undertow. To have Jetty’s own ResourceHandler serve them instead, which supports range requests natively, works outside the servlet mapping, but bypasses the Micronaut filter chain, set:
micronaut.server.jetty.native-static-resources=true
micronaut.server.jetty.native-static-resources: true
"micronaut.server.jetty.native-static-resources" = true
micronaut.server.jetty.nativeStaticResources = true
{
"micronaut.server.jetty.native-static-resources" = true
}
{
"micronaut.server.jetty.native-static-resources": true
}
Access Log Configuration
To configure the Jetty Access Log:
micronaut.server.jetty.access-log.enabled=true
micronaut.server.jetty.access-log.filename=/tmp/access.log
micronaut.server.jetty.access-log.retain-days=10
micronaut.server.jetty.access-log.pattern=%{client}a - %u %t "%r" %s %O
micronaut.server.jetty.access-log.enabled: true
micronaut.server.jetty.access-log.filename: /tmp/access.log
micronaut.server.jetty.access-log.retain-days: 10
micronaut.server.jetty.access-log.pattern: >
%{client}a - %u %t "%r" %s %O
"micronaut.server.jetty.access-log.enabled" = true
"micronaut.server.jetty.access-log.filename" = "/tmp/access.log"
"micronaut.server.jetty.access-log.retain-days" = 10
"micronaut.server.jetty.access-log.pattern" = "%{client}a - %u %t "%r" %s %O"
micronaut.server.jetty.accessLog.enabled = true
micronaut.server.jetty.accessLog.filename = "/tmp/access.log"
micronaut.server.jetty.accessLog.retainDays = 10
micronaut.server.jetty.accessLog.pattern = "%{client}a - %u %t \"%r\" %s %O"
{
"micronaut.server.jetty.access-log.enabled" = true
"micronaut.server.jetty.access-log.filename" = "/tmp/access.log"
"micronaut.server.jetty.access-log.retain-days" = 10
"micronaut.server.jetty.access-log.pattern" = "%{client}a - %u %t \"%r\" %s %O"
}
{
"micronaut.server.jetty.access-log.enabled": true,
"micronaut.server.jetty.access-log.filename": "/tmp/access.log",
"micronaut.server.jetty.access-log.retain-days": 10,
"micronaut.server.jetty.access-log.pattern": "%{client}a - %u %t \"%r\" %s %O"
}
Jetty support for Logback access
If you want to use Logback access library with jetty you have to provide logback dependency on classpath:
implementation("ch.qos.logback.access:logback-access-common")
<dependency>
<groupId>ch.qos.logback.access</groupId>
<artifactId>logback-access-common</artifactId>
</dependency>
By default, it will check for logback-access.xml in resources folder.
8 Tomcat Server
To use Tomcat as a server add the following dependency:
implementation("io.micronaut.servlet:micronaut-http-server-tomcat")
<dependency>
<groupId>io.micronaut.servlet</groupId>
<artifactId>micronaut-http-server-tomcat</artifactId>
</dependency>
| Tomcat is supported with GraalVM native image |
If you plan to produce a WAR file then the dependency should be developmentOnly.
|
To customize the Tomcat server you can use the following configuration properties:
| Property | Type | Description | Default value |
|---|---|---|---|
|
|||
|
|||
|
boolean |
||
|
java.nio.charset.Charset |
||
|
int |
||
|
java.lang.String |
||
|
java.lang.Integer |
||
|
long |
||
|
long |
||
|
java.time.Duration |
||
|
java.time.Duration |
||
|
java.time.Duration |
||
|
java.lang.String |
||
|
boolean |
||
|
boolean |
||
|
|||
|
java.lang.String |
||
|
java.lang.String |
||
|
boolean |
||
|
boolean |
||
|
boolean |
||
|
boolean |
||
|
boolean |
||
|
boolean |
||
|
boolean |
||
|
int |
||
|
java.lang.String |
The protocol to use. Defaults to org.apache.coyote.http11.Http11NioProtocol. |
|
|
boolean |
Sets whether the Tomcat runtime is enabled. |
To disable the Tomcat runtime when it is present on the classpath:
micronaut.server.tomcat.enabled=false
micronaut.server.tomcat.enabled: false
"micronaut.server.tomcat.enabled" = false
micronaut.server.tomcat.enabled = false
{
"micronaut.server.tomcat.enabled" = false
}
{
"micronaut.server.tomcat.enabled": false
}
This is useful when multiple servlet runtimes are available and you want Micronaut to ignore Tomcat during startup.
Or you can register a BeanCreatedEventListener:
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import org.apache.catalina.startup.Tomcat;
import jakarta.inject.Singleton;
@Singleton
public class TomcatServerCustomizer implements BeanCreatedEventListener<Tomcat> {
@Override
public Tomcat onCreated(BeanCreatedEvent<Tomcat> event) {
Tomcat tomcat = event.getBean();
// perform customizations...
return tomcat;
}
}
Access Log Configuration
To configure the Tomcat Access Log:
micronaut.server.tomcat.access-log.enabled=true,
micronaut.server.tomcat.access-log.pattern=combined
micronaut.server.tomcat.access-log.directory=/var/logs
micronaut.server.tomcat.access-log.enabled: true,
micronaut.server.tomcat.access-log.pattern: combined
micronaut.server.tomcat.access-log.directory: /var/logs
"micronaut.server.tomcat.access-log.enabled" = "true,"
"micronaut.server.tomcat.access-log.pattern" = "combined"
"micronaut.server.tomcat.access-log.directory" = "/var/logs"
micronaut.server.tomcat.accessLog.enabled = "true,"
micronaut.server.tomcat.accessLog.pattern = "combined"
micronaut.server.tomcat.accessLog.directory = "/var/logs"
{
"micronaut.server.tomcat.access-log.enabled" = "true,"
"micronaut.server.tomcat.access-log.pattern" = "combined"
"micronaut.server.tomcat.access-log.directory" = "/var/logs"
}
{
"micronaut.server.tomcat.access-log.enabled": "true,",
"micronaut.server.tomcat.access-log.pattern": "combined",
"micronaut.server.tomcat.access-log.directory": "/var/logs"
}
9 Undertow Server
To use Undertow as a server add the following dependency:
implementation("io.micronaut.servlet:micronaut-http-server-undertow")
<dependency>
<groupId>io.micronaut.servlet</groupId>
<artifactId>micronaut-http-server-undertow</artifactId>
</dependency>
| Undertow is not supported with GraalVM native image. Use Jetty or Tomcat if native image support is required. See UNDERTOW-1408. |
If you plan to produce a WAR file then the dependency should be developmentOnly.
|
To customize the Undertow server you can use the following configuration properties:
| Property | Type | Description | Default value |
|---|---|---|---|
|
|||
|
|||
|
boolean |
||
|
java.nio.charset.Charset |
||
|
int |
||
|
java.lang.String |
||
|
java.lang.Integer |
||
|
long |
||
|
long |
||
|
java.time.Duration |
||
|
java.time.Duration |
||
|
java.time.Duration |
||
|
java.lang.String |
||
|
boolean |
||
|
boolean |
||
|
|||
|
java.lang.String |
||
|
java.lang.String |
||
|
boolean |
||
|
boolean |
||
|
boolean |
||
|
boolean |
||
|
boolean |
||
|
boolean |
||
|
boolean |
||
|
int |
||
|
boolean |
Sets whether the Undertow runtime is enabled. |
|
|
java.util.Map |
Sets the worker options. |
|
|
java.util.Map |
Sets the socket options. |
|
|
java.util.Map |
The server options. |
To disable the Undertow runtime when it is present on the classpath:
micronaut.server.undertow.enabled=false
micronaut.server.undertow.enabled: false
"micronaut.server.undertow.enabled" = false
micronaut.server.undertow.enabled = false
{
"micronaut.server.undertow.enabled" = false
}
{
"micronaut.server.undertow.enabled": false
}
This is useful when multiple servlet runtimes are available and you want Micronaut to ignore Undertow during startup.
Or you can register a BeanCreatedEventListener:
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import io.undertow.Undertow;
import jakarta.inject.Singleton;
@Singleton
public class UndertowServerCustomizer implements BeanCreatedEventListener<Undertow.Builder> {
@Override
public Undertow.Builder onCreated(BeanCreatedEvent<Undertow.Builder> event) {
Undertow.Builder undertowBuilder = event.getBean();
// perform customizations...
return undertowBuilder;
}
}
Access Log Configuration
To configure the Undertow Access Log:
micronaut.server.undertow.access-log.enabled=true,
micronaut.server.undertow.access-log.pattern=combined
micronaut.server.undertow.access-log.output-directory=/var/logs
micronaut.server.undertow.access-log.enabled: true,
micronaut.server.undertow.access-log.pattern: combined
micronaut.server.undertow.access-log.output-directory: /var/logs
"micronaut.server.undertow.access-log.enabled" = "true,"
"micronaut.server.undertow.access-log.pattern" = "combined"
"micronaut.server.undertow.access-log.output-directory" = "/var/logs"
micronaut.server.undertow.accessLog.enabled = "true,"
micronaut.server.undertow.accessLog.pattern = "combined"
micronaut.server.undertow.accessLog.outputDirectory = "/var/logs"
{
"micronaut.server.undertow.access-log.enabled" = "true,"
"micronaut.server.undertow.access-log.pattern" = "combined"
"micronaut.server.undertow.access-log.output-directory" = "/var/logs"
}
{
"micronaut.server.undertow.access-log.enabled": "true,",
"micronaut.server.undertow.access-log.pattern": "combined",
"micronaut.server.undertow.access-log.output-directory": "/var/logs"
}
10 Built-In Java HTTP Server Runtime
To use a server runtime based on the Java built-in Http Server, add the following dependency:
implementation("io.micronaut.servlet:micronaut-http-server-jdk")
<dependency>
<groupId>io.micronaut.servlet</groupId>
<artifactId>micronaut-http-server-jdk</artifactId>
</dependency>
To customize the built-in JDK HTTP server runtime you can use the following configuration properties:
| Property | Type | Description | Default value |
|---|---|---|---|
|
|||
|
|||
|
boolean |
||
|
java.nio.charset.Charset |
||
|
int |
||
|
java.lang.String |
||
|
java.lang.Integer |
||
|
long |
||
|
long |
||
|
java.time.Duration |
||
|
java.time.Duration |
||
|
java.time.Duration |
||
|
java.lang.String |
||
|
boolean |
||
|
boolean |
||
|
|||
|
java.lang.String |
||
|
java.lang.String |
||
|
boolean |
||
|
boolean |
||
|
boolean |
||
|
boolean |
||
|
boolean |
||
|
boolean |
||
|
boolean |
||
|
int |
||
|
boolean |
Sets whether the JDK HTTP server runtime is enabled. |
To disable the built-in JDK HTTP server runtime when it is present on the classpath:
micronaut.server.jdk.enabled=false
micronaut.server.jdk.enabled: false
"micronaut.server.jdk.enabled" = false
micronaut.server.jdk.enabled = false
{
"micronaut.server.jdk.enabled" = false
}
{
"micronaut.server.jdk.enabled": false
}
HTTPS
The built-in server supports TLS through the same SSL configuration as the other runtimes. With micronaut.ssl.enabled set to true the server listens for HTTPS on micronaut.server.ssl.port (default 8443, or a random port in the test environment) instead of the HTTP port. The key store, trust store, protocols, ciphers and client authentication are read from micronaut.server.ssl. Unlike the servlet containers the built-in server has a single listener, so it does not also serve plain HTTP when TLS is enabled.
Access Log
The built-in server can write an access log in the common log format, one line per request, to an SLF4J logger:
micronaut.server.jdk.access-logger.enabled=true
micronaut.server.jdk.access-logger.logger-name=HTTP_ACCESS_LOGGER
micronaut.server.jdk.access-logger.exclusions[0]=/health.*
micronaut:
server:
jdk:
access-logger:
enabled: true
logger-name: HTTP_ACCESS_LOGGER
exclusions:
- /health.*
micronaut = {server = {jdk = {access-logger = {enabled = true, logger-name = "HTTP_ACCESS_LOGGER", exclusions = ["/health.*"]}}}}
micronaut {
server {
jdk {
accessLogger {
enabled = true
loggerName = "HTTP_ACCESS_LOGGER"
exclusions = ["/health.*"]
}
}
}
}
{
micronaut {
server {
jdk {
access-logger {
enabled = true
logger-name = "HTTP_ACCESS_LOGGER"
exclusions = ["/health.*"]
}
}
}
}
}
{
"micronaut": {
"server": {
"jdk": {
"access-logger": {
"enabled": true,
"logger-name": "HTTP_ACCESS_LOGGER",
"exclusions": ["/health.*"]
}
}
}
}
}
logger-name defaults to HTTP_ACCESS_LOGGER, as on the Netty server, and exclusions are regular expressions matched against the request path. Beans of type com.sun.net.httpserver.Filter are applied to every context of the server in bean order, which is how the access log is installed and can be used for other per-exchange concerns.
11 HTTP POJA Application
HTTP POJA allows creating Micronaut applications that consume and respond to HTTP requests with streams. By default, the application will read requests from standard input stream and write responses to standard output. The requests can only be answered in a serial manner.
Blocking work also stays on the POJA request thread by default, so using @ExecuteOn(TaskExecutors.BLOCKING) does not create additional Micronaut executor threads for POJA applications. If you need different behavior, configure the blocking executor explicitly with the standard micronaut.executors.blocking.* properties.
Currently HTTP POJA is based on Apache HTTP Core library.
This feature allows creating simple applications that launch and respond on demand with minimal overhead. The module is suitable for usage with systemd on Linux or launchd on MacOS. Examples are given below.
To use the HTTP POJA feature add the following dependencies:
implementation("io.micronaut.servlet:micronaut-http-poja-apache")
<dependency>
<groupId>io.micronaut.servlet</groupId>
<artifactId>micronaut-http-poja-apache</artifactId>
</dependency>
testImplementation("io.micronaut.servlet:micronaut-http-poja-test")
<dependency>
<groupId>io.micronaut.servlet</groupId>
<artifactId>micronaut-http-poja-test</artifactId>
<scope>test</scope>
</dependency>
To customize the HTTP POJA you can use the following configuration properties:
| Property | Type | Description | Default value |
|---|---|---|---|
|
int |
The size of the buffer that is used to read and parse the HTTP request (in bytes). Default value is 8192 (8Kb). |
8192 |
|
int |
The size of the buffer that is used to write the HTTP response (in bytes). Default value is 8192 (8Kb). |
8192 |
|
boolean |
When true, the inherited channel will be used by if present. Otherwise, STDIN and STDOUT will be used. |
true |
| Property | Type | Description | Default value |
|---|---|---|---|
|
boolean |
Sets whether the Apache POJA runtime is enabled. |
To disable the Apache HTTP POJA runtime when it is present on the classpath:
poja.apache.enabled=false
poja.apache.enabled: false
"poja.apache.enabled" = false
poja.apache.enabled = false
{
"poja.apache.enabled" = false
}
{
"poja.apache.enabled": false
}
This is useful when multiple Micronaut runtimes are available and you want Micronaut to ignore the POJA runtime during startup.
Use HTTP POJA with launchd on MacOS
If you have built a HTTP POJA application as a native image executable, create the following plist file and
replace [executable] with your executable path.
| If you are unfamiliar with building native image executables refer to Micronaut Creating First Graal App guide. |
If you do not wish to use native image prepend java and -jar program arguments and use the jar instead.
|
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.poja</string>
<key>Enabled</key>
<false/>
<key>ProgramArguments</key>
<array>
<string>[executable]</string>
<string>-Dpoja.apache.useInheritedChannel=false</string>
</array>
<key>Sockets</key>
<dict>
<key>Listeners</key>
<dict>
<key>SockServiceName</key>
<string>8080</string>
<key>SockType</key>
<string>stream</string>
<key>SockProtocol</key>
<string>TCP</string>
</dict>
</dict>
<key>StandardErrorPath</key>
<string>/tmp/com.example.poja.log</string>
<key>inetdCompatibility</key>
<dict>
<key>Wait</key>
<false/>
</dict>
<key>KeepAlive</key>
<false/>
</dict>
</plist>
Load the plist file with launchd:
launchctl load ~/Library/LaunchAgents/com.example.poja.plist
Then the configured application will respond on port 8080:
curl localhost:8080
Use HTTP POJA with systemd on Linux
If you have built a HTTP POJA application as a native image executable, create the following files and
replace [executable] with your executable path.
| If you are unfamiliar with building native image executables refer to Micronaut Creating First Graal App guide. |
If you do not wish to use native image prepend java and -jar program arguments and use the jar instead.
|
[Unit]
Description=Socket to launch poja example on incoming connection
[Socket]
ListenStream=127.0.0.1:8080
Accept=yes
[Install]
WantedBy=sockets.target
[Unit]
Description=Example Poja Service
Requires=examplepoja.socket
[Service]
Type=simple
ExecStart=[executable] -Dpoja.apache.useInheritedChannel=false
ExecStop=/bin/kill $MAINPID
KillMode=process
StandardInput=socket
StandardOutput=socket
StandardError=journal
[Install]
WantedBy=multi-user.target
Change selinux policy to allow systemd to use executable in the desired location with:
chcon -R -t bin_t [executable parent directory]
Enable and start listening on the socket with systemctl:
sudo systemctl enable examplepoja.socket
sudo systemctl start examplepoja.socket
Then the configured application will respond on port 8080:
curl localhost:8080
Use HTTP POJA with systemd-socket-activate on Linux
To test your application with systemd-socket-activate run:
systemd-socket-activate --inetd -a -l /tmp/http-poja.sock [executable]
In a separate terminal send a request to the socket:
curl --unix-socket /tmp/http-poja.sock http://localhost/
12 Parity with the Netty Server
The servlet runtimes implement the same Micronaut HTTP server contract as the default Netty server and are checked against the same HTTP server TCK. The table below records where each runtime stands for the capabilities that have historically differed. "Yes" means supported and covered by a test in this repository or in the TCK.
| Capability | Jetty | Tomcat | Undertow | JDK HttpServer |
POJA | Notes |
|---|---|---|---|---|---|---|
Request bodies streamed as they arrive |
Yes |
Yes |
Yes |
Blocking |
Blocking |
Containers with asynchronous support read a large or chunked body through a |
Request without |
Yes |
Yes |
Yes |
Yes |
No |
RFC 9112 framing. POJA still decodes a body that was never sent. |
Multipart: |
Yes |
Yes |
Yes |
No |
No |
Parsed by the container ( |
|
Yes |
Yes |
Yes |
Yes |
No |
Applies to text, JSON, form and multipart bodies; a declared length over the limit is refused without reading the body. |
|
Yes |
Yes |
Yes |
Yes |
Yes |
Bodies that are complete when the route returns announce their length; streamed bodies are chunked. |
Body-less responses: HEAD, 204, 304 |
Yes |
Yes |
Yes |
Yes |
Yes |
|
File responses: |
Yes |
Yes |
Yes |
Yes |
Yes |
|
Server-sent events |
Yes |
Yes |
Yes |
Yes |
Yes |
Events are flushed as they are produced. |
Response compression |
Yes |
Yes |
Yes |
No |
No |
|
HTTP/2 |
Yes |
Yes |
Yes |
No |
No |
|
TLS |
Yes |
Yes |
Yes |
Yes |
No |
Shared |
Virtual threads ( |
Yes |
Yes |
Yes |
Yes |
No |
Tomcat keeps its virtual-thread executor when |
Graceful shutdown ( |
Yes |
Yes |
Yes |
Yes |
No |
The server stops accepting (Jetty connector shutdown, Tomcat connector pause, Undertow 503) and waits for in-flight requests within the grace period. The JDK server only waits. |
Access log |
Yes |
Yes |
Yes |
Yes |
No |
Each container’s own log; the JDK server under |
|
Yes |
Yes |
Yes |
Yes |
No |
The proxied response body is forwarded. POJA has no server to proxy to. |
Cookie attributes (SameSite, Max-Age, several |
Yes |
Yes |
Yes |
Yes |
Yes |
|
Static resources: mappings, |
Yes |
Yes |
Yes |
Yes |
Yes |
One resolver on every runtime, inside the filter chain. Jetty’s own handler is available with |
WebSocket ( |
Yes |
Yes |
Yes |
No |
No |
Bridged onto each container’s Jakarta WebSocket implementation. |
Runtime positioning
Jetty, Tomcat and Undertow are full runtimes for production use. The built-in JDK HttpServer runtime is intended for development and small tools: it has no HTTP/2, no compression and no multipart parsing, and it keeps at most 200 idle connections. POJA (plain old Java application) runs one request at a time over standard input and output and receives correctness fixes only.
13 Known Issues
There are some known issues with Servlet integration to the Micronaut Framework. See also Parity with the Netty server for what each runtime supports.
Payara deployment annotation scanning
Payara may scan Micronaut libraries in a WAR for Jakarta lifecycle annotations before Micronaut starts.
When that happens, deployment can fail on internal Micronaut classes such as DefaultWatchThread.
Add an application src/main/webapp/WEB-INF/web.xml with metadata-complete="true" as described in the
Payara deployment section.
Static resources outside the servlet mapping
Static resources are served by the Micronaut servlet, so their paths must fall under micronaut.servlet.mapping. A narrowed mapping such as /api/* leaves paths outside it unreachable on every container; on Jetty, micronaut.server.jetty.native-static-resources serves them with the container’s own handler instead.
Unannotated StreamingFileUpload arguments
A StreamingFileUpload controller argument without @Part is not bound on the servlet runtimes, because the container has already parsed the multipart body. Annotate the argument with @Part, or use CompletedFileUpload.
14 FAQ
Where can I find the source code?
You can find the source code of this project in this repository:
How do I configure Multipart handling?
Multipart handling is enabled by default, as it is on the Netty server. Set micronaut.server.multipart.enabled to false to turn it off, and see the configuration properties for the limits that apply to uploads.
How do I configure Static Resource handling for the embedded server?
Static resources are not enabled by default. See Serving Static Resources for how to configure paths to static resources.
How do I enable HTTPS for the embedded server?
See Securing the Server with HTTPS and the configuration properties for ServerSslConfiguration.
How do I shut the server down gracefully?
Set micronaut.lifecycle.graceful-shutdown.enabled to true, as for the Netty server. When the application stops, the embedded server first stops taking new requests and then waits, for up to the configured grace period, for the requests already in progress to complete before the container is stopped. How new requests are refused depends on the container: Jetty stops accepting connections, Tomcat pauses its connectors, and Undertow answers new requests with 503 Service Unavailable. The built-in Java HTTP server only waits for in-flight requests.
15 Breaking Changes
This section documents breaking changes between Micronaut Servlet versions:
Micronaut Servlet 6.2.0
The request size limit applies to every body
micronaut.server.max-request-size (10MB by default) was only passed to the container’s multipart configuration, so a plain JSON, text or binary body of any size was accepted. The servlet runtimes now enforce it as the Netty server does: a body that declares a larger Content-Length is refused with 413 Request Entity Too Large without being read, and a body without a declared length is cut off with the same status once it grows past the limit. That includes a URL-encoded form without a declared length, whose fields are decoded from bytes that passed through the limit rather than parsed by the container unbounded. Raise micronaut.server.max-request-size for applications that receive larger bodies.
Jetty serves static resources through the Micronaut resolver
Jetty served micronaut.router.static-resources with its own ResourceHandler, outside the Micronaut servlet, while Tomcat, Undertow, the JDK server and the Netty server all resolve them with Micronaut’s StaticResourceResolver inside the request pipeline. Jetty now uses the shared path too, so static resources behave the same on every runtime:
-
Micronaut filters see static resource requests. In particular the CORS filter applies, so a request from an origin that is not allowed is refused with
403 Forbidden, where Jetty’s handler answered200without CORS headers. -
The
Cache-Controlheader comes frommicronaut.server.responses.file(private, max-age=60by default), and conditional requests withIf-Modified-Sincereceive304 Not Modified. -
The Jetty-only
micronaut.router.static-resources.*.cache-controlsetting is not applied.
Set micronaut.server.jetty.native-static-resources to true to restore Jetty’s ResourceHandler, including the cache-control setting and its own CORS handling.
Responses are compressed by default
Servlet containers did not compress responses at all, while the Netty server compresses anything above 1KB that the client accepts. micronaut.servlet.compression.enabled now defaults to true, and is wired to each container’s own encoder rather than reimplemented as a filter, so the cases that make compression awkward, HEAD, ranges, already encoded bodies and the Vary header, are settled by code that already handles them.
Two things change for Jetty, Tomcat and Undertow applications:
-
A response over
micronaut.servlet.compression.threshold(1KB) whose content type is inmicronaut.servlet.compression.content-typesis sent gzipped when the client sendsAccept-Encoding: gzip. -
Those responses carry
Vary: Accept-Encoding, which is required so a cache does not serve a compressed body to a client that cannot read it.
Set micronaut.servlet.compression.enabled to false to restore the previous behaviour. The JDK server runtime does not support compression.
Virtual threads take effect on Undertow, and set thread limits aside
micronaut.servlet.enable-virtual-threads has defaulted to true since Micronaut Servlet 4, but it only took effect on Jetty and Tomcat. Undertow ignored it and ran every servlet invocation on its XNIO worker pool, eight threads per core by default, so a blocking controller was capped at that pool’s size. On JDK 21 and later Undertow now runs the servlet deployment on a virtual thread per request, as the other containers do.
A consequence on Tomcat and Undertow: virtual threads are not pooled, so micronaut.servlet.max-threads and micronaut.servlet.min-threads no longer bound request concurrency while they are enabled. Tomcat used to replace its virtual-thread executor with a platform pool of that size, silently turning virtual threads off; it now keeps the virtual threads and logs a warning that the limit does not apply. Undertow’s WORKER_TASK_MAX_THREADS still sizes the worker pool that accepts connections, but not the threads that run routes. An application that relied on max-threads to cap load on a downstream resource, such as a JDBC pool, should set micronaut.servlet.enable-virtual-threads to false to size a platform pool as before, or bound the resource itself.
Multipart handling is enabled by default
micronaut.server.multipart.enabled has no default of its own. Its isEnabled() reported false when the property was unset, while the Netty server and the shared request lifecycle read the raw value and treat unset as enabled. The servlet configuration followed isEnabled(), so no MultipartConfigElement was registered and the container never parsed multipart bodies: parts reached no controller argument and getParts() was unavailable.
Servlet containers now follow the same rule as the rest of the framework, so an unset value means enabled.
Two consequences for Jetty, Tomcat and Undertow applications:
-
Multipart requests are parsed where they previously were not, so
@Partarguments,CompletedFileUploadandgetParts()start working without configuration. -
Micronaut’s multipart limits now apply to those uploads.
micronaut.server.multipart.max-file-sizedefaults to 1MB andmicronaut.server.max-request-sizeto 10MB, so an upload that previously passed unchecked may now be rejected. Raise the limits, or setmicronaut.server.multipart.enabledtofalseto restore the previous behaviour.
Micronaut Servlet 5.0.0
Deprecations
-
The Singleton constructor
io.micronaut.servlet.engine.DefaultServletHttpHandler(ApplicationContext) deprecated previously has been removed. `DefaultServletHttpHandler(ApplicationContext, ConversionService)is used instead. -
The abstract class constructor
io.micronaut.servlet.http.ServletHttpHandler(ApplicationContext)deprecated previously has been removed.ServletHttpHandler(ApplicationContext, ConversionService)is used instead.
Micronaut Servlet 3.3.4
Binding network interface
Previously, the default servlet engine will bind to all network interfaces.
This is a security risk.
Now, the default servlet engine will bind to localhost only.
To restore the original functionality, you need to configure micronaut.server.host, or set the HOST environment variable.
16 Repository
You can find the source code of this project in this repository: