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.
{
decorator(next -> ctx -> {
User user = ...;
String role = ctx.getRoute().attribute("Role");
if (user.hasRole(role)) {
return next.apply(ctx);
}
throw new StatusCodeException(StatusCode.FORBIDDEN);
});
}{
decorator(
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() {
...
}
}
{
decorator(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 {
...
}
}
{
decorator {
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("/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
{
// (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.Decorator[].
A decorator takes the next handler in the pipeline and produces a new handler:
interface Decorator {
Handler apply(Handler next);
}{
decorator(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 "decorator";
});
}{
/** Kotlin uses implicit variables: `ctx` and `next` */
decorator {
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("/") {
"decorator"
}
}-
Saves start time
-
Proceed with execution (pipeline)
-
Compute and print latency
-
Returns a response
|
Note
|
One or more decorator 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=complte] 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 decorator/before/after filters).
{
decorator(next -> ctx -> {
long start = System.currentTimeInMillis();
ctx.onComplete(context -> { (1)
long end = System.currentTimeInMillis(); (2)
System.out.println("Took: " + (end - start));
});
});
}{
decorator {
val start = System.currentTimeInMillis()
ctx.onComplete { (1)
val end = System.currentTimeInMillis() (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 decorator(s) tied to a single handler:
{
// Increment +1
decorator(next -> ctx -> {
Number n = (Number) next.apply(ctx);
return 1 + n.intValue();
});
// Increment +1
decorator(next -> ctx -> {
Number n = (Number) next.apply(ctx);
return 1 + n.intValue();
});
get("/1", ctx -> 1); // (1)
get("/2", ctx -> 2); // (2)
}{
// Increment +1
decorator {
val n = next.apply(ctx) as Int
1 + n
}
// Increment +1
decorator {
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 = decorator(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 decorator defined on top of the handler will be stacked/chained into a new handler.
|
Note
|
Decorator 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
decorator(next -> ctx -> {
Number n = (Number) next.apply(ctx);
return 1 + n.intValue();
});
get("/1", ctx -> 1); // (1)
// Increment +1
decorator(next -> ctx -> {
Number n = (Number) next.apply(ctx);
return 1 + n.intValue();
});
get("/2", ctx -> 2); // (2)
}{
// Increment +1
decorator {
val n = next.apply(ctx) as Int
1 + n
}
get("/1") { 1 } // (1)
// Increment +1
decorator {
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 decorator looks like:
{
// Increment +1
decorator(next -> ctx -> {
Number n = (Number) next.apply(ctx);
return 1 + n.intValue();
});
route(() -> { // (1)
// Multiply by 2
decorator(next -> ctx -> {
Number n = (Number) next.apply(ctx);
return 2 * n.intValue();
});
get("/4", ctx -> 4); // (2)
});
get("/1", ctx -> 1); // (3)
}{
// Increment +1
decorator {
val n = next.apply(ctx) as Int
return 1 + n
}
route { // (1)
// Multiply by 2
decorator {
val n = next.apply(ctx) as Int
1 + n
}
get("/4") { 4 } // (2)
}
get("/3") { 3 } // (3)
}Output:
-
Introduce a new scope via
routeoperator -
/4⇒9 -
/1⇒2
It is a normal decorator 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.
{
route(() -> {
get("/", ctx -> "Hello");
});
}{
route {
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/application into a new one.
Composition is available through the javadoc:Router[use, io.jooby.Router] operator:
public class Foo extends Jooby {
{
get("/foo", Context::pathString);
}
}
public class Bar extends Jooby {
{
get("/bar", Context::pathString);
}
}
public class App extends Jooby {
{
use(new Foo()); // (1)
use(new Bar()); // (2)
get("/app", Context::pathString); // (3)
}
}class Foo: Kooby({
get("/foo") { ctx.getRequestPath() }
})
class Bar: Kooby({
get("/bar") { ctx.getRequestPath() }
})
class App: Kooby({
use(Foo()) // (1)
use(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::pathString);
}
}
public class App extends Jooby {
{
use("/prefix", new Foo()); // (1)
}
}class Foo: Kooby({
get("/foo") { ctx.getRequestPath() }
})
class App: Kooby({
use("/prefix", Foo()) // (1)
})-
Now all routes from
Foowill be prefixed with/prefix. Output:/prefix/foo⇒/prefix/foo
|
Tip
|
Composition is a great option for modularization. You can easily develop/test/deploy each application independently and compose them all in another application. We do provide MVC API as another alternative for modularization. |
Dynamic routing is looks similar to composition but enabled/disabled routes at runtime
using a predicate.
Suppose you own two version of an API and for some time you need to support both: old and new API:
public class V1 extends Jooby {
{
get("/api", ctx -> "v1");
}
}
public class V2 extends Jooby {
{
get("/api", ctx -> "v2");
}
}
public class App extends Jooby {
{
use(ctx -> ctx.header("version").value().equals("v1"), new V1()); // (1)
use(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({
use(ctx -> ctx.header("version").value().equals("v1"), V1()); // (1)
use(ctx -> ctx.header("version").value().equals("v2"), V2()); // (2)
})Output:
-
/api⇒v1; whenversionheader isv1 -
/api⇒v2; whenversionheader isv2
Done {love}!