Avaje Http servers & client

The goal is to be better than JAX-RS by using source code generation (via Java annotation processing).

License Source API Docs Issues Releases
Apache2 Github Javadoc Github Latest 1.0

 

Better in terms of being much lighter and faster at runtime by using source code generation (java annotation processors) to adapt annotated rest controllers with (@Path, @Get, @Post etc) to Javalin, Helidon SE and similar http servers.

Effectively we are replacing Jersey or RestEasy with source code generation and the capabilities of Javalin and Helidon SE.

Better also in terms of being simpler when compared with the internals of Jersey or RestEasy. It turns out we don't need to generate much code at all and that the generated code is very simple, readable and of course developers can navigate to it, add break points and debug just as if we wrote it all manually ourselves.

What we lose in doing this is built in Content negotiation. For example, if we need endpoints that serve response content as either JSON or XML content based on request headers then we would to handle this ourselves.

Summary

JAX-RS Annotations

As we are using Java annotation processing we our generators are exposed to more information than is obtained via reflection at runtime. This means we can reduce annotation verbosity making nicer cleaner API's.

For a comparison with JAX-RS goto here.

A design decision has been to not use JAX-RS annotations at this stage. This is because the JAX-RS annotations are a lot more verbose than we desire and because they are not provided as a nice clean separate dependency. The JAX-RS API has a lot of stuff that we do not need.

Dependencies

Maven

Add avaje-inject and avaje-http-api dependencies.

<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-inject</artifactId>
  <version>1.1</version>
</dependency>

<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-http-api</artifactId>
  <version>1.0</version>
</dependency>

Annotation processors

Add the annotation processors avaje-inject-generator and either avaje-http-javalin-generator to target Javalin or avaje-http-helidon-generator to target Helidon SE.

We typically use provided scope for annotation processors.

<!-- Annotation processors -->
<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-inject-generator</artifactId>
  <version>1.1</version>
  <scope>provided</scope>
</dependency>
<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-http-javalin-generator</artifactId>
  <version>1.0</version>
  <scope>provided</scope>
</dependency>

Note that if there are other annotation processors and they are specified via maven-compiler-plugin annotationProcessorPaths then we add avaje-http-javalin-generator there instead.

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <configuration>
    <annotationProcessorPaths> <!-- All annotation processors specified here -->
      <path>
        <groupId>io.avaje</groupId>
        <artifactId>avaje-inject-generator</artifactId>
        <version>1.1</version>
      </path>
      <path>
        <groupId>io.avaje</groupId>
        <artifactId>avaje-http-javalin-generator</artifactId>
        <version>1.0</version>
      </path>
      <path>
          ... other annotation processor ...
      </path>
    </annotationProcessorPaths>
  </configuration>
</plugin>

Gradle

See the example at: examples/javalin-gradle-java-basic/build.gradle

Use Gradle version 5.2 or greater which has better support for annotation processing.

Also review the IntelliJ IDEA Gradle settings - see below.

Optional: OpenAPI plugin

Optionally add the io.dinject.openapi plugin to have the openapi.json (swagger) to be generated into src/main/resources/public.

plugins {
  ...
  id('io.dinject.openapi') version('1.2')
}

Dependencies

Add avaje-inject and avaje-http-api as compile dependencies.
Add avaje-http-javalin-generator and javalin-generator as annotation processors.

dependencies {
  ...
  compile('io.avaje:avaje-inject:1.1')
  compile('io.avaje:avaje-http-api:1.0')

  annotationProcessor('io.avaje:avaje-inject-generator:1.1')
  annotationProcessor('io.avaje:avaje-http-javalin-generator:1.0')
}

Kotlin KAPT

For use with Kotlin we change the annotationProcessor to be kapt for the Kotlin compiler.

dependencies {
  ...
  kapt('io.avaje:avaje-inject-generator:1.1')
  kapt('io.avaje:avaje-http-javalin-generator:1.0')
}

OpenAPI Plugin configuration

We can change the location of the generated openapi file by adding an openapi configuration section in build.gradle.

