Vert.x and OpenAPI
In the shiny new world of the API Economy your API definition and its enforcement is everything. The current standard for REST based APIs is OpenAPI. What it gives you is a JSON or YAML file that describes how your API looks like. There is a whole zoo of tools around that allow to visualize, edit, run Mock servers or generate client and server code.
My favorite editor for OpenAPI specs is Apicurio, a project driven by RedHat. It strikes a nice balance between being UI driven and leaving you access to the full source code of your specification.
What to do with it
Your API specification defines:
- the endpoints (a.k.a the URLS that you can use)
- the mime types that can be sent or will received
- the parameters in the path (the URL)
- the parameters in the query (the part that looks like
?color=red&shape=circle
) - the body to send and receive
- the authentication / authorization requirements
- the potential status codes (we love 2xx)
To handle all this, it smells like boilerplate or, if you are lucky, ready library. vert.x has the later. It provides the API Contract module that is designed to handle all this for you. You simply add the module to your pom.xml
and load your json or yaml OpenApi specification file:
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-api-contract</artifactId>
<version>3.8.1</version>
</dependency>
The documentation shows the code to turn the OpenApi speccification into a Router factory:
OpenAPI3RouterFactory.create(
vertx,
"https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v3.0/petstore.yaml",
ar -> {
if (ar.succeeded()) {
// Spec loaded with success
OpenAPI3RouterFactory routerFactory = ar.result();
} else {
// Something went wrong during router factory initialization
Throwable exception = ar.cause();
}
});
As you can see, you can load the spec from an URL (there's an auth option too). So while your API is evolving using Apicurio you can live load the latest and greated from the live preview (should make some interesting breakages ;-) ).
You then add your routes using routerFactory.addHandlerByOperationId("awesomeOperation",this::awesomeOperationHandler)
. Vert.x doesn't use the path to match the handler, but the operationId. This allows you to update path information without breaking your code. There is a detailed how-to document describing the steps.
Generate a skeleton for vert.x
As long as you haven't specified a handler for an operation, Vert.x will automatically reply with 501 Not implemented
and not throw any error. To give you a headstart, you can generate the base code. First option is to head to start.vertx.io to generate a standard project skeleton, saving you the manual work of creating all dependencies in your pom.xml
file. Using "Show dependency panel" provides a convenient way to pick the modules you need.
But there are better ways. You can use an OpenAPI Generator or the advanced Vert.x Starter courtesy of Paulo Lopes. In his tool you specify what it shall generate in a dropdown that defaults to "Empty Project". Once you change that to "OpenAPI Server" the form will alloow you to upload your OpenAPI specification and you get a complete project rendered with all handler stubs including the security handler. There's also a JavaScript version available.
Auto-load route handlers
Having a skeleton for starters makes it easy to understand the moving parts. However when your API is still rapidly evolving you constantly need to edit your verticle containing the RouterFactory to add new handlers using addHandlerByOperationId
. So I thought: a little reflection can go a long way.
What if the OperationId determines the class name of the handler to use? I used the following constraints:
- All handlers are members of the same Java package
- All handlers use the same constructor. What worked for me: a constructor giving the handler access to vert.x and my configuration in form of a JSON object
- The name of the handler is OperationIdHandler where, to conform with Java practises the first letter would be capitalized
As a result a few lines of code will try to load handler classes for all routes presented in the OpenAPI specs.
Starting the verticle
@Override
public void start(final Promise<Void> startFuture) {
// String fileName =
// this.getClass().getResource(Constants.OPENAPI_FILE_NAME).getFile();
final String fileName = Constants.OPENAPI_FILE_NAME;
OpenAPI3RouterFactory.create(this.getVertx(),
fileName,
ar -> {
if (ar.succeeded()) {
final OpenAPI3RouterFactory routerFactory = ar.result();
this.mountHandlersAndListen(routerFactory, startFuture);
} else {
startFuture.fail(ar.cause());
}
});
}
Getting all operationIds from the OpenAPI spec
/**
* Extracts the operationId from the openapi.json file to get a list of required
* mount points
*
* @return list with operationIds
*/
private List<String> getOperationIds() {
final List<String> result = new ArrayList<>();
// Gets the openapi.json file content readily parsed into JSON
final JsonObject openApi = this.getOpenAPI();
final JsonObject paths = openApi.getJsonObject("paths", new JsonObject());
paths.forEach(path -> {
final JsonObject methods = (JsonObject) path.getValue();
methods.forEach(method -> {
final Object o = method.getValue();
if (o instanceof JsonObject) {
final JsonObject methodContent = (JsonObject) o;
final String operationID = methodContent.getString("operationId");
if (operationID != null) {
result.add(operationID);
}
}
});
});
return result;
}
Mounting routes
private void mountHandlersAndListen(final OpenAPI3RouterFactory routerFactory, final Promise<Void> startFuture) {
final RouterFactoryOptions rfo = new RouterFactoryOptions();
rfo.setMountNotImplementedHandler(true).setRequireSecurityHandlers(true);
final List<String> operationIds = this.getOperationIds();
// Path handlers
operationIds.forEach(id -> this.loadHandlers(routerFactory, id));
// Security handler basic
routerFactory.addSecurityHandler("basic", new BasicHandler());
// Security handler JWT
// Simple handler, don't use in production
final JWTAuth provider = JWTAuth.create(this.vertx, new JWTAuthOptions()
.addPubSecKey(new PubSecKeyOptions()
.setAlgorithm("HS256")
.setPublicKey(this.config().getString(Constants.CONFIG_JWTSECRET, Constants.CONFIG_JWTSECRET))
.setSymmetric(true)));
final AuthHandler authHandler = JWTAuthHandler.create(provider);
authHandler.addAuthority("user");
routerFactory.addSecurityHandler("jwt", authHandler);
final int port = 8089;
this.router = routerFactory.getRouter();
this.server = this.vertx.createHttpServer(new HttpServerOptions().setPort(port));
this.server.requestHandler(this.router).listen();
startFuture.complete();
}
Load available handlers
private void loadHandlers(final OpenAPI3RouterFactory routerFactory, final String id) {
final String className = "com.somepackage.handlers."
+ id.substring(0, 1).toUpperCase()
+ id.substring(1).toLowerCase()
+ "Handler";
try {
// All Handlers need a constructor that makes vertx and config available
final Class handlerClass = Class.forName(className);
final Class[] paramClasses = new Class[] { Vertx.class, JsonObject.class };
final Constructor constructor = handlerClass.getConstructor(paramClasses);
final Handler<RoutingContext> handler = (Handler) constructor.newInstance(this.getVertx(), this.config());
routerFactory.addHandlerByOperationId(id, handler);
this.logger.debug("Added operation id: " + id);
} catch (final Exception e) {
this.logger.warn("Could not load Handler:" + className);
}
}
Conclusion
Using this approach you don't need to touch your router Verticle. Once the OpenAPI spec is updated, the log will contain a warning about a missing class.
Implementing that class is the only action to get your new route operational.
As ususal YMMV!
Posted by Stephan H Wissel on 06 September 2019 | Comments (0) | categories: Java vert.x WebDevelopment