Avaje Jex
Discord | Source | API Docs | Issues | Releases |
---|---|---|---|---|
Discord | Github | Javadoc | Github |
An uncommonly known fact is that the JDK comes built
in with an http server. Avaje Jex is a lightweight (~110kb) wrapper over the built-in api with some key
enhancements.
- Fluent API
- Path/Query parameter parsing
- Virtual Threads enabled by default
- Json (de)serialization SPI
- Automatic Compression
- Context abstraction over HttpExchange to easily retrieve and send request/response data.
Quick Start
1. Add avaje-jex dependencies.
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-jex</artifactId>
<version>${avaje.jex.version}</version>
</dependency>
<!-- perhaps also a json dependency like avaje jsonb or jackson --->
2. Create basic server
Below is an example of a basic server.
Jex.create()
.get("/", ctx -> ctx.text("hello"))
.get("/one/{id}", ctx -> ctx.text("one-" + ctx.pathParam("id")))
.filter(
(ctx, chain) -> {
System.out.println("before request");
chain.proceed();
System.out.println("after request");
})
.error(
IllegalStateException.class,
(ctx, exception) -> ctx.status(500).text(exception.getMessage()))
.port(8080)
.start();
Quick Start with Avaje Http
If you find yourself pining for the JAX-RS style of controllers, you can have avaje-http generate jex adapters for your annotated classes.
Add dependencies
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-jex</artifactId>
<version>${jex.version}</version>
</dependency>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-http-api</artifactId>
<version>${avaje.http.version}</version>
</dependency>
<!-- Annotation processor -->
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-http-jex-generator</artifactId>
<version>${avaje.http.version}</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
JDK 23+
In JDK 23+, annotation processors are disabled by default, you will need to add a flag to re-enable.
<properties>
<maven.compiler.proc>full</maven.compiler.proc>
</properties>
Define a Controller
package org.example.hello;
import io.avaje.http.api.Controller;
import io.avaje.http.api.Get;
import java.util.List;
@Controller("/widgets")
public class WidgetController {
private final HelloComponent hello;
public WidgetController(HelloComponent hello) {
this.hello = hello;
}
@Get("/{id}")
Widget getById(int id) {
return new Widget(id, "you got it"+ hello.hello());
}
@Get()
List<Widget> getAll() {
return List.of(new Widget(1, "Rob"), new Widget(2, "Fi"));
}
record Widget(int id, String name){};
}
This will generate routing code that we can register using any JSR-330 compliant DI:
@Generated("avaje-jex-generator")
@Singleton
public class WidgetController$Route implements Routing.HttpService {
private final WidgetController controller;
public WidgetController$Route(WidgetController controller) {
this.controller = controller;
}
@Override
public void add(Routing routing) {
routing.get("/widgets/{id}", this::_getById);
routing.get("/widgets", this::_getAll);
}
private void _getById(Context ctx) throws IOException {
ctx.status(200);
var id = asInt(ctx.pathParam("id"));
ctx.json(controller.getById(id));
}
private void _getAll(Context ctx) throws IOException {
ctx.status(200);
ctx.json(controller.getAll());
}
}
JSR-330 DI Usage
You can use whatever DI library you like.
public class Main {
public static void main(String[] args ) {
List<Routing.HttpService> services = // Retrieve HttpServices via DI;
Jex.create().routing(services).start();
}
}
Handling Requests
With Jex, there are three main handler types: endpoint-handlers, filters, and exception-handlers.
Each kind of handler accept a Context
instance as one of the parameters and have a void
return type.
You use methods like ctx.write(result)
, ctx.json(obj)
, or ctx.html(html)
to
set the response which will be returned to
the user.
Endpoint Handlers
Endpoint handlers are the main handler type that define your API.
You can add a GET handler to serve data to a client, or a POST handler to receive some data.
Handlers. Common methods are supported directly on the Routing
class (GET, POST, PUT, PATCH, DELETE,
HEAD, OPTIONS, TRACE)
Endpoint-handlers are matched in the order they are defined.
Jex app = Jex.create();
Routing routing = Jex.create().routing();
routing.get("/output", ctx -> {
// some code
ctx.json(object);
});
//alternatively can use a consumer to configure
app.routing(r ->
r.post("/input", ctx -> {
// some code
ctx.status(201).text("posted");
}));
//or can use convenience methods directly on jex
app.post("/something", ctx -> {
// some code
ctx.status(201).html(object);
});
Handler paths can include path-parameters. These are available via ctx.pathParam("key"):
var app = Jex.create();
// the {} syntax does not allow slashes ('/') as part of the parameter
app.get("/hello/{name}", ctx -> ctx.write("Hello: " + ctx.pathParam("name")));
// the <> syntax allows slashes ('/') as part of the parameter
app.get("/hello/<name>", ctx -> ctx.write("Hello: " + ctx.pathParam("name")));
Handler paths can also include wildcard parameters:
Jex.create()
.get("/path/*",
ctx -> ctx.write("You are here because " + ctx.path() + " matches " + ctx.matchedPath())
));
Context
The Context object provides you with everything you need to handle a http-request. It contains/wraps the JDK
HttpExchange
, and has convenience methods for extracting data from a request or sending a
response.
See the Context Javadoc for a description of all the methods available
Some Context Methods: (click to expand)
Method | Description | |
---|---|---|
Request Methods | ||
attribute("name") |
Returns an attribute on the request. | |
attribute("name", value) |
Sets an attribute on the request. | |
basicAuthCredentials() |
Returns basic auth credentials (or null if not set). | |
body() |
Returns the request body as string. | |
bodyAsBytes() |
Returns the request body as array of bytes. | |
bodyAsClass(clazz) |
Returns the request body as specified class (deserialized from JSON). | |
bodyAsType(type) |
Returns the request body as specified (possibly generic) type (deserialized from JSON). | |
bodyAsInputStream() |
Returns the underlying input stream of the request. | |
contentLength() |
Returns content length of the request body. | |
contentType() |
Returns request content type. | |
cookie("name") |
Returns request cookie by name. | |
cookieMap() |
Returns map of all request cookies. | |
exchange() |
Returns the underlying HttpExchange backing the context. | |
formParam("name") |
Returns form parameter by name, as string. | |
formParams("name") |
Returns list of form parameters by name. | |
formParamMap() |
Returns map of all form parameters. | |
header("name") |
Returns request header by name. | |
headerMap() |
Returns map of all request headers. | |
path() |
Returns request path. | |
pathParam("name") |
Returns path parameter by name as string. | |
pathParamMap() |
Returns map of all path parameters. | |
method() |
Returns request methods (GET, POST, etc). | |
queryParam("name") |
Returns query param by name as string. | |
queryParams("name") |
Returns list of query parameters by name. | |
queryParamMap() |
Returns map of all query parameters. | |
url() |
Returns request url. | |
fullUrl() |
Returns request url + query string. | |
Response Methods | ||
contentType("type") |
Sets the response content type. | |
header("name", "value") |
Sets response header by name. | |
write("result") |
Writes the given string content directly to the response outputstream. | |
write(byteArray) |
Writes the given bytes directly to the response outputstream. | |
write(inputStream) |
Writes input stream to the response outputstream. Will send response with chunked enconding if the size exceeds the buffer | |
redirect("/path", code) |
Redirects to the given path with the given status code. | |
status(code) |
Sets the response status code. | |
status() |
Returns the current response status code. | |
cookie("name", "value", maxAge) |
Sets response cookie by name, with value and max-age (optional). | |
cookie(cookie) |
Sets cookie using jex Cookie class. | |
removeCookie("name", "/path") |
Removes cookie by name and path (optional). | |
json(obj) |
Sets content type to json, then serializes the object to the response outputstream. | |
jsonStream(obj) |
Sets content type to x-json-stream, then serializes the object to the response outputstream. | |
html("html") |
Calls write(string), and also sets content type to html. | |
render("/template.tmpl", model) |
Calls html(renderedTemplate). |
Filters
Filters are used to pre/post process incoming requests. Pre-processing occurs before the application's exchange
handler is invoked, and post-processing occurs after the exchange handler returns.
Filters are organized in chains, and are executed in the order they were
registered. This can be useful for adding authentication, caching, extra logging, etc.
Each filter in the chain invokes the next filter within its own filter(Context, FilterChain)
implementation. The final filter in the chain invokes the application's exchange handler.
Jex.create()
.filter(
(ctx, chain) -> {
System.out.println("before request");
// proceed to the next filter in the chain, or the endpoint handler if at the end of
// the chain
chain.proceed();
System.out.println("after request");
});
The Filter may decide to terminate the chain, by not calling the method. In this case, the filter must send the response to the request, as the application's exchange handler will not be invoked.
Exceptions
Exception handlers give you a way of handling thrown exceptions during request processin.
var app = Jex.create();
app.error(NullPointerException.class, (ctx, e) -> {
// handle nullpointers here
});
app.error(Exception.class, (ctx, e) -> {
// handle general exceptions here
// will not trigger if more specific exception handler found
});
HttpResponseException
Jex comes with a built in class called HttpResponseException
, which can be used for default responses.
If not caught by an exception handler, the exception is automatically converted into a response and sent to the
user.
Access Management
When registering a route, you can specify security roles that can be accessed from within a request.
A common way to manage access is to use a filter as seen in the example below:
//custom enum for access roles
enum Access implements Role {
USER,
ADMIN
}
public static void main(String[] args) {
Jex.create()
.get("/user", ctx -> ctx.text("user"), Access.USER)
.get("/admin", ctx -> ctx.text("admin"), Access.ADMIN)
.filter(
(ctx, chain) -> {
// some user defined function that determines the role
Access userRole = getUserRole(ctx);
// the route's registered roles can be retrieved by the Context interface
if (!ctx.routeRoles().contains(userRole)) {
// request can be aborted by throwing an exception or by not calling chain.proceed()
throw new HttpResponseException(403, "unauthorized");
}
chain.proceed();
});
}
Json Content
Jex has a JsonService
SPI to allow for (de)serialization from/to json. It requires only the toJson
/fromJson
methods to be implemented.
If jackson or avaje-jsonb is present on the class path, jex will create a default JsonService automatically if none are manually registered.
When a JsonService is available, we can use the Context methods bodyAsClass
and
bodyAsType
to deserialize json and the json
method to serialize and send a response
back
Jex.create()
.jsonService(new JacksonJsonService())
.post(
"/json",
ctx -> {
MyBody body = ctx.bodyAsClass(MyBody.class);
ctx.json(new CustomResponse());
});
Static Resources
There is a separate module that allows you to serve static resources from the classpath or filesystem.
It provides a StaticContent
class to configure the location and http path of your static resources,
as well as
other attributes.
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-jex-static-content</artifactId>
<version>${avaje.jex.version}</version>
</dependency>
StaticContent singleFile =
StaticContent.createFile("src/main/resources/example.txt").httpPath("/single").build();
StaticContent directoryCP =
StaticContent.createCP("/public/").httpPath("/").directoryIndex("index.html").build();
Jex app =
Jex.create()
.plugin(singleFile) // will serve the src/main/resources/example.txt
.plugin(directoryCP); // will serve files from the /public classpath directory
Available Configuration: (click to expand)
Method | Description |
---|---|
directoryIndex("index.html") |
The index file to be served when a directory is requested. |
route("/public") |
Sets the HTTP path and security role for the static resource handler. |
preCompress() |
Sent resources will be pre-compressed and cached in memory when this is enabled. |
putMimeTypeMapping("sus", "application/sus") |
Adds a custom file extension MIME mapping to the configuration. |
putResponseHeader("key", value) |
Adds a new response header to the configuration. |
resourceLoader(ClassResourceLoader.fromClass(clazz)) |
Sets a custom resource loader for loading class/module path resources. |
skipFilePredicate(ctx -> !ctx.path().contains("/skip")) |
Sets a predicate to filter files based on the request context. |
Server Configuration
Below are the available configuration options for the Jex server:
Method | Description |
---|---|
compression(c->{...}) |
Configures compression settings using a consumer function. |
contextPath("/") |
Sets the contextPath passed to the underlying HttpServer. |
executor(executor) |
Sets the executor service used to handle incoming requests. (Defaults to a virtual thred executor) |
health(true) |
Enables/Disables a default health endpoint. |
host("host") |
Sets the host on which the HttpServer will bind to. |
httpsConfig(httpsConfigurator) |
Enables https with the provided
HttpsConfigurator .
|
ignoreTrailingSlashes(false) |
Configures whether trailing slashes in request URIs should be ignored. |
jsonService(jsonService) |
Sets the JSON service used for (de)serialization. |
port(8080) |
Sets the port number on which the HttpServer will listen for incoming requests. |
renderer("ftl", ftlRenderer) |
Registers a template renderer for a specific file extension. |
socketBacklog(int backlog) |
Sets the underlying HttpServer socket backlog. |
Configuring Https
The JDK HttpsServer can be configured using a HttpsConfigurator
instance to provide SSL termination.
The below example demonstrates how to configure a HttpsConfigurator
with an SSLContext
.
char[] passphrase = "changeit".toCharArray();
try {
// Load the keystore
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream("keystore.jks"), passphrase);
// Set up the key manager factory
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, passphrase);
// Set up the trust manager factory
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(ks);
// Set up the SSL context
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
// Create Server
var https = new HttpsConfigurator(sslContext);
Jex.create()
.get("/", ctx -> ctx.text("Hello World"))
.config(c -> c.httpsConfig(https).port(8443))
.start();
} catch (Exception e) {
e.printStackTrace();
}
Alternate HttpServer Implementations
The JDK provides an SPI to swap the underlying HttpServer
, so you can easily use jex with
alternate implementations by adding them as a dependency.
Robaho
Robaho's server is a zero-dependency implementation with some key optimizations. In certain benchmarks, performance seems to be increased by 10x over the built-in server and up to 5x over the jetty implementation.
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-jex</artifactId>
<version>${jex.version}</version>
</dependency>
<dependency>
<groupId>io.github.robaho</groupId>
<artifactId>httpserver</artifactId>
<version>${robaho.version}</version>
</dependency>
Jetty
Jetty is a classic embedded server with a long and distinguished history.
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-jex</artifactId>
<version>${jex.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http-spi</artifactId>
<version>${jetty.version}</version>
</dependency>