openapi {
  destination = 'other/my-api.json'
}

IntelliJ IDEA with Gradle

We want to delegate the build to Gradle (to properly include the annotation processing) so check our IDEA settings.

Settings / Build / Compiler / Annotation processors

Ensure that Enable annotation processing is disabled so that the build is delegated to Gradle (including the annotation processing):

Settings / Build / Build tools / Gradle

Make sure Build and run is delegated to Gradle.

Optionally set Run tests using to Gradle but leaving it to IntelliJ IDEA should be ok.

Controllers

@Controller
@Path("/contacts")
class ContactController {

  @Get("/{id}")
  Contact getById(long id) {
    ...
  }

  @Post
  void save(Contact contact) {
    ...
  }

  @Delete("/{id}")
  void deleteById(long id) {
    ...
  }
  ...
}

@Controller

Create controllers with @Path and @Controller. @Path("...") provides the path segment that is prepended to any path segments defined by on methods using @Get, @Post, @Put etc.

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

Methods on the controller that are annotated with @Get, @Post, @Put, @Delete matching HTTP verbs.

@Controller
@Path("/contacts")
class ContactController {

  private final ContactService contactService;

  @Inject
  ContactController(ContactService contactService) {
    this.contactService = contactService;
  }

  @Get("/{id}")
  Contact getById(long id) {
    ...
  }

  @Get("/find/{type}")
  List<Contact> findByType(String type, @QueryParam String lastName) {
    ...
  }

  @Post
  void save(Contact contact) {
    ...
  }

  ...
}

The controllers can have dependencies injected. The ContactController above will have the ContactService dependency injected by avaje-inject.

Controllers are singleton scoped by default

By default controllers are singleton scoped. If the controllers have a dependency on Javalin context, Helidon ServerRequest or ServerResponse then they automatically become request scoped.

@Path

@Path is put on the controller class. The path is prepended to the paths specified by @Get, @Post etc.

@Path("/") is used for the root context path.

Example

The URI's for the RootController below would be:

GET /
GET /foo
@Controller
@Path("/")
class RootController {

  @Produces(MediaType.TEXT_PLAIN)
  @Get
  String hello() {
    return "Hello world";
  }

  @Produces(MediaType.TEXT_PLAIN)
  @Get("foo")
  String helloFoo() {
    return "Hello Foo";
  }

}

The URI's for the CustomerController below are:

GET /customer
GET /customer/active
GET /customer/active/{customerType}
@Controller
@Path("/customer")
class CustomerController {

  @Get
  List<Customer> findAll() {
    ...
  }

  @Get("/active")
  List<Customer> findActive() {
    ...
  }

  @Get("/active/{customerType}")
  List<Customer> findByType(String customerType) {
    ...
  }
}

Path parameters

Path parameters start with { and end with }.

For example {id}, {name}, {startDate}.

The path parameter names need to be matched by method parameter names on the controller. For example:

@Get("/{id}/{startDate}/{type}")
List<Bazz> findBazz(long id, LocalDate startDate, String type) {

  // id, startDate, type all match method parameter names
  ...
}

This means that unlike JAX-RS we do not need a @PathParam annotation and this makes our code less verbose and nicer to read.

Note that the JAX-RS equivalent to the above is below. The method declaration starts to get long and harder to read quite quickly.

// JAX-RS "annotation noise" with @PathParam

@GET
@Path("/{id}/{startDate}/{sort}")
List<Bazz> findBazz(@PathParam("id") long id, @PathParam("startDate") LocalDate startDate, @PathParam("sort") String sort) {

  // we start getting "annotation noise" ...
  // making the code hard to read

}

Matrix parameters

Matrix parameters are optional sub-parameters that relate to a specific segment of the path. They are effectively an alternative to using query parameters where we have optional parameters that relate to a specific path segment.

// 'type' path segment has matrix parameters 'category' and 'vendor'

@Get("/products/{type;category;vendor}/available")
List<Product> products(String type, String category, String vendor) {
  ...
}
// example URI's

