The javadoc:Router[] is the heart of Jooby and consist of:
-
Routing algorithm (radix tree)
-
One or more javadoc:Route[text="routes"]
-
Collection of operator over javadoc:Route[text="routes"]
A javadoc: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) (2)
get("/foo") {
"foo" // (3)
}
// Get example with path variable
get("/foo/{id}") {
ctx.path("id").value()
}
// Post example
post("/") {
ctx.body().value()
}
}-
HTTP method/verb, like:
GET,POST, etc… -
Path pattern, like:
/foo,/foo/{id}, etc… -
Handler function
The javadoc:Route.Handler[text="handler"] function always produces a result, which is send it back to the client.
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");
}{
get("/foo") {
"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);
});
}{
use(
val user = ...
val role = ctx.route.attribute("Role")
if (user.hasRole(role)) {
return next.apply(ctx)
} else {
throw 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"));
});
}@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Role (val value: String)
@Path("/path")
class AdminResource {
@Role("admin")
fun doSomething() : Any {
...
}
}
{
use {
println(ctx.route.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.
{
// (1)
get("/user/{id}", ctx -> {
int id = ctx.path("id").intValue(); // (2)
return id;
});
}{
// (1)
get("/user/{id}") {
val id = ctx.path("id").intValue() // (2)
id
}
}-
Defines a path variable
id -
Retrieve the variable
idasint
{
// (1)
get("/file/{file}.{ext}", ctx -> {
String filename = ctx.path("file").value(); // (2)
String ext = ctx.path("ext").value(); // (3)
return filename + "." + ext;
});
}{
// (1)
get("/file/{file}.{ext}") {
val filename = ctx.path("file").value() // (2)
val ext = ctx.path("ext").value() // (3)
filename + "." + ext
}
}-
Defines two path variables:
fileandext -
Retrieve string variable:
file -
Retrieve string variable:
ext
{
// (1)
get("/profile/{id}?", ctx -> {
String id = ctx.path("id").value("self"); // (2)
return id;
});
}{
// (1)
get("/profile/{id}?") {
val id = ctx.path("id").value("self") // (2)
id
}
}-
Defines an optional path variable
id. The trailing?make it optional. -
Retrieve the variable
idasStringwhen present or use a default value:self.
The trailing ? makes the path variable optional. The route matches:
-
/profile -
/profile/eespina
{
// (1)
get("/user/{id:[0-9]+}", ctx -> {
int id = ctx.path("id").intValue(); // (2)
return id;
});
}{
// (1)
get("/user/{id:[0-9]+}") {
val id = ctx.path("id").intValue() // (2)
id
}
}`-
Defines a path variable:
id. Regex expression is everything after the first:, like:[0-9]+ -
Retrieve an int value
Optional syntax is also supported for regex path variable: /user/{id:[0-9]+}?:
-
matches
/user -
matches
/user/123
{
// (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)
get("/articles/*") {
val catchall = ctx.path("*").value() // (2)
catchall
}
get("/articles/*path") {
val path = ctx.path("path").value() // (3)
path
}
}-
The trailing
*defines acatchallpattern -
We access to the
catchallvalue using the*character -
Same example, but this time we named the
catchallpattern and we access to it usingpathvariable name.
|
Note
|
A |
Application logic goes inside a javadoc:Route.Handler[text=handler]. A
javadoc:Route.Handler[text=handler] is a function that accepts a javadoc:Context[text=context]
object and produces a result.
A javadoc:Context[text=context] allows you to interact with the HTTP Request and manipulate the
HTTP Response.
|
Note
|
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)
}{
get("/user/{id}") { ctx.path("id").value() } // (1)
get("/user/me") { "my profile" } // (2)
get("/users") { "users" } // (3)
get("/users") { "new users" } // (4)
}Output:
-
GET /user/ppicapiedra⇒ppicapiedra -
GET /user/me⇒my profile -
Unreachable ⇒ override it by next route
-
GET /users⇒new usersnotusers
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.
Cross cutting concerns such as response modification, verification, security, tracing, etc. is available via javadoc: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";
});
}{
/** Kotlin uses implicit variables: `ctx` and `next` */
filter {
val start = System.currentTimeMillis() // (1)
val response = next.apply(ctx) // (2)
val end = System.currentTimeMillis()
val took = end - start
println("Took: " + took + "ms") // (3)
response // (4)
}
get("/") {
"filter"
}
}-
Saves start time
-
Proceed with execution (pipeline)
-
Compute and print latency
-
Returns a response
|
Note
|
One or more filter on top of a handler produces a new handler. |
The javadoc:Route.Before[text=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 "...";
});
}{
before {
ctx.setResponseHeader("Server", "Jooby")
}
get("/") {
"..."
}
}The javadoc:Route.After[text=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";
});
}{
after {
println("Hello $result") (1)
ctx.setResponseHeader("foo", "bar") (2)
}
get("/") {
"Jooby"
}
}-
Prints
Jooby -
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");
});
}{
after {
println("Hello $result") (1)
ctx.setResponseHeader("foo", "bar") (2)
}
get("/") {
ctx.send("Jooby")
}
}-
Prints
null(no value) -
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 javadoc:Context[isResponseStarted]:
{
after((ctx, result, failure) -> {
if (ctx.isResponseStarted()) {
// Don't modify response
} else {
// Safe to modify response
}
});
}{
after {
if (ctx.responseStarted) {
// Don't modify response
} else {
// Safe to modify response
}
}
}|
Note
|
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)
}
});
}{
after {
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)
}
});
}{
after {
if (failure is MyBusinessException) {
ctx.send("Recovering from something") (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)
});
}{
after {
...
throw AnotherException();
}
get("/") { ctx ->
...
throw OriginalException()
}
error { ctx, failure, code) ->
val originalException = failure (1)
val anotherException = failure.getSuppressed()[0] (2)
}
}-
Will be
OriginalException -
Will be
AnotherException
The javadoc:Route.Complete[text=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));
});
});
}{
use {
val start = System.currentTimeMillis()
ctx.onComplete { (1)
val end = System.currentTimeMillis() (2)
println("Took: " + (end - start))
}
}
}-
Attach a completion listener
-
Run after response has been fully written
Completion listeners are invoked in reverse order.
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)
}{
// Increment +1
use {
val n = next.apply(ctx) as Int
1 + n
}
// Increment +1
use {
val n = next.apply(ctx) as Int
1 + n
}
get("/1") { 1 } // (1)
get("/2") { 2 } // (2)
}Output:
-
/1⇒3 -
/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.
|
Note
|
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
{
use("/*", (req, rsp, chain) -> {
// remote call, db call
});
// ...
}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 |
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)
}{
// Increment +1
use {
val n = next.apply(ctx) as Int
1 + n
}
get("/1") { 1 } // (1)
// Increment +1
use {
val n = next.apply(ctx) as Int
1 + n
}
get("/2") { 2 } // (2)
}Output:
-
/1⇒2 -
/2⇒4
The javadoc:Router[route, java.lang.Runnable] and javadoc:Router[path, java.lang.String, java.lang.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)
}{
// Increment +1
filter {
val n = next.apply(ctx) as Int
return 1 + n
}
routes { // (1)
// Multiply by 2
filter {
val n = next.apply(ctx) as Int
2 * n
}
get("/4") { 4 } // (2)
}
get("/1") { 1 } // (3)
}Output:
-
Introduce a new scope via
routeoperator -
/4⇒9 -
/1⇒2
It is a normal filter inside of one of the group operators.
As showed previously, the javadoc:Router[route, java.lang.Runnable] operator push a new route scope
and allows you to selectively apply one or more routes.
{
routes(() -> {
get("/", ctx -> "Hello");
});
}{
routes {
get("/") {
"Hello"
}
}
}Route operator is for grouping one or more routes and apply cross cutting concerns to all them.
In similar fashion the javadoc:Router[path, java.lang.String, java.lang.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)
...
});
}{
path("/api/user") { // (1)
get("/{id}") { ...} // (2)
get("/") { ...} // (3)
post("/") { ...} // (4)
...
});
}-
Set common prefix
/api/user -
GET /api/user/{id} -
GET /api/user -
POST /api/user
Composition is a technique for building modular applications. You can compose one or more router into a new one.
Composition is available through the javadoc:Router[mount, io.jooby.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)
}
}class Foo: Kooby({
get("/foo") { ctx.getRequestPath() }
})
class Bar: Kooby({
get("/bar") { ctx.getRequestPath() }
})
class App: Kooby({
mount(Foo()) // (1)
mount(Bar()) // (2)
get("/app") { ctx.getRequestPath() } // (3)
})-
Imports all routes from
Foo. Output:/foo⇒/foo -
Imports all routes from
Bar. Output:/bar⇒/bar -
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)
}
}class Foo: Kooby({
get("/foo") { ctx.getRequestPath() }
})
class App: Kooby({
mount("/prefix", Foo()) // (1)
})-
Now all routes from
Foowill 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.
Alternatively, you can install a standalone application into another one using the javadoc:Jooby[install, io.jooby.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)
}
}class Foo: Kooby({
get("/foo") { ... }
})
class Bar: Kooby({
get("/bar") { ... }
})
class App: Kooby({
install(::Foo) // (1)
install(::Bar) // (2)
})-
Imports all routes, services, callbacks, etc… from
Foo. Output:/foo⇒/foo -
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!
}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)
}
}class V1: Kooby({
get("/api") { "v1" }
})
class V2: Kooby({
get("/api") { "v2" }
})
class App: Kooby({
mount(ctx -> ctx.header("version").value().equals("v1"), V1()); // (1)
mount(ctx -> ctx.header("version").value().equals("v2"), V2()); // (2)
})Output:
-
/api⇒v1; whenversionheader isv1 -
/api⇒v2; whenversionheader isv2
Done {love}!
This model let you run multiple applications on single server instance. Each application
works like a standalone application, they don’t share any kind of services.
public class Foo extends Jooby {
{
setContextPath("/foo");
get("/hello", ctx -> ...);
}
}
public class Bar extends Jooby {
{
setContextPath("/bar");
get("/hello", ctx -> ...);
}
}
import static io.jooby.Jooby.runApp;
public class MultiApp {
public static void main(String[] args) {
runApp(args, List.of(Foo::new, Bar::new));
}
}import io.jooby.kt.Kooby.runApp
fun main(args: Array<String>) {
runApp(args, ::Foo, ::Bar)
}You write your application as always and them you deploy them using the runApp method.
|
Important
|
Due to nature of logging framework (static loading and initialization) the logging bootstrap
might not work as you expected. It is recommend to use just the |