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 (~105kb) wrapper over the built-in api with some key
enhancements.
- Fluent API
- Path parameter parsing
- Virtual Threads
- Json (de)serialization
- 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();
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);
}));
//or can use convenience methods directly on jex
app.post("/something", ctx -> {
// some code
ctx.status(201);
});
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 the underlying
JDK HttpExchange
, and a bunch of convenience methods for extracting data from a request and sending a
response.
See the Context Javadoc for a description of all the methods available
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 organised 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 applications 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, because the application's exchange handler will not be invoked.
Exceptions
Exception handlers give you a way of handling thrown exceptions during request processing;
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 returns a user role
Access userRole = getUserRole(ctx);
// routeRoles are provided through 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 for static resources that allows you to serve static resources from the classpath or filesystem.
It provides a StaticContent
class to configure the location, http path of your static resources, and 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