GET /products/chair/available
GET /products/chair;category=kitchen/available
GET /products/chair;category=kitchen;vendor=jfk/available
// 'type' has matrix parameters 'category' and 'vendor'
// 'range' has matrix parameter 'style'

@Get("/products/{type;category;vendor}/{range;style}")
List<Product> products(String type, String category, String vendor, String range, String style) {
  ...
}
// example URI's

GET /products/chair/commercial
GET /products/chair;category=kitchen/domestic
GET /products/chair;category=kitchen/commercial;style=contemporary
GET /products/chair/commercial;style=classical

JAX-RS @MatrixParam

Our matrix parameters are equivalent to JAX-RS except that they relate by convention to method parameters of the same name and we do not need explicit @MatrixParam.

The JAX-RS equivalent to the above is below. The method declaration starts to get long and harder to read quite quickly.

// JAX-RS "annotation noise" with @MatrixParam and @PathParam

@GET
@Path("/products/{type;category;vendor}/{range;style}")
List<Product> products(@PathParam("type") String type, @MatrixParam("category") String category, @MatrixParam("vendor") String vendor, @PathParam("type") String range, @MatrixParam("style") String style) {

  // we start getting "annotation noise" ...
  // making the code hard to read
  ...
}

@QueryParam

We explicitly specify query parameters using @QueryParam.

// Explicit query parameter order-by

@Get("/{bornAfter}")
List<Cat> findCats(LocalDate bornAfter, @QueryParam("order-by") String orderBy) {
  ...
}

Implied query parameters

Query parameters can be implied by not being a path parameter. That is, if a method parameter does not match a path parameter then it is implied to be a query parameter.

The following 3 declarations are exactly the same with all 3 having a query parameters for orderBy

@Get("/{bornAfter}")
List<Cat> findCats(LocalDate bornAfter, @QueryParam("orderBy") String orderBy) {
  ...
}

@Get("/{bornAfter}")
List<Cat> findCats(LocalDate bornAfter, @QueryParam String orderBy) {
  ...
}

@Get("/{bornAfter}")
List<Cat> findCats(LocalDate bornAfter, String orderBy) {  // orderBy implied as query parameter
  ...
}

Note that we must explicitly use @QueryParam when the query parameter is not a valid java identifier. For example, if the query parameter includes a hyphen then we must use @QueryParam explicitly.

Example

We must use an explicit @QueryParam when the parameter name includes a hyphen like order-by.

// order-by is not a valid java identifier
// ... so we must use explicit @QueryParam here

@Get
List<Cat> findCats(@QueryParam("order-by") String orderBy) {
  ...
}

Query parameter types

Query parameters can be one of the following types: String, Integer, Long, Short, Float, Double, Boolean, BigDecimal, UUID, LocalDate, LocalTime or LocalDateTime

Query parameters are considered optional / nullable.

@Default

We can use @Default to specify a default value for a query parameter.
// orderBy defaults to "age"

@Get("{bornAfter}")
List<Cat> findCats(LocalDate bornAfter, @Default("age") String orderBy) {
  ...
}

@BeanParam

When we have a lot of query parameters or if we have query parameters that are common to many endpoints then we look to use @BeanParam.

We can create a bean and all the properties on the bean default to being query parameters and annotate this with @BeanParam.

public class CommonParams {

  public Long firstRow;
  public Long maxRows;
  public String sortBy;
  public String filter;
}

We annotate the bean with @BeanParam

@Get("search/{type}")
List<Cat> findCats(String type, @BeanParam CommonParams params) {

  ...
}

The generated Javalin code for the above is:

ApiBuilder.get("/cats/search/:type", ctx -> {
  ctx.status(200);
  String type = ctx.pathParam("type");
  CommonParams params =  new CommonParams();
  params.firstRow = toLong(ctx.queryParam("firstRow"));
  params.maxRows = toLong(ctx.queryParam("maxRows"));
  params.sortBy = ctx.queryParam("sortBy");
  params.filter = ctx.queryParam("filter");

  ctx.json(controller.findCats(type, params));
});

