Embedded Jetty server with handlers for legacy Java applications
Introduction
Jetty is a very powerful yet lightweight Java
library since ages that helps you create servers and clients for HTTP (literally all the versions), Web Sockets, OSGI, JMX, JAAS and much more.
This post will deal with embedding a Jetty server in a Java application and adding handlers for different requests. I’m assuming you have a basic understanding of Java and Maven and have familiarity with different HTTP methods like GET, POST, PUT, DELETE, etc.
Jetty Server Architecture
An incoming request is handled using 4 components - Threadpool, Connectors, Handlers and Server. Server
is the core of the system that handles the management of servers and the entire lifecycle of the server. Connectors
help in accepting various requests over different protocols like HTTP, HTTPS, etc. Handlers
are the components that process the incoming request. Threadpools
are like tiny workers that make the system multi-threaded and help in handling multiple requests at the same time.
An incoming request is first accepted at the Connector
. Then it is sent to the Server
which connects it to the Handler
where the main response is generated. And the response is generated using a particular thread as per Threadpool
.
Using Jetty in a Java application
New Java App
First, let’s create a new Java
app using Maven
.
> mvn archetype:generate \
-DgroupId = com.schwiftycold.poc \
-DartifactId = poc_jetty_server \
-DarchetypeArtifactId = maven-archetype-quickstart \
-DinteractiveMode = false
Add the following properties to your POM
file. This will set the Java
version and language encoding for the app.
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>20</maven.compiler.source>
<maven.compiler.target>20</maven.compiler.target>
</properties>
Add Jetty dependency
Add the following dependency in your pom.xml
file.
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>11.0.3</version>
</dependency>
...
</dependencies>
Main server
The main component of the system is the server class. This class will create a server context and register connectors and handlers. Let’s call this class MainServer
.
Threadpool
There are different kinds of Threadpool offered by Jetty like QueuedThreadPool
, ExecutorThreadPool
, ScheduledThreadPool
, etc.
QueuedThreadPool
maintains a fixed number of threads and a queue to manage incoming requests. When a request comes and the thread is busy then it is added to the queue and will be picked when a thread is available. And, due to its nature, this is widely used for handling http
requests.
ScheduledThreadPool
extends QueuedThreadPool
to handle scheduled tasks. It provides us with a scheduler to execute tasks at a certain interval. It is generally used for scheduling background tasks.
ExecutorThreadPool
enables you to use custom Executor
as the Threadpool for Jetty.
Here, we will be using QueuedThreadPool
.
ThreadPool threadPool = new QueuedThreadPool();
Server
The server is the essential component that manages the connectors and handlers. It takes the Threadpool to create a new server instance.
Server server = new Server(threadPool);
Connector
A connector allows us to accept a variety of different protocols like HTTP
, HTTPS
, Unix domain socket
, etc.
HTTP
A simple HTTP
connector can be initialized using the ServerConnector
class. And you can change the port to listen to using, setPort
function. By default, the port is 8080
.
ServerConnector connector = new ServerConnector(server);
connector.setPort(9120);
server.setConnectors(new Connector[]{connector});
HTTPS
For using HTTPS
you’ll need to register the SSL/TLS
certificate. We won’t be using this here but a quick look into this will give us a glance of the infinite possibilities.
First, you will need the sslContextFactory which defines your SSL/TLS
configurations.
SslContextFactory sslContextFactory = new SslContextFactory();
sslContextFactory.setKeyStorePath("/path/to/keystore.jks");
sslContextFactory.setKeyStorePassword("keystore-password");
sslContextFactory.setKeyManagerPassword("key-password");
ServerConnector httpsConnector = new ServerConnector(
server,
new SslConnectionFactory(sslContextFactory, "http/1.1")
);
httpsConnector.setPort(8443);
server.setConnectors(new Connector[] {httpsConnector});
Handlers
Handlers are the ones that will be creating or response. For this, let’s create a new singleton class. Let’s set this to null
for now.
server.setHandler(null);
Final code
public class MainServer {
public static void startServer() {
try {
ThreadPool threadPool = new QueuedThreadPool();
Server server = new Server(threadPool);
ServerConnector connector = new ServerConnector(server);
connector.setPort(9120);
server.setConnectors(new Connector[]{connector});
server.setHandler(null);
server.start();
server.join();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Handler class
Handler class is where we will write our logic to process the request. So this requires a special attention.
Let’s create a new class that extends AbstractHandler
Extend AbstractHandler
The AbstractHandler
required us to implement a handle
function. This function is triggered on every new request.
The handle method uses four parameters. The target
parameter denotes the endpoint that was triggered. If we trigger the URL, http://localhost:9120/hi
then the target will take the value as /hi
.
baseRequest
and request
denote the same thing but in a different context. baseRequest
is Jetty specific whereas request
donates the servlet-specific APIs. Here, we won’t be using the servlet APIs.
The response
is the processed result generated by the server which will be sent to the client.
public class MainHandler extends AbstractHandler{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
System.out.println("target: " + target);
System.out.println("baseRequest: " + baseRequest);
System.out.println("request: " + request);
System.out.println("response: " + response);
}
}
The output of the above code would be something as follows on triggering with curl
.
> curl -X POST "http://localhost:9120/hi" -d "p=value"
target: /hi
baseRequest: Request(POST http://localhost:9120/hi)@57ffc3ca
request: Request(POST http://localhost:9120/hi)@57ffc3ca
response: HTTP/1.1 200
Here, you can see the target as /hi
, the request denotes the method call and reference to the request object and the response is just 200 which denotes a success. We can manipulate this information to create responses based on different inputs.
Manage different methods
We can get the request method using the response object by calling the function, getMethod
.
public void handle(...) {
if ("POST".equalsIgnoreCase(baseRequest.getMethod())) {
System.out.println("POST request received");
}
else if ("GET".equalsIgnoreCase(baseRequest.getMethod())) {
System.out.println("GET request received");
}
}
Now, calling curl as above will print POST request received
on the server side.
Extract the query parameters
The query parameter and its values can be extracted from the base request object by using the getParameterNames
method. Here, I’m parsing the values and storing them in a Map
for accessing them later.
if ("POST".equalsIgnoreCase(baseRequest.getMethod())) {
System.out.println("POST request received");
Map<String, String> queryParams = new HashMap<>();
for (Enumeration<String> e = request.getParameterNames(); e.hasMoreElements();) {
String name = e.nextElement();
String[] values = request.getParameterValues(name);
for (String value : values) {
queryParams.put(name, value);
}
}
System.out.println("Query params: " + queryParams);
...
Now, you can see the below response on the server side.
POST request received
Query params: {p=value}
Generating the result
The last part is generating the result for specific triggers. This is done by using getWriter
function to write to the response. You can also set the status of the response using setStatus
method. You can create more if
statements for each endpoint.
try {
if (target.startsWith("/hi")) {
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().println("Hello World");
baseRequest.setHandled(true);
}
} catch (Exception e) {
e.printStackTrace();
}
Final handle method
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
if ("POST".equalsIgnoreCase(baseRequest.getMethod())) {
System.out.println("POST request received");
Map<String, String> queryParams = new HashMap<>();
for (Enumeration<String> e = request.getParameterNames(); e.hasMoreElements();) {
String name = e.nextElement();
String[] values = request.getParameterValues(name);
for (String value : values) {
queryParams.put(name, value);
}
}
System.out.println("Query params: " + queryParams);
System.out.println("Target: " +target.startsWith("/hi"));
try {
if (target.startsWith("/hi")) {
System.out.println("Target: " +target);
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().println("Hello World");
baseRequest.setHandled(true);
}
}
catch (Exception e) {
e.printStackTrace();
}
} else if ("GET".equalsIgnoreCase(baseRequest.getMethod())) {
System.out.println("GET request received");
}
}
Connect Handler and MainServer
Before testing your new server, you’ll also need to add the handler which can be done using the server’s setHandler
method.
server.setHandler(new MainHandler());
Now calling the hi
endpoint will return Hello World
as a response.
> curl -X POST "http://localhost:9120/hi" -d "p=value"
Hello World
Conclusion
In this post, we saw how to configure Jetty
server and set handlers to generate responses for a particular endpoint trigger. This looks like a long post but it revolves around just creating a ‘Hello World’ server using Jetty
. You can find the code for the above here.