wissel.net

Usability - Productivity - Business - The web - Singapore & Twins

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:
  • 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)
Ideally when defining a route and a handler, a verticle or a potential message for the EventBus, all necessary code stays contained in the respective source code file. The challenge here is to register the components when the application starts. Your main Verticle doesn't know what components are in your application and manually maintain a loader code is a pain to keep in sync (besides leading to merge conflicts when working in a team).
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 on 02 April 2016 | Comments (0) | categories: vert.x

Comments

  1. No comments yet, be the first to comment