Avaje HTTP Server & Client
Library that generates extremely fast adapter code for Javalin and Helidon SE/Nima APIs via Annotation Processing.
License | Source | API Docs | Issues | Releases |
---|---|---|---|---|
Apache2 | Github | Javadoc | Github |
This library enables your service to be fast and light 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 web routing http servers.
Effectively we are replacing Jersey or RestEasy with source code generation and the capabilities of Javalin or Helidon SE (web routing).
It turns out we don't need to generate much code at all and that the generated code is very simple and readable. 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 automatic 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
- Provides a similar programming style to JAX-RS and Spring MVC
- Light weight by using code generation - no reflection, no extra overhead
- Automatically generates Swagger/OpenAPI documentation
- Allows use of underlying Javalin/Helidon request/response constructs as needed
- Supports request scope injection of Javalin Context, Helidon request and response
- Supports using Bean validation on request payloads
- Requires Fewer annotations than typical JAX-RS - avoid annotation overload
JAX-RS Annotations
As we are using Java annotation processing 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.
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 extra weight that we do not need.
HTTP Client
Avaje http client is a lightweight wrapper of JDK HttpClient that also supports Client API with annotation processing to generate source to implement the API.
Quick Start
1. Add avaje-inject and avaje-http-api dependencies.
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-inject</artifactId>
<version>${avaje-inject.version}</version>
</dependency>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-http-api</artifactId>
<version>${avaje-http.version}</version>
</dependency>
2. Add the generator module for your desired microframework as a annotation processor.
<!-- Annotation processors -->
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-inject-generator</artifactId>
<version>${avaje-inject.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-http-javalin-generator</artifactId>
<version>${avaje-http.version}</version>
<scope>provided</scope>
</dependency>
3. Define a Controller (These APT processors work with both Java and Kotlin.)
package org.example.hello;
import io.avaje.http.api.Controller;
import io.avaje.http.api.Get;
import io.avaje.http.api.Path;
import java.util.List;
@Path("/widgets")
@Controller
public class WidgetController {
private final HelloComponent hello;
public WidgetController(HelloComponent hello) {
this.hello = hello;
}
@Get("/{id}")
Widget getById(int id) {
return new Widget(id, "you got it"+ hello.hello());
}
@Get()
List<Widget> getAll() {
return List.of(new Widget(1, "Rob"), new Widget(2, "Fi"));
}
record Widget(int id, String name){};
}
Usage
The natural way to use the generated adapters is to get a DI library to find and wire them. This is what the below examples do and they use Avaje to do this.
Note that there isn't a requirement to use Avaje for dependency injection. Any DI library that can find and wire the generated @Singleton beans can be used. You can even use Dagger2 or Guice to wire the controllers if you so desire.
Usage with Javalin
The annotation processor will generate controller classes implementing the WebRoutes interface, which means we can get all the WebRoutes and register them with Javalin using:
var routes = BeanScope.builder().build().list(WebRoutes.class);
Javalin.create()
.routes(() -> routes.forEach(WebRoutes::registerRoutes))
.start();
Usage with Helidon SE
The annotation processor will generate controller classes implementing the Helidon Service interface, which we can use get all the Services and register them with Helidon `RoutingBuilder`.
var routes = BeanScope.builder().build().list(Service.class);
var routingBuilder = Routing.builder().register(routes.stream().toArray(Service[]::new));
WebServer.builder()
.addMediaSupport(JacksonSupport.create())
.routing(routingBuilder)
.build()
.start();
Helidon Nima Usage
The annotation processor will generate controller classes implementing the Helidon HttpService interface, which we can use get all the services and register them with the Helidon `HttpRouting`.
var routes = BeanScope.builder().build().list(HttpService.class);
final var builder = HttpRouting.builder();
for (final HttpService httpService : routes) {
httpService.routing(builder);
}
WebServer.builder()
.addRouting(builder.build())
.build()
.start();
Dependencies
Maven
See the quick start example
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:8.10')
compile('io.avaje:avaje-http-api:1.20')
annotationProcessor('io.avaje:avaje-inject-generator:8.10')
annotationProcessor('io.avaje:avaje-http-javalin-generator:1.20')
}
Kotlin KAPT
For use with Kotlin we change the annotationProcessor to be kapt
for the Kotlin compiler.
dependencies {
...
kapt('io.avaje:avaje-inject-generator:8.10')
kapt('io.avaje:avaje-http-javalin-generator:1.20')
}
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 @Controller
.
You can provide a path segment that is prepended
to any path segments defined by on methods using @Get
,
@Post
, @Put
etc. There are three ways to prepend a path.
1. Directly put the path in the controller annotation.
@Controller("/customers")
class CustomerController {
...
}
2. Use @Path
and @Controller
@Controller
@Path("/customers")
class CustomerController {
...
}
3. Use @Path
on an Interface and @Controller
on an implementing class
@Path("/customers")
interface CustomerController {
...
}
@Controller
class CustomerControllerImpl implements CustomerController {
...
}
Web Methods on a controller are annotated with HTTP annotations like @Get
,
@Post
, @Put
, @Delete
.
@Controller("/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 can easily 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 {
@Get
@Produces(MediaType.TEXT_PLAIN)
String hello() {
return "Hello world";
}
@Get("foo")
@Produces(MediaType.TEXT_PLAIN)
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, LocalDateTime, or Enums(Will use Enum.valueOf(EnumType, parameter)
).
To get multivalue parameters we can use List<T>
or Set<T>
where T
is any of the previously mentioned types.
To get all query parameters define a parameter of type Map<List<T>>
.
Query parameters are considered optional / nullable.
@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.
Post JSON
Avaje auto detects that a parameter is a request body if the type is a POJO
/byte[]
/InputStream
and not marked with a @BeanParam
annotation. To mark a string parameter as a body, use the @BodyString
annotation.
@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.bodyStreamAsClass(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) {
...
}
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);
});
@Produces
Use @Produces
to modify the response content type and generated OpenAPI definition.
When not specified, we default to application/json
. We can set the default http status code for the method as well.
If not specified, the default status codes for the different http verbs are as follows:
GET(200)
POST(201)
PUT(200, void methods 204)
PATCH(200, void methods 204)
DELETE(200, void methods 204)
@Path("/")
@Controller
class RootController {
private Service service;
//send plain text
@Get
@Produces(MediaType.TEXT_PLAIN)
String hello() {
return "Hello world";
}
// default json
@Get("obj")
Example helloObj() {
return new Example();
}
// we can also send our data as a byte array
@Get("png")
@Produces(MediaType.IMAGE_PNG)
byte[] helloByte() {
return service.getPNG();
}
// use Javalin Context for our response
// in this case Produces is only needed for the OpenAPI generation
@Get("ctx")
@Produces(MediaType.IMAGE_PNG)
void helloCTX(Context ctx) {
service.writeResponseDirectly(ctx.outputStream());
}
}
@Header
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
...
}
@Cookie
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));
}
@Default
We can use @Default
to specify a default value for a Query Parameter/Header/Cookie/Form Parameter.
@Get("/catty")
List<Cat> findCats(@Header @Default("age") String orderBy, @Default({"1", "2"}) List<Integer> numbersOfLimbs) {
...
}
@Get
Annotate methods with @Get
for HTTP GET web routes.
@Controller("/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
@Get
Response save(HelloDto dto, Context context) {
// use Javalin context as desired
ctx.status(202);
...
return new Response();
}
@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
@Get
Response save(HelloDto dto, ServerRequest request, ServerResponse response) {
// use Helidon server request or response as desired
...
return new Response();
}
@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("/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");
...
}
}
Instrumenting the Server Context
The @InstrumentServerContext
annotation marks a controller method to be instrumented with RequestContextResolver
. For the execution of the controller method, the server context will be stored by the given implementation of RequestContextResolver
.
By default, a RequestContextResolver
implementation using ThreadLocals is provided to store the Context instance. When using virtual threads, it may be better to provide an implementation using Scoped Values.
Using RequestContextResolver
RequestContextResolver resolver = ...
@Get
@InstrumentServerContext
void helloWorld(long id) {
Context ctx = resolver.currentRequest().orElseThrow().response()
ctx.result("success");
...
}
Type conversions
There are built in type conversions for the following types:
- int, long, boolean, Integer, Long, Boolean
- BigDecimal, UUID, LocalDate, LocalTime, LocalDateTime
- Enum Types via valueOf (will use toUpperCase on the query parameter)
For multivalue parameters like query parameters or headers, we can use List<T>
or Set<T>
where T
is any of the previously mentioned 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,
List<Long> longs
) {
...
}
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"));
List<Long> longs = list(PathTypeConversion::toLong, ctx.queryParams("longs"));
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 through the validator interface. We can validate a request body and Form Bean/BeanParam
Example: HelloController
Add @Valid
Add a jakarta/javax/avaje @Valid
annotation on controllers/methods and the types 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
class HelloForm {
@NotBlank
private String name;
private String email;
//getters/setters/constructors
}
@Valid
class HelloBean {
@NotBlank
private String name;
private String email;
//getters/setters/constructors
}
@Valid
class BodyClass {
@NotBlank
private String somefield;
//getters/setters/constructors
}
@Valid
@Controller("/baz")
class BazController {
@Form
@Post("/form")
void saveForm(HelloForm helloForm) {
...
}
@Post("/bean")
void saveBean(@BeanParam HelloBean helloBean) {
...
}
@Post("/body")
void saveBody(BodyClass body) {
...
}
The generated code now includes validation of the beans before they are passed to the controller method. The generated code is:
ApiBuilder.post("/baz/form", ctx -> {
ctx.status(201);
HelloForm helloForm = new HelloForm(
ctx.formParam("name"),
ctx.formParam("email")
);
validator.validate(helloForm); // validation added here !!
controller.saveForm(helloForm);
});
ApiBuilder.post("/baz/bean", ctx -> {
ctx.status(201);
HelloBean helloBean = new HelloBean(
ctx.queryParam("name"),
ctx.queryParam("email")
);
validator.validate(helloBean); // validation added here !!
controller.saveBean(helloBean);
});
ApiBuilder.post("/baz/body", ctx -> {
ctx.status(201);
var body = ctx.bodyAsClass(BodyClass.class);
validator.validate(body); // validation added here !!
controller.saveBody(helloBean);
});
Custom Validation
For custom validation, you can can implement the avaje validator interface yourself and add custom logic.
import io.avaje.http.api.Validator;
@Singleton
public class BeanValidator implements Validator {
@Override
public void validate(Object bean) {
//do validation
// if validation fails throw something
}
}
Using Hibernate
Add a dependency on avaje-http-hibernate-validator. This will transitively bring in a dependency on hibernate-validator which will be used to validate.
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-http-hibernate-validator</artifactId>
<!-- use 2.9 for javax validation -->
<version>3.2</version>
</dependency>
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());
});
Roles
We can optionally add declarative security role checking for Javalin and Jex.
Example
@Controller
@Path("/customers")
class CustomerController {
@Roles({AppRoles.ADMIN, AppRoles.BASIC_USER})
@Get("/{id}")
Customer find(int id) {
...
}
Javalin Roles
Example reference test-javalin - HelloController
Step 1: Create an enum that implements io.javalin.security.RouteRole
Create an enum that implements io.javalin.security.RouteRole
.
import io.javalin.security.RouteRole;
public enum AppRoles implements RouteRole {
ANYONE, ADMIN, BASIC_USER, ORG_ADMIN
}
Step 2: Create a Roles / PermittedRoles annotation
Create an annotation that has a short name that ends with Roles or PermittedRoles. The annotation must follow this naming convention to be detected by the annotation processor (code generation).
The annotation must have a value()
attribute that specifies the
enum role type specified in Step 1.
Example
package org.example.myapp.web;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Specify permitted roles.
*/
@Target(value={METHOD, TYPE})
@Retention(value=RUNTIME)
public @interface Roles {
/**
* Specify the permitted roles.
*/
AppRoles[] value() default {};
}
Step 3: Use the annotation
Add the annotation to any controller or controller methods as desired.
Example
@Roles({AppRoles.ADMIN, AppRoles.BASIC_USER})
@Get("/{id}")
Customer find(int id) { ...
Step 4: Javalin AccessManager
Ensure that Javalin is setup with an AccessManager to implement the role check.
Javalin app = Javalin.create(config -> {
...
config.accessManager((handler, ctx, permittedRoles) -> {
// implement role permission check
...
});
});
Jex Roles
Example reference test-jex - HelloController
Step 1: Create an enum that implements io.avaje.jex.Role
Create an enum that implements io.avaje.jex.Role
.
import io.avaje.jex.Role
public enum AppRoles implements Role {
ANYONE, ADMIN, BASIC_USER, ORG_ADMIN
}
Step 2: Create a Roles / PermittedRoles annotation
Create an annotation that has a short name that ends with Roles or PermittedRoles. The annotation must follow this naming convention to be detected by the annotation processor (code generation).
The annotation must have a value()
attribute that specifies the
enum role type specified in Step 1.
Example
package org.example.myapp.web;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Specify permitted roles.
*/
@Target(value={METHOD, TYPE})
@Retention(value=RUNTIME)
public @interface Roles {
/**
* Specify the permitted roles.
*/
AppRoles[] value() default {};
}
Step 3: Use the annotation
Add the annotation to controller methods or the controller as desired.
Example
@Roles({AppRoles.ADMIN, AppRoles.BASIC_USER})
@Get("/{id}")
Customer find(int id) { ...
Step 4: Jex AccessManager
Ensure that Jex is setup with an AccessManager to implement the role check.
Example
jex.accessManager((handler, ctx, permittedRoles) -> {
...
})
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 OpenAPI Controller.
Generated OpenAPI Definition.
Javadoc
The annotation processor reads the javadoc (and Kotlin documentation) of the controller methods to generate the openAPI definition.
The javadoc summary, description, @param and @return
documentation are used to create the OpenAPI Operation definitions.
The processor reads all the request and response types as OpenAPI component schema. The various annotations like @Header
,@Param
, and @Produces
also modify the generated OpenAPI docs.
/**
* Example of Open API Get (up to the first period is the operation summary).
*
* basic Post (This Javadoc description is added to the operation description)
*
* @param b the body (the param docs are used for query/header/body description)
* @return funny phrase (this part of the javadoc is added to the response desc)
*/
@Post("/post")
ResponseModel endpoint(RequestModel) {
...
}
In Addition, the generator can read the request/response class javadoc to generate openAPI component description.
class ResponseModel {
/** field one */
String field1;
/** field two */
String field1;
}
@Consumes
Adding the @Consumes
annotation to a controller method let's you control what media type should be generated for the request body in the openAPI definition. This is useful when you are dealing with non-standard request content types.
@Deprecated
Adding Javas's @Deprecated
annotation to a controller method marks it as deprecated in the openAPI definition.
@OpenApiResponse
This annotation lets you specify alternate endpoint response status codes/descriptions/types. This is useful for defining error scenarios, and when using the underlying Javalin/Helidon contructs in void methods.
@Post("/post")
@OpenAPIResponse(responseCode = "200", description = "overrides @return javadoc description")
@OpenAPIResponse(responseCode = "201") // Will use @return javadoc description
@OpenAPIResponse(
responseCode = "404",
description = "User not found (Will not have an associated response schema)")
@OpenAPIResponse(
responseCode = "500",
description = "Some other Error (Will have this error class as the response class reference)",
type = ErrorResponse.class)
ResponseModel endpoint() {}
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.
@Tags
Put @Tags
on controller methods to add tags to the OpenAPI Operation documentation.
@SecurityScheme and @SecurityRequirement
Put @SecurityScheme or @SecurityRequirement
on controller methods to add to the OpenAPI Operation 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.avaje.openapi') version('1.0')
}
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.avaje</groupId>
<artifactId>openapi-maven-plugin</artifactId>
<version>1.0</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.avaje</groupId>
<artifactId>openapi-maven-plugin</artifactId>
<version>1.0</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?
- The JAX-RS API dependency also has a LOT of other stuff we don't want
- We can improve on JAX-RS making our controllers less verbose
- We may support JAX-RS annotations at some future point
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 |