∞ do more, more easily
1. Introduction
Jooby is a modern, performant and easy to use web framework for Java and Kotlin built on top of your favorite web server.
import io.jooby.Jooby;
public class App extends Jooby {
{
get("/", ctx -> "Welcome to Jooby!");
}
public static void main(String[] args) {
runApp(args, App::new);
}
}
Latest Release: 3.5.0. Looking for a previous version?
|
1.1. Features
-
Lightweight and Fast. See Tech Empower Benchmark
-
Increase productivity with Hot-Reload for development
-
Script/lambda routes using fluent API
-
MVC routes using Jooby or JAX-RS annotations
-
OpenAPI 3 support
-
Reactive responses (Completable Futures, RxJava, Reactor, SmallRye types and Kotlin Coroutines)
-
Multi-server including Jetty, Netty and Undertow
-
Make the jump to full-stack framework with the extension/plugin mechanism and a variety of modules
1.2. Script API
Script API (a.k.a. script routes) provides a fluent DSL based on lambda
functions, free of
reflection and annotations.
We usually extend Jooby
and define routes in the instance initializer:
import io.jooby.Jooby;
public class App extends Jooby {
{
get("/", ctx -> "Hello Jooby!");
}
public static void main(String[] args) {
runApp(args, App::new);
}
}
For Java applications we favor extending Jooby
because the DSL looks better (no need
to prefix the get
method with a variable).
This is not strictly necessary (of course); you may prefer to do it without extending Jooby
:
import io.jooby.Jooby;
public class App {
public static void main(String[] args) {
runApp(args, app -> {
app.get("/", ctx -> "Hello Jooby!");
});
}
}
For Kotlin, it doesn’t matter which one you choose. The DSL looks great with or without extending
Kooby
.
1.3. MVC API
The MVC API (a.k.a. MVC routes) uses annotation to define routes and byte code generation to execute them.
import io.jooby.annotation.*;
public class MyController {
@GET
public String sayHi() {
return "Hello Jooby!";
}
}
public class App {
public static void main(String[] args) {
runApp(args, app -> {
app.use(new MyController());
});
}
}
Read more about MVC and JAX-RS support in the MVC API chapter.
2. Getting Started
The best way of getting started is using the jooby console
. It is a small application that generates
Jooby projects very quickly.
Features
-
Maven or Gradle build
-
Java or Kotlin application
-
Script or MVC routes
-
Jetty, Netty or Undertow application
-
Uber/Fat jar or Stork native launcher
-
Dockerfile
To install the console:
-
Download jooby-cli.zip
-
Unzip
jooby-cli.zip
in your user home directory (or any other directory you prefer) -
Find the native launchers in the
bin
directory
You might want to add the native launcher |
To simplify documentation we use |
jooby set -w ~/Source
All code will be saved inside the ~/Source
directory.
Workspace directory is ready!
Now type jooby
and hit ENTER.
After prompt, type help create
:
jooby> help create
Missing required parameter: <name>
Usage: jooby create [-dgikms] [--server=<server>] <name>
Creates a new application
<name> Application name or coordinates (groupId:artifactId:
version)
-d, --docker Generates a Dockerfile
-g, --gradle Generates a Gradle project
-i Start interactive mode
-k, --kotlin Generates a Kotlin application
-m, --mvc Generates a MVC application
-s, --stork Add Stork Maven plugin to build (Maven only)
--server=<server> Choose one of the available servers: jetty, netty or
undertow
jooby>
The create
command generates a Jooby application. Some examples:
jooby> create myapp
jooby> create myapp --kotlin
Since 3.x
Kotlin was removed from core, you need to the Maven Gradle
|
jooby> create myapp --gradle
jooby> create myapp --gradle --kotlin
Maven and Java are the default options but you can easily override these with -g -k
or -gk
(order doesn’t matter).
Along with the build and language, the create
command adds two test classes: UnitTest
and IntegrationTest
.
Passing the -m
or --mvc
generates an MVC application:
jooby> create myapp --mvc
The --server
option, allow you to choose between: (J)etty, (N)etty or (U)ndertow:
jooby> create myapp --server undertow
Maven/Gradle configuration generates an uber/fat
jar at package time. Maven builds supports
generation of Stork launchers.
jooby> create myapp --stork
There is a -d
or --docker
option which generates a Dockerfile
jooby> create myapp --docker
The default package in all these examples is set to app
. To get full control of groupId, package, version, etc., use the interactive mode:
jooby> create myapp -i
2.1. Code Snippets
For simplicity and brevity we are going to skip the runApp
function and extending Jooby
.
Code example will look like:
{
get("/", ctx -> "Snippet");
}
The use of application class
or runApp function
will be included when strictly necessary.
3. Router
The Router is the heart of Jooby and consist of:
3.1. Route
A Route consists of three part:
{
(1) (2)
get("/foo", ctx -> {
return "foo"; (3)
});
// Get example with path variable
get("/foo/{id}", ctx -> {
return ctx.path("id").value();
});
// Post example
post("/", ctx -> {
return ctx.body().value();
});
}
1 | HTTP method/verb, like: GET , POST , etc… |
2 | Path pattern, like: /foo , /foo/{id} , etc… |
3 | Handler function |
The handler function always produces a result, which is send it back to the client.
3.1.1. Attributes
Attributes let you annotate a route at application bootstrap time. It functions like static metadata available at runtime:
{
get("/foo", ctx -> "Foo")
.attribute("foo", "bar");
}
An attribute consist of a name and value. Values can be any object. Attributes can be accessed at runtime in a request/response cycle. For example, a security module might check for a role attribute.
{
use(next -> ctx -> {
User user = ...;
String role = ctx.getRoute().attribute("Role");
if (user.hasRole(role)) {
return next.apply(ctx);
}
throw new StatusCodeException(StatusCode.FORBIDDEN);
});
}
In MVC routes you can set attributes via annotations:
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Role {
String value();
}
@Path("/path")
public class AdminResource {
@Role("admin")
public Object doSomething() {
...
}
}
{
use(next -> ctx -> {
System.out.println(ctx.getRoute().attribute("Role"));
});
}
The previous example will print: admin.
You can retrieve all the attributes of the route by calling ctx.getRoute().getAttributes()
.
Any runtime annotation is automatically added as route attributes following these rules: - If the annotation has a value method, then we use the annotation’s name as the attribute name. - Otherwise, we use the method name as the attribute name.
3.2. Path Pattern
3.2.1. Static
{
get("/foo", ctx -> "Foo");
}
3.2.2. Variable
{
(1)
get("/user/{id}", ctx -> {
int id = ctx.path("id").intValue(); (2)
return id;
});
}
1 | Defines a path variable id |
2 | Retrieve the variable id as int |
{
(1)
get("/file/{file}.{ext}", ctx -> {
String filename = ctx.path("file").value(); (2)
String ext = ctx.path("ext").value(); (3)
return filename + "." + ext;
});
}
1 | Defines two path variables: file and ext |
2 | Retrieve string variable: file |
3 | Retrieve string variable: ext |
{
(1)
get("/profile/{id}?", ctx -> {
String id = ctx.path("id").value("self"); (2)
return id;
});
}
1 | Defines an optional path variable id . The trailing ? make it optional. |
2 | Retrieve the variable id as String when present or use a default value: self . |
The trailing ?
makes the path variable optional. The route matches:
-
/profile
-
/profile/eespina
3.2.3. Regex
{
(1)
get("/user/{id:[0-9]+}", ctx -> {
int id = ctx.path("id").intValue(); (2)
return id;
});
}
1 | Defines a path variable: id . Regex expression is everything after the first : , like: [0-9]+ |
2 | Retrieve an int value |
Optional syntax is also supported for regex path variable: /user/{id:[0-9]+}?
:
-
matches
/user
-
matches
/user/123
3.2.4. * Catchall
{
(1)
get("/articles/*", ctx -> {
String catchall = ctx.path("*").value(); (2)
return catchall;
});
get("/articles/*path", ctx -> {
String path = ctx.path("path").value(); (3)
return path;
});
}
1 | The trailing * defines a catchall pattern |
2 | We access to the catchall value using the * character |
3 | Same example, but this time we named the catchall pattern and we access to it using path
variable name. |
A |
3.3. Handler
Application logic goes inside a handler. A
handler is a function that accepts a context
object and produces a result
.
A context allows you to interact with the HTTP Request
and manipulate the
HTTP Response
.
Incoming request matches exactly ONE route handler. If there is no handler, produces a |
{
get("/user/{id}", ctx -> ctx.path("id").value()); (1)
get("/user/me", ctx -> "my profile"); (2)
get("/users", ctx -> "users"); (3)
get("/users", ctx -> "new users"); (4)
}
Output:
1 | GET /user/ppicapiedra ⇒ ppicapiedra |
2 | GET /user/me ⇒ my profile |
3 | Unreachable ⇒ override it by next route |
4 | GET /users ⇒ new users not users |
Routes with most specific path pattern (2
vs 1
) has more precedence. Also, is one or more routes
result in the same path pattern, like 3
and 4
, last registered route hides/overrides previous route.
3.3.1. Filter
Cross cutting concerns such as response modification, verification, security, tracing, etc. is available via Route.Filter.
A filter
takes the next
handler in the pipeline and produces a new
handler:
interface Filter {
Handler apply(Handler next);
}
{
use(next -> ctx -> {
long start = System.currentTimeMillis(); (1)
Object response = next.apply(ctx); (2)
long end = System.currentTimeMillis();
long took = end - start;
System.out.println("Took: " + took + "ms"); (3)
return response; (4)
});
get("/", ctx -> {
return "filter";
});
}
1 | Saves start time |
2 | Proceed with execution (pipeline) |
3 | Compute and print latency |
4 | Returns a response |
One or more filter on top of a handler produces a new handler. |
3.3.2. Before
The before filter runs before a handler
.
A before
filter takes a context
as argument and don’t produces a response. It expected to operates
via side effects (usually modifying the HTTP response).
interface Before {
void apply(Context ctx);
}
{
before(ctx -> {
ctx.setResponseHeader("Server", "Jooby");
});
get("/", ctx -> {
return "...";
});
}
3.3.3. After
The after filter runs after a handler
.
An after
filter takes three arguments. The first argument is the HTTP context
, the second
argument is the result/response from a functional handler or null
for side-effects handler,
the third and last argument is an exception generates from handler.
It expected to operates via side effects, usually modifying the HTTP response (if possible) or for cleaning/trace execution.
interface After {
void apply(Context ctx, Object result, Throwable failure);
}
{
after((ctx, result, failure) -> {
System.out.println(result); (1)
ctx.setResponseHeader("foo", "bar"); (2)
});
get("/", ctx -> {
return "Jooby";
});
}
1 | Prints Jooby |
2 | Add a response header (modifies the HTTP response) |
If the target handler is a functional handler modification of HTTP response is allowed it.
For side effects handler the after filter is invoked with a null
value and isn’t allowed to modify the HTTP response.
{
after((ctx, result, failure) -> {
System.out.println(result); (1)
ctx.setResponseHeader("foo", "bar"); (2)
});
get("/", ctx -> {
return ctx.send("Jooby");
});
}
1 | Prints null (no value) |
2 | Produces an error/exception |
Exception occurs because response was already started and its impossible to alter/modify it.
Side-effects handler are all that make use of family of send methods, responseOutputStream and responseWriter.
You can check whenever you can modify the response by checking the state of isResponseStarted():
{
after((ctx, result, failure) -> {
if (ctx.isResponseStarted()) {
// Don't modify response
} else {
// Safe to modify response
}
});
}
An after handler is always invoked. |
The next examples demonstrate some use cases for dealing with errored responses, but keep in mind that an after handler is not a mechanism for handling and reporting exceptions that’s is a task for an Error Handler.
{
after((ctx, result, failure) -> {
if (failure == null) {
db.commit(); (1)
} else {
db.rollback(); (2)
}
});
}
Here the exception is still propagated given the chance to the Error Handler to jump in.
{
after((ctx, result, failure) -> {
if (failure instanceOf MyBusinessException) {
ctx.send("Recovering from something"); (1)
}
});
}
1 | Recover and produces an alternative output |
Here the exception wont be propagated due we produces a response, so error handler won’t be execute it.
In case where the after handler produces a new exception, that exception will be add to the original exception as suppressed exception.
{
after((ctx, result, failure) -> {
...
throw new AnotherException();
});
get("/", ctx -> {
...
throw new OriginalException();
});
error((ctx, failure, code) -> {
Throwable originalException = failure; (1)
Throwable anotherException = failure.getSuppressed()[0]; (2)
});
}
1 | Will be OriginalException |
2 | Will be AnotherException |
3.3.4. Complete
The complete listener run at the completion of a request/response cycle (i.e. when the request has been completely read, and the response has been fully written).
At this point it is too late to modify the exchange further. They are attached to a running context (not like a filter/before/after filters).
{
use(next -> ctx -> {
long start = System.currentTimeInMillis();
ctx.onComplete(context -> { (1)
long end = System.currentTimeInMillis(); (2)
System.out.println("Took: " + (end - start));
});
});
}
1 | Attach a completion listener |
2 | Run after response has been fully written |
Completion listeners are invoked in reverse order.
3.4. Pipeline
Route pipeline (a.k.a route stack) is a composition of one or more use(s) tied to a single handler
:
{
// Increment +1
use(next -> ctx -> {
Number n = (Number) next.apply(ctx);
return 1 + n.intValue();
});
// Increment +1
use(next -> ctx -> {
Number n = (Number) next.apply(ctx);
return 1 + n.intValue();
});
get("/1", ctx -> 1); (1)
get("/2", ctx -> 2); (2)
}
Output:
1 | /1 ⇒ 3 |
2 | /2 ⇒ 4 |
Behind the scene, Jooby builds something like:
{
// Increment +1
var increment = use(next -> ctx -> {
Number n = (Number) next.apply(ctx);
return 1 + n.intValue();
});
Handler one = ctx -> 1;
Handler two = ctx -> 2;
Handler handler1 = increment.then(increment).then(one);
Handler handler2 = increment.then(increment).then(two);
get("/1", handler1);
get("/2", handler2);
}
Any filter
defined on top of the handler will be stacked/chained into a new handler.
Filter without path pattern
This was a hard decision to make, but we know is the right one. Jooby 1.x uses a path pattern to
define The Jooby 1.x
Suppose there is a bot trying to access and causing lot of In Jooby 2.x this won’t happen anymore. If there is a matching handler, the |
3.4.1. Order
Order follows the what you see is what you get approach. Routes are stacked in the way they were added/defined.
{
// Increment +1
use(next -> ctx -> {
Number n = (Number) next.apply(ctx);
return 1 + n.intValue();
});
get("/1", ctx -> 1); (1)
// Increment +1
use(next -> ctx -> {
Number n = (Number) next.apply(ctx);
return 1 + n.intValue();
});
get("/2", ctx -> 2); (2)
}
Output:
1 | /1 ⇒ 2 |
2 | /2 ⇒ 4 |
3.4.2. Scoped Filter
The route(Runnable) and path(String,Runnable) operators are used to group one or more routes.
A scoped filter
looks like:
{
// Increment +1
use(next -> ctx -> {
Number n = (Number) next.apply(ctx);
return 1 + n.intValue();
});
routes(() -> { (1)
// Multiply by 2
use(next -> ctx -> {
Number n = (Number) next.apply(ctx);
return 2 * n.intValue();
});
get("/4", ctx -> 4); (2)
});
get("/1", ctx -> 1); (3)
}
Output:
1 | Introduce a new scope via route operator |
2 | /4 ⇒ 9 |
3 | /1 ⇒ 2 |
It is a normal filter
inside of one of the group operators.
3.5. Grouping routes
As showed previously, the route(Runnable) operator push a new route scope
and allows you to selectively apply one or more routes.
{
routes(() -> {
get("/", ctx -> "Hello");
});
}
Route operator is for grouping one or more routes and apply cross cutting concerns to all them.
In similar fashion the path(String,Runnable) operator groups one or more routes under a common path pattern.
{
path("/api/user", () -> { (1)
get("/{id}", ctx -> ...); (2)
get("/", ctx -> ...); (3)
post("/", ctx -> ...); (4)
...
});
}
1 | Set common prefix /api/user |
2 | GET /api/user/{id} |
3 | GET /api/user |
4 | POST /api/user |
3.6. Composing
3.6.1. Mount
Composition is a technique for building modular applications. You can compose one or more router into a new one.
Composition is available through the mount(Router) operator:
public class Foo extends Jooby {
{
get("/foo", Context::getRequestPath);
}
}
public class Bar extends Jooby {
{
get("/bar", Context::getRequestPath);
}
}
public class App extends Jooby {
{
mount(new Foo()); (1)
mount(new Bar()); (2)
get("/app", Context::getRequestPath); (3)
}
}
1 | Imports all routes from Foo . Output: /foo ⇒ /foo |
2 | Imports all routes from Bar . Output: /bar ⇒ /bar |
3 | Add more routes . Output /app ⇒ /app |
public class Foo extends Jooby {
{
get("/foo", Context::getRequestPath);
}
}
public class App extends Jooby {
{
mount("/prefix", new Foo()); (1)
}
}
1 | Now all routes from Foo will be prefixed with /prefix . Output: /prefix/foo ⇒ /prefix/foo |
The mount operator only import routes. Services, callbacks, etc… are not imported. Main application is responsible for assembly all the resources and services required by imported applications.
3.6.2. Install
Alternatively, you can install a standalone application into another one using the install(Jooby) operator:
public class Foo extends Jooby {
{
get("/foo", ctx -> ...);
}
}
public class Bar extends Jooby {
{
get("/bar", ctx -> ...);
}
}
public class App extends Jooby {
{
install(Foo::new); (1)
install(Bar::new); (2)
}
}
1 | Imports all routes, services, callbacks, etc… from Foo . Output: /foo ⇒ /foo |
2 | Imports all routes, services, callbacks, etc… from Bar . Output: /bar ⇒ /bar |
This operator lets you for example to deploy Foo
as a standalone application or integrate it into a main one called App3508
.
The install operator shares the state of the main application, so lazy initialization (and therefore instantiation) of
any child applications is mandatory.
For example, this won’t work:
{
Foo foo = new Foo();
install(() -> foo); // Won't work
}
The Foo
application must be lazy initialized:
{
install(() -> new Foo()); // Works!
}
3.7. Dynamic Routing
Dynamic routing looks similar to composition but enables/disables routes at runtime
using a predicate
.
Suppose you own two versions of an API
and for some time you need to support both the old
and new
APIs:
public class V1 extends Jooby {
{
get("/api", ctx -> "v1");
}
}
public class V2 extends Jooby {
{
get("/api", ctx -> "v2");
}
}
public class App extends Jooby {
{
mount(ctx -> ctx.header("version").value().equals("v1"), new V1()); (1)
mount(ctx -> ctx.header("version").value().equals("v2"), new V2()); (2)
}
}
Output:
1 | /api ⇒ v1 ; when version header is v1 |
2 | /api ⇒ v2 ; when version header is v2 |
Done ♡!
3.8. Options
3.8.1. Hidden Method
The setHiddenMethod(String) option allow clients to override the HTTP method using a hidden form field.
<form method="post" action="/form">
<input type="hidden" name="_method" value="put">
</form>
import io.jooby.Jooby;
...
{
setHiddenMethod("_method"); (1)
put("/form", ctx -> { (2)
});
}
1 | Configure hidden method property to read the form field: _method |
2 | Execute the put version of /form |
The default implementation looks for a form field from POST form/multipart request. You can provide a different strategy.
import io.jooby.Jooby;
...
{
setHiddenMethod(ctx -> ctx.header("X-HTTP-Method-Override").toOptional()); (1)
}
1 | Look for a request header: X-HTTP-Method-Override |
3.8.2. Trust Proxy
The setTrustProxy(boolean) option enables parsing of X-Forwarded-*
headers.
import io.jooby.Jooby;
...
{
setTrustProxy(true) (1)
get("/", ctx -> {
String remoteAddress = ctx.getRemoteAddress(); (2)
String scheme = ctx.getScheme(); (3)
String host = ctx.getHost(); (4)
int port = ctx.getPort(); (5)
...
});
}
1 | Set trust proxy |
2 | Set remote address from X-Forwarded-For |
3 | Set scheme from X-Forwarded-Proto |
4 | Set host from X-Forwarded-Host |
5 | Set port from X-Forwarded-Host or X-Forwarded-Port |
This should only be installed behind a reverse proxy that has been configured to send the
|
4. Context
A Context allows you to interact with the HTTP Request and manipulate the HTTP Response.
In most of the cases you can access the context object as a parameter of your route handler:
{
get("/", ctx -> { /* do important stuff with variable 'ctx'. */ });
}
If you need to access the context via the service registry or dependency injection, you need to explicitly request the registration of it as a service with the following invocation:
{
setContextAsService(true);
}
Important to note that the context is a request scoped object, it’s only available through the service registry while the request it belongs to is being processed.
Context also provides derived information about the current request such as a
matching locale (or locales) based on the Accept-Language
header (if presents). You may use
the result of locale() or locales() to present content matching to
the user’s language preference.
The above methods use Locale.lookup(…)
and Locale.filter(…)
respectively to perform the
language tag matching. See their overloads if you need to plug in your own matching strategy.
To leverage language matching however, you need to tell Jooby which languages your application
supports. This can be done by either setting the application.lang
configuration property
to a value compatible with the
Accept-Language
header:
application.lang = en, en-GB, de
or calling the setLocales(List) or setLocales(Locale…) method at runtime:
{
setLocales(Locale.GERMAN, new Locale("hu", "HU"));
}
If you don’t set the supported locales explicitly, Jooby uses a single locale provided by
Locale.getDefault()
.
4.1. Parameters
There are several parameter types: header
, cookie
, path
, query
, form
, multipart
,
session
and flash
. All them share a unified/type-safe API for accessing and manipulating their values.
We are going to describe them briefly in the next sections, then go into specific features of the Value API.
There is also a parameter lookup feature by which you can access a parameter from any combination of the above types with well-defined priority.
4.1.1. Header
HTTP headers allow the client and the server to pass additional information with the request or the response.
{
get("/", ctx -> {
String token = ctx.header("token").value(); (1)
Value headers = ctx.headers(); (2)
Map<String, String> headerMap = ctx.headerMap(); (3)
...
});
}
1 | Header variable token |
2 | All headers as Value |
3 | All headers as map |
4.1.2. Cookie
Request cookies are send to the server using the Cookie
header, but we do provide a simple
key/value
access to them:
{
get("/", ctx -> {
String token = ctx.cookie("token").value(); (1)
Map<String, String> cookieMap = ctx.cookieMap(); (2)
...
});
}
1 | Cookie variable token |
2 | All cookies as map |
4.1.3. Path
Path parameter are part of the URI
. To define a path variable you need to use the {identifier}
notation.
{
get("/{id}" ctx -> ctx.path("id").value()); (1)
get("/@{id}" ctx -> ctx.path("id").value()); (2)
get("/file/{name}.{ext}", ctx -> cxt.path("name") + "." + ctx.path("ext")); (3)
get("/file/*", ctx -> ctx.path("*")) (4)
get("/{id:[0-9]+}", ctx -> ctx.path("id)) (5)
}
1 | Path variable id |
2 | Path variable id prefixed with @ |
3 | Multiple variables name and ext |
4 | Unnamed catchall path variable |
5 | Path variable with a regular expression |
{
get("/{name}", ctx -> {
String pathString = ctx.getRequestPath(); (1)
Value path = ctx.path(); (2)
Map<String, String> pathMap = ctx.pathMap(); (3)
String name = ctx.path("name").value(); (4)
...
});
}
1 | Access to the raw path string:
|
2 | Path as Value object:
|
3 | Path as Map<String, String> object:
|
4 | Path variable name as String :
|
4.1.4. Query
Query String is part of the URI
that start after the ?
character.
{
get("/search", ctx -> {
String queryString = ctx.queryString(); (1)
QueryString query = ctx.query(); (2)
Map<String, List<String>> queryMap = ctx.queryMultimap(); (3)
String q = ctx.query("q").value(); (4)
SearchQuery searchQuery = ctx.query(SearchQuery.class); (5)
...
});
}
class SearchQuery {
public final String q;
public SearchQuery(String q) {
this.q = q;
}
}
1 | Access to raw queryString:
|
2 | Query String as QueryString object:
|
3 | Query string as multi-value map
|
4 | Access to decoded variable q :
|
5 | Query string as SearchQuery
|
4.1.5. Formdata
Formdata is expected to be in HTTP body, or for as part of the URI
for GET
requests.
Data is expected to be encoded as application/x-www-form-urlencoded
.
{
post("/user", ctx -> {
Formdata form = ctx.form(); (1)
Map<String, List<String>> formMap = ctx.formMultimap(); (2)
String userId = ctx.form("id").value(); (3)
String pass = ctx.form("pass").value(); (4)
User user = ctx.form(User.class); (5)
...
});
}
class User {
public final String id;
public final String pass;
public User(String id, String pass) {
this.id = id;
this.pass = pass;
}
}
curl -d "id=root&pass=pwd" -X POST http://localhost:8080/user
1 | Form as Formdata ⇒ {id=root, pass=pwd} |
2 | Form as multi-value map ⇒ {id=root, pass=[pwd]} |
3 | Form variable id ⇒ root |
4 | Form variable pass ⇒ pwd |
5 | Form as User object ⇒ User(id=root, pass=pwd) |
4.1.6. Multipart
Form-data must be present in the HTTP body and encoded as multipart/form-data
:
{
post("/user", ctx -> {
Multipart multipart = ctx.multipart(); (1)
Map<String, List<String> multipartMap = ctx.multipartMultimap(); (2)
String userId = ctx.multipart("id").value(); (3)
String pass = ctx.multipart("pass").value(); (4)
FileUpload pic = ctx.file("pic"); (5)
User user = ctx.multipart(User.class); (6)
...
});
}
class User {
public final String id;
public final String pass;
public final FileUpload pic;
public User(String id, String pass, FileUpload pic) {
this.id = id;
this.pass = pass;
this.pic = pic;
}
}
curl -F id=root -F pass=root -F pic=@/path/to/local/file/profile.png http://localhost:8080/user
1 | Form as Multipart ⇒ {id=root, pass=pwd, pic=profile.png} |
2 | Form as multi-value map ⇒ {id=root, pass=[pwd]} |
3 | Form variable id ⇒ root |
4 | Form variable pass ⇒ pwd |
5 | FileUpload variable pic |
6 | Form as User object ⇒ User(id=root, pass=pwd, pic=profile.png) |
File Upload
File upload are available ONLY for multipart requests. Java Kotlin
|
4.1.7. Session
Session parameters are available via session() or sessionOrNull() methods. HTTP Session is covered in his own chapter, but here is a quick look:
Session session = ctx.session(); (1)
String attribute = ctx.session("attribute").value(); (2)
1 | Find an existing Session or create one |
2 | Get a session attribute |
4.1.8. Flash
Flash parameters are designed to transport success/error messages between requests. It is similar to a Session but the lifecycle is shorter: data is kept for only one request.
get("/", ctx -> {
return ctx.flash("success").value("Welcome!"); (3)
});
post("/save", ctx -> {
ctx.flash().put("success", "Item created"); (1)
return ctx.sendRedirect("/"); (2)
});
1 | Set a flash attribute: success |
2 | Redirect to home page |
3 | Display an existing flash attribute success or shows Welcome! |
Flash attributes are implemented using an HTTP Cookie
. To customize the cookie
(its name defaults to jooby.flash
) use the setFlashCookie(Cookie) method:
{
setFlashCookie(new Cookie("myflash").setHttpOnly(true));
// or if you're fine with the default name
getFlashCookie().setHttpOnly(true);
}
4.1.9. Parameter Lookup
You can search for parameters in multiple sources with an explicitly defined priority using the lookup() or lookup(String,ParamSource…) method:
get("/{foo}", ctx -> {
String foo = ctx.lookup("foo", ParamSource.QUERY, ParamSource.PATH).value();
return "foo is: " + foo;
});
get("/{foo}", ctx -> {
String foo = ctx.lookup().inQuery().inPath().get("foo").value();
return "foo is: " + foo;
});
In case of a request like /bar?foo=baz
, foo is: baz
will be returned since the query parameter
takes precedence over the path parameter.
4.1.10. Client Certificates
If mutual TLS is enabled, you can access the client’s certificates from the context. The first certificate in the list is the peer certificate, followed by the ca certificates in the chain (the order is preserved).
get("/{foo}", ctx -> {
List<Certificate> certificates = ctx.getClientCertificates(); (1)
Certificate peerCertificate = certificates.get(0); (2)
});
1 | Get all of the certificates presented by the client during the SSL handshake. |
2 | Get only the peer certificate. |
4.2. Value API
The Value is an unified and type-safe API across all parameter types:
-
Header
-
Path
-
Query
-
Formdata/Multipart
For learning purpose we are going to show all the Value features using query parameters, but keep in mind these features apply to all the parameter types.
4.2.1. Single value
Single value is available via value()
or [type]Value()
functions:
{
get("/", ctx -> {
String name = ctx.query("name").value(); (1)
float score = ctx.query("score").floatValue(); (2)
boolean enabled = ctx.query("enabled").booleanValue(); (3)
BigDecimal decimal = ctx.query("decimal").value(BigDecimal::new); (4)
...
});
}
The value()
family methods always retrieve a value
. If there is no value, a
BadRequest(400)
response is generated. So single value parameters are required:
1 | Access to query parameter q and convert to String :
|
2 | Access to query parameter score and convert to float :
|
3 | Access to query parameter enabled and convert to boolean :
|
4 | Access to query parameter decimal and convert to BigDecimal :
|
4.2.2. Default and Optional value
Default and optional value are available in two different ways:
-
Providing a default value
-
Requesting an
java.util.Optional
object
{
get("/search", ctx -> {
String q = ctx.query("q").value("*:*"); (1)
return q;
});
get("/search", ctx -> {
Optional<String> q = ctx.query("q").toOptional(); (2)
return q;
});
}
1 | Access to query variable q and convert to String with a default value of : .
|
2 | Access to query variable q and convert to Optional<String> :
|
4.2.3. Multiple values
Multiple values are available via functions:
{
get("/", ctx -> {
List<String> q = ctx.query("q").toList(); (1)
List<Integer> n = ctx.query("n").toList(Integer.class); (2)
List<BigDecimal> decimals = ctx.query("d").toList(BigDecimal::new); (3)
...
});
}
1 | Multi-value query parameter q as List<String> :
|
2 | Multi-value query parameter as List<Integer>
|
3 | Multi-value query parameter as List<BigDecimal>
|
4.2.4. Structured data
The Value API provides a way to traverse and parse structured data:
/?user.name=root&user.pass=pass
{
get("/", ctx -> {
Value user = ctx.query("user"); (1)
String name = user.get("name").value(); (2)
String pass = user.get("pass").value(); (3)
String email = user.get("email").value("none"); (4)
...
}}
}
1 | Get the user node |
2 | Get the name value from user node |
3 | Get the pass value from user node |
4 | Get the mail value from user node. This is an optional value. |
The get(String) takes a path
and returns another value. The returning
value may or may not exists.
Syntax
Structured data decoder supports dot
and bracket
notation:
?member.firstname=Pedro&member.lastname=Picapiedra
?member[firstname]=Pedro&member[lastname]=Picapiedra
?members[0]firstname=Pedro&members[0]lastname=Picapiedra
POJO
Structured data decoder is able to reconstruct a POJO (Plain Old Java Object) from:
We are going to use a Group
and Member
objects to demonstrate how the decoder works:
class Member {
public final String firstname;
public final String lastName;
public Member(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
}
class Group {
public final String id;
public final List<Member> members;
public Member(String id, List<Member> members) {
this.id = id;
this.members = members;
}
}
/?firstname=Pedro&lastName=Picapiedra
{
get("/", ctx -> {
Member member = ctx.query(Member.class);
...
});
}
/?member.firstname=Pedro&member.lastName=Picapiedra
{
get("/", ctx -> {
Member member = ctx.query("member").to(Member.class);
...
});
}
Tabular data uses the bracket array notation:
/?[0]firstname=Pedro&[0]lastName=Picapiedra&[1]firstname=Pablo&[2]lastname=Marmol
{
get("/", ctx -> {
List<Member> members = ctx.query().toList(Member.class);
...
});
}
/?id=flintstones&members[0]firstname=Pedro&members[0]lastName=Picapiedra
{
get("/", ctx -> {
Group group = ctx.query(Group.class);
...
});
}
The target POJO
must follow one of these rules:
-
Has a zero argguments/default constructor, or
-
Has only one constructor
-
Has multiple constructors, but only one is annotated with Inject
The decoder matches HTTP parameters in the following order:
-
As constructor arguments
-
As setter method
HTTP parameter name which are not a valid Java identifier must be annotated with Named:
class Member {
public final String firstname;
public final String lastname;
public Member(@Named("first-name") String firstname, @Named("last-name") String lastname) {
....
}
}
♡♡
4.3. Request Body
Raw request body
is available via body() method:
{
post("/string", ctx -> {
String body = ctx.body().value(); (1)
...
});
post("/bytes", ctx -> {
byte[] body = ctx.body().bytes(); (2)
...
});
post("/stream", ctx -> {
InputStream body = ctx.body().stream(); (3)
...
});
}
1 | HTTP Body as String |
2 | HTTP Body as byte array |
3 | HTTP Body as InputStream |
This give us the raw body
.
4.3.1. Message Decoder
Request body parsing is achieved using the MessageDecoder functional interface.
public interface MessageDecoder {
<T> T decode(Context ctx, Type type) throws Exception;
}
MessageDecoder has a single decode
method that takes two input arguments: (context, type)
and returns a single result of the given type.
{
FavoriteJson lib = new FavoriteJson(); (1)
decoder(MediaType.json, (ctx, type) -> { (2)
byte[] body = ctx.body().bytes(); (3)
return lib.fromJson(body, type); (4)
});
post("/", ctx -> {
MyObject myObject = ctx.body(MyObject.class); (5)
});
}
1 | Choose your favorite jackson library |
2 | Check if the Content-Type header matches application/json |
3 | Read the body as byte[] |
4 | Parse the body and use the requested type |
5 | Route handler now call the body(Type) function to trigger the decoder function |
Jooby comes with a Maven Gradle
|
4.4. Response Body
Response body is generated from handler
function:
{
get("/", ctx -> {
ctx.setResponseCode(200); (1)
ctx.setResponseType(MediaType.text); (2)
ctx.setResponseHeader("Date", new Date()); (3)
return "Response"; (4)
});
}
1 | Set status code to OK(200) . This is the default status code |
2 | Set content-type to text/plain . This is the default content-type |
3 | Set the date header |
4 | Send a Response string to the client |
4.4.1. Message Encoder
Response enconding is achieved using the MessageEncoder functional interface.
public interface MessageEncoder {
byte[] encode(@NonNull Context ctx, @NonNull Object value) throws Exception;
}
MessageEncoder has a single encode
method that accepts two input arguments: (context, result)
and
produces a result.
{
FavoriteJson lib = new FavoriteJson(); (1)
encoder(MediaType.json, (ctx, result) -> { (2)
String json = lib.toJson(result); (3)
ctx.setDefaultResponseType(MediaType.json); (4)
return json; (5)
});
get("/item", ctx -> {
MyObject myObject = ...;
return myObject; (6)
});
}
1 | Choose your favorite jackson library |
2 | Check if the Accept header matches application/json |
3 | Convert result to JSON |
4 | Set default Content-Type to application/json |
5 | Produces JSON response |
6 | Route handler returns a user defined type |
Jooby comes with a Maven Gradle
|
5. MVC API
MVC API is an alternative way to define routes in Jooby. It generates source code to define and execute routes.
If you use Gradle 6.0 or a later version, you can leverage incremental annotation processing support, which means that Gradle only compiles classes that changed since the last compilation, and only runs annotation processing on those changed classes.
The annotation processor has two options allowing you to control incremental processing behavior:
tasks.withType(JavaCompile) {
options.compilerArgs += [
'-parameters',
'-Ajooby.incremental=true',
'-Ajooby.services=true'
]
}
By setting jooby.incremental
to false
you can disable incremental processing entirely, which means
the regardless what’s changed, the whole project is recompiled each time. Defaults to true
.
The generated bytecode is responsible for registering routes, retrieving and invoking your controllers. Jooby loads these classes with Java’s service-provider loading facility by default. To make this work, a so-called provider configuration file needs to be created alongside with the generated classes. The content of this file is dependent on all MVC controllers, therefore the annotation processor must operate in aggregating mode, in which all generated classes are rewritten each time.
You may disable the generation of the provider configuration file by setting jooby.services
to false
(the default is true
). This allows the annotation processor to run in isolating mode: if you
change e.g. HelloController
only, then only the class responsible for registering the routes for
HelloController
will be regenerated. This however will force Jooby to load the generated classes
with reflection instead of the service-provider loading facility.
The package annotation
contains all the annotations available for MVC routes.
import io.jooby.annotation.*;
@Path("/mvc") (1)
public class Controller {
@GET (2)
public String sayHi() {
return "Hello Mvc!";
}
}
public class App extends Jooby {
{
mvc(new Controller_()); (3)
}
public static void main(String[] args) {
runApp(args, App::new);
}
}
1 | Set a path pattern. The @Path annotation is enable at class or method level |
2 | Add a HTTP method |
3 | Register/install the generated controller in the main application |
5.1. Getting Started
To create a new MVC project open the jooby
console and type:
jooby create myapp --mvc
The jooby console takes care of all configuration steps required by the annotation processing tool.
5.2. Registration
Mvc routes need to be registered (no classpath scanning). Registration is done from your application class:
public class App extends Jooby {
{
mvc(new MyController_());
}
public static void main(String[] args) {
runApp(args, App::new);
}
}
The mvc(MvcExtension) install the mvc route. The generated controller instantiate the controller
or requires
it when an jakarta.inject.Inject
annotated controller is present.
5.3. Parameters
HTTP parameter provision is available via *Param
annotations.
There is also a Param annotation which allows to retrieve parameters from multiple sources.
5.3.1. Header
Provisioning of headers is available via HeaderParam annotation:
public class MyController {
@GET
public Object provisioning(@HeaderParam String token) { (1)
...
}
}
1 | Access to HTTP header named token |
Compared to JAX-RS the parameter name on @*Param
annotation is completely optional, but required for
non valid Java names:
public class MyController {
@GET
public Object provisioning(@HeaderParam("Last-Modified-Since") long lastModifiedSince) {
...
}
}
5.3.2. Cookie
Provisioning of cookies is available via CookieParam annotation:
public class MyController {
@GET
public Object provisioning(@CookieParam String token) { (1)
...
}
}
1 | Access to cookie named token |
Compared to JAX-RS the parameter name on @*Param
annotation is completely optional, but required for
non valid Java names:
public class MyController {
@GET
public Object provisioning(@CookieParam("token-id") String tokenId) {
...
}
}
5.3.3. Path
For path parameters the PathParam annotation is required:
public class MyController {
@Path("/{id}")
public Object provisioning(@PathParam String id) {
...
}
}
5.3.4. Query
For query parameters the QueryParam annotation is required:
public class MyController {
@Path("/")
public Object provisioning(@QueryParam String q) {
...
}
}
5.3.5. Formdata/Multipart
For formdata/multipart parameters the FormParam annotation is required:
public class MyController {
@Path("/")
@POST
public Object provisioning(@FormParam String username) {
...
}
}
5.3.6. Body
Body parameter doesn’t require an annotation:
public class MyController {
@Path("/")
@POST
public Object provisioning(MyObject body) {
...
}
}
5.3.7. Bind
You can use the BindParam annotation which allow custom mapping from HTTP request.
public class Controller {
@GET("/{foo}")
public String bind(@BindParam MyBean bean) {
return "with custom mapping: " + bean;
}
}
public record MyBean(String value) {
public static MyBean of(Context ctx) {
// build MyBean from HTTP request
}
}
It works as:
Alternative you can specify the factory class:
@BindParam(MyFactoryClass.class)
And/or function name:
@BindParam(value = MyFactoryClass.class, fn = "fromContext")
5.3.8. Flash
Provisioning of flash attribute is available via FlashParam annotation:
public class MyController {
@GET
public Object provisioning(@FlashParam String success) { (1)
...
}
}
1 | Access to flash named success |
5.3.9. Session
Provisioning of session attribute is available via SessionParam annotation:
public class MyController {
@GET
public Object provisioning(@SessionParam String userId) { (1)
...
}
}
1 | Access to session attribute named userId |
Provisioning of Session is available too:
public class MyController {
@GET
public Object provisioning(Session session) { (1)
...
}
}
1 | If no session exists yet, new session will be created |
To avoid this, just use java.util.Optional<Session>
as type.
5.3.10. Context
Provisioning of context attributes is available via ContextParam annotation:
public class MyController {
@GET
public Object provisioning(@ContextParam String userId) { (1)
...
}
}
1 | Access to context attribute named userId |
Provisioning of all attributes is available too:
public class MyController {
@GET
public Object provisioning(@ContextParam Map<String, Object> attributes) { (1)
...
}
}
1 | All context attributes must be set as arguments. They must be declared as Map<String, Object> |
5.3.11. Multiple Sources
You can use the Param annotation to search for a parameter in multiple sources. The sources and their precedence can be specified as follows:
public class FooController {
@GET("/{foo}")
public String multipleSources(@Param({ QUERY, PATH }) String foo) {
return "foo is: " + foo;
}
}
In case of a request like /bar?foo=baz
, foo is: baz
will be returned since the query parameter
takes precedence over the path parameter.
5.4. Responses
5.4.1. Status Code
The default status code is Success(200)
, except for void
methods with the @DELETE
annotation which is set to No Content(204)
.
There are two options if you need a different status code:
-
Add a Context parameter and set the setResponseCode(StatusCode)
-
Returns a StatusCode instance
5.4.2. NonBlocking
Method returning a CompletableFuture
, Single
, Maybe
, Flowable
, Mono
or Flux
is
considered a non-blocking route.
Kotlin suspend functions are supported too:
class SuspendMvc {
@GET
@Path("/delay")
suspend fun delayed(ctx: Context): String {
delay(100)
return ctx.getRequestPath()
}
}
fun main(args: Array<String>) {
runApp(args) {
use(SuspendMvc())
}
}
A non-blocking route run on the event loop (by default) where blocking is NOT allowed. For more details please checkout the non-blocking responses section.
5.5. Execution model
The MVC routes follows the execution model described in Execution Model. To run application logic in the EVENT_LOOP:
public class App extends Jooby {
{
mvc(new MyController());
}
public static void main(String[] args) {
runApp(args, EVENT_LOOP, App::new); (1)
}
}
1 | Start the application in the EVENT_LOOP execution mode |
Similarly, if you need to run all mvc routes in the WORKER execution mode:
public class App extends Jooby {
{
dispatch(() -> {
mvc(new MyBlockingController()); (1)
});
}
public static void main(String[] args) {
runApp(args, EVENT_LOOP, App::new);
}
}
1 | Wrap the controller using the dispatch operator |
One drawback with this approach is that the entire controller is now going to be executed in the worker or custom executor. For more fine grain control use the Dispatch annotation:
public class MyController {
@GET("/nonblocking")
public String nonblocking() { (1)
return "I'm nonblocking";
}
@GET("/blocking")
@Dispatch
public String blocking() { (2)
return "I'm blocking";
}
}
1 | MVC route run in EVENT_LOOP mode. Blocking is NOT allowed it. |
2 | MVC route run in WORKER mode. Blocking is allowed it. |
The Dispatch annotation supports custom executor using an executor name.
public class MyController {
@GET("/blocking")
@Dispatch("single") (1)
public String blocking() {
return "I'm blocking";
}
}
1 | Dispatch to an executor named it single |
Executor must be registered using via services or executor utility method:
{
executor("single", Executors.newSingleThreadExecutor());
mvc(new MyController());
}
The executor must be registered before the MVC route/controller.
5.6. JAX-RS Annotations
Alternative you can use JAX-RS annotations to define MVC routes.
import javax.ws.rs.GET;
import javax.ws.rs.Path;
@Path("/jaxrs")
public class Resource {
@GET
public String getIt() {
return "Got it!";
}
}
Annotations work exactly like the Jooby MVC annotations, but keep in mind we don’t implement the JAX-RS specification and there is no immediate plan to do it.
The main reason to support JAX-RS annotations is to let you plug-in third-party tools that rely on them (mostly annotations processors).
5.7. Annotation Processor Options
Option | Value | Default Value | Description |
---|---|---|---|
jooby.debug |
boolean |
true |
Run processor in debug mode |
jooby.incremental |
boolean |
true |
Hints maven/gradle to do incremental compilation. Useful for development. |
jooby.services |
boolean |
true |
Generates META-INF/services metadata |
jooby.skipAttributeAnnotations |
array |
[] |
Skip annotation during byte code generation (i.e. don’t generate them as route attributes) |
jooby.handler |
string |
[] |
Add custom handler mapping. |
jooby.mvcMethod |
boolean |
false |
Set the Route.mvcMethod when true. |
jooby.routerPrefix |
string |
Prefix for generated class |
|
jooby.routerSuffix |
string |
_ |
Suffix for generated class |
5.7.1. Setting options
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<!-- if using lombok, it must be placed before the jooby-apt -->
<!-- if using avaje-inject, it must be placed after lombok, but before the jooby-apt -->
<path>
<groupId>io.jooby</groupId>
<artifactId>jooby-apt</artifactId>
<version>${jooby.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<compilerArg>
-Ajooby.debug=false
</compilerArg>
<compilerArg>
-Ajooby.incremental=true
</compilerArg>
<compilerArg>
-Ajooby.services=true
</compilerArg>
<compilerArg>
-Ajooby.skipAttributeAnnotations=FooAnnotation,BarAnnotation
</compilerArg>
<compilerArg>
-Ajooby.handler=myhandler
</compilerArg>
</compilerArgs>
</configuration>
</plugin>
Please note that the order of annotation processors is important. For example, if you’re using |
6. Static Files
Static files are available via assets(String) route. The assets
route
supports classpath and file-system resources.
{
assets("/static/*"); (1)
}
1 | Map all the incoming request starting with /static/ to the root of classpath
|
{
assets("/static/*", Paths.get("www")); (1)
}
1 | Map all the incoming request starting with /static/ to a file system directory www
|
Individual file mapping is supported too:
{
assets("/myfile.js", "/static/myfile.js");
}
6.1. Static Site
The assets
route works for static sites too. Just need to use a special path mapping:
{
Path docs = Paths.get("docs"); (1)
assets("/docs/?*", docs); (2)
}
1 | Serve from docs directory |
2 | Use the /?* mapping |
The key difference is the /?*
mapping. This mapping add support for base root mapping:
-
GET
/docs
⇒/docs/index.html
-
GET
/docs/index.html
⇒/docs/index.html
-
GET
/docs/about.html
⇒/docs/about.html
-
GET
/docs/note
⇒/docs/note/index.html
6.2. SPAs
The assets
route works for single page applications (SPAs) too. Just need to use a special path mapping plus a fallback asset:
{
AssetSource docs = AssetSource.create(Paths.get("docs")); (1)
assets("/docs/?*", new AssetHandler("index.html", docs)); (2)
}
1 | Serve from docs directory |
2 | Use the /?* mapping and uses index.html as fallback asset |
SPAs mode never generates a NOT FOUND (404)
response, unresolved assets fallback to index.html
6.3. Options
The AssetHandler automatically handles E-Tag
and Last-Modified
headers. You can
control these headers programmatically:
{
assets("/static/*", Paths.get("www"))
.setLastModified(false)
.setEtag(false);
}
The maxAge
option set a Cache-Control
header:
{
assets("/static/*", Paths.get("www"))
.setMaxAge(Duration.ofDays(365))
}
There is also a setNoCache() method that explicitly forbids web browsers to cache assets.
You can use different cache configurations for different assets based on asset name if you specify a function via cacheControl(Function):
{
assets("/static/*", Paths.get("www"))
.cacheControl(path -> {
if (path.endsWith("dont-cache-me.html")) {
return CacheControl.noCache(); // disable caching
} else if (path.equals("foo.js")) {
return CacheControl.defaults()
.setETag(false)
.setMaxAge(Duration.ofDays(365));
} else {
return CacheControl.defaults(); // AssetHandler defaults
}
});
}
The asset handler generates a 404
response code when requested path is not found. You can change this by throwing
an exception or generating any other content you want:
{
assets("/static/*", Paths.get("www"))
.notFound(ctx -> {
throw new MyAssetException();
});
error(MyAssetException.class, (ctx, cause, code) -> {
// render MyAssetException as you want
});
}
7. Templates
Templates are available via ModelAndView and requires a TemplateEngine implementation.
{
install(new MyTemplateEngineModule()); (1)
get("/", ctx -> {
MyModel model = ...; (2)
return new ModelAndView("index.html", model); (3)
});
}
1 | Install a template engine |
2 | Build the view model |
3 | Returns a ModelAndView instance |
ModelAndView allows you to specify the desired locale used for template rendering:
{
install(new MyTemplateEngineModule());
get("/", ctx -> {
MyModel model = ...;
return new ModelAndView("index.html", model)
.setLocale(Locale.GERMAN); (1)
});
}
1 | Set the preferred locale |
If no locale is specified explicitly, a locale matched by the Accept-Language
header of the current
request is used.
Not all template engines support setting and using a specific locale. If you use such a template engine, the above configuration does not have any effect. |
7.1. Template Engine
Template engine does the view rendering/encoding. Template engine extends a MessageEncoder
by accepting a ModelAndView
instance and produces a String
result.
The extensions() method list the number of file extension that a template engine
supports. Default file extension is: .html
.
The file extension is used to locate the template engine, when a file extension isn’t supported
an IllegalArgumentException
is thrown.
The file extension allow us to use/mix multiple template engines too:
{
install(new HandlebarsModule()); (1)
install(new FreemarkerModule()); (2)
get("/first", ctx -> {
return new ModelAndView("index.hbs", model); (3)
});
get("/second", ctx -> {
return new ModelAndView("index.ftl", model); (4)
});
}
1 | Install Handlebars |
2 | Install Freemarker |
3 | Render using Handlebars, .hbs extension |
4 | Render using Freemarker, .ftl extension |
Checkout all the available template engines provided by Jooby.
7.2. View Model
Since Jooby 3.1.x
the model can be anything object you like, previous version requires to be always map
. There
are two implementations of ModelAndView
:
-
ModelAndView(String view, Object model)
-
MapModelAndView(String view, Map<String, Object> model)
8. Session
Session is accessible via
-
sessionOrNull(): which find an existing session
-
session(): which find an existing session or create a new one
Sessions have a lot of uses cases but the most commons are: authentication, storing information about current user, etc.
A session attribute must be a String or a primitive. The session doesn’t allow storing of arbitrary objects. It’s intended as a simple mechanism to store basic data (not an object graph).
-
In-Memory sessions - which you should combine with an a sticky sessions proxy if you plan to run multiple instances.
-
Cookie sessions signed with a secret key
-
JSON Web Token sessions
8.1. In-Memory Session
Default session store uses memory to save session data. This store:
-
Uses a cookie/header to read/save the session ID
-
Store session data in-memory
{
get("/", ctx -> {
Session session = ctx.session(); (1)
session.put("foo", "bar"); (2)
return session.get("foo").value(); (3)
});
}
1 | Find an existing session or create a new session |
2 | Set a session attribute |
3 | Get a session attribute |
Session token/ID is retrieved it from request cookie. Default session cookie is jooby.sid. To customize cookie details:
{
setSessionStore(SessionStore.memory(new Cookie("SESSION"))); (1)
get("/", ctx -> {
Session session = ctx.session();
session.put("foo", "bar");
return session.get("foo").value();
});
}
1 | Set an in-memory session store with a custom cookie named: SESSION |
Alternative you can use a request header to retrieve a session token/ID:
{
setSessionStore(SessionStore.memory(SessionToken.header("TOKEN"))); (1)
get("/", ctx -> {
Session session = ctx.session();
session.put("foo", "bar");
return session.get("foo").value();
});
}
1 | Session Token/ID comes from HTTP header TOKEN |
You can mix cookie and header tokens:
{
setSessionStore(SessionStore.memory(SessionToken.comibe(SessionToken.cookie("SESSION"), SessionToken.header("TOKEN")))); (1)
get("/", ctx -> {
Session session = ctx.session();
session.put("foo", "bar");
return session.get("foo").value();
});
}
1 | Session Token/ID comes from HTTP Cookie SESSION or HTTP header TOKEN (in that order) |
8.2. Signed Session
This is a stateless session store that expects to find session token on each request. The server doesn’t keep any state.
-
Session data is retrieve/save from/into HTTP Cookie or Header
-
Session data is (un)signed with
HmacSHA256
. Key must be 256 bits long (32 bytes)
Data sign/unsign is done using sign(String,String) and unsign(String,String).
{
String secret = "super secret key"; (1)
setSessionStore(SessionStore.signed(secret)); (2)
get("/", ctx -> {
Session session = ctx.session();
session.put("foo", "bar");
return session.get("foo").value();
});
}
1 | A secret key is required to signed the data |
2 | Creates a cookie session store using the secret |
Like with memory
session store you can use HTTP headers:
{
String secret = "super secret key"; (1)
setSessionStore(SessionStore.signed(secret, SessionToken.header("TOKEN"))); (2)
get("/", ctx -> {
Session session = ctx.session();
session.put("foo", "bar");
return session.get("foo").value();
});
}
8.3. Stores
In addition to built-in memory stores we do provide:
9. Web Sockets
Adding a WebSocket:
{
ws("/ws", (ctx, configurer) -> { (1)
configurer.onConnect(ws -> {
ws.send("Connected"); (2)
});
configurer.onMessage((ws, message) -> {
ws.send("Got " + message.value()); (3)
});
configurer.onClose((ws, statusCode) -> {
(4)
});
configurer.onError((ws, cause) -> {
// 5
});
});
}
1 | Add a WebSocket handler. Useful to initialize resources |
2 | On WebSocket connect/open send a message back to client. Useful to initialize resources |
3 | On new message send back to client |
4 | WebSocket is about to close, you must free/release any acquire resources |
5 | WebSocket found a exception. Useful to log the error and provide an alternative response is the WebSocket is still open |
You are free to access to HTTP context from WebSocket configurer or callback, but it is disallowed to modify the HTTP context or produces a response from it:
{
ws("/ws/{key}", (ctx, configurer) -> {
String key = ctx.path("key").value(); (1)
String foo = ctx.session().get("foo").value(); (2)
...
});
}
1 | Access to path variable: key |
2 | Access to session variable: foo |
9.1. Structured data
Structure data is supported using the Value API and the render() method:
import io.jooby.jackson.JacksonModule;
{
install(new JackonModule()); (1)
ws("/ws", (ctx, configurer) -> {
configurer.onMessage((ws, message) -> {
MyObject myobject = message.to(MyObject.class); (2)
ws.render(myobject); (3)
})
});
}
1 | Install Jackson module (required for JSON decoding/encoding) |
2 | Parse/decode message to MyObject |
3 | Encode myobject as JSON and send to client |
Alternative you explicit tells with decoder/encoder to use consumes/produces attributes:
import io.jooby.jackson.JacksonModule;
{
install(new JackonModule()); (1)
ws("/ws", (ctx, configurer) -> {
configurer.onMessage((ws, message) -> {
MyObject myobject = message.to(MyObject.class); (2)
ws.render(myobject); (3)
})
})
.consumes(MediaType.json)
.produces(MediaType.json);
}
Structure messages depends/requires a MessageDecoder and MessageEncoder. In this example both are provided by the JacksonModule.
9.2. Options
9.2.1. Connection Timeouts
Jooby timeouts idle connections that have no activity after 5 minutes. You can
control this behaviour by setting the websocket.idleTimeout
property:
websocket.idleTimeout = 1h
See duration format
9.2.2. Max size
Max size is set to 128K
you can override it like:
websocket.maxSize = 128K
See sizes in bytes
10. Server-Sent Events
Server-sent Events (SSE) is a mechanism that allows the server to push data to the client once the client-server connection is established. After the connection has been established by the client, the server can send to the client whenever a new chunk of data is available. In contrast with websockets, SSE can only be used to send from the server to the client and not the other way round.
{
sse("/sse", sse -> { (1)
sse.send("Welcome"); (2)
});
}
1 | Connection established |
2 | Send a message |
10.1. Message Options
Additional message options are available via ServerSentMessage:
{
sse("/sse", sse -> {
sse.send(
new ServerSentMessage("...")
.setEvent("myevent")
.setId(myId)
.setRetry(1000)
);
});
}
Options are documented at Event stream format.
10.2. Connection Lost
The sse.onClose(Runnable)
callback allows you to clean and release resources on connection close.
A connection is closed when you call the sse.close() method or when the remote client closes the
connection.
{
sse("/sse", sse -> {
sse.onClose(() -> {
// clean up
});
});
}
10.3. Keep Alive
The keep alive time feature can be used to prevent connections from timing out:
{
sse("/sse", sse -> {
sse.keepAlive(15, TimeUnit.SECONDS)
});
}
The previous example will send a ':' message (empty comment) every 15 seconds to keep the
connection alive. If the client drops the connection, then the sse.onClose(Runnable)
event will
be fired.
This feature is useful when you want to detect close events without waiting for the next time you send an event. If on the other hand your application already generates events every 15 seconds, the use of keep alive is unnecessary.
11. Execution Model
Jooby is a flexible performant microframework providing both blocking and non-blocking APIs for building web applications in Java and Kotlin.
In this chapter we are going to learn about Jooby execution model, more specifically:
-
Execute code on the event loop
-
Safely execution of blocking code
-
Working with non-blocking types, like:
CompletableFuture
, Reactive Streams, Kotlin Coroutines, etc.
11.1. Mode
11.1.1. Event Loop
The EVENT_LOOP mode allows us to run a route handler from the event loop (a.k.a as non-blocking mode).
import static io.jooby.ExecutionMode.EVENT_LOOP;
import static io.jooby.Jooby.runApp;
public class App extends Jooby {
{
get("/", ctx -> "I'm non-blocking!" );
}
public static void main(String[] args) {
runApp(args, EVENT_LOOP, App::new);
}
}
The EVENT_LOOP mode is the more advanced execution mode and requires you carefully design and implement your application due to that BLOCKING IS NOT ALLOWED
What if you need to block?
The dispatch(Runnable) operator moves execution to a worker executor which allows to do blocking calls:
import static io.jooby.ExecutionMode.EVENT_LOOP;
import static io.jooby.Jooby.runApp;
public class App extends Jooby {
{
get("/", ctx -> {
return "I'm non-blocking!";
});
dispatch(() -> {
// All the routes defined here are allowed to block:
get("/db-list", ctx -> {
/** Safe to block! */
Object result = ...; // Remote service, db call, etc..
return result;
});
});
}
public static void main(String[] args) {
runApp(args, EVENT_LOOP, App::new);
}
}
By default, the dispatch(Runnable) operator moves execution to the server worker executor (executor provided by web server).
You can provide your own worker executor at application level or at dispatch level:
import static io.jooby.ExecutionMode.EVENT_LOOP;
import static io.jooby.Jooby.runApp;
public class App extends Jooby {
{
// Application level executor
worker(Executors.newCachedThreadPool());
// Dispatch to application level executor which is cached thread pool
dispatch(() -> {
...
});
// Dispatch to a explicit executor
Executor cpuIntensive = Executors.newSingleThreadExecutor();
dispatch(cpuIntesive, () -> {
...
});
}
public static void main(String[] args) {
runApp(args, EVENT_LOOP, App:new);
}
}
11.1.2. Worker
The WORKER mode allows us to do blocking calls from a route handler (a.k.a blocking mode). You just write code without worrying about blocking calls.
import static io.jooby.ExecutionMode.WORKER;
import static io.jooby.Jooby.runApp;
public class App extends Jooby {
{
get("/", ctx -> {
/** Safe to block! */
Object result = // Remote service, db call, etc..
return result;
});
}
public static void main(String[] args) {
runApp(args, WORKER, App::new);
}
}
Like with EVENT_LOOP mode, you can provide your own worker executor:
import static io.jooby.ExecutionMode.WORKER;
import static io.jooby.Jooby.runApp;
public class App extends Jooby {
{
worker(Executors.newCachedThreadPool());
get("/", ctx -> {
/** Safe to block from cached thread pool! */
Object result = // Remote service, db call, etc..
return result;
});
}
public static void main(String[] args) {
runApp(args, WORKER, App::new);
}
}
While running in WORKER mode, Jooby internally does the dispatch call to the worker executor. This is done per route, not globally. |
11.1.3. Default
The DEFAULT execution mode is a mix between WORKER and EVENT_LOOP modes. This (as name implies) is the default execution mode in Jooby.
Jooby detects the route response type and determines which execution mode fits better.
If the response type is considered non-blocking, then it uses the event loop. Otherwise, it uses the worker executor.
A response type is considered non-blocking when route handler produces:
-
A
CompletableFuture
type
import static io.jooby.Jooby.runApp;
public class App extends Jooby {
{
get("/non-blocking", ctx -> {
return CompletableFuture
.supplyAsync(() -> "I'm non-blocking!") (1)
});
get("/blocking", ctx -> {
return "I'm blocking"; (2)
});
}
public static void main(String[] args) {
runApp(args, App::new);
}
}
1 | CompletableFuture is a non-blocking type, run in event loop |
2 | String is a blocking type, run in worker executor |
You are free to use non-blocking types in all the other execution mode too. Non-blocking response types are not specific to the default mode execution. All the default mode does with them is to dispatch or not to a worker executor. |
11.2. Worker Executor
This section described some details about the default worker executor provided by web server. The worker executor is used when:
-
Application mode was set to WORKER
-
Application mode was set to EVENT_LOOP and there is a dispatch(Runnable) call
Each web server provides a default worker executor:
-
Netty: The Netty server implementation multiply the number of available processors (with a minimum of 2) by 8.
workerThreads = Math.max(Runtime.getRuntime().availableProcessors(), 2) * 8
For example 8
cores gives us 64
worker threads.
-
Undertow: The Undertow server implementation multiply the number of available processors by 8.
workerThreads = Runtime.getRuntime().availableProcessors() * 8
For 8
cores gives us 64
worker threads.
-
Jetty: The Jetty server implementation uses the default configuration with
200
worker threads.
These are sensible defaults suggested by the server implementation. If you need to increase/decrease worker threads:
{
configureServer(server -> {
server.workerThreads(Number);
});
}
12. Responses
This chapter covers some special response types, like raw responses
, streaming
, file download
, non-blocking
, etc…
12.1. Raw
Raw responses are NOT processed by a message encoder. These response types are considered raw
:
-
String/CharSequence
-
byte[]
-
java.nio.ByteBuffer/io.netty.buffer.ByteBuf
-
java.io.File/java.io.InputStream/java.nio.file.Path/java.nio.channels.FileChannel
{
get("/json", ctx -> {
ctx.setContentType(MediaType.json);
return "{\"message\": \"Hello Raw Response\"}";
});
}
No matter if there is a JSON encoder installed, a raw response is always send directly to client.
12.2. Streaming / Chunked
Streaming/chunked API is available via:
-
responseStream(): blocking API that provides an
OutputStream
-
responseWriter(): blocking API that provides a
PrintWriter
-
responseSender(): non-blocking API that provides a Sender
Only one of these methods must be call it per request. At the time you call one of these methods Jooby
automatically adds the Transfer-Encoding: chunked
header when Content-Length
is missing.
All the three APIs have a close
method. You must call it once you finish.
{
get("/chunk", ctx -> {
try(Writer writer = ctx.responseWriter()) { (1)
writer.write("chunk1"); (2)
...
writer.write("chunkN");
}
return ctx; (3)
});
}
1 | Get the Writer inside a try-with-resources statement, so close it automatically. |
2 | Write chunks |
3 | Return the Context |
There is an overloaded version (for Java mainly) that let you skip the try-with-resources and automatically close the writer/stream:
{
get("/chunk", ctx -> {
return ctx.responseWriter(writer -> { (1)
writer.write("chunk1"); (2)
...
writer.write("chunkN");
});
});
}
12.3. File download
The FileDownload is used to generate file downloads, i.e. responses with
Content-Disposition
header. You can use the convenience subclasses AttachedFile
or InlineFile to set the header value to attachment
or inline
respectively.
{
get("/download-file", ctx -> {
Path source = Paths.get("logo.png");
return new AttachedFile(source); (1)
});
get("/view-stream", ctx -> {
InputStream source = ...;
return new InlineFile("myfile.txt", source); (2)
});
}
1 | Send a download from an InputStream |
2 | Send a download from a File |
Another possibility is to use one of the static builder methods of FileDownload
and specify
the download type (attachment or inline) later.
FileDownload.Builder produceDownload(Context ctx) {
return FileDownload.build(...);
}
{
get("/view", ctx -> produceDownload(ctx).inline());
get("/download", ctx -> produceDownload(ctx).attachment());
}
12.4. NonBlocking
Non-blocking responses are a new feature of Jooby 2.x.
From user point of view there is nothing special about them, you just write your route handler as usually do with blocking types.
Before we jump to each of the supported types, we need to learn what occurs in the pipeline when there is a non-blocking route handler.
{
mode(EVENT_LOOP); (1)
use(ReactiveSupport.concurrent()); (2)
get("/non-blocking", ctx -> {
return CompletableFuture (3)
.supplyAsync(() -> {
... (4)
});
})
}
1 | App run in event loop |
2 | Indicates we want to go non-blocking and handle CompletableFuture responses. |
3 | Value is provided from event loop. No blocking code is permitted |
4 | Value is computed/produces from completable future context |
Running your App3508
in worker mode works identically, except for we are able to do blocking calls:
{
mode(WORKER); (1)
use(ReactiveSupport.concurrent()); (2)
get("/blocking", ctx -> {
return CompletableFuture (3)
.supplyAsync(() -> {
... (4)
});
})
}
1 | App run in worker mode |
2 | Indicates we want to go non-blocking and handle CompletableFuture responses. |
3 | Value is provided from worker mode. Blocking code is permitted |
4 | Value is computed/produces from completable future context |
Running your App3508
in default mode works identically to running in the event loop mode:
{
mode(DEFAULT); (1)
use(ReactiveSupport.concurrent()); (2)
get("/non-blocking", ctx -> {
return CompletableFuture (3)
.supplyAsync(() -> {
... (4)
});
})
}
1 | App run in worker mode |
2 | Indicates we want to go non-blocking and handle CompletableFuture responses. |
3 | Value is provided from worker mode. Blocking code is permitted |
4 | Value is computed/produces from completable future context |
The default mode mimics the event loop mode execution when route produces a non-blocking type.
12.4.1. CompletableFuture
CompletableFuture is considered a non-blocking type which is able to produces a single result:
{
use(ReactiveSupport.concurrent());
get("/non-blocking", ctx -> {
return CompletableFuture
.supplyAsync(() -> "Completable Future!")
.thenApply(it -> "Hello " + it);
})
}
Completable future responses require explicit handler setup ONLY in script/lambda routes. For MVC routes you don’t need to setup any handler. It is done automatically based on route response type. |
12.4.2. Mutiny
1) Add the SmallRye Mutiny dependency:
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-mutiny</artifactId>
<version>3.5.0</version>
</dependency>
2) Write code:
Uni
import io.jooby.mutiny;
import io.smallrye.mutiny.Uni;
{
// Add response handler:
use(Mutiny.mutiny());
get("/non-blocking", ctx -> {
return Uni.createFrom()
.completionStage(supplyAsync(() -> "Uni"))
.map(it -> "Hello " + it);
})
}
Multi
import io.jooby.mutiny;
import io.smallrye.mutiny.Multi;
{
// Add response handler:
use(Mutiny.mutiny());
get("/non-blocking", ctx -> {
return Multi.createFrom().range(1, 11)
.map(it -> it + ", ");
})
}
For Multi, Jooby builds a chunked
response. That:
-
Set the
Transfer-Encoding: chunked
header -
Each item means new
chunk
send it to client
Mutiny responses require explicit handler setup ONLY in script/lambda routes. For MVC routes you don’t need to setup any handler. It is done automatically based on route response type. |
12.4.3. RxJava
1) Add the RxJava dependency:
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-rxjava3</artifactId>
<version>3.5.0</version>
</dependency>
2) Write code:
Single
import io.jooby.rxjava3.Reactivex;
{
use(Reactivex.rx());
get("/non-blocking", ctx -> {
return Single
.fromCallable(() -> "Single")
.map(it -> "Hello " + it);
})
}
Flowable
import io.jooby.rxjava3.Reactivex;
{
use(Reactivex.rx());
get("/non-blocking", ctx -> {
return Flowable.range(1, 10)
.map(it -> it + ", ");
})
}
For Flowable, Jooby builds a chunked
response. That:
-
Set the
Transfer-Encoding: chunked
header -
Each item means new
chunk
send it to client
Rx responses require explicit handler setup ONLY in script/lambda routes. For MVC routes you don’t need to setup any handler. It is done automatically based on route response type. |
12.4.4. Reactor
1) Add the Reactor dependency:
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-reactor</artifactId>
<version>3.5.0</version>
</dependency>
2) Write code:
Mono
import io.jooby.Reactor;
{
use(Reactor.reactor());
get("/non-blocking", ctx -> {
return Mono
.fromCallable(() -> "Mono")
.map(it -> "Hello " + it);
})
}
Flux
import io.jooby.Reactor;
{
use(Reactor.reactor())
get("/non-blocking", ctx -> {
return Flux.range(1, 10)
.map(it -> it + ", ");
})
}
For Flux, Jooby builds a chunked
response. That:
-
Set the
Transfer-Encoding: chunked
header -
Each item means new
chunk
send it to client
Reactor responses require explicit handler setup ONLY in script/lambda routes. For MVC routes you don’t need to setup any handler. It is done automatically based on route response type. |
12.4.5. Kotlin Coroutines
{
coroutine {
get("/") {
delay(100) (1)
"Hello Coroutines!" (2)
}
}
}
1 | Call a suspending function |
2 | Send response to client |
{
coroutine {
get("/") {
ctx.doSomething() (1)
}
}
}
suspend fun Context.doSomething(): String {
delay(100) (2)
return "Hello Coroutines!" (3)
}
1 | Call extension suspending function |
2 | Call a suspending function or do a blocking call |
3 | Send response to client |
A coroutine works like any of the other non-blocking types. You start Jooby using the event loop or default mode, Jooby creates a coroutine context to execute it.
Jooby uses the worker executor to creates a coroutine context. As described in worker executor section this is provided by the web server implementation unless you provided your own.
{
worker(Executors.newCachedThreadPool())
coroutine {
get("/") {
val n = 5 * 5 (1)
delay(100) (2)
"Hello Coroutines!" (3)
}
}
}
1 | Statement run in the worker executor (cached thread pool) |
2 | Call a suspending function |
3 | Produces a response |
Coroutines always run in the worker executor. There is an experimental API where coroutines run in the caller thread(event loop in this case) until a suspending function is found.
Jooby allows you to use this experimental API by setting the coroutineStart
option:
{
coroutine(CoroutineStart.UNDISPATCHED) {
get("/") {
val n = 5 * 5 (1)
delay(100) (2)
"Hello Coroutines!" (3)
}
}
}
1 | Statement run in the event loop (caller thread) |
2 | Call a suspending function and dispatch to worker executor |
3 | Produces a response from worker executor |
You can also extend the CoroutineContext
in which the coroutine routes run:
{
coroutine {
launchContext { MDCContext() } (1)
get("/") {
...
}
}
}
1 | The lambda is run before launching each coroutine, so it can customize the CoroutineContext for
the request, e.g. store/restore MDC, transaction, or anything else that your handlers need. |
♡ ♡!
12.5. Send methods
Jooby provides a family of send()
methods that produces a response via side effects.
{
get("/", ctx -> {
return ctx.send("Hello World!");
});
}
Beside we operate via side effects, the route still returns something. This is required because a route handler is a function which always produces a result.
All the send methods returns the current Context
, this signal Jooby that we want to operate via
side effects ignoring the output of the route handler.
Family of send methods include:
13. Error Handler
Jooby catches application exception using the ErrorHandler class. The
DEFAULT error handler produces simple HTML page or JSON based on the value
of the ACCEPT
header and log the exception.
Not Found message: Page not found status code: 404
{ "message": "Page not found", "status": 404, "reason": "Not Found" }
GET /xx 404 Not Found io.jooby.StatusCodeException: Not found at ... at ... at ... at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)
The StatusCodeException works as generic exception that let you specify an status code.
throw new StatusCodeException(StatusCode.FORBIDDEN); throw new StatusCodeException(StatusCode.NOT_FOUND); ...
These exception types have a default status code:
-
IllegalArgumentException: BAD_REQUEST(400) (or sub-classes of it)
-
NoSuchElementException: BAD_REQUEST(400) (or sub-classes of it)
-
FileNotFound: NOT_FOUND(404) (or sub-classes of it)
-
Exception: SERVER_ERROR(500) (or sub-classes of it)
To set a custom status code, an entry should be added it to the error code map:
{
errorCode(MyException.class, StatusCode.XXX);
}
13.1. Custom Error Handler
You can provide your own error handler using the error(ErrorHandler) method:
{
error((ctx, cause, statusCode) -> { (1)
Router router = ctx.getRouter();
router.getLog().error("found `{}` error", statusCode.value(), cause); (2)
ctx.setResponseCode(statusCode);
ctx.send("found `" + statusCode.value() + "` error"); (3)
});
}
1 | Add a global/catch-all exception handler |
2 | Log the error to logging system |
3 | Send an error response to the client |
You can use the render(Object) object which looks for a registered MessageEncoder or TemplateEngine.
The next example produces a HTML or JSON response based on the value of the Accept
header.
import static io.jooby.MediaType.json;
import static io.jooby.MediaType.html;
{
install(new MyTemplateEngineModule()); (1)
install(new MyJsonModule()); (2)
error((ctx, cause, statusCode) -> {
Router router = ctx.getRouter();
router.getLog().error("found `{}` error", statusCode.value(), cause);
if (ctx.accept(json)) { (3)
Map error = ...;
ctx.render(error); (4)
} else {
// fallback to html
Map error = ...;
ctx.render(new ModelAndView("error.template", error)); (5)
}
});
}
1 | Install one of the available template engines |
2 | Install one of the available json modules |
3 | Test if the accept header matches the application/json content type |
4 | Render json if matches |
5 | Render html as fallback |
13.2. Catch by Code
In addition to the generic/global error handler you can catch specific status code:
import static io.jooby.StatusCode.NOT_FOUND;
{
error(NOT_FOUND, (ctx, cause, statusCode) -> {
ctx.send(statusCode); (1)
});
}
1 | Send 404 response to the client |
Here we kind of silence all the 404
response due we don’t log anything and send an empty response.
The send(StatusCode) method send an empty response to the client |
13.3. Catch by Exception
In addition to the generic/global error handler you can catch specific exception type:
{
error(MyException.class, (ctx, cause, statusCode) -> {
// log and process MyException
});
}
14. Problem Details
Most APIs have a way to report problems and errors, helping the user understand when something went wrong and what the issue is. The method used depends on the API’s style, technology, and design. Handling error reporting is an important part of the overall API design process.
You could create your own error-reporting system, but that takes time and effort, both for the designer and for users who need to learn the custom approach. Thankfully, there’s a standard called IETF RFC 7807 (later refined in RFC 9457) that can help.
By adopting RFC 7807
, API designers don’t have to spend time creating a custom solution, and users benefit by recognizing a familiar format across different APIs.
If it suits the API’s needs, using this standard benefits both designers and users alike.
Jooby
provides built-in support for Problem Details
.
14.1. Set up ProblemDetails
To enable the ProblemDetails
, simply add the following line to your configuration:
problem.details.enabled = true
This is the bare minimal configuration you need. It enables a global error handler that catches all exceptions, transforms them into Problem Details compliant format and renders the response based on the Accept header value. It also sets the appropriate content-type in response (e.g. application/problem+json, application/problem+xml)
All supported settings include:
problem.details {
enabled = true
log4xxErrors = true (1)
muteCodes = [401, 403] (2)
muteTypes = ["com.example.MyMutedException"] (3)
}
1 | By default, only server errors (5xx) will be logged. You can optionally enable the logging of client errors (4xx). If DEBUG logging level is enabled, the log will contain a stacktrace as well. |
2 | You can optionally mute some status codes completely. |
3 | You can optionally mute some exceptions logging completely. |
14.2. Creating problems
HttpProblem
class represents the RFC 7807
model. It is the main entity you need to work with to produce the problem.
14.2.1. Static helpers
There are several handy static methods to produce a simple HttpProblem
:
-
HttpProblem.valueOf(StatusCode status)
- will pick the title by status code. Don’t overuse it, the problem should have meaningfultitle
anddetail
when possible. -
HttpProblem.valueOf(StatusCode status, String title)
- with customtitle
-
HttpProblem.valueOf(StatusCode status, String title, String detail)
- withtitle
anddetail
HttpProblem
extends RuntimeException
so you can naturally throw it (as you do with exceptions):
import io.jooby.problem.HttpProblem;
get("/users/{userId}", ctx -> {
var userId = ctx.path("userId").value();
User user = userRepository.findUser(userId);
if (user == null) {
throw HttpProblem.valueOf(StatusCode.NOT_FOUND,
"User Not Found",
"User with ID %s was not found in the system.".formatted(userId)
);
}
...
});
Resulting response:
{
"timestamp": "2024-10-05T14:10:41.648933100Z",
"type": "about:blank",
"title": "User Not Found",
"status": 404,
"detail": "User with ID 123 was not found in the system.",
"instance": null
}
14.2.2. Builder
Use builder to create a rich problem instance with all properties:
throw HttpProblem.builder()
.type(URI.create("http://example.com/invalid-params"))
.title("Invalid input parameters")
.status(StatusCode.UNPROCESSABLE_ENTITY)
.detail("'Name' may not be empty")
.instance(URI.create("http://example.com/invalid-params/3325"))
.build();
14.3. Adding extra parameters
RFC 7807
has a simple extension model: APIs are free to add any other properties to the problem details object, so all properties other than the five ones listed above are extensions.
However, variadic root level fields are usually not very convenient for (de)serialization (especially in statically typed languages). That’s why HttpProblem
implementation grabs all extensions under a single root field parameters
. You can add parameters using builder like this:
throw HttpProblem.builder()
.title("Order not found")
.status(StatusCode.NOT_FOUND)
.detail("Order with ID $orderId could not be processed because it is missing or invalid.")
.param("reason", "Order ID format incorrect or order does not exist.")
.param("suggestion", "Please check the order ID and try again")
.param("supportReference", "/support")
.build();
Resulting response:
{
"timestamp": "2024-10-06T07:34:06.643235500Z",
"type": "about:blank",
"title": "Order not found",
"status": 404,
"detail": "Order with ID $orderId could not be processed because it is missing or invalid.",
"instance": null,
"parameters": {
"reason": "Order ID format incorrect or order does not exist.",
"suggestion": "Please check the order ID and try again",
"supportReference": "/support"
}
}
14.4. Adding headers
Some HTTP
codes (like 413
or 426
) require additional response headers, or it may be required by third-party system/integration. HttpProblem
support additional headers in response:
throw HttpProblem.builder()
.title("Invalid input parameters")
.status(StatusCode.UNPROCESSABLE_ENTITY)
.header("my-string-header", "string")
.header("my-int-header", 100)
.build();
14.5. Respond with errors details
RFC 9457
finally described how errors should be delivered in HTTP APIs.
It is basically another extension errors
on a root level. Adding errors is straight-forward using error()
or errors()
for bulk addition in builder:
throw HttpProblem.builder()
...
.error(new HttpProblem.Error("First name cannot be blank", "/firstName"))
.error(new HttpProblem.Error("Last name is required", "/lastName"))
.build();
In response:
{
...
"errors": [
{
"detail": "First name cannot be blank",
"pointer": "/firstName"
},
{
"detail": "Last name is required",
"pointer": "/lastName"
}
]
}
If you need to enrich errors with more information feel free to extend |
14.6. Custom Exception
to HttpProblem
Apparently, you may already have many custom Exception
classes in the codebase, and you want to make them Problem Details
compliant without complete re-write. You can achieve this by implementing HttpProblemMappable
interface. It allows you to control how exceptions should be transformed into HttpProblem
if default behaviour doesn’t suite your needs:
import io.jooby.problem.HttpProblemMappable;
public class MyException implements HttpProblemMappable {
public HttpProblem toHttpProblem() {
return HttpProblem.builder()
...
build();
}
}
14.7. Custom Problems
Extending HttpProblem
and utilizing builder functionality makes it really easy:
public class OutOfStockProblem extends HttpProblem {
private static final URI TYPE = URI.create("https://example.org/out-of-stock");
public OutOfStockProblem(final String product) {
super(builder()
.type(TYPE)
.title("Out of Stock")
.status(StatusCode.BAD_REQUEST)
.detail(String.format("'%s' is no longer available", product))
.param("suggestions", List.of("Coffee Grinder MX-17", "Coffee Grinder MX-25"))
);
}
}
14.8. Custom Exception Handlers
All the features described above should give you ability to rely solely on built-in global error handler. But, in case you still need custom exception handler for some reason, you still can do it:
{
...
error(MyCustomException.class, (ctx, cause, code) -> {
MyCustomException ex = (MyCustomException) cause;
HttpProblem problem = ... ; (1)
ctx.getRouter().getErrorHandler().apply(ctx, problem, code); (2)
});
}
1 | Transform exception to HttpProblem |
2 | Propagate the problem to ProblemDetailsHandler . It will handle the rest. |
Do not attempt to render |
15. Handlers
This section describes some built-in handler provided by Jooby.
15.1. AccessLogHandler
The AccessLogHandler logs incoming requests using the NCSA format (a.k.a common log format).
import io.jooby.Jooby;
import io.jooby.AccessLogHandler;
...
{
use(new AccessLogHandler()); (1)
get("/", ctx -> {
...
});
}
1 | Install AccessLogHandler |
Prints a message like:
127.0.0.1 - - [04/Oct/2016:17:51:42 +0000] "GET / HTTP/1.1" 200 2 3
Message is represented by:
-
Remote Address.
-
User ID (or dash when missing)
-
Date and time
-
HTTP method, requestPath and protocol
-
Response Status Code
-
Response Content-Length (or dash when missing)
-
Time took to process the request in milliseconds
Extra request or response headers can be appended at the end using the available methods:
If you run behind a reverse proxy that has been configured to send the X-Forwarded-* header, please consider to use trust proxy option. |
15.2. CorsHandler
Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP headers to tell a browser to let a web application running at one origin (domain) have permission to access selected resources from a server at a different origin. A web application executes a cross-origin HTTP request when it requests a resource that has a different origin (domain, protocol, or port) than its own origin.
Jooby supports CORS out of the box. By default, CORS requests will be rejected. To enable processing of CORS requests, use the CorsHandler:
import io.jooby.Jooby;
import io.jooby.CorsHandler;
...
{
use(new CorsHandler()); (1)
path("/api", () -> {
// API methods
});
}
1 | Install CorsHandler with defaults options |
Default options are:
-
origin:
*
-
credentials:
true
-
allowed methods:
GET
,POST
-
allowed headers:
X-Requested-With
,Content-Type
,Accept
andOrigin
-
max age:
30m
;
To customize default options use Cors:
import io.jooby.Jooby;
import io.jooby.CorsHandler;
...
{
Cors cors = new Cors()
.setMethods("GET", "POST", "PUT"); (1)
use(new CorsHandler(cors)); (2)
path("/api", () -> {
// API methods
});
}
1 | Specify allowed methods |
2 | Pass cors options to cors handler |
Optionally cors options can be specified in the application configuration file:
cors {
origin: "*"
credentials: true
methods: [GET, POST],
headers: [Content-Type],
maxAge: 30m
exposedHeaders: [Custom-Header]
}
import io.jooby.Jooby;
import io.jooby.CorsHandler;
...
{
Cors cors = Cors.from(getConfig()); (1)
use(new CorsHandler(cors));
path("/api", () -> {
// API methods
});
}
1 | Load cors options from application configuration file |
15.3. CsrfHandler
The Cross Site Request Forgery Handler helps to protect from (CSRF) attacks. Cross-site request forgeries are a type of malicious exploit whereby unauthorized commands are performed on behalf of an authenticated user.
Jooby automatically generates a CSRF "token" for each active user session managed by the application. This token is used to verify that the authenticated user is the one actually making the requests to the application.
Anytime you define an HTML form in your application, you should include a hidden CSRF token field in the form so that the CSRF protection middleware can validate the request
<form method="POST" action="...">
<input name="csrf" value="{{csrf}}" type="hidden" />
...
</form>
The csrf
is a request attribute created by the CsrfHandler handler and rendered by a
template engine. Here {{csrf}}
we use Handlebars template engine (as example).
The CsrfHandler handler, will automatically verify that the token in the request input matches the token stored in the session.
The token defaults name is csrf
and can be provided as:
-
header
-
cookie
-
form parameter
Configuration methods:
-
setTokenGenerator(Function): Set a custom token generator. Defaults uses a random UUID.
-
setRequestFilter(Predicate): Set a custom request filter. Defaults is to process
POST
,PUT
,PATCH
andDELETE
.
15.4. GracefulShutdown
The GracefulShutdown extension waits for existing requests to finish.
import io.jooby.Jooby;
import io.jooby.GracefulShutdown;
...
{
install(new GracefulShutdown()); (1)
// other routes go here
}
1 | Install GracefulShutdown. |
Incoming request are resolved as Service Unavailable(503)
. Optionally you can specify a max
amount of time to wait before shutdown:
install(new GracefulShutdown(Duration.ofMinutes(1)));
This extension must be installed at very beginning of your route pipeline.
15.5. HeadHandler
Jooby doesn’t support HTTP HEAD
requests by default. To support them you have two options:
-
Use the built-in
HeadHandler
-
Write your own head handler
The HeadHandler supports HEAD
requests over existing GET
handlers.
import io.jooby.Jooby;
import io.jooby.HeadHandler;
...
{
use(new HeadHandler()); (1)
get("/", ctx -> {
...
});
}
1 | Install HeadHandler |
HEAD /
produces an empty response with a Content-Length
header (when possible) and any other
header produce it by the GET
handler.
The GET
handler is executed but produces an empty response.
15.6. RateLimitHandler
Rate limit handler using Bucket4j.
Add the dependency to your project:
<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-core</artifactId>
<version>8.0.1</version>
</dependency>
{
Bandwidth limit = Bandwidth.simple(10, Duration.ofMinutes(1));
Bucket bucket = Bucket4j.builder().addLimit(limit).build(); (1)
before(new RateLimitHandler(bucket)); (2)
}
1 | Creates a bucket |
2 | Install the RateLimitHandler |
{
before(new RateLimitHandler(remoteAddress -> {
Bandwidth limit = Bandwidth.simple(10, Duration.ofMinutes(1));
return Bucket4j.builder().addLimit(limit).build();
}));
}
{
before(new RateLimitHandler(key -> {
Bandwidth limit = Bandwidth.simple(10, Duration.ofMinutes(1));
return Bucket4j.builder().addLimit(limit).build();
}, "ApiKey"));
}
{
Bandwidth limit = Bandwidth.simple(10, Duration.ofMinutes(1));
Bucket bucket = Bucket4j.builder().addLimit(limit).build(); (1)
before(new RateLimitHandler(bucket)); (2)
}
1 | Creates a bucket |
2 | Install the RateLimitHandler |
{
ProxyManager<String> buckets = ...;
before(RateLimitHandler.cluster(key -> {
return buckets.getProxy(key, () -> {
return ...;
});
}));
}
For using it inside a cluster you need to configure one of the bucket4j options for clustering.
15.7. SSLHandler
The SSLHandler forces client to use HTTPS by redirecting non-HTTPS calls to the HTTPS version.
import io.jooby.Jooby;
import io.jooby.SSLHandler;
...
{
setServerOptions(new ServerOptions().setSecurePort(8443));
before(new SSLHandler()); (1)
get("/", ctx -> {
return ctx.getScheme();
});
}
1 | Install SSLHandler |
The SSL Handler recreates the HTTPs URL version using the Host
header, if you are behind a proxy
you will need to use the X-Forwarded-Host
header. To do that set the trust proxy option.
Optionally, you can specify the host to use:
import io.jooby.Jooby;
import io.jooby.SSLHandler;
...
{
setServerOptions(new ServerOptions().setSecurePort(8443));
before(new SSLHandler("myhost.org")); (1)
get("/", ctx -> {
return ctx.getScheme();
});
}
For more information about SSL, please check the configure SSL section.
If you run behind a reverse proxy that has been configured to send the X-Forwarded-* header, please consider to add the proxy-peer-address.adoc to your pipeline. |
15.8. TraceHandler
Jooby doesn’t support HTTP Trace
requests by default. To support them you have two options:
-
Use the built-in
TraceHandler
-
Write your own trace handler
The TraceHandler supports TRACE
requests over existing handlers.
import io.jooby.Jooby;
import io.jooby.TraceHandler;
...
{
use(new TraceHandler()); (1)
get("/", ctx -> {
...
});
}
1 | Install TraceHandler |
TRACE /
performs a message loop-back test along the path to the target resource, providing a
useful debugging mechanism.
16. Configuration
Application configuration is based on config library. Configuration can by default be provided in either Java properties, JSON, and HOCON files.
Jooby allows overriding any property via system properties or environment variables.
16.1. Environment
The application environment is available via the Environment class, which allows specifying one or many unique environment names.
The active environment names serve the purpose of allowing loading different configuration files depending on the environment. Also, Extension modules might configure application services differently depending on the environment too. For example: turn on/off caches, reload files, etc.
{
Environment env = getEnvironment();
}
The active environment names property is set in one of this way:
-
As program argument:
java -jar myapp.jar application.env=foo,bar
; or justjava -jar myapp.jar foo,bar
This method works as long you start the application using one of the runApp methods
|
-
As system property:
java -Dapplication.env=foo,bar -jar myapp.jar
-
As environment variable:
application.env=foo,bar
The getEnvironment() loads the default environment.
16.2. Default Environment
The default environment is available via loadEnvironment(EnvironmentOptions) method.
This method search for an application.conf
file in three location (first-listed are higher priority):
-
${user.dir}/conf
. This is a file system location, useful is you want to externalize configuration (outside of jar file) -
${user.dir}
. This is a file system location, useful is you want to externalize configuration (outside of jar file) -
classpath://
(root of classpath). No external configuration, configuration file lives inside the jar file
We use $user.dir to reference System.getProperty("user.dir") . This system property is set
by the JVM at application startup time. It represent the current directory from where the JVM was
launch it.
|
└── conf
└── application.conf
└── myapp.jar
A call to:
Environment env = getEnvironment();
Loads the application.conf
from conf
directory. You get the same thing if you
move the application.conf
to myapp.jar
directory.
└── myapp.jar
└── application.conf (file inside jar)
Jooby favors file system property loading over classpath property loading. So, if there is a property file either in the current directory or conf directory it hides the same file available in the classpath. |
16.3. Overrides
Property overrides is done in multiple ways (first-listed are higher priority):
-
Program arguments
-
System properties
-
Environment variables
-
Environment property file
-
Property file
foo = foo
{
Environment env = getEnvironment(); (1)
Config conf = env.getConfig(); (2)
System.out.println(conf.getString("foo")); (3)
}
1 | Get environment |
2 | Get configuration |
3 | Get foo property and prints foo |
At runtime you can override properties using:
java -jar myapp.jar foo=argument
Example prints: argument
java -Dfoo=sysprop -jar myapp.jar
Prints: syspro
foo=envar java -jar myapp.jar
Prints: envar
If you have multiple properties to override, it is probably better to collect all them into a new file and use active environment name to select them.
└── application.conf
└── application.prod.conf
foo = foo
bar = devbar
bar = prodbar
prod
environmentjava -jar my.app application.env=prod
Or just
java -jar my.app prod
You only need to override the properties that changes between environment not all the properties. |
The application.conf
defines two properties : foo
and bar
, while the environment property file
defines only bar
.
For Multiple environment activation you need to separate them with ,
(comma):
prod
and cloud
environmentjava -jar my.app application.env=prod,cloud
16.4. Custom environment
Custom configuration and environment are available too using:
-
The EnvironmentOptions class, or
-
Direct instantiation of the Environment class
{
Environment env = setEnvironmentOptions(new EnvOptions() (1)
.setFilename("myapp.conf")
)
}
1 | Load myapp.conf using the loading and precedence mechanism described before |
The setEnvironmentOptions(EnvironmentOptions) method loads, set and returns the environment.
To skip/ignore Jooby loading and precedence mechanism, just instantiate and set the environment:
{
Config conf = ConfigFatory.load("/path/to/myapp.conf"); (1)
Environment env = new Env(customConfig, "prod"); (2)
setEnvironment(env); (3)
}
1 | Loads and parses configuration |
2 | Create a new environment with configuration and (optionally) active names |
3 | Set environment on Jooby instance |
Custom configuration is very flexible. You can reuse Jooby mechanism or provide your own. The only thing to keep in mind is that environment setting must be done at very early stage, before starting the application. |
16.5. Logging
Jooby uses Slf4j for logging which give you some flexibility for choosing the logging framework.
16.5.1. Logback
The Logback is probably the first alternative for Slf4j due its natively implements the SLF4J API. Follow the next steps to use logback in your project:
1) Add dependency
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.12</version>
</dependency>
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
That’s all! Slf4j is going to redirect log message to logback.
16.5.2. Log4j2
The Log4j2 project is another good alternative for logging. Follow the next steps to use logback in your project:
1) Add dependencies
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.24.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.24.1</version>
</dependency>
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Appenders>
<Console name="stdout">
<PatternLayout pattern="%d [%t] %-5level: %msg%n%throwable" />
</Console>
</Appenders>
<Loggers>
<Root level="INFO" additivity="true">
<AppenderRef ref="stdout" />
</Root>
</Loggers>
</Configuration>
All these extensions are considered valid: .xml
, .propertines
, .yaml
and .json
. As well as log4j2
for file name.
16.5.3. Environment logging
Logging is integrated with the environment names. So it is possible to have a file name:
-
logback[.name].xml
(for loggback) -
log4j[.name].xml
(for log4j2)
Jooby favors the environment specific logging configuration file over regular/normal logging configuration file.
conf
└── logback.conf
└── logback.prod.conf
To use logback.prod.conf
, start your application like:
java -jar myapp.jar application.env=prod
The logging configuration file per environment works as long you don’t use static loggers before application has been start it. The next example won’t work:
The There are a couple of solution is for this:
|
16.6. Application Properties
These are the application properties that Jooby uses:
Property name | Description | Default value |
---|---|---|
application.charset |
Charset used by your application. Used by template engine, HTTP encoding/decoding, database driver, etc. |
|
application.env |
The active environment names. Use to identify |
|
application.lang |
The languages your application supports. Used by |
A single locale provided by |
application.logfile |
The logging configuration file your application uses. You don’t need to set this property, see logging configuration. |
|
application.package |
The base package of your application. |
|
application.pid |
JVM process ID. |
The native process ID assigned by the operating system. |
application.startupSummary |
The level of information logged during startup. |
|
application.tmpdir |
Temporary directory used by your application. |
|
See AvailableSettings for more details.
17. Testing
This section will show you how to run unit and integration tests with Jooby.
17.1. Unit Testing
1) Add Jooby test dependency:
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-test</artifactId>
<version>3.5.0</version>
</dependency>
2) Write your application:
public class App extends Jooby {
{
get("/", ctx -> "Easy unit testing!");
}
}
3) Write your test:
import io.jooby.test.MockRouter;
public class TestApp {
@Test
public void test() {
MockRouter router = new MockRouter(new App());
assertEquals("OK", router.get("/").value());
}
}
Simple and easy ♡!
The MockRouter returns the value produced by the route handler. It is possible to get access and check response metadata:
public class App extends Jooby {
{
get("/", ctx -> ctx
.setResponseCode(StatusCode.OK)
.send("Easy unit testing")
);
}
}
public class TestApp {
@Test
public void test() {
MockRouter router = new MockRouter(new App());
router.get("/", response -> {
assertEquals(StatusCode.OK, response.getStatusCode());
assertEquals("Easy unit testing", response.value(String.class));
});
}
}
For more complex route context interaction or responses, you can pass in a MockContext:
public class App extends Jooby {
{
post("/", ctx -> {
String name = ctx.form("name").value();
return name;
});
}
}
public class TestApp {
@Test
public void test() {
MockRouter router = new MockRouter(new App());
MockContext context = new MockContext();
context.setForm(Formdata.create(context)
.put("name", "Test!")
);
assertEquals("Test!", router.post("/", context).value());
}
}
Alternative you can provide your own mock context:
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class TestApp {
@Test
public void test() {
Value name = mock(Value.class);
when(name.value()).thenReturn("Test!");
Context context = mock(Context.class);
when(context.form("name")).thenReturn(name);
MockRouter router = new MockRouter(new App());
assertEquals("Test!", router.post("/", context).value());
}
}
♡ ♡!
For MVC routes you might prefer to write a unit test using a mock library. No need to use
MockRouter
, but it is possible too.
17.1.1. Options
-
setFullExecution(): the MockRouter class ONLY execute the route handler. For executing the entire pipeline use: setFullExecution().
-
setLateInit(): extension modules usually run at the time they are installed it. This might not be ideally for unit tests. To delay extension initialization use the setLateInit() mode.
Unit testing is simple and easy in Jooby. The MockRouter let you execute the route function, while the MockContext allows you to create an light-weight and mutable context where you can set HTTP parameters, body, headers, etc.
17.2. Integration Testing
Integration tests are supported via JUnit 5 extension mechanism.
1) Add Jooby test dependency:
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-test</artifactId>
<version>3.5.0</version>
</dependency>
2) Write your application:
public class App extends Jooby {
{
get("/", ctx -> "Easy testing!");
}
}
3) Write your test:
import io.jooby.JoobyTest;
@JoobyTest(App.class)
public class TestApp {
static OkHttpClient client = new OkHttpClient();
@Test
public void test() {
Request request = new Request.Builder()
.url("http://localhost:8911")
.build();
try (Response response = client.newCall(request).execute()) {
assertEquals("Easy testing!", response.body().string());
}
}
}
The example uses OkHttp client, but you are free to use any other HTTP client.
Simple and easy ♡!
The JoobyTest takes care of start and stop the application.
The default port is: 8911
. Application port can be configured directly using the port() method:
@JoobyTest(value = App.class, port = 9999)
If port is set to zero(0) a random port is selected. You can inject the server port in your test like:
@JoobyTest(App.class)
public void test(int serverPort) {
}
@JoobyTest(App.class)
public void anotherTest(int serverPort) {
}
The parameter name must be serverPort
and be of type int
. This injection let you access to the
random port used for the method-level application test.
There is a serverPath
value too, which is the entire path to the server:
@JoobyTest(App.class)
public void test(String serverPath) { (1)
}
The serverPath
variable contains the entire path: http://localhost:port/contextPath
.
Here is the list of available injectable values:
-
int serverPort
: Give you the port where the application is listening. This is named type injection (name and type are required). -
String serverPath
: Give you the entire server path where the application is listening. This is named type injection (name and type are required). -
io.jooby.Environment
: Give you access to the application environment. This is a type injection (name no matter). -
com.typesafe.config.Config
: Give you access to the application environment. This is a type injection (name no matter). -
io.jooby.Jooby
: Give you access to the application. This is a type injection (name no matter).
These values can be injected via parameter or instance fields.
The JoobyTest annotation starts the application using the test
environment name. You can
creates a conf/application.test.conf
file to override any other values for testing purpose.
17.2.1. Arguments
Application arguments are supported using a factory method
strategy:
public class App extends Jooby {
public App(String argument) { (1)
get("/", ctx -> "Easy testing!");
}
}
1 | Application requires a String argument |
Write a test:
import io.jooby.JoobyTest;
public class TestApp {
@JoobyTest(value = App.class, factoryMethod = "createApp") (1)
public void test() {
Request request = new Request.Builder()
.url("http://localhost:8911")
.build();
try (Response response = client.newCall(request).execute()) {
assertEquals("Easy testing!", response.body().string());
}
}
public App createApp() { (2)
return new App("Argument"); (3)
}
}
1 | Specify a factory method: createApp |
2 | Creates the method: must be public and without arguments |
3 | Creates your application |
If you prefer the annotation at class level (shared application between tests) the factory method must be static.
18. Development
The jooby run
tool allows to restart your application on code changes without exiting the JVM.
This feature is also known as hot reload/swap. Makes you feel like coding against a script language where you modify your code and changes are visible immediately.
The tool uses the JBoss Modules library that effectively reload application classes.
For now jooby run
is available as Maven and Gradle plugins.
18.1. Usage
1) Add build plugin:
<plugins>
...
<plugin>
<groupId>io.jooby</groupId>
<artifactId>jooby-maven-plugin</artifactId>
<version>3.5.0</version>
</plugin>
...
</plugins>
2) Set main class
<properties>
<application.class>myapp.App</application.class>
</properties>
3) Run application
mvn jooby:run
18.2. Compilation & Restart
Changing a java
or kt
file triggers a compilation request. Compilation is executed by
Maven/Gradle using an incremental build process.
If compilation succeed, application is restarted.
Compilation errors are printed to the console by Maven/Gradle.
Changing a .conf
, .properties
file triggers just an application restart request. They don’t trigger
a compilation request.
Compiler is enabled by default, except for Eclipse users. Plugin checks for .classpath
file in
project directory, when found plugin compiler is OFF and let Eclipse compiles the code.
18.3. Options
The next example shows all the available options with their default values:
<plugins>
...
<plugin>
<groupId>io.jooby</groupId>
<artifactId>jooby-maven-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<mainClass>${application.class}</mainClass> (1)
<restartExtensions>conf,properties,class</restartExtensions> (2)
<compileExtensions>java,kt</compileExtensions> (3)
<port>8080</port> (4)
<waitTimeBeforeRestart>500</waitTimeBeforeRestart> (5)
<useSingleClassLoader>false</useSingleClassLoader> (6)
</configuration>
</plugin>
...
</plugins>
1 | Application main class |
2 | Restart extensions. A change on these files trigger a restart request. |
3 | Source extensions. A change on these files trigger a compilation request, followed by a restart request. |
4 | Application port |
5 | How long to wait after last file change to restart. Default is: 500 milliseconds. |
6 | Use a single/fat class loader to run your application. This is required on complex project classpath where you start seeing weird reflection errors. This was the default mode in Jooby 2.x. The new model since 3.x uses a modular classloader which improves restart times and memory usage making it faster. Default is: false . |
For Maven and Gradle there are two variant mvn jooby:testRun
and ./gradlew joobyTestRun
they work
by expanding the classpath to uses the test
scope or source set.
19. Packaging
This section describes some packaging and distribution options.
19.1. Single jar
This is the default deployment option where you create a single jar (a.k.a fat/uber jar) for your application.
The jooby-cli takes care of configures everything for single jar distribution. Next example shows how to do it in case you created your application manually. |
<build>
<plugins>
...
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<id>uber-jar</id>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>${application.class}</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
...
</plugins>
</build>
Maven users:
mvn clean package
Gradle users:
./gradlew shadowJar
19.2. Stork
Stork is packaging, launch and deploy tool for Java apps.
Stork is only available for Maven projects |
To configure stork:
1) Creates a src/etc/stork/stork.yml
file (file location is important):
# Name of application (make sure it has no spaces)
name: "${project.artifactId}"
# Display name of application (can have spaces)
display_name: "${project.name}"
# Type of launcher (CONSOLE or DAEMON)
type: DAEMON
# Java class to run
main_class: "${application.class}"
domain: "${project.groupId}"
short_description: "${project.artifactId}"
# Platform launchers to generate (WINDOWS, LINUX, MAC_OSX)
# Linux launcher is suitable for Bourne shells (e.g. Linux/BSD)
platforms: [ LINUX ]
# Working directory for app
# RETAIN will not change the working directory
# APP_HOME will change the working directory to the home of the app
# (where it was intalled) before running the main class
working_dir_mode: RETAIN
# Minimum version of java required (system will be searched for acceptable jvm)
min_java_version: "1.8"
# Min/max fixed memory (measured in MB)
min_java_memory: 512
max_java_memory: 512
# Min/max memory by percentage of system
#min_java_memory_pct: 10
#max_java_memory_pct: 20
# Try to create a symbolic link to java executable in <app_home>/run with
# the name of "<app_name>-java" so that commands like "ps" will make it
# easier to find your app
symlink_java: true
2) Configure Maven Tiles plugin:
<build>
<plugins>
<plugin>
<groupId>io.repaint.maven</groupId>
<artifactId>tiles-maven-plugin</artifactId>
<version>2.40</version>
<extensions>true</extensions>
<configuration>
<tiles>
<tile>io.jooby:jooby-stork:3.5.0</tile>
</tiles>
</configuration>
</plugin>
</plugins>
</build>
3) Run mvn package
Stork zip file will be available in the target
directory.
20. Server
There are three server implementations:
Servers are automatically registered based on their presence on the project classpath.
To use Jetty, add the dependency:
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-jetty</artifactId>
<version>3.5.0</version>
</dependency>
To use Netty, add the dependency:
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-netty</artifactId>
<version>3.5.0</version>
</dependency>
To use Undertow, add the dependency:
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-undertow</artifactId>
<version>3.5.0</version>
</dependency>
Only one server dependency must be available on classpath. |
20.1. Manual setup
Servers are automatically loaded using ServiceLoader
API. If you need to add it manually:
import io.jooby.netty.NettyServer;
{
install(new NettyServer());
}
Server might or might not provide way to configure it (depends on Server). For example JettyServer
let you customize the ThreadPool
, here is an example to setup Jetty with Loom(Virtual Threads):
import io.jooby.jetty.JettyServer;
import java.util.concurrent.Executors;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
{
// Use virtual Thread in Java 19 with --enable-preview
QueuedThreadPool worker = new QueuedThreadPool();
worker.setReservedThreads(0);
worker.setVirtualThreadsExecutor(Executors.newVirtualThreadPerTaskExecutor());
install(new JettyServer(worker));
}
20.2. Options
Server options are available via ServerOptions class:
{
setServerOptions(new ServerOptions()
.setBufferSize(16384)
.setCompressionLevel(6)
.setPort(8080)
.setIoThreads(16)
.setWorkerThreads(64)
.setGzip(false)
.setSingleLoop(false)
.setDefaultHeaders(true)
.setMaxRequestSize(10485760)
.setSecurePort(8433)
.setSsl(SslOptions.selfSigned())
.setHttpsOnly(false)
.setHttp2(true)
.setExpectContinue(true)
);
}
-
bufferSize: Buffer size used by server for reading/writing data. Default is:
16k
. -
compressionLevel: Gzip support. Set compression level. Value between:
0..9
. -
port: Server HTTP port or
0
for random port. Default is:8080
. -
ioThreads: Number of IO threads used by the server. Used by Netty and Undertow. Default is:
Runtime.getRuntime().availableProcessors() * 2
-
workerThreads: Number of worker (a.k.a application) threads. Default is:
ioThreads * 8
. -
gzip: Gzip support. Default is:
false
. Deprecated in favor of compressionLevel. -
singleLoop: Indicates if the web server should use a single loop/group for doing IO or not. Netty only.
-
defaultHeaders: Configure server to set the following headers:
Date
,Content-Type
andServer
headers. -
maxRequestSize: Maximum request size in bytes. Request exceeding this value results in 413(REQUEST_ENTITY_TOO_LARGE) response. Default is
10mb
. -
securePort: Enable HTTPS. This option is fully covered in next section.
-
ssl: SSL options with certificate details. This option is fully covered in next section.
-
isHttpsOnly: bind only to HTTPS port, not HTTP. This requires SSL options to be configured.
-
isHttp2: Enable HTTP 2.0.
-
isExpectContinue: Whenever 100-Expect and continue requests are handled by the server. This is off by default, except for Jetty which is always ON.
Server options are available as application configuration properties too:
server.bufferSize = 16384
server.compressionLevel = 6
server.port = 8080
server.ioThreads = 16
server.workerThreads = 64
server.gzip = false
server.singleLoop = false
server.defaultHeaders = true
server.maxRequestSize = 10485760
server.securePort = 8443
server.ssl.type = self-signed | PKCS12 | X509
server.httpsOnly = false
server.http2 = true
server.expectContinue = false
20.3. HTTPS Support
Jooby supports HTTPS out of the box. By default HTTPS is disabled and all requests are served using HTTP. Jooby supports two certificate formats:
-
PKCS12 (this is the default format)
-
X.509
The SslOptions class provides options to configure SSL:
-
cert: A PKCS12 or X.509 certificate chain file in PEM format. Most commonly, a .crt file for X509 and .p12 for PKCS12. It can be an absolute path or a classpath resource. Required.
-
key: A PKCS#8 private key file in PEM format. Most commonly a .key file. It can be an absolute path or a classpath resource. Required when using X.509 certificates.
-
password: Password to use. Required when using PKCS12 certificates.
{
setServerOptions(new ServerOptions()
.setSecurePort(8443) (1)
);
}
1 | Set secure port and use a self-signed certificate |
Once SSL is enabled application logs print something like:
listening on: http://localhost:8080/ https://localhost:8443/
The |
A better option for development is the mkcert tool:
mkcert -pkcs12 localhost
mkcert localhost
20.3.1. Using X.509
To use a valid X.509 certificate, for example one created with Let’s Encrypt. You will need the .crt
and .key
files:
{
SslOptions ssl = SslOptions.x509("path/to/server.crt", "path/to/server.key");
setServerOptions(new ServerOptions()
.setSsl(ssl) (1)
);
}
1 | Creates a SslOptions using X509 certificates path |
Certificate (.crt) and private key (.key) location can be file system or class path locations.
Optionally you can define the SSL options in your application configuration file:
server {
ssl {
type: X509,
cert: "path/to/server.crt",
key: "path/to/server.key"
}
}
{
setServerOptions(new ServerOptions()
.setSsl(SslOptions.from(getConfig()))
);
}
20.3.2. Using PKCS12
To use a valid PKCS12 certificate:
{
SslOptions ssl = SslOptions.pkcs12("path/to/server.p12", "password");
setServerOptions(new ServerOptions()
.setSsl(ssl) (1)
);
}
1 | Creates SslOptions using PKCS12 certificates path |
Certificate (.p12 location can be file system or class path locations.
Optionally you can define the SSL options in your application configuration file:
server {
ssl {
type: PKCS12,
cert: "path/to/server.p12",
password: "password"
}
}
{
setServerOptions(new ServerOptions()
.setSsl(SslOptions.from(getConfig()))
);
}
20.3.3. Client Authentication (Mutual TLS)
To enable 2-way TLS (Mutual TLS), set the trust certificate and client authentication. Setting the trust certificate is required if using self-signed or custom generated certificates so that the server will trust the client’s certificate signing authority.
{
SslOptions ssl = SslOptions.pkcs12("path/to/server.p12", "password")
.setTrustCert(Files.newInputStream("path/to/trustCert")) (1)
.setTrustPassword("password") (2)
.setClientAuth(SslOptions.ClientAuth.REQUIRED); (3)
setServerOptions(new ServerOptions()
.setSsl(ssl)
);
}
1 | Set the trust certificate path. |
2 | Set the trust certificate password. |
3 | Set the client authentication mode. Possible values are REQUIRED, REQUESTED, or NONE. Default is NONE. |
Optionally you can define these SSL options in your application configuration file:
server {
ssl {
type: PKCS12,
cert: "path/to/server.p12",
password: "password",
trust {
cert: "path/to/trustCert",
password: "password"
}
clientAuth: REQUIRED
}
}
{
setServerOptions(new ServerOptions()
.setSsl(SslOptions.from(getConfig()))
);
}
20.3.4. TLS protocol
Default protocol is TLSv1.3, TLSv1.2
. To override, just do:
{
setServerOptions(new ServerOptions()
.setSsl(new SslOptions().setProtocol("TLSv1.3", "TLSv1.2"))
);
}
If a listed protocol is not supported, it is ignored; however, if you specify a list of protocols, none of which are supported, an exception will be thrown.
TLSv1.3 protocol is available in
|
20.3.5. OpenSSL
SSL support is provided using built-in JDK capabilities. Jooby offers an OpenSSL support using Conscrypt.
To enable, just add the required dependency:
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-conscrypt</artifactId>
<version>3.5.0</version>
</dependency>
Conscrypt is a Java Security Provider (JSP) that implements parts of the Java Cryptography Extension (JCE) and Java Secure Socket Extension (JSSE). It uses BoringSSL to provide cryptographic primitives and Transport Layer Security (TLS) for Java applications on Android and OpenJDK.
20.4. HTTP/2 Support
HTTP2 support is provided across web server implementation. You need to enabled http2
option
programmatically or via application.conf
properties.
{
setServerOptions(new ServerOptions()
.setHttp2(true)
.setSecurePort(8433)
);
get("/", ctx -> {
ctx.getProtocol()
})
}
To use HTTP/2 from browsers you need TLS (the h2 protocol) please refer to HTTPS support to configure TLS. |
There is no support for HTTP/2 Push. |
21. Extensions and Services
Jooby comes with a simple extension mechanism. The Extension API allows to configure , extend an application by adding shared/single services, infrastructure/technical concerns like dependency injection, database connection pools, cron-job, etc.
Services are shared/singleton objects, usually with a clear lifecycle for starting and stopping them.
21.1. Writing Custom Extension
We are going to develop a custom extension that configure a DataSource
service.
import io.jooby.Extension;
public class MyExtension implements Extension {
public void install(Jooby application) {
DataSource dataSource = createDataSource(); (1)
ServiceRegistry registry = application.getServices(); (2)
registry.put(DataSource.class, dataSource); (3)
application.onStop(dataSource::close) (4)
}
}
1 | Create the service |
2 | Access to service registry |
3 | Add it to the service registry |
4 | Close/release service on application stop |
Let’s install the extension and use the service!!
public class App extends Jooby {
{
install(new MyExtension()); (1)
get("/", ctx -> {
MyDataSource ds = require(MyDataSource.class); (2)
// ...
});
}
}
1 | Install the extension |
2 | Use the service |
Services are accessible via require(Class).
In addition to services, an extension module may provides infrastructure routes, body decoder/encoder, template engines, etc.
The extension mechanism is a simple way of reusing code and decoupling technical features from business logic.
More advanced techniques are describe in the Dependency Injection section.
22. Dependency Injection
22.1. Avaje Inject
1) Add Avaje Inject to your project
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-avaje-inject</artifactId>
<version>3.5.0</version>
</dependency>
2) Configure annotation processor
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>...</version>
<configuration>
<annotationProcessorPaths>
<!-- if using lombok, it must be placed before the avaje-inject-generator -->
<!-- avaje-inject-generator must be placed before the jooby-apt -->
<path>
<groupId>io.avaje</groupId>
<artifactId>avaje-inject-generator</artifactId>
<version>10.4</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Please note that the order of annotation processors is important. For example, if you’re using |
3) Install Avaje Inject:
public class App extends Jooby {
{
install(AvajeInjectModule.of()); (1)
get("/", ctx -> {
MyService service = require(MyService.class); (2)
return service.doSomething();
});
}
public static void main(String[] args) {
runApp(args, App::new);
}
}
1 | Install Avaje Inject module |
2 | The require(Class) call is now resolved by Avaje Inject |
22.1.1. Property Injection
Configuration properties can be injected using the @Named
annotation. As Avaje checks beans at compile time, @External
is required to prevent false-positive compilation errors:
currency = USD
@Singleton
public class BillingService {
@Inject
public BillingService(@External @Named("currency") String currency) {
...
}
}
22.1.2. MVC routes
Avaje Inject will also provisioning MVC routes
public class App extends Jooby {
{
install(AvajeInjectModule.of()); (1)
mvc(MyController.class); (2)
}
public static void main(String[] args) {
runApp(args, App::new);
}
}
1 | Install Avaje Inject module |
2 | Register a MVC route |
The lifecycle of MyController
is now managed by Avaje Inject.
In Avaje Inject, the dependency graph is typically validated when the application compiles. As beans provided by Jooby Modules are registered at runtime, you must add @External
when injecting these runtime beans into @Singleton
classes to inform the avaje processor that these beans are provided at runtime.
22.2. Dagger
1) Add Dagger to your project
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger</artifactId>
<version>2.20</version>
</dependency>
2) Configure annotation processor
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>...</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>com.google.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<version>2.20</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
3) Bootstrap Dagger from application:
import static io.jooby.Jooby.runApp;
public class App extends Jooby {
{
/** Dagger: */
AppComponent dagger = DaggerAppComponent.builder() (1)
.build();
get("/", ctx -> {
MyService service = dagger.getMyService(); (2)
return service.doSomething();
});
}
public static void main(String[] args) {
runApp(args, App::new);
}
}
1 | Bootstrap dagger component |
2 | Use dagger provided objects |
|
22.2.1. MVC routes
Integration of MVC routes with Dagger is as simple as:
import static io.jooby.Jooby.runApp;
public class App extends Jooby {
{
/** Dagger: */
AppComponent dagger = DaggerAppComponent.builder() (1)
.build();
mvc(dagger.myController()); (2)
}
public static void main(String[] args) {
runApp(args, App::new);
}
}
1 | Bootstrap dagger component |
2 | Register MVC route provided by Dagger |
Due the static nature of Dagger mvc integration identical to normal usage. For custom scopes/lifecycles
Dagger generate a jakarta.inject.Provider
on such use cases you need to switch and use the provider
version of the mvc
method:
import static io.jooby.Jooby.runApp;
public class App extends Jooby {
{
/** Dagger: */
AppComponent dagger = DaggerAppComponent.builder() (1)
.build();
mvc(MyController.class, dagger.myController()); (2)
}
public static void main(String[] args) {
runApp(args, App::new);
}
}
1 | Bootstrap dagger component |
2 | Register MVC route using a Dagger provider |
22.3. Guice
1) Add Guice dependency to your project:
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-guice</artifactId>
<version>3.5.0</version>
</dependency>
2) Install Guice:
import io.jooby.guice.GuiceModule;
import io.jooby.kt.runApp;
public class App extends Jooby {
{
install(new GuiceModule()); (1)
get("/", ctx -> {
MyService service = require(MyService.class); (2)
return service.doSomething();
});
}
public static void main(String[] args) {
runApp(args, App::new);
}
}
1 | Install Guice module |
2 | The require(Class) call is now resolved by Guice |
22.3.1. Property Injection
Configuration properties can be injected using the @Named
annotation:
currency = USD
import javax.injext.Named;
import javax.injext.Inject;
public class BillingService {
@Inject
public BillingService(@Named("currency") String currency) {
...
}
}
22.3.2. MVC routes
Guice will also provisioning MVC routes
import io.jooby.guice.GuiceModule;
import io.jooby.kt.runApp
public class App extends Jooby {
{
install(new GuiceModule()); (1)
mvc(MyController.class); (2)
}
public static void main(String[] args) {
runApp(args, App::new);
}
}
1 | Install Guice module |
2 | Register a MVC route |
The lifecycle of MyController
is now managed by Guice. Also:
-
In Guice, the default scope is
prototype
(creates a new instance per request) -
If you prefer a single instance add the
jakarta.inject.Singleton
annotation
23. Modules
Modules are a key concept for building reusable and configurable pieces of software.
Modules (unlike in other frameworks) are thin and do a lot of work to bootstrap and configure an external library, but they DO NOT provide a new level of abstraction nor [do] they provide a custom API to access functionality in that library. Instead they expose the library components as they are.
Modules are distributed as separated jar/dependency and usually implement the Extension API.
In general they provide a builder
class to create the and configure the external library from
configuration properties.
Available modules are listed next.
23.1. Cloud
-
AWS-SDK v2: Amazon Web Service module SDK 2.
-
AWS SDK v1: Amazon Web Service module SDK 1.
23.3. Validation
-
Avaje Validator: Avaje Validator module.
-
Hibernate Validator: Hibernate Validator module.
23.5. Event Bus
-
Camel: Camel module for Jooby.
23.6. JSON
-
Gson: Gson module for Jooby.
-
Jackson: Jackson module for Jooby.
-
JSON-B: JSON-B module for Jooby.
-
Avaje-JsonB: Avaje-JsonB module for Jooby.
23.7. OpenAPI
-
OpenAPI: OpenAPI supports.
23.8. Template Engine
-
Handlebars: Handlebars template engine.
-
JStachio: JStachio template engine.
-
jte: jte template engine.
-
Freemarker: Freemarker template engine.
-
Pebble: Pebble template engine.
-
Rocker: Rocker template engine.
-
Thymeleaf: Thymeleaf template engine.
24. Appendix
24.1. Upgrading from 2.x to 3.x
You will find here notes/tips about how to migrate from 2.x to 3.x.
This is a work in progress document, if something is wrong or missing please report to Github or better edit this file and fix it |
24.1.1. Requirements
-
Java 17 as minimum
24.1.2. module-info.java
Jooby is now compatible with Java Module system.
Almost all Jooby components are now Java Modules, but not all them. For those where wasn’t
possible the Jooby module contains the Automatic-Module-Name
manifest entry.
24.1.3. Kotlin
Kotlin was removed from core, you need to the jooby-kotlin
dependency:
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-kotlin</artifactId>
<version>3.5.0</version>
</dependency>
24.1.4. jakarta
2.x |
3.x |
javax.servlet |
jakarta.servlet |
javax.inject |
jakarta.inject |
javax.persistence |
jakarta.persistence |
24.1.5. Modules
2.x |
3.x |
jooby-kotlin |
Added |
jooby-weld |
Removed |
jooby-archetype |
Removed |
jooby-utow |
Renamed: jooby-undertow |
jooby-commons-email |
Renamed: jooby-commons-mail |
jooby-http2-jetty |
Merged into: jooby-netty |
jooby-http2-netty |
Merged into: jooby-netty |
jooby-http2-undertow |
Merged into: jooby-undertow |
24.1.6. Package renames
2.x |
3.x |
Module |
io.jooby.annotations |
io.jooby.annotation |
jooby (core) |
io.jooby |
io.jooby.test |
jooby-test |
io.jooby (Kotlin) |
io.jooby.kt |
removed from jooby, now in jooby-kotlin |
io.jooby.graphql |
io.jooby.graphiql |
jooby-graphiql |
io.jooby.graphql |
io.jooby.graphql.playground |
jooby-graphql-playground |
io.jooby.json |
io.jooby.gson |
jooby-gson |
io.jooby.json |
io.jooby.jackson |
jooby-jackson |
io.jooby.di |
io.jooby.guice |
jooby-guice |
io.jooby.di |
io.jooby.spring |
jooby-spring |
io.jooby.aws |
io.jooby.awssdkv1 |
jooby-awssdk-v1 |
io.jooby.email |
io.jooby.commons.mail |
jooby-commons-mail |
io.jooby.utow |
io.jooby.undertow |
jooby-undertow |
24.1.7. Class renames
2.x |
3.x |
Module |
io.jooby.Route.Decorator |
io.jooby.Route.Filter |
jooby (core) |
io.jooby.Kooby |
io.jooby.kt.Kooby |
jooby-kotlin (new module) |
io.jooby.jetty.Jetty |
io.jooby.jetty.JettyServer |
jooby-jetty |
io.jooby.netty.Netty |
io.jooby.netty.NettyServer |
jooby-netty |
io.jooby.utow.Utow |
io.jooby.undertow.UndertowServer |
jooby-undertow |
io.jooby.AccessLogHandler |
io.jooby.handler.AccessLogHandler |
jooby (core) |
io.jooby.Cors |
io.jooby.handler.Cors |
jooby (core) |
io.jooby.CorsHandler |
io.jooby.handler.CorsHandler |
jooby (core) |
io.jooby.CsrfHandler |
io.jooby.handler.CsrfHandler |
jooby (core) |
io.jooby.HeadHandler |
io.jooby.handler.HeadHandler |
jooby (core) |
io.jooby.RateLimitHandler |
io.jooby.handler.RateLimitHandler |
jooby (core) |
io.jooby.SSLHandler |
io.jooby.handler.SSLHandler |
jooby (core) |
io.jooby.TraceHandler |
io.jooby.handler.TraceHandler |
jooby (core) |
io.jooby.WebVariables |
io.jooby.handler.WebVariables |
jooby (core) |
io.jooby.Asset |
io.jooby.handler.Asset |
jooby (core) |
io.jooby.AssetHandler |
io.jooby.handler.AssetHandler |
jooby (core) |
io.jooby.AssetSource |
io.jooby.handler.AssetSource |
jooby (core) |
io.jooby.CacheControl |
io.jooby.handler.CacheControl |
jooby (core) |
24.1.8. Method renames
2.x |
3.x |
Description |
Router.decorator(Decorator) |
Router.use(Filter) |
|
SslOptions.setCert(String) |
SslOptions.setCert(InputStream) |
Replaced the string argument with |
SslOptions.setTrustCert(String) |
SslOptions.setTrustCert(InputStream) |
Replaced the string argument with |
SslOptions.setPrivateKey(String) |
SslOptions.setPrivateKey(InputStream) |
Replaced the string argument with |
FileUpload.destroy |
FileUpload.close |
|
Context.attribute(String) |
Context.getAttribute(String) |
|
Context.attribute(String, Object) |
Context.setAttribute(String, Object) |
|
Context.multipart* |
Context.form* |
All Context.multipart related methods where merged into Context.form |
Context.query<Type>() |
Context.query(Type::class) |
Kotlin |
Context.form<Type>() |
Context.form(Type::class) |
Kotlin |
Context.body<Type>() |
Context.body(Type::class) |
Kotlin |
24.1.9. Dependencies
2.x |
3.x |
Module |
Slf4j 1.x |
Slf4j 2.x |
jooby (core) |
Jetty 9.x |
Jetty 11.x |
jooby-jetty |
Guice 5.x |
Guice 7.x |
jooby-guice |
24.1.10. Reactive support
Reactive libraries has been removed from core to his own module.
2.x |
3.x |
rxjava |
jooby-rxjava3 |
reactor |
jooby-reactor |
All reactive libraries requires explicit handler while using script/lambda routes. More details on NonBlocking responses.
24.2. Upgrading from 1.x to 2.x
You will find here notes/tips about how to migrate from 1.x to 2.x.
24.2.1. Maven coordinates
org.jooby
became io.jooby
. Hence, use <groupId>org.jooby</groupId>
for all dependencies.
24.2.2. Modules
1.x |
2.x |
jooby-apitool |
no real equivalent, use a combination of jooby-maven-plugin and jooby-swagger-ui |
jooby-hbv |
n/a |
jooby-lang-kotlin |
not needed anymore, part of core now |
jooby-servlet |
n/a |
24.2.3. API
API still similar/equivalent in 2.x. Except for the one listed below:
1.x |
2.x |
org.jooby.Module |
io.jooby.Extension |
org.jooby.Env |
io.jooby.Environment |
org.jooby.Mutant |
io.jooby.Value |
org.jooby.Render |
io.jooby.MessageEncoder |
org.jooby.Parser |
io.jooby.MessageDecoder |
org.jooby.Err |
io.jooby.StatusCodeException |
org.jooby.Results |
- (removed) |
org.jooby.Result |
- (removed) |
24.2.4. Route Pipeline
The concept of route pipeline still applies for 2.x but works different.
In 1.x there is no difference between handler and filter (including before and after). The way to chain multiple handler/filter was like:
{
use("*", (req, rsp, chain) -> {
System.out.println("first");
// Moves execution to next handler: second
chain.next(req, rsp);
});
use("*", (req, rsp, chain) -> {
System.out.println("second");
// Moves execution to next handler: third
chain.next(req, rsp);
});
get("/handler", req -> {
return "third";
});
}
A filter in 1.x requires a path pattern, here we use a wide matcher *
for first
and second
filters.
Both of this filters are going to be executed before the real handler
.
{
use(next -> ctx -> {
System.out.println("first");
// Moves execution to next handler: second
return next.apply(ctx);
});
use(next -> ctx -> {
System.out.println("second");
// Moves execution to next handler: third
return next.apply(ctx);
});
get("/handler", ctx -> {
return "third";
});
}
Execution is identical to 1.x. The first
and second
decorators are executed before the handler.
Differences with 1.x are:
-
Route.Decorator doesn’t support a path pattern. In 1.x the path pattern is required for a filter.
-
Only the handler supports a path pattern and HTTP-method.
-
A handler might have zero or more decorator.
-
In 2.x we chain all the decorator defined before the handler.
The routing matching algorithm in 2.x is more efficient and fast, because:
-
Matches a single path pattern (due decorator lacks of path pattern)
-
Uses a radix tree, not regular expression like in 1.x
-
It never executes a decorator if there isn’t a matching handler
More detailed explanation of route pipeline is available in the router pipeline documentation.