JAX-RS @BeanParam

Our @BeanParam is virtually the same as JAX-RS @BeanParam except the properties default to being query parameters where as with JAX-RS we need to annotate each of the properties. We can do this because we also have @Form and "Form beans".

BeanParam with @Header @Cookie properties

The properties on a "bean" default to being query parameters. We put @Header or @Cookie on properties that are instead headers or cookies.

public class CommonParams {

  public Long firstRow;
  public Long maxRows;
  public String sortBy;
  public String filter

  @Header
  public String ifModifiedSince;

  @Cookie
  public String myState;
}

@Post

Annotate methods with @Post for HTTP POST web routes. The Post request takes a body and by default that is expected to be in JSON form.

Post JSON

A method with @Post is by default expecting a JSON body.

@Post
void save(Customer customer) {
  ...
}

Generated for Javalin

The code generated code for Javalin for save() above is:

ApiBuilder.post("/customers", ctx -> {
  ctx.status(201);
  Customer customer = ctx.bodyAsClass(Customer.class);
  controller.save(customer);
});

@Form

If a method has both @Post and @Form then the method parameters default to be form parameters.

In the following example name, email and url all default to be form parameters.

@Form @Post("register")
void register(String name, String email, String url) {
  ...
}

@FormParam

For the example above we could alternatively use explicit @FormParam on each of the form parameters rather than @Form. We then get:

@Post("register")
void register(@FormParam String name, @FormParam String email, @FormParam String url) {
  ...
}

The expectation is that we most often would use @Form because it reduces "annotation noise" and that we will very rarely use @FormParam. Potentially we only use @FormParam if the parameter name includes hyphen or similar characters that are not valid Java/Kotlin identifiers.

Generated for Javalin

The generated Javalin code for both cases above is the same:

ApiBuilder.post("/customers/register", ctx -> {
  ctx.status(201);
  String name = ctx.formParam("name");
  String email = ctx.formParam("email");
  String url = ctx.formParam("url");
  controller.register(name, email, url);
});

@Default

We can use @Default to specify a default value for form parameters.

@Form @Post("register")
void register(String name, String email, @Default("http://localhost") String url) {
  ...
}

@Form "Form Beans"

In the case where we are posting a form with a lot of parameters we can define a bean with properties for each of the form parameters rather than have a method with lots of arguments.

"Form beans" can have a constructor with arguments. They do not require a no-arg constructor.

Using a form bean can make the code nicer and gives us a nicer option to use validation annotations on the "form bean" properties.

public class MyForm {

  @Size(min=2, max=100)
  public String name;
  public String email;
  public String url;
}
@Form @Post("register")
void register(MyForm myForm) {
  ...
}
The generated Javalin code for the above is.
ApiBuilder.post("/contacts/register", ctx -> {
  ctx.status(201);
  MyForm myForm =  new MyForm();
  myForm.name = ctx.formParam("name");
  myForm.email = ctx.formParam("email");
  myForm.url = ctx.formParam("url");

  controller.register(myForm);
});

"Form beans" are nice with forms with lots of properties because they de-clutter our code and the generated code takes care of putting the values into our bean properties so that we don't have to write that code.

This use of @Form is very similar to JAX-RS @BeanParam except that the bean properties default be being form parameters. With JAX-RS we would put a @FormParam on every property that is a form parameter which becomes a lot of annotation noise on a large form.

Kotlin data class

Kotlin data classes are a natural fit for form beans.

data class SaveForm(var id: Long, var name: String, var someDate: LocalDate?)


@Form @Post
fun saveIt(form: SaveForm) {

  ...
}

The generated code for the above controller method is:

ApiBuilder.post("/", ctx -> {
  ctx.status(201);
  SaveForm form =  new SaveForm(
    asLong(checkNull(ctx.formParam("id"), "id")),     // non-nullable type
    checkNull(ctx.formParam("name"), "name"),         // non-nullable type
    toLocalDate(ctx.formParam("someDate"))
  );

  controller.saveIt(form);
});

