Annotations to supercharge your vert.x development
ProjectCastle is well under way. Part of it, the part talking to Domino, is written in Java8 and vert.x. With some prior experience in node.js development vert.x will look familiar: base on event loop and callbacks, you develop in a very similar way. The big differences: vert.x runs on the JVM8, it is by nature of the JVM multi-threaded, features an event bus and is polyglot - you can develop in a mix of languages: Java, JavaScript, Jython, Groovy etc.
This post reflects some of the approaches I found useful developing with vert.x in Java. There are 3 components which are core to vert.x development:
Java annotations to the rescue! If you are new to annotations, go and check out this tutorial to get up to speed. For my project I defined three of them, with one being able to be applied multiple times.
and the repeatability annotation (new with Java8):
Since traversing all classes is expensive, we have one method that does that once calling the 3 different annotation processors once for each class
Left as an exercise to the reader: runs the analysis at compile time instead of runtime.
As usual: YMMV
This post reflects some of the approaches I found useful developing with vert.x in Java. There are 3 components which are core to vert.x development:
-
Verticle
A unit of compute running with an event loop. Usually you start one Verticle (optional with multiple instances) as your application, but you might want/need to start additional ones for longer running tasks. A special version is the worker verticle, that runs from a thread pool to allow execution of blocking operations -
EventBus
The different components of your application message each other via the EventBus. Data send over the EventBus can be a String, a JsonObject or a buffer. You also can send any arbitrary Java class as message once you have defined a codec for it -
Route
Like in node.js a vert.x web application can register routes and their handlers to react on web input under various conditions. Routes can be defined using URLs, HTTP Verbs, Content-Types ( for POST/PUT/PATCH operations)
Java annotations to the rescue! If you are new to annotations, go and check out this tutorial to get up to speed. For my project I defined three of them, with one being able to be applied multiple times.
CastleRequest
A class annotated with CastleRequest registers its handler with the EventBus, so the class can be sent over the EventBus and get encoded/decode appropriately. A special value for the annotation is "self" which indicates, that the class itself implements the MessageCodec interface
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface CastleRequest {
// We use value to ease the syntax
// to @CastleRequest(NameOfCodec)
// Special value: self = class implements the MessageCodec interface
String value();
}
CastleRoute
This annotation can be assigned multiple times, so 2 annotation interfaces are needed
@Documented
@Repeatable(CastleRoutes.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface CastleRoute {
String route();
String description();
String mimetype() default "any";
String method() default "any";
}
and the repeatability annotation (new with Java8):
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface CastleRoutes {
CastleRoute[] value();
}
CastleVerticle
Classes marked with this annotation are loaded as verticles. They can implement listeners to the whole spectrum of vert.x listening capabilities
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface CastleVerticle {
String type() default "worker";
int instances() default 0;
boolean multithreaded() default false;
}
Loading it all
With a little glue code (see below), all Verticles, Routes and Codecs get loaded by looking up the annotations. You add a new file or remove one: no additional action is required.Since traversing all classes is expensive, we have one method that does that once calling the 3 different annotation processors once for each class
private void loadClassesByAnnotation(final Router router) {
try {
// Get the classes from the current loader to check for more
final ClassPath classPath = ClassPath.from(this.getClass().getClassLoader());
// Extract the package name from the full class name
String packageName = this.getClass().getName();
packageName = packageName.substring(0, packageName.lastIndexOf("."));
// Get all classes in this tree
for (final ClassInfo classInfo : classPath.getTopLevelClassesRecursive(packageName)) {
@SuppressWarnings("rawtypes")
final Class candidate = classInfo.load();
this.loadVerticleByAnnotation(candidate);
this.loadCodecByAnnotation(candidate);
this.loadRouteByAnnotation(candidate);
}
} catch (IOException e) {
this.logger.error(e);
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void loadCodecByAnnotation(final Class candidate) {
EventBus eventBus = this.vertx.eventBus();
final Annotation[] annotation = candidate.getAnnotationsByType(CastleRequest.class);
// Registering all Codecs
for (int i = 0; i < annotation.length; i++) {
try {
final CastleRequest cr = (CastleRequest) annotation[i];
final String codecClassName = cr.value();
Object codecCandidate;
if (codecClassName.equalsIgnoreCase("self")) {
// The object contains its own message codec
codecCandidate = candidate.newInstance();
} else {
codecCandidate = Class.forName(codecClassName).newInstance();
}
if (codecCandidate instanceof MessageCodec) {
MessageCodec messageCodec = (MessageCodec) codecCandidate;
eventBus.registerDefaultCodec(candidate, messageCodec);
} else {
this.logger.error("Class with CastleRequest Annotation has wrong codec type:" + codecClassName);
}
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
this.logger.error(e);
}
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private void loadRouteByAnnotation(final Class candidate) {
final Annotation[] annotation = candidate.getAnnotationsByType(CastleRoute.class);
for (int i = 0; i < annotation.length; i++) {
try {
final CastleRoute cr = (CastleRoute) annotation[i];
// We got a class that shall be used as a router
if (CastleRouteHandler.class.isAssignableFrom(candidate)) {
// If a class has multiple routes assigned we might load them more
// than once but that's acceptable
final Class
toBeloaded = candidate;
CastleRouteHandler crh = toBeloaded.newInstance();
// Finally initialization and recording in our list
crh.init(this.vertx, router, cr.method(), cr.route(), cr.mimetype(), cr.description());
this.routeList.put(cr.route(), cr.description());
this.routeHandlers.add(crh);
} else {
this.logger.error("Class with CastleRouter Annotation has wrong type:" + candidate.getName());
}
} catch (InstantiationException | IllegalAccessException e) {
this.logger.error(e);
}
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void loadVerticleByAnnotation(final Class candidate) {
final Annotation[] annotation = candidate.getAnnotationsByType(CastleVerticle.class);
for (int i = 0; i < annotation.length; i++) {
if (Verticle.class.isAssignableFrom(candidate)) {
final CastleVerticle cv = (CastleVerticle) annotation[i];
final String verticleID = candidate.getName();
final DeploymentOptions options = new DeploymentOptions();
if (cv.type().equalsIgnoreCase("worker")) {
options.setWorker(true);
}
// Overwrite the instances if number is specified
if (cv.instances() > 0) {
options.setInstances(cv.instances());
}
options.setMultiThreaded(cv.multithreaded());
this.vertx.deployVerticle(verticleID, options, result -> {
if (result.succeeded()) {
this.logger.info(verticleID + " started as " + result.result());
this.localVerticles.add(result.result());
} else {
this.logger.error(result.cause());
}
});
} else {
this.logger.error("Class with CastleVerticle Annotation has wrong type:" + candidate.getName());
}
}
}
public interface CastleRouteHandler {
public String getDescription();
// Return the actual route
public String getRoute();
// The methods that needs to be overwritten in each instance
// One for each method we do support
public void handleDelete(final RoutingContext ctx);
public void handleGet(final RoutingContext ctx);
public void handleHead(final RoutingContext ctx);
public void handleOptions(final RoutingContext ctx);
public void handlePost(final RoutingContext ctx);
public void handlePut(final RoutingContext ctx);
public void handlePatch(final RoutingContext ctx);
// initialize the actual route
public AbstractCastleRouteHandler init(final Vertx vertx, final Router router, final String method, final String route, String mimetype, String description);
}
public AbstractCastleRouteHandler init(final Vertx vertx, final Router router, final String method, final String route,
final String mimetype, final String description) {
this.vertx = vertx;
this.description = description;
this.route = route;
this.logger.info("Loading " + this.getClass().getName() + " for " + this.getRoute() + " .. " + description);
Route localRoute = null;
if ("any".equalsIgnoreCase(method) || "GET".equalsIgnoreCase(method)) {
localRoute = router.get(route).handler(this::handleGet);
}
if ("any".equalsIgnoreCase(method) || "POST".equalsIgnoreCase(method)) {
localRoute = router.post(route).handler(this::handlePost);
}
if ("any".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method)) {
localRoute = router.put(route).handler(this::handlePut);
}
if ("any".equalsIgnoreCase(method) || "PATCH".equalsIgnoreCase(method)) {
localRoute = router.put(route).handler(this::handlePatch);
}
if ("any".equalsIgnoreCase(method) || "DELETE".equalsIgnoreCase(method)) {
localRoute = router.delete(route).handler(this::handleDelete);
}
if (localRoute != null && !"any".equalsIgnoreCase(mimetype)) {
localRoute.consumes(mimetype);
}
// Methods that need to be always available
router.options(route).handler(this::handleOptions);
router.options(route).handler(this::handleHead);
return this;
}
Left as an exercise to the reader: runs the analysis at compile time instead of runtime.
As usual: YMMV
Posted by Stephan H Wissel on 02 April 2016 | Comments (0) | categories: vert.x