Javalin controllers

javalin-generator is an APT processor that allows us to write controllers with annotations like @Path and @Get

Goals

  • Provide a similar programming style to JAX-RS and Spring
  • Add no extra weight. We generate source code - no reflection, no extra overhead
  • Generate code that we would otherwise write ourselves
  • Controllers should be nice and readable - not overly littered with annotations
  • Expose and use Javalin Context as needed
  • Automatically generate Swagger/OpenAPI documentation
  • Support using Bean validation on request payloads

In general our controllers should be more testable with dependencies that can be mocked or stubbed and with controller methods having less dependency on 'web constructs' like path, query and form parameters (we can more easily unit test controllers).

Examples

Example projects found in dinject/examples for Gradle and Maven for both Java and Kotlin.

Introduction

Introduction to using javalin-generator to generate web routes for Javalin.

Example

For the controller below:

@Controller
@Path("/customers")
class CustomerController {

  final MyDependency myDependency;

  CustomerController(MyDependency myDependency) {
    this.myDependency = myDependency;
  }

  @Get(":id")
  Customer getById(UUID id) {
    ...
  }

  @Roles(AppRoles.ADMIN)
  @Get("/type/:type")
  List<Customer> findByType(String type, String orderBy) {
    ...
  }
}

The javalin-generator generates the code below.

  • A bean annotated with @Singleton (A) ... picked up for Dependency injection
  • A bean that implements WebRoutes (B) ... so we can register them all with Javalin
  • Registers the web routes to Javalin ApiBuilder (C)
  • Obtains path parameters and query parameters (D)
  • Passes the path and query parameters to the controller methods (E)
  • Converts to and from JSON the controller method response or body (F)
  • Adapter @Roles for the web route (G)
@Generated("io.dinject.javalin.generator")
@Singleton                                                       // (A)
public class CustomerController$route implements WebRoutes {     // (B)

 private final CustomerController controller;

 public CustomerController$route(CustomerController controller) {
   this.controller = controller;
 }

  @Override
  public void registerRoutes() {

    ApiBuilder.get("/customers/:id", ctx -> {                   // (C)
      ctx.status(200);
      UUID id = asUUID(ctx.pathParam("id"));                    // (D)
      ctx.json(controller.getById(id));                         // (E), (F)
    });

    ApiBuilder.get("/customers/type/:type", ctx -> {            // (C)
      ctx.status(200);
      String type = ctx.pathParam("type");                      // (D)
      String orderBy = ctx.queryParam("orderBy");               // (D)
      ctx.json(controller.findByType(type, orderBy));           // (E), (F)
    }, roles(ADMIN));                                           // (E)

  }

}

As the generated bean is a @Singleton DInject puts it into the DI context. All these generated beans implement WebRoutes so we can get them all out of the DI context to register them with Javalin.

When can register all the WebRoutes with Javalin.

public static void main(String[] args) {

  Javalin app = Javalin.create().disableStartupBanner();

  // get all WebRoutes from DI Context
  List<WebRoutes> webRoutes = SystemContext.getBeans(WebRoutes.class);

  // register all WebRoutes with Javalin
  app.routes(() -> webRoutes.forEach(WebRoutes::registerRoutes));

  ...

  // start Javalin
  app.start(8080);
}

Dependency injection

The natural way to use the generated "Javalin controller adapters" is to get a DI library to find and wire them. This is what the examples do and they use DInject to do this.

Note that there isn't a strict requirement to use DInject for dependency injection. Any DI library that can find and wire the @Singleton bean generated could be used. We could use Dagger2 or Guice to wire the controllers if we felt that was a better option (but yes I think DInject is the better option for DI and that is what I'd be recommending).