If the form bean has Kotlin non-nullable types (id and name above) then the generated code includes a null check when populating the bean (the checkNull() method).

If there is not a value for a non-nullable Kotlin property then a validation error will be thrown at that point (this validation exception is thrown relatively early compared to using bean validation on Java form beans).

Form beans with @QueryParam, @Header, @Cookie properties

The properties on a "form bean" default to being form parameters. We put @QueryParam, @Header or @Cookie on properties that are instead query params, headers or cookies.

public class MyForm {

  @Size(min=2, max=100)
  public String name;
  public String email;
  public String url;

  @QueryParam
  public Boolean overrideFlag;

  @Header
  public String ifModifiedSince;

  @Cookie
  public String myState;
}

The generated code populates from query params, headers and cookies. The generated code is:

ApiBuilder.post("/contacts/register", ctx -> {
  ctx.status(201);
  MyForm myForm =  new MyForm();
  myForm.name = ctx.formParam("name");
  myForm.email = ctx.formParam("email");
  myForm.url = ctx.formParam("url");
  myForm.overrideFlag = toBoolean(ctx.queryParam("overrideFlag"));     // queryParam !!
  myForm.ifModifiedSince = ctx.header("If-Modified-Since");            // header !!
  myForm.myState = ctx.cookie("myState");                              // cookie !!

  controller.register(myForm);
});

Use @Header for a header parameter. It the header parameter name is not explicitly specified then it is the init caps snake case of the parameter name.

userAgent -> User-Agent

lastModified -> Last-Modified

@Post
Bar postIt(Foo payload, @Header("User-Agent") String userAgent) { // explicit
  ...
}

@Post
Bar postIt(Foo payload, @Header String userAgent) { // User-Agent
  ...
}

@Get
Bazz find(@Header String lastModified) { // Last-Modified
  ...
}

Use @Cookie for a Cookie parameter.

@Post("bar/{name}")
Bar bar(String name, @Cookie("my-cookie") String myCookie) {
  ...
}

@Post("foo/{name}")
Foo foo(String name, @Cookie String myCookie) {
  ...
}

The generated Helidon code for the method above is:

private void _foo(ServerRequest req, ServerResponse res) {
  String name = req.path().param("name");
  String myCookie = req.headers().cookies().first("myCookie").orElse(null);
  res.send(controller.fooMe(name, myCookie));
}

@Get

Annotate methods with @Get for HTTP GET web routes.

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

  @Get
  List<Customer> getAll() {
    ...
  }

  @Get("/{id}")
  Customer getById(long id) {
    ...
  }

  @Get("/{id}/contacts")
  List<Contacts> getContacts(long id) {
    ...
  }

}

@Delete

Annotate methods with @Delete for HTTP DELETE web routes.

@Put

Annotate methods with @Put for HTTP PUT web routes.

@Patch

Annotate methods with @Patch for HTTP PUT web routes.

Context

Javalin Context

The Javalin Context can be passed as a method argument or injected as a dependency of the controller.

Context as method argument

@Post
void save(HelloDto dto, Context context) {
  // use Javalin context as desired
  ...
}

Helidon request/response

Helidon has ServerRequest and ServerResponse and these can be passed as a method argument of injected as a dependency of the controller.

ServerRequest/ServerResponse as method argument

@Post
void save(HelloDto dto, ServerRequest request, ServerResponse response) {
  // use Helidon server request or response as desired
  ...
}

Controllers are singleton scoped by default

By default controllers are singleton scoped. When we pass context objects like Javalin Context or Helidon ServerRequest as method arguments then the controllers remain singleton scoped.

Request scoped controllers

We can define the Javalin context, Helidon ServerRequest or ServerResponse as a dependency to be injected using constructor injection or field injection (rather than passed as a method argument).

avaje-inject knows that these types need to be injected per request and automatically makes the controller request scoped.

Request scoped means that a new instance of the controller will be instantiated for each request.

Example

The following ContactController has the Javalin Context as a constructor injected dependency. The controller is request scoped and instantiated per request.

