Skip to content

Latest commit

 

History

History
895 lines (693 loc) · 19.8 KB

File metadata and controls

895 lines (693 loc) · 19.8 KB

MVC API

The MVC API provides an annotation-driven alternative to the Script API for defining routes. Jooby uses an annotation processor to generate source code that defines and executes these routes. By default, the generated classes are suffixed with an underscore (_).

If you use Gradle 6.0 or later, or a modern Maven setup, Jooby leverages incremental annotation processing. This means the compiler only processes classes that have changed since the last build, significantly speeding up compilation times.

You can control incremental processing via compiler arguments:

build.gradle
tasks.withType(JavaCompile) {
    options.compilerArgs += [
        '-parameters',
        '-Ajooby.incremental=true'
    ]
}
Kotlin (kapt)
kapt {
  arguments {
    arg('jooby.incremental', true)
  }
}

By setting jooby.incremental=false, you disable incremental processing entirely, forcing a full recompilation of the project every time. (Defaults to true).

The io.jooby.annotation package contains all the annotations available for MVC routes.

MVC API Example:
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);
  }
}
Kotlin
import io.jooby.annotation.*
import io.jooby.kt.runApp

@Path("/mvc")                  // (1)
class Controller {

  @GET                         // (2)
  fun sayHi(): String {
    return "Hello Mvc!"
  }
}

fun main(args: Array<String>) {
  runApp(args) {
    mvc(Controller_())        // (3)
  }
}
  1. Set a base path pattern. The @Path annotation can be applied at the class or method level.

  2. Define the HTTP method.

  3. Register the generated controller (Controller_) in the main application.

Getting Started

To quickly create a new MVC project, use the jooby console:

jooby create myapp --mvc

The Jooby CLI automatically configures the Maven/Gradle build and sets up the annotation processor for you.

Registration

Unlike some frameworks, Jooby does not use classpath scanning. MVC routes must be explicitly registered in your application configuration:

Simple MVC Route Registration
public class App extends Jooby {
  {
    mvc(new MyController_());
  }

  public static void main(String[] args) {
    runApp(args, App::new);
  }
}
Kotlin
import io.jooby.kt.runApp

fun main(args: Array<String>) {
  runApp(args) {
    mvc(MyController_())
  }
}

The javadoc:Jooby[mvc, io.jooby.MvcExtension] method installs the MVC routes. You can pass an instance directly, or if the controller constructor is annotated with @Inject (e.g., jakarta.inject.Inject), the generated code will attempt to resolve the dependencies from the registry.

Parameters

HTTP parameter extraction is handled via @*Param annotations.

You can also use the generic javadoc:annotation.Param[] annotation to extract a parameter from multiple sources with a specific fallback order.

Header

Extract headers using the javadoc:annotation.HeaderParam[] annotation:

Headers
public class MyController {

  @GET
  public String handle(@HeaderParam String token) {  // (1)
    // ...
  }
}
Kotlin
class MyController {

  @GET
  fun handle(@HeaderParam token: String): String {  // (1)
    // ...
  }
}
  1. Accesses the HTTP header named token.

Unlike JAX-RS, specifying the parameter name inside the annotation is optional. Jooby infers it from the variable name. However, you must provide the name explicitly if the HTTP header is not a valid Java identifier:

Invalid Java Identifier
public class MyController {

  @GET
  public String handle(@HeaderParam("Last-Modified-Since") long lastModifiedSince) {
    // ...
  }
}
Kotlin
class MyController {

  @GET
  fun handle(@HeaderParam("Last-Modified-Since") lastModifiedSince: Long): String {
    // ...
  }
}

Extract cookies using the javadoc:annotation.CookieParam[] annotation:

Cookies
public class MyController {

  @GET
  public String handle(@CookieParam String token) {  // (1)
    // ...
  }
}
Kotlin
class MyController {

  @GET
  fun handle(@CookieParam token: String): String {  // (1)
    // ...
  }
}
  1. Accesses the cookie named token.

