- Lightweight JAX-RS (RestEasy) like annotation processor for vert.x verticles.
- You are highly encouraged to participate and improve upon the existing code.
- Please report any issues discovered.
If this project help you reduce time to develop?
Keep it running and
for cookies and coffee or become a sponsor.
JAVA 11+
<dependency>
<groupId>com.zandero</groupId>
<artifactId>rest.vertx</artifactId>
<version>1.1.1</version>
</dependency>
NOTE: Last Java 8 compatible version
<dependency>
<groupId>com.zandero</groupId>
<artifactId>rest.vertx</artifactId>
<version>1.0.6.1</version>
</dependency>
See also: change log
If you are using vert.x version 3.* then use version 0.9.* of Rest.vertx
Version 1.0.4 and higher are only compatible with vert.x version 4 and higher and introduce some breaking changes:
- @RolesAllowed authorization throws 403 Forbidden exception where before it was a 401 Unauthorized exception
- Authentication and authorization flow is now aligned with vert.x and @RolesAllowed processing is done by an AuthorizationProvider implementation
- @RolesAllowed annotations still work but should be replaced with @Authenticate and @Authorize annotations instead
This project uses:
- the superb IntelliJ Idea
- the
excellent
Java Profiler
A high level overview of how rest.vertx works:
Step 1 - annotate a class with JAX-RS annotations
@Path("/test")
public class TestRest {
@GET
@Path("/echo")
@Produces(MediaType.TEXT_HTML)
public String echo() {
return "Hello world!";
}
}
Step 2 - register annotated class as REST API
TestRest rest=new TestRest();
Router router=RestRouter.register(vertx,rest);
vertx.createHttpServer()
.requestHandler(router)
.listen(PORT.get());
or alternatively
Router router=Router.router(vertx);
TestRest rest=new TestRest();
RestRouter.register(router,rest);
vertx.createHttpServer()
.requestHandler(router)
.listen(PORT.get());
or alternatively use RestBuilder helper to build up endpoints.
version 0.5 or later
Alternatively RESTs can be registered by class type only.
Router router=RestRouter.register(vertx,TestRest.class);
vertx.createHttpServer()
.requestHandler(router)
.listen(PORT.get());
version 0.7 or later
Rest endpoints, error handlers, writers and readers can be bound in one go using the RestBuilder.
Router router=new RestBuilder(vertx)
.register(RestApi.class,OtherRestApi.class)
.reader(MyClass.class,MyBodyReader.class)
.writer(MediaType.APPLICATION_JSON,CustomWriter.class)
.errorHandler(IllegalArgumentExceptionHandler.class)
.errorHandler(MyExceptionHandler.class)
.build();
or
router=new RestBuilder(router)
.register(AdditionalApi.class)
.build();
Each class can be annotated with a root (or base) path @Path("/rest").
In order to be registered as a REST API endpoint the class public method must have a @Path annotation.
@Path("/api")
public class SomeApi {
@GET
@Path("/execute")
public String execute() {
return "OK";
}
}
OR - if class is not annotated the method @Path is taken as the full REST API path.
public class SomeApi {
@GET
@Path("/api/execute")
public String execute() {
return "OK";
}
}
> GET /api/execute/
NOTE: multiple identical paths can be registered - if response is not terminated (ended) the next method is executed. However this should be avoided whenever possible.
Both class and methods support @Path variables.
// RestEasy path param style
@GET
@Path("/execute/{param}")
public String execute(@PathParam("param") String parameter){
return parameter;
}
> GET /execute/that -> that
// vert.x path param style
@GET
@Path("/execute/:param")
public String execute(@PathParam("param") String parameter){
return parameter;
}
> GET /execute/this -> this
// RestEasy path param style with regular expression {parameter:>regEx<}
@GET
@Path("/{one:\\w+}/{two:\\d+}/{three:\\w+}")
public String oneTwoThree(@PathParam("one") String one,@PathParam("two") int two,@PathParam("three") String three){
return one+two+three;
}
> GET /test/4/you -> test4you
since version 0.8.7 or later
Also possible are Vert.x style paths with regular expressions.
// VertX style path :parameter:regEx
@GET
@Path("/:one:\\d+/minus/:two:\\d+")
public Response test(int one,int two){
return Response.ok(one-two).build();
}
> GET /12/minus/3 -> 9
Query variables are defined using the @QueryParam annotation.
In case method arguments are not nullable they must be provided or a 400 bad request response follows.
@Path("calculate")
public class CalculateRest {
@GET
@Path("add")
public int add(@QueryParam("one") int one, @QueryParam("two") int two) {
return one + two;
}
}
> GET /calculate/add?two=2&one=1 -> 3
In case needed a request reader can be assigned to provide the correct variable:
@GET
public int getDummyValue(@QueryParam("dummy") @RequestReader(DummyReader.class) Dummy dummy){
return dummy.value;
}
since version 0.8.7 or later
Query variables are decoded by default
If the original (non decoded) value is desired, we can use the @Raw annotation.
@GET
@Path("/decode")
public String echoGetQuery(@QueryParam("decoded") String decodedQuery,
@QueryParam("raw") @Raw String rawQuery){
> GET /decode?decoded=hello+world -> decoded = "hello world"
> GET /decode?raw=hello+world -> raw = "hello+world"
Matrix parameters are defined using the @MatrixParam annotation.
@GET
@Path("{operation}")
public int calculate(@PathParam("operation") String operation,@MatrixParam("one") int one,@MatrixParam("two") int two){
switch(operation){
case"add":
return one+two;
case"multiply":
return one*two;
default:
return 0;
}
}
> GET /add;one=1;two=2 -> 3
Rest.Vertx tries to convert path, query, cookie, header and other variables to their corresponding Java types.
Basic (primitive) types are converted from string to given type - if conversion is not possible a 400 bad request response follows.
Complex java objects are converted according to @Consumes annotation or @RequestReader request body reader associated.
Complex java object annotated with @BeanParam annotation holding fields annotated with @PathParam, @QueryParam ...
Option 1 - The @Consumes annotation mime/type defines the reader to be used when converting request body.
In this case a build in JSON converter is applied.
@Path("consume")
public class ConsumeJSON {
@POST
@Path("read")
@Consumes("application/json")
public String add(SomeClass item) {
return "OK";
}
}
Option 2 - The @RequestReader annotation defines a ValueReader to convert a String to a specific class, converting:
- request body
- path
- query
- cookie
- header
@Path("consume")
public class ConsumeJSON {
@POST
@Path("read")
@Consumes("application/json")
@RequestReader(SomeClassReader.class)
public String add(SomeClass item) {
return "OK";
}
}
Option 3 - An RequestReader is globally assigned to a specific class type.
RestRouter.getReaders().register(SomeClass.class,SomeClassReader.class);
@Path("consume")
public class ConsumeJSON {
@POST
@Path("read")
public String add(SomeClass item) {
return "OK";
}
}
Option 4 - An RequestReader is globally assigned to a specific mime type.
RestRouter.getReaders().register("application/json",SomeClassReader.class);
@Path("consume")
public class ConsumeJSON {
@POST
@Path("read")
@Consumes("application/json")
public String add(SomeClass item) {
return "OK";
}
}
First appropriate reader is assigned searching in following order:
- use parameter ValueReader
- use method ValueReader
- use class type specific ValueReader
- use mime type assigned ValueReader
- use general purpose ValueReader
Option 5 - @BeanParam argument is constructed via vert.x RoutingContext.
since version 0.9.0 or later
@POST
@Path("/read/{param}")
public String read(@BeanParam BeanClazz bean){
...
}
public class BeanClazz {
@PathParam("param")
private String path;
@QueryParam("query")
@Raw
private String query;
@HeaderParam("x-token")
private String token;
@CookieParam("chocolate")
private String cookie;
@MatrixParam("enum")
private MyEnum enumValue;
@FormParam("form")
private String form;
@BodyParam
@DefaultValue("empty")
private String body;
}
OR via constructor
public BeanClazz(@PathParam("param") String path,
@HeaderParam("x-token") boolean xToken,
@QueryParam("query") @Raw int query,
@CookieParam("chocolate") String cookie){
...
}
If no specific ValueReader is assigned to a given class type, rest.vertx tries to instantiate the class:
- converting String to primitive type if class is a String or primitive type
- using a single String constructor
- using a single primitive type constructor if given String can be converted to the specific type
- using static methods fromString(String value) or valueOf(String value) (in that order)
Rest.vertx tries to be smart and checks all readers and writers type compatibility.
Meaning if a REST method returns a String then a String compatible writer is expected.
In case the check is to strong (preventing some fancy Java generics or inheritance) the @SuppressCheck annotation can be applied to skip the check.
NOTE: This will not prevent a writer/reader runtime exception in case of type incompatibility!
@SuppressCheck
public class TestSuppressedWriter implements HttpResponseWriter<Dummy> {
@Override
public void write(Dummy result, HttpServerRequest request, HttpServerResponse response) {
response.end(result.name);
}
}
Cookies, HTTP form and headers can also be read via @CookieParam, @HeaderParam and @FormParam annotations.
@Path("read")
public class TestRest {
@GET
@Path("cookie")
public String readCookie(@CookieParam("SomeCookie") String cookie) {
return cookie;
}
}
@Path("read")
public class TestRest {
@GET
@Path("header")
public String readHeader(@HeaderParam("X-SomeHeader") String header) {
return header;
}
}
@Path("read")
public class TestRest {
@POST
@Path("form")
public String readForm(@FormParam("username") String user, @FormParam("password") String password) {
return "User: " + user + ", is logged in!";
}
}
We can provide default values in case parameter values are not present with @DefaultValue annotation.
@DefaultValue annotation can be used on:
- @PathParam
- @QueryParam
- @FormParam
- @CookieParam
- @HeaderParam
- @Context
public class TestRest {
@GET
@Path("user")
public String read(@QueryParam("username") @DefaultValue("unknown") String user) {
return "User is: " + user;
}
}
> GET /user -> "User is: unknown
> GET /user?username=Foo -> "User is: Foo
Additional request bound variables can be provided as method arguments using the @Context annotation.
Following types are by default supported:
- @Context HttpServerRequest - vert.x current request
- @Context HttpServerResponse - vert.x response (of current request)
- @Context Vertx - vert.x instance
- @Context EventBus - vert.x EventBus instance
- @Context RoutingContext - vert.x routing context (of current request)
- @Context User - vert.x user entity (if set)
- @Context RouteDefinition - vertx.rest route definition (reflection of Rest.Vertx route annotation data)
@GET
@Path("/context")
public String createdResponse(@Context HttpServerResponse response,@Context HttpServerRequest request){
response.setStatusCode(201);
return request.uri();
}
If desired a custom context provider can be implemented to extract information from request into a object.
The context provider is only invoked in when the context object type is needed. Use addProvider() method on **
RestRouter** or RestBuilder to register a context provider.
public class TokenProvider implements ContextProvider<Token> {
@Override
public Token provide(HttpServerRequest request) throws Throwable {
String token = request.getHeader("X-Token");
if (token != null) {
return new Token(token);
}
return null;
}
}
RestRouter.addProvider(Token.class,TokenProvider.class);
or
RestRouter.addProvider(Token.class,request->{
String token=request.getHeader("X-Token");
if(token!=null){
return new Token(token);
}
return null;
});
or
public class Token {
public String token;
public Token(HttpServerRequest request) {
token = request.getHeader("X-Token");
}
}
RestRouter.addProvider(Token.class,Token::new)
@GET
@Path("/token")
public String readToken(@Context Token token){
return token.getToken();
}
If @Context for given class can not be provided than a 400 @Context can not be provided exception is thrown
While processing a request a custom context can be pushed into the vert.x routing context data storage.
This context data can than be utilized as a method argument. The pushed context is thread safe for the current request.
The main difference between a context push and a context provider is that the context push is executed on every request, while the registered provider is only invoked when needed!
In order to achieve this we need to create a custom handler that pushes the context before the REST endpoint is called:
Router router=Router.router(vertx);
router.route().handler(pushContextHandler());
router=RestRouter.register(router,new CustomContextRest());
vertx.createHttpServer()
.requestHandler(router)
.listen(PORT.get());
private Handler<RoutingContext> pushContextHandler(){
return context->{
RestRouter.pushContext(context,new MyCustomContext("push this into storage"));
context.next();
};
}
or
RestRouter.provide(TokenProvider.class); // push of context provider
A pushed context is handy in case we wan't to make sure some context related object is always present (on every request), ie. session / user ...
Then the context object can than be used as a method argument
@Path("custom")
public class CustomContextRest {
@GET
@Path("/context")
public String createdResponse(@Context MyCustomContext context) {
}
}
version 0.8.6 or later
A custom context reader can be applied to a @Context annotated variable to override the global context providers.
@GET
@Path("/token")
@ContextReader(TokenProvider.class)
public String createdResponse(@Context Token token){
return token.token;
}
// or
@GET
@Path("/token")
public String createdResponse(@ContextReader(TokenProvider.class) @Context Token token){
return token.token;
}
version 0.9.1 or later
In case needed a custom body handler can be provided for all body handling requests.
BodyHandler bodyHandler=BodyHandler.create("my_upload_folder");
RestRouter.setBodyHandler(bodyHandler);
Router router=RestRouter.register(vertx,UploadFileRest.class);
or
BodyHandler handler=BodyHandler.create("my_upload_folder");
Router router=new RestBuilder(vertx)
.bodyHandler(handler)
.register(UploadFileRest.class)
.build();
Metod results are converted using response writers.
Response writers take the method result and produce a vert.x response.
Example of a simple response writer:
@Produces("application/xml") // content-type header
@Header("X-Status: I'm a dummy") // additional static headers
public class DummyWriter implements HttpResponseWriter<Dummy> {
@Override
public void write(Dummy data, HttpServerRequest request, HttpServerResponse response) {
response.status(200); // for illustration ... needed only when overriding 200
String out = data.name + "=" + data.value;
response.end("<custom>" + out + "</custom>");
}
}
Option 1 - The @Produces annotation mime/type defines the writer to be used when converting response.
In this case a build in JSON writer is applied.
@Path("produces")
public class ConsumeJSON {
@GET
@Path("write")
@Produces("application/json")
public SomeClass write() {
return new SomeClass();
}
}
Option 2 - The @ResponseWriter annotation defines a specific writer to be used.
@Path("produces")
public class ConsumeJSON {
@GET
@Path("write")
@Produces("application/json")
@ResponseWriter(SomeClassWriter.class) // writer will be used for this REST call only
public SomeClass write() {
return new SomeClass();
}
}
Global writers are used in case no other writer is specified for given type or content-type!
Option 3 - An ResponseWriter is globally assigned to a specific class type.
RestRouter.getWriters().register(SomeClass.class,SomeClassWriter.class);
RestRouter.getWriters().register("application/json",SomeClassWriter.class);
RestRouter.getWriters().register(SomeClassWriter.class); // where SomeClassWriter is annotated with @Produces("application/json")
Option 4 - An ResponseWriter is globally assigned to a specific mime type.
RestRouter.getWriters().register(MyClass.class,MyJsonWriter.class);
RestRouter.getWriters().register("application/json",MyJsonWriter.class);
RestRouter.getWriters().register(MyJsonWriter.class); // where MyJsonWriter is annotated with @Produces("application/json")
@Path("produces")
public class ConsumeJSON {
@GET
@Path("write")
@Produces("application/json") // appropriate content-type writer will be looked up
public SomeClass write() {
return new SomeClass();
}
}
First appropriate writer is assigned searching in following order:
- use assigned method ResponseWriter
- use class type specific writer
- use mime type assigned writer
- use general purpose writer (call to .toString() method of returned object)
In order to manipulate returned response, we can utilize the @Context HttpServerResponse.
@GET
@Path("/login")
public HttpServerResponse vertx(@Context HttpServerResponse response){
response.setStatusCode(201);
response.putHeader("X-MySessionHeader",sessionId);
response.end("Hello world!");
return reponse;
}
NOTE in order to utilize the JAX Response.builder() an existing JAX-RS implementation must be provided.
Vertx.rest uses the Glassfish Jersey implementation for testing:
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-common</artifactId>
<version>2.22.2</version>
</dependency>
@GET
@Path("/login")
public Response jax(){
return Response
.accepted("Hello world!!")
.header("X-MySessionHeader",sessionId)
.build();
}
version 1.0.4 or later
Authentication and Authorization steps:
-
RestAuthorizationProvider reads the request and provides the appropriate Credentials for the AuthorizationProvider
-
AuthorizationProvider uses the supplied Credentials to identify the User accessing the REST endpoint
- if the user is not identified a 401 Unauthorized exception is thrown
-
the User entity is then provided to the AuthorizationProvider to check if user is allowed accessing the REST endpoint
- User entity should return a list of allowed Authorizations to be matched against the provider authorization
- if the user is not allowed to access the REST endpoint a 403 Forbidden exception is thrown
In order to authenticate a user we can annotate a REST interface/method with the @Authenticate annotation.
The @Authenticate annotation needs RestAuthenticationProvider implementation.
import com.zandero.rest.annotation.*;
@Path("secure")
@Authenticate(MyAuthenticator.class)
public class ConsumeJSON {
@GET
@Path("path")
@Authorize(MyAuthorizationProvider.class)
public String OnlyAccessibleToCreditedUsers() {
return "I'm VIP";
}
}
Example of RestAuthenticationProvider implementation:
public class MyAuthenticator implements RestAuthenticationProvider {
@Override
public void authenticate(JsonObject credentials, Handler<AsyncResult<User>> resultHandler) {
// process credentials if provided
String token = credentials != null ? credentials.getString("token") : null; // expecting credentials to be instance of TokenCredentials
// search for the user with session expressed in token
User user = null;
if (token != null) {
user = getUserWithSession(token);
}
if (user != null) {
resultHandler.handle(Future.succeededFuture(user));
} else {
resultHandler.handle(Future.failedFuture("Missing authentication token"));
}
}
@Override
public void authenticate(Credentials credentials, Handler<AsyncResult<User>> resultHandler) {
if (credentials instanceof TokenCredentials) {
TokenCredentials token = (TokenCredentials) credentials;
authenticate(token.toJson(), resultHandler);
} else {
resultHandler.handle(Future.failedFuture(new BadRequestException("Missing authentication token")));
}
}
@Override
public Credentials provideCredentials(RoutingContext context) {
String token = context.request().getHeader("X-Token");
return token != null ? new TokenCredentials(token) : null; // token might be null
}
}
public class MyAuthorizationProvider implements AuthorizationProvider {
@Override
public String getId() {
return "MyAuthorizationProvider";
}
@Override
public void getAuthorizations(User user, Handler<AsyncResult<Void>> handler) {
if (PermissionBasedAuthorization.create("HasPermission").match(user)) {
handler.handle(Future.succeededFuture());
} else {
handler.handle(Future.failedFuture("You are not allowed in!"));
}
}
}
Example of a simple User entity with PermissionBasedAuthorization to be matched in MyAuthorizationProvider
public class MyUser extends UserImpl {
private final String role;
public MyUser(String role) {
this.role = role;
}
/**
* @return all authorizations that user can be matched against
*/
@Override
public Authorizations authorizations() {
return new AuthorizationsImpl().add("MyAuthorizationProvider",
PermissionBasedAuthorization.create(role));
}
}
Authorization and Authentication throw Unauthorized and Forbidden exceptions respectively.
But we can customize Authorization/Authentication provider by simply providing a custom exception when handling failure.
handler.handle(Future.failedFuture(new MyException("custom")));
The thrown exception is taken over and can be further processed with an Exception handler to produce the desired output.
We can define global authentication/authorization providers for all routes.
RestBuilder builder=new RestBuilder(vertx)
.authenticateWith(MyAuthenticator.class)
.authorizeWith(new MyAuthorizationProvider())
.register(EchoRest.class);
vertx.createHttpServer()
.requestHandler(router)
.listen(PORT.get());
Route based authentication/authorization providers override globally defined providers.
Up until version: 0.9.*
Since version 1.0.* one should use @Authenticate and @Authorize annotations instead!
@RollesAllowed, @PermitAll and @DenyAll will still work.
User access is checked in case REST API is annotated with:
- @RolesAllowed(role), @RolesAllowed(role_1, role_2, ..., role_N) - check if user is in any given role
- @PermitAll - allow everyone
- @DenyAll - deny everyone
User access is checked against the vert.x User entity stored in RoutingContext, calling the User.isAuthorised(role, handler) method.
In order to make this work, we need to fill up the RoutingContext with a User entity.
Depricated: vert.x 3 example
public void init(){
// 1. register handler to initialize User
Router router=Router.router(vertx);
router.route().handler(getUserHandler());
// 2. REST with @RolesAllowed annotations
TestAuthorizationRest testRest=new TestAuthorizationRest();
RestRouter.register(router,testRest);
vertx.createHttpServer()
.requestHandler(router)
.listen(PORT.get());
}
// simple hanler to push a User entity into the vert.x RoutingContext
public Handler<RoutingContext> getUserHandler(){
return context->{
// read header ... if present ... create user with given value
String token=context.request().getHeader("X-Token");
// set user ...
if(token!=null){
context.setUser(new SimulatedUser(token)); // push User into context
}
context.next();
};
}
Depricated: vert.x 3 example
@GET
@Path("/info")
@RolesAllowed("User")
public String info(@Context User user){
if(user instanceof SimulatedUser){
SimulatedUser theUser=(SimulatedUser)user;
return theUser.name;
}
return"hello logged in "+user.principal();
}
Example of User implementation:
Depricated: vert.x 3 example
public class SimulatedUser extends AbstractUser {
private final String role; // role and role in one
private final String name;
public SimulatedUser(String name, String role) {
this.name = name;
this.role = role;
}
/**
* permission has the value of @RolesAllowed annotation
*/
@Override
protected void doIsPermitted(String permission, Handler<AsyncResult<Boolean>> resultHandler) {
resultHandler.handle(Future.succeededFuture(role != null && role.equals(permission)));
}
/**
* serialization of User entity
*/
@Override
public JsonObject principal() {
JsonObject json = new JsonObject();
json.put("role", role);
json.put("name", name);
return json;
}
@Override
public void setAuthProvider(AuthProvider authProvider) {
// not utilized by Rest.vertx
}
}
In case needed we can implement a custom value reader.
A value reader must:
- implement ValueReader interface
- linked to a class type, mime type or @RequestReader
Example of RequestReader:
/**
* Converts request body to JSON
*/
public class MyCustomReader implements ValueReader<MyNewObject> {
@Override
public MyNewObject read(String value, Class<MyNewObject> type) {
if (value != null && value.length() > 0) {
return new MyNewObject(value);
}
return null;
}
}
Using a value reader is simple:
Register as global reader:
Global readers are used in case no other reader is specified for given type or content-type!
RestRouter.getReaders().register(MyNewObject.class,MyCustomReader.class);
RestRouter.getReaders().register("application/json",MyCustomReader.class);
RestRouter.getReaders().register(MyCustomReader.class); // if reader is annotated with @Consumes("application/json")
// or
new RestBuilder(vertx).reader(MyNewObject.class,MyCustomReader.class);
new RestBuilder(vertx).reader("appplication/json",MyCustomReader.class);
new RestBuilder(vertx).reader(MyCustomReader.class); // if reader is annotated with @Consumes("application/json")
Use only local on specific REST endpoint:
@Path("read")
public class ReadMyNewObject {
@POST
@Path("object")
@RequestReader(MyCustomReader.class) // MyCustomReader will provide the MyNewObject to REST API
public String add(MyNewObject item) {
return "OK";
}
// or
@PUT
@Path("object")
public String add(@RequestReader(MyCustomReader.class) MyNewObject item) {
return "OK";
}
}
We can utilize request readers also on queries, headers and cookies:
@Path("read")
public class ReadMyNewObject {
@GET
@Path("query")
public String add(@QueryParam("value") @RequestReader(MyCustomReader.class) MyNewObject item) {
return item.getName();
}
}
In case needed we can implement a custom response writer.
A request writer must:
- implement HttpResponseWriter interface
- linked to a class type, mime type or @ResponseWriter
Example of ResponseWriter:
/**
* Converts request body to JSON
*/
public class MyCustomResponseWriter implements HttpResponseWriter<MyObject> {
/**
* result is the output of the corresponding REST API endpoint associated
*/
@Override
public void write(MyObject data, HttpServerRequest request, HttpServerResponse response) {
response.putHeader("X-ObjectId", data.id);
response.end(data.value);
}
}
Using a response writer is simple:
Register as global writer:
RestRouter.getWriters().register(MyObject.class,MyCustomResponseWriter.class);
// or
new RestBuilder(vertx).writer(MyObject.class,MyCustomResponseWriter.class);
Use only local on specific REST endpoint:
@Path("write")
public class WriteMyObject {
@GET
@Path("object")
@ResponseWriter(MyCustomResponseWriter.class) // MyCustomResponseWriter will take output and fill up response
public MyObject output() {
return new MyObject("test", "me");
}
}
By default Rest.Vertx binds application/json mime type to internal JsonValueReader and JsonResponseWriter to read and write JSONs. This reader/writer utilizes Jackson with Vert.x internal io.vertx.core.json.Json.mapper ObjectMapper.
In order to change serialization/deserialization of JSON via Jackson the internal io.vertx.core.json.Json.mapper should be altered.
Alternatively you can override the build in JSON -> Object mapping by providing your own JsonReader:
@Consumes("application/json")
public class MyJsonReader<T> implements ValueReader<T> {
@Override
public T read(String value, Class<T> type) {
if (StringUtils.isNullOrEmptyTrimmed(value)) {
return null;
}
return YOUR_MAPPER.readValue(value, type);
}
}
And then registering the reader:
RestRouter router = RestBuilder(router)
// JSON readers
.reader("application/json", MyJsonReader.class);
By default routes area added to the Router in the order they are listed as methods in the class when registered. One can manually change the route REST order with the @RouteOrder annotation.
By default each route has the order of 0.
If route order is != 0 then vertx.route order is set. The higher the order - the later each route is listed in Router.
Order can also be negative, e.g. if you want to ensure a route is evaluated before route number 0.
Example: despite multiple identical paths the route order determines the one being executed.
@RouteOrder(20)
@GET
@Path("/test")
public String third(){
return"third";
}
@RouteOrder(10)
@GET
@Path("/test")
public String first(){
return"first";
}
@RouteOrder(15)
@GET
@Path("/test")
public String second(){
return"second";
}
>GET/test->"first"
version 0.8.6 or later
Rest events are a useful when some additional work/action must be performed based on the response produced.
For instance, we want to send out a registration confirmation e-mail on a 200 response (a successful registration).
Rest events are triggered after the response has been generated, but before the REST has ended.
One or more events are executed synchronously after the REST execution.
The order of events triggered is not defined, nor should one event rely on the execution of another event.
Rest events can be bound to:
- http response code
- thrown exception
- or both
This is the place to trigger some async operation via event bus, or some other response based operation.
A RestEvent processor must implement the RestEvent interface (similar to ResponseWriters). The event input is either the
produced response entity or the exception thrown.
If the event/entity pair does not match, the event is not triggered.
@GET
@Path("trigger/{status}")
@Events({@Event(SimpleEvent.class), // triggered on OK respons >=200 <300
@Event(value = FailureEvent.class, exception = IllegalArgumentException.class), // triggered via exception thrown
@Event(value = SimpleEvent.class, response = 301)}) // triggered on response code 301
public Dummy returnOrFail(@PathParam("status") int status){
if(status>=200&&status< 300){
return new Dummy("one","event");
}
if(status>=300&&status< 400){
response.setStatusCode(301);
return new Dummy("two","failed");
}
throw new IllegalArgumentException("Failed: "+status);
}
public class SimpleEvent implements RestEvent<Dummy> {
@Override
public void execute(Dummy entity, RoutingContext context) throws Throwable {
System.out.println("Event triggered: " + entity.name + ": " + entity.value);
context.vertx().eventBus().send("rest.vertx.testing", JsonUtils.toJson(entity)); // send as JSON to event bus ...
}
}
public class FailureEvent implements RestEvent<Exception> {
@Override
public void execute(Exception entity, RoutingContext context) throws Throwable {
log.error("Error: ", entity);
}
}
version 0.7.4 or later
Router router=new RestBuilder(vertx)
.enableCors("*",true,1728000,allowedHeaders,HttpMethod.OPTIONS,HttpMethod.GET)
.register(apiRest) // /api endpoint
.notFound(RestNotFoundHandler.class) // rest not found (last resort)
.build();
or
RestRouter.enableCors(router, // to bind handler to
allowedOriginPattern, // origin pattern
allowCredentials, // alowed credentials (true/false)
maxAge, // max age in seconds
allowedHeaders, // set of allowed headers
methods) // list of methods or empty for all
Unhandled exceptions can be addressed via a designated ExceptionHandler:
- for a given method path
- for a given root path
- globally assigned to the RestRouter
NOTE: An exception handler is a designated response writer bound to a Throwable class
If no designated exception handler is provided, a default exception handler kicks in trying to match the exception type with a build in exception handler.
Exception handlers are bound to an exception type - first matching exception / handler pair is used.
public class MyExceptionClass extends Throwable {
private final String error;
private final int status;
public MyExceptionClass(String message, int code) {
error = message;
status = code;
}
public String getError() {
return error;
}
public int getStatus() {
return status;
}
}
// bind exception handler to exception type
public class MyExceptionHandler implements ExceptionHandler<MyExceptionClass> {
@Override
public void write(MyExceptionClass result, HttpServerRequest request, HttpServerResponse response) {
response.setStatusCode(result.getCode());
response.end(result.getError());
}
}
...
// throw your exception
@GET
@Path("/throw")
@CatchWith(MyExceptionHandler.class)
public String fail(){
throw new MyExceptionClass("Not implemented.",404);
}
> GET /throw -> 404 Not implemented
Both class and methods support @CatchWith annotation.
@CatchWith annotation must provide an ExceptionHandler implementation that handles the thrown exception:
@GET
@Path("/test")
@CatchWith(MyExceptionHandler.class)
public String fail(){
throw new IllegalArgumentExcetion("Bang!");
}
public class MyExceptionHandler implements ExceptionHandler<Throwable> {
@Override
public void write(Throwable result, HttpServerRequest request, HttpServerResponse response) {
response.setStatusCode(406);
response.end("I got this ... : '" + result.getMessage() + "'");
}
}
Alternatively multiple handlers can be bound to a method / class, serving different exceptions.
Handlers are considered in order given, first matching handler is used.
@GET
@Path("/test")
@CatchWith({IllegalArgumentExceptionHandler.class, MyExceptionHandler.class})
public String fail(){
throw new IllegalArgumentException("Bang!");
}
public class IllegalArgumentExceptionHandler implements ExceptionHandler<IllegalArgumentException> {
@Override
public void write(IllegalArgumentException result, HttpServerRequest request, HttpServerResponse response) {
response.setStatusCode(400);
response.end("Invalid parameters '" + result.getMessage() + "'");
}
}
public class MyExceptionHandler implements ExceptionHandler<MyExceptionClass> {
@Override
public void write(MyExceptionClass result, HttpServerRequest request, HttpServerResponse response) {
response.setStatusCode(result.getStatus());
response.end(result.getError());
}
}
The global error handler is invoked in case no other error handler is provided or no other exception type maches given
handlers.
In case no global error handler is associated a default (generic) error handler is invoked.
Router router=RestRouter.register(vertx,SomeRest.class);
RestRouter.getExceptionHandlers().register(MyExceptionHandler.class);
vertx.createHttpServer()
.requestHandler(router)
.listen(PORT.get());
or alternatively we bind multiple exception handlers.
Handlers are considered in order given, first matching handler is used.
Router router = RestRouter.register(vertx, SomeRest.class);
RestRouter.getExceptionHandlers().register(MyExceptionHandler.class, GeneralExceptionHandler.class);
version 0.7.4 or later
To ease page/resource not found handling a special notFound() handler can be be utilized.
We can
- handle a subpath (regular expression pattern) where a handler was not found
- handle all not matching requests
Router router=new RestBuilder(vertx)
.register(MyRest.class)
.notFound(".*\\/other/?.*",OtherNotFoundHandler.class) // handle all calls to an /other request
.notFound("/rest/.*",RestNotFoundHandler.class) // handle all calls to /rest subpath
.notFound(NotFoundHandler.class) // handle all other not found requests
.build();
or
RestRouter.notFound(router,"rest",RestNotFoundHandler.class);
The not found handler must extend NotFoundResponseWriter:
public class NotFoundHandler extends NotFoundResponseWriter {
@Override
public void write(HttpServerRequest request, HttpServerResponse response) {
response.end("404 HTTP Resource: '" + request.path() + "' not found!");
}
}
version 0.8 or later
Rest.vertx simplifies serving of static resource files. All you need to do is to create a REST endpoint that returns the relative path of the desired resource file, bound with FileResponseWriter writer.
For example:
@Path("docs")
public class StaticFileRest {
@GET
@Path("/{path:.*}")
@ResponseWriter(FileResponseWriter.class)
public String serveDocFile(@PathParam("path") String path) {
return "html/" + path;
}
}
will load resource file in html/{path} and return it's content.
> GET docs/page.html -> returns page.html content via FileResponseWriter
version 0.9.1 or later
Example of a REST endpoint handling file upload.
// 1. provide a BodyHandler
BodyHandler bodyHandler=BodyHandler.create("my_upload_folder");
RestBuilder builder=new RestBuilder(vertx)
.bodyHandler(bodyHandler)
.register(UploadFileRest.class);
// 2. implement File upload rest to handle incoming files
@Path("/upload")
public class UploadFileRest {
@POST
@Path("/file")
public String upload(@Context RoutingContext context) {
Set<FileUpload> fileUploadSet = context.fileUploads();
if (fileUploadSet == null || fileUploadSet.isEmpty()) {
return "missing upload file!";
}
Iterator<FileUpload> fileUploadIterator = fileUploadSet.iterator();
List<String> urlList = new ArrayList<>();
while (fileUploadIterator.hasNext()) {
FileUpload fileUpload = fileUploadIterator.next();
urlList.add(fileUpload.uploadedFileName());
}
return StringUtils.join(urlList, ", ");
}
}
version 0.8.1 or later
By default all REST utilize vertx().executeBlocking() call. Therefore the vertx event loop is not blocked. It will utilize the default vertx thread pool:
DeploymentOptions options=new DeploymentOptions();
options.setWorkerPoolSize(poolSize);
options.setMaxWorkerExecuteTime(maxExecuteTime);
options.setWorkerPoolName("rest.vertx.example.worker.pool");
vertx.deployVerticle(new RestVertxVerticle(settings),options);
Responses are always terminated (ended).
If desired a REST endpoint can return io.vertx.core.Future and will be executed asynchronously waiting for the future object to finish. If used with non default (provided) HttpResponseWriter the response must be terminated manually.
This should be used in case we need to use a specific vertx worker pool
... thus we can manually execute the Future<> with that specific worker pool.
The output writer is determined upon the Future type returned. If returned future object is null then due to Java generics limitations, the object type can not be determined. Therefore the response will be produced by the best matching response writer instead.
suggestion: wrap null responses to object instances
Deprecated example for vert.x 3, applicable to versions prior 1.0.4
since vert.x 4 Promise and CompletableFuture should be used instead
WorkerExecutor executor=Vertx.vertx().createSharedWorkerExecutor("SlowServiceExecutor",20);
@GET
@Path("async")
public Future<Dummy> create(@Context Vertx vertx)throws InterruptedException{
Future<Dummy> res=Future.future();
asyncCall(executor,res);
return res;
}
public void asyncCall(WorkerExecutor executor,Future<Dummy> value)throws InterruptedException{
executor.executeBlocking(
fut->{
try{
Thread.sleep(1000);
}
catch(InterruptedException e){
value.fail("Fail");
}
value.complete(new Dummy("async","called"));
fut.complete();
},
false,
fut->{}
);
}
version 8.0 or later
Allows @Inject (JSR330) injection of RESTs, writers and readers.
To provide injection an InjectionProvider interface needs to be implemented.
Router router=new RestBuilder(vertx)
.injectWith(GuiceInjectionProvider.class)
.register(GuicedRest.class)
.build();
or
RestRouter.injectWith(GuiceInjectionProvider.class);
Following is a simple implementation of a Guice injection provider.
public class GuiceInjectionProvider implements InjectionProvider {
private final Injector injector;
public GuiceInjectionProvider(Module[] modules) {
injector = Guice.createInjector(modules);
}
@SuppressWarnings("unchecked")
@Override
public Object getInstance(Class clazz) {
return injector.getInstance(clazz);
}
}
Router router=new RestBuilder(vertx).injectWith(new GuiceInjectionProvider(getModules())).build();
vertx.createHttpServer()
.requestHandler(router)
.listen(PORT.get());
private Module[]getModules(){
return new Module[]{
new ServiceModule(),
new SecurityModule()...
};
}
import javax.ws.rs.core.Context;
public MyServiceImpl implements MyService{
private final OtherService other;
@Inject
public MyServiceImpl(OtherService service){
other=service;
}
public String call(){
return"something";
}
}
@Path("rest")
public class GuicedRest {
private final MyService service;
@Inject
public GuicedRest(MyService someService) {
service = someService;
}
@GET
@Path("test")
public String get() {
return service.call();
}
}
Injection can also be used od RequestReader, ResponseWriters or ExceptionHandler if needed.
since version 0.8.1 or later
Rest api classes can not use @Context fields, @Context is provided via method parameters instead.
In case needed a RequestReader, ResponseWriter or ExceptionHandler can use a @Context annotated field, see Request context for details.
Use @Context fields only when really necessary, as the readers, writers and handlers are not cached but initialized on the fly on every request when needed.
This is done in order to ensure thread safety, so one context does not jump into another thread.
public class StringWriter implements HttpResponseWriter<String> {
@Context
RoutingContext context;
@Override
public void write(String path, HttpServerRequest request, HttpServerResponse response) throws FileNotFoundException {
if (context.data().get("myData") == null) {
...
} else { ...}
}
since version 0.8.1 or later
- All registered REST classes are singletons by default, no need to annotate them with @Singleton annotation.
- By default, all HttpResponseWriter, ValueReader, ExceptionHandler, RestAuthenticationProviders and AuthoriziationProvider classes are singletons that are cached once initialized.
- In case HttpResponseWriter, ValueReader, ExceptionHandler, RestAuthenticationProviders and AuthoriziationProvider are utilizing a @Context field they are initialized on every request for thread safety
since version 0.8.6 or later
To disabled caching use the @NoCache annotation.
@NoCache
public class NotCachedClass() {
}
since version 0.8.4 or later
Rest.vertx can utilize any JSR 380 validation implementation, we only need to provide the appropriate validator
implementation.
For instance we can use Hibernate implementation:
HibernateValidatorConfiguration configuration=Validation.byProvider(HibernateValidator.class)
.configure();
Validator validator=configuration.buildValidatorFactory().getValidator();
Link validator with rest.vertx:
Router router=new RestBuilder(vertx)
.validateWith(validator)
.register(Rest.class)
.build();
or
RestRouter.validateWith(validator);
and annotate REST calls:
@POST("valid")
public int sum(@Max(10) @QueryParam("one") int one,
@Min(1) @QueryParam("two") int two,
@Valid Dummy body){
return one+two+body.value;
}
In case of a violation a 400 Bad request response will be generated using ConstraintExceptionHandler.
Additional to REST endpoints @Produces can also be applied to response writers.
This will add the appropriate content-type header to the output, plus will register writer to the given content-type if
no other association is given.
Example:
@Produces("application/json")
public class JsonExceptionHandler implements ExceptionHandler<String> {
@Override
public void write(String result, HttpServerRequest request, HttpServerResponse response) {
...
}
}
since version 0.8.4 or later
The @Header annotation adds one or multiple static header to the response. It can be applied either to REST endpoints or to response writers.
Example:
@Header("X-Status-Reason: Validation failed")
public class ConstraintExceptionHandler implements ExceptionHandler<ConstraintException> {
@Override
public void write(ConstraintException result, HttpServerRequest request, HttpServerResponse response) {
...
}
}
since version 1.0.4 or later
The @Header annotation can be used instead of @Consumes and @Produces annotation directly on the REST endpoint. Rest.vertx assigns 'Accept' headers to readers and 'Content-Type' and all other response headers to writers.
Example:
@GET
@Path("/produce")
@Header({"Accept: application/json", "Content-Type: application/json"})
public String getAsJson(){
return"I'm Johnson";
}
// is the same as
@GET
@Path("/produce")
@Consumes("application/json")
@Produces("application/json")
public String getAsJson(){
return"I'm Johns son!";
}
On a request @Context must be dynamically provided by the method using it. This means that no caching is possible and
every class or chain of classes must be created on each and every request.
This might cause unecessary processing overhead - avoid it if you can.
The intended workflow is as follows:
- request gets read by RequestReader and deserialized into a Java Object
- REST method uses the request input to call a service and produce a result
- REST method returns the result as a Java Object
- RequestWriter or ExceptionHandler take the produced result and write the vertx response
Rest.vertx uses Slf4j logging API. In order to see all messages produced by Rest.vertx use a Slf4j compatible logging implementation.
<logger name="com.zandero.rest" level="DEBUG"/>
Instead of the following:
@GET
@Path("/test")
@Consumes("application/json")
@Produces("application/json")
public String method(){...}
a shorthand form can be used combining all into one
@Get("/test")
@Consumes("application/json")
@Produces("application/json")
public String method(){...}
or even:
@Get(value = "/test", consumes = "application/json", produces = "application/json")
public String method(){...}
Tips and tricks to writing unit tests for your REST endpoints.
For instance, we have the following REST endpoint
import javax.ws.rs.Produces;
@Path("test")
public class EchoRest {
@GET
@Path("echo")
@Produces("application/json")
public String echo() {
return "echo";
}
}
We can test the REST endpoint like this:
NOTE: once a ExceptionHandler, Writer, Reader ... etc. is registered it is cached. Therefore before each test it is recommended to call RestRouter.clearCache() to make sure you have a clean set up.
@ExtendWith(VertxExtension.class)
class EchoTest {
public static final String API_ROOT = "/";
protected static final int PORT = 4444;
public static final String HOST = "localhost";
protected static Vertx vertx = null;
protected static VertxTestContext vertxTestContext;
protected static WebClient client;
public static void before() {
vertx = Vertx.vertx();
vertxTestContext = new VertxTestContext();
// Important ... this clears any registered writers/readers/exception handlers ...
// and provides a clean slate for the next test
RestRouter.clearCache();
client = WebClient.create(vertx);
}
@BeforeAll
static void start() {
before();
Router router = Router.router(vertx);
RestRouter.register(router, EchoRest.class);
vertx.createHttpServer()
.requestHandler(router)
.listen(PORT.get());
}
@AfterEach
void lastChecks(Vertx vertx) {
vertx.close(vertxTestContext.succeedingThenComplete());
}
@AfterAll
static void close() {
vertx.close();
}
@Test
void getEcho(VertxTestContext context) {
client.get(PORT.get(), HOST, "/test/echo").as(BodyCodec.string())
.send(context.succeeding(response -> context.verify(() -> {
assertEquals(200, response.statusCode());
assertEquals("\"echo\"", response.body());
context.completeNow();
})));
}
}
The @BeforeAll, @AfterEach and @AfterAll methods should be moved into a helper class from there all test classes can be extended.