// Automatically becomes request scoped
//  ... because Javalin Context is a dependency
//  ... controller instantiated per request

@Controller
@Path("/contacts")
class ContactController {

  private final ContactService contactService;

  private final Context context; // Javalin Context

  // Inject Javalin context via constructor
  @Inject
  ContactController(Context context, ContactService contactService) {
    this.context = context;
    this.contactService = contactService;
  }

  @Get("/{id}")
  Contact getById(long id) {
    // use the javalin context ...
    var fooCookie = context.cookieStore("foo");
    ...
  }
}
Example

In this example ProductController has the Helidon ServerRequest and ServerResponse injected using field injection rather than constructor injection. Note that when using field injection they can not be final and can not be private.

// Automatically becomes request scoped
//  ... because Helidon request and response are a dependency
//  ... controller instantiated per request

@Controller
@Path("/products")
class ProductController {

  private final MyService myService;

  @Inject
  ServerRequest request; // Helidon request field injected

  @Inject
  ServerRequest response; // Helidon response field injected

  @Inject
  ProductController(MyService myService) {
    this.myService = myService;
  }

  @Get("/{id}")
  Contact getById(long id) {
    // use the helidon request ...
    var fooCookie = request.headers().cookies().first("foo");
    ...
  }
}

Type conversions

There are built in type conversions for the following types:

In the following example there is a type conversion for startDate and active.

@Get("/{id}/{name}")
Hello hello(int id, String name, LocalDate startDate, Boolean active) {
  ...
}

For example, the Javalin generated code below includes the type conversion with toLocalDate() and toBoolean().

ApiBuilder.get("/hello/:id/:name", ctx -> {
  ctx.status(200);
  int id = asInt(ctx.pathParam("id"));
  String name = ctx.pathParam("name");
  LocalDate startDate = toLocalDate(ctx.queryParam("startDate"));
  Boolean active = toBoolean(ctx.queryParam("active"));
  ctx.json(controller.hello(id, name, startDate, active));
});

Exception handlers

If a parameter fails type conversion then InvalidPathArgumentException is thrown. This exception is typically mapped to a 404 response in the exception handler.

Note that path conversions imply the value can NOT be null. All other parameter types are considered optional/nullable.

InvalidTypeArgumentException is throw for non-path parameter conversions that fail such as conversions in form beans, query parameters, headers and cookies.

We should register exception handlers for these 2 exceptions like the handlers below:

app.exception(InvalidPathArgumentException.class, (exception, ctx) -> {

  Map<String, String> map = new LinkedHashMap<>();
  map.put("path", ctx.path());
  map.put("message", "invalid path argument");
  ctx.json(map);
  ctx.status(404);
});

app.exception(InvalidTypeArgumentException.class, (exception, ctx) -> {

  Map<String, String> map = new LinkedHashMap<>();
  map.put("path", ctx.path());
  map.put("message", "invalid type argument");
  ctx.json(map);
  ctx.status(400);
});

Bean validation

We can optionally add bean validation.

Example: HelloController

Add dependency

Add a dependency on avaje-http-hibernate-validator. This will transitively bring in a dependency on hibernate-validator.

<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-http-hibernate-validator</artifactId>
  <version>1.0</version>
</dependency>

Add @Valid

Add @Valid annotation on controllers that we want bean validation to be included for. When we do this controller methods that take a request payload will then have the request bean (populated by JSON payload or form parameters) validated before it is passed to the controller method.

For the controller method below:

@Valid
@Controller
@Path("/baz")
class BazController  {