As with headers, provide the explicit name if the cookie key contains dashes or invalid Java characters (e.g., @CookieParam("token-id") String tokenId).

Path

Extract path variables using the javadoc:annotation.PathParam[] annotation:

PathParam
public class MyController {

  @Path("/{id}")
  public String handle(@PathParam String id) {
    // ...
  }
}
Kotlin
class MyController {

  @Path("/{id}")
  fun handle(@PathParam id: String): String {
    // ...
  }
}
Query

Extract query string variables using the javadoc:annotation.QueryParam[] annotation:

QueryParam
public class MyController {

  @Path("/")
  public String handle(@QueryParam String q) {
    // ...
  }
}
Kotlin
class MyController {

  @Path("/")
  fun handle(@QueryParam q: String): String {
    // ...
  }
}
Formdata / Multipart

Extract form-data or multipart parameters using the javadoc:annotation.FormParam[] annotation:

FormParam
public class MyController {

  @POST("/")
  public String handle(@FormParam String username) {
    // ...
  }
}
Kotlin
class MyController {

  @POST("/")
  fun handle(@FormParam username: String): String {
    // ...
  }
}
Request Body

The HTTP request body does not require an explicit annotation. Simply define the POJO in the method signature:

HTTP Body
public class MyController {

  @POST("/")
  public String handle(MyObject body) {
    // ...
  }
}
Kotlin
class MyController {

  @POST("/")
  fun handle(body: MyObject): String {
    // ...
  }
}
Bind

The javadoc:annotation.BindParam[] annotation allows for custom data binding from the HTTP request directly into an object.

Using the annotation
public class Controller {

  @GET("/{foo}")
  public String bind(@BindParam MyBean bean) {
    return "with custom mapping: " + bean;
  }
}
Kotlin
class Controller {

  @GET("/{foo}")
  fun bind(@BindParam bean: MyBean) = "with custom mapping: $bean"
}
Writing the mapping function
public record MyBean(String value) {

  public static MyBean of(Context ctx) {
    // Build MyBean entirely from the Context
    return new MyBean(ctx.path("foo").value());
  }
}
Kotlin
class MyBean(val value: String) {

  companion object {
       @JvmStatic
       fun of(ctx: Context): MyBean {
          // Build MyBean entirely from the Context
          return MyBean(ctx.path("foo").value())
       }
  }
}

How @BindParam works:

  • It looks for a public method/function on the target class that accepts a javadoc:Context[] and returns the target type.

  • By default, it looks for this factory method on the parameter type itself (MyBean), but will fall back to searching the Controller class.

Alternatively, you can specify a distinct factory class and/or method name:

@BindParam(MyFactoryClass.class)
@BindParam(value = MyFactoryClass.class, fn = "fromContext")
Flash

Extract flash attributes using the javadoc:annotation.FlashParam[] annotation:

Flash
public class MyController {

  @GET
  public String handle(@FlashParam String success) {  // (1)
    // ...
  }
}
Kotlin
class MyController {

  @GET
  fun handle(@FlashParam success: String): String {  // (1)
    // ...
  }
}
  1. Accesses the flash attribute named success.

Session

Extract specific session attributes using the javadoc:annotation.SessionParam[] annotation:

Session Attribute
public class MyController {

  @GET
  public String handle(@SessionParam String userId) {  // (1)
    // ...
  }
}
Kotlin
class MyController {

  @GET
  fun handle(@SessionParam userId: String): String {  // (1)
    // ...
  }
}
  1. Accesses the session attribute named userId.

You can also request the entire javadoc:Session[] object:

Session Object
public class MyController {

  @GET
  public String handle(Session session) {  // (1)
    // ...
  }
}
Kotlin
class MyController {

  @GET
  fun handle(session: Session): String {  // (1)
    // ...
  }
}
  1. If no session exists yet, a new session will be created. To avoid this and only retrieve an existing session, use Optional<Session> as the parameter type.

Context

Extract specific context attributes using the javadoc:annotation.ContextParam[] annotation:

Context Attribute
public class MyController {

  @GET
  public String handle(@ContextParam String userId) {  // (1)
    // ...
  }
}
Kotlin
class MyController {

  @GET
  fun handle(@ContextParam userId: String): String {  // (1)
    // ...
  }
}
  1. Accesses the context attribute named userId.

You can also request all javadoc:Context[getAttributes, text="attributes"] at once:

All Context Attributes
public class MyController {

  @GET
  public String handle(@ContextParam Map<String, Object> attributes) {  // (1)
    // ...
  }
}
Kotlin
class MyController {

  @GET
  fun handle(@ContextParam attributes: Map<String, Any>): String {  // (1)
    // ...
  }
}
  1. To retrieve all context attributes, the parameter must be typed as a Map<String, Object> (or Map<String, Any> in Kotlin).

Multiple Sources

Use the javadoc:annotation.Param[] annotation to search for a parameter across multiple sources with an explicitly defined fallback order:

Multiple Sources
import static io.jooby.annotation.ParamSource.QUERY;
import static io.jooby.annotation.ParamSource.PATH;

public class FooController {

  @GET("/{foo}")
  public String multipleSources(@Param({ QUERY, PATH }) String foo) {
    return "foo is: " + foo;
  }
}
Kotlin
import io.jooby.annotation.ParamSource.QUERY
import io.jooby.annotation.ParamSource.PATH

class FooController {

  @GET("/{foo}")
  fun multipleSources(@Param(QUERY, PATH) foo: String) = "foo is: $foo"
}

If a request is made to /bar?foo=baz, the result will be foo is: baz because the QUERY parameter takes precedence over the PATH parameter in the annotation array.

Responses

Projections

The MVC module provides first-class support for Projections via annotations. This allows you to define the response view declaratively, keeping your controller logic clean and focused on data retrieval.

Usage

There are two ways to define a projection in an MVC controller.

You can annotate your method with @Project and provide the selection DSL:

Via @Project Annotation
@GET
@Project("(id, name)")
public List<User> listUsers() {
  return service.findUsers();
}

Alternatively, you can define the projection directly within the HTTP method annotation (e.g., @GET, @POST) using the projection attribute:

Via HTTP Method Attribute
@GET(value = "/users", projection = "(id, name, email)")
public List<User> listUsers() {
  return service.findUsers();
}
Automatic Wrapping

The Jooby Annotation Processor automatically handles the conversion of your method’s return type. You are not forced to return a Projected instance; you can simply return your POJO or Collection, and Jooby will wrap it for you at compile-time.

However, if you need manual control (for example, to dynamically toggle validation), you can still return a Projected instance explicitly:

@GET
public Projected<User> getUser(String id) {
  User user = service.findById(id);
  return Projected.wrap(user)
      .failOnMissingProperty(true)
      .include("(id, status)");
}
Note

For more details on the Selection DSL syntax and available JSON engines, please refer to the Core Projections documentation.

Status Code

The default HTTP status code returned by an MVC route is 200 OK, except for void methods annotated with @DELETE, which automatically return 204 No Content.

If you need to return a different status code, you have two options: 1. Inject the javadoc:Context[] into your method and call javadoc:Context[setResponseCode, io.jooby.StatusCode]. 2. Return a javadoc:StatusCode[] instance directly from the method.

NonBlocking

Any MVC method returning a non-blocking type (CompletableFuture, Single, Maybe, Flowable, Mono, Flux) is automatically handled as a non-blocking route.

Kotlin suspend functions are also supported natively:

Kotlin Coroutines
class SuspendMvc {
  @GET
  @Path("/delay")
  suspend fun delayed(ctx: Context): String {
    delay(100)
    return ctx.getRequestPath()
  }
}

fun main(args: Array<String>) {
  runApp(args) {
    mvc(SuspendMvc_())
  }
}

A non-blocking route runs on the event loop by default, where blocking is NOT allowed. For more details, see the NonBlocking Responses section.

Execution Model

MVC routes follow the standard Jooby Execution Model.

By default, if your route returns a blocking type (like a String or a POJO), Jooby automatically dispatches the execution to the worker executor. If it returns a non-blocking type (or is a suspend function), it runs on the event loop.

If you need explicit control over where a specific blocking MVC route executes, use the javadoc:annotation.Dispatch[] annotation:

Dispatch Annotation
public class MyController {

  @GET("/blocking")
  @Dispatch              // (1)
  public String blocking() {
    return "I'm blocking";
  }
}
Kotlin
import io.jooby.annotation.*

class MyController {

  @GET("/blocking")
  @Dispatch              // (1)
  fun blocking(): String {
    return "I'm blocking"
  }
}
  1. Forces the route to run in the WORKER executor, safely allowing blocking calls.

The javadoc:annotation.Dispatch[] annotation also supports routing execution to a named, custom executor:

Dispatch to custom executor
public class MyController {

  @GET("/blocking")
  @Dispatch("single")         // (1)
  public String blocking() {
    return "I'm blocking";
  }
}
Kotlin
import io.jooby.annotation.*

class MyController {

  @GET("/blocking")
  @Dispatch("single")         // (1)
  fun blocking(): String {
    return "I'm blocking"
  }
}
  1. Dispatches execution to the executor registered under the name single.

The custom executor must be registered in the application before the MVC route is registered:

Custom executor registration
{
  executor("single", Executors.newSingleThreadExecutor());

  mvc(new MyController_());
}
Kotlin
{
  executor("single", Executors.newSingleThreadExecutor())

  mvc(MyController_())
}

JAX-RS Annotations

Alternatively, you can use JAX-RS annotations to define MVC routes.

Resource
import javax.ws.rs.GET;
import javax.ws.rs.Path;

@Path("/jaxrs")
public class Resource {

  @GET
  public String getIt() {
    return "Got it!";
  }
}
Kotlin
import javax.ws.rs.GET
import javax.ws.rs.Path

@Path("/jaxrs")
class Resource {

  @GET
  fun getIt(): String {
    return "Got it!"
  }
}

These annotations work exactly like the Jooby native MVC annotations.

(Note: Jooby does not implement the full JAX-RS specification, nor is there a plan to do so. Support for these annotations exists primarily to allow integration with third-party tools, like Swagger/OpenAPI generators, that rely on them).

Generated Router

For each MVC controller class, a new class is generated ending with an underscore (_). This generated class mimics the constructors of the source class. (If the constructor is annotated with @Inject, a default constructor is automatically generated).

Any annotations found on the controller methods will be persisted as route attributes, unless explicitly excluded by the jooby.skipAttributeAnnotations compiler option.

You can access the generated routes at runtime:

{
  var routes = mvc(new MyController_());
  routes.forEach(route -> {
    // Modify or inspect the route
  });
}

Annotation Processor Options

Option Type Default Description

jooby.debug

boolean

true

Runs the annotation processor in debug mode.

jooby.incremental

boolean

true

Hints to Maven/Gradle to perform incremental compilation. Essential for fast development iteration.

jooby.skipAttributeAnnotations

array

[]

A comma-separated list of annotations to skip during bytecode generation (i.e., do not attach them as route attributes).

jooby.mvcMethod

boolean

false

Sets the Route.mvcMethod property on the generated route when true.

jooby.routerPrefix

string

Adds a prefix to the generated class name.

jooby.routerSuffix

string

_

Sets the suffix for the generated class name.

Setting Options
Maven
  <plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
      <annotationProcessorPaths>
        <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.skipAttributeAnnotations=FooAnnotation,BarAnnotation</compilerArg>
      </compilerArgs>
    </configuration>
  </plugin>
Gradle
tasks.withType(JavaCompile) {
    options.compilerArgs += [
        '-parameters',
        '-Ajooby.debug=false',
        '-Ajooby.incremental=true',
        '-Ajooby.skipAttributeAnnotations=FooAnnotation,BarAnnotation'
    ]
}
Important

The execution order of annotation processors is critical. If you are using Lombok and Avaje Inject alongside Jooby, the configuration order must be: lombokavaje-injectjooby-apt.