Javalin controllers

DInject provides dependency injection which is good but when using Javalin we want a little more.

The javalin-generator is an APT processor that allows us to write controllers with annotations like @Path and @Get etc and the javalin-generator will generate code that adapts Javalin Web route to the controller methods

Examples

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

Goals

  • Generate code that we would otherwise write ourselves
  • Add no extra weight. We generate source code - no reflection, no extra overhead
  • Controllers should be nice and readable - not overly littered with annotations
  • Expose and use Javalin Context as much we like

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)
  • A bean that implements WebRoutes (B)
  • 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);
}

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.