  @Form
  @Post
  void saveForm(HelloForm helloForm) {
    ...
  }

The generated code now includes a validation of the helloForm before it is passed to the controller method. The generated code is:

ApiBuilder.post("/baz", ctx -> {
  ctx.status(201);
  HelloForm helloForm =  new HelloForm(
    ctx.formParam("name"),
    ctx.formParam("email")
  );
  helloForm.url = ctx.formParam("url");
  helloForm.startDate = toLocalDate(ctx.formParam("startDate"));

  validator.validate(helloForm);       // validation added here !!
  controller.saveForm(helloForm);
});

ValidationException handler

Add an exception handler for ValidationException like the one below. With bean validation we collect all the validation errors and these are included in the exception as a map keyed by the property path.

exception.getErrors() in the handler below is returning a Map<String, Object>

app.exception(ValidationException.class, (exception, ctx) -> {

  Map<String,Object> map = new LinkedHashMap<>();
  map.put("message", exception.getMessage());
  map.put("errors", exception.getErrors());
  ctx.json(map);
  ctx.status(exception.getStatus());
});

OpenAPI

An OpenAPI description of the API is always generated.

The annotation processor that generates the web route adapters also generates a OpenAPI (swagger) definition of all the endpoints that the controllers define.

Example javalin-maven-java-basic

Javadoc

The annotation processor reads the javadoc (and Kotlin documentation) for the controller methods and the description, @param and @return documentation is all used in the OpenAPI definition. The processor reads all the request and response types as OpenAPI component schema.

Validation annotations - @NotNull, @Size, @Email etc

The javax bean validation annotations @NotNull, @Size and @Email are read as well as detecting Kotlin non-nullable types as required properties.

These are used to specify required properties, min max lengths and property format.

Swagger annotations

We can add a dependency on io.swagger.core.v3:swagger-annotations:2.0.8 and add swagger annotations.

@OpenAPIDefinition

We use @OpenAPIDefinition to define a title and description for the api. This annotation would often go on the Main method class or on package-info.java in the controllers package.

Example @OpenAPIDefinition

@OpenAPIDefinition(
  info = @Info(
    title = "Example service",
    description = "Example Javalin controllers with Java and Maven"))

@Hidden

Put @Hidden on controller methods that we want to exclude from the OpenAPI documentation.

Gradle plugin

The gradle plugin by default puts the generated openapi.json file into src/main/resource/public

To add the plugin:

plugins {
  ...
  id('io.dinject.openapi') version('1.2')
}

To configure the openapi.json file to go to another location add a openapi configuration section in build.gradle like:

openapi {
  destination = 'other/my-api.json'
}

Maven plugin

The maven plugin by default puts the generated openapi.json file into src/main/resource/public

To add the plugin into build / plugins section:

<plugin>
  <groupId>io.dinject</groupId>
  <artifactId>openapi-maven-plugin</artifactId>
  <version>1.2</version>
  <executions>
    <execution>
      <id>main</id>
      <phase>process-classes</phase>
      <goals>
        <goal>openapi</goal>
      </goals>
    </execution>
  </executions>
</plugin>

To configure the openapi.json file to go to another location add a configuration section element like:

<plugin>
  <groupId>io.dinject</groupId>
  <artifactId>openapi-maven-plugin</artifactId>
  <version>1.2</version>
  <configuration>
    <destination>other/my-api.json</destination>
  </configuration>
  <executions>
    <execution>
      <id>main</id>
      <phase>process-classes</phase>
      <goals>
        <goal>openapi</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Serving openapi.json

When the openapi.json file is in src/main/resources/public it can be served by using addStaticFiles on the JavalinConfig like:

Javalin app = Javalin.create(config -> {
  ...
  config.addStaticFiles("public", Location.CLASSPATH);
});

JAX-RS

With the annotation processor we have chosen to use our own annotations rather than use JAX-RS.

Why not use the standard JAX-RS annotations?

 

avaje http

JAX-RS

@Controller -
@Path @Path

HTTP Methods

@Delete @DELETE + @Path
@Get @GET + @Path
@Post @POST + @Path
@Put @PUT + @Path
@Patch @PATCH + @Path

Bean parameters

@Form -
@BeanParam @BeanParam

Parameters

Not needed (implied) @PathParam
Not needed (implied) @MatrixParam
@FormParam + @Default @FormParam + @DefaultValue
@QueryParam + @Default @QueryParam + @DefaultValue
@Header @HeaderParam
@Cookie @CookieParam