Skip to content

Retiring Model Binding

Travis Parks edited this page Nov 8, 2020 · 13 revisions

During my initial implementation of Javalin MVC, I made the choice to use a ModelBinder class. With appropriate caching and optimization, there's little runtime overhead for using this approach. Furthermore, most applications will mostly utilize JSON models which are going to require significantly more runtime reflection to parse than any overhead the model binder will incur.

Some negatives of using a model binder is it always works in terms of objects. If you are binding a primitive, such as a an int, there's an unavoidable boxing and unboxing operation which puts the integer on the heap (Integer) and then immediately converts it back to an int. The overhead is even more exaggerated when binding object fields and setters. Firstly, setting the field or calling the setter must be done reflectively, similar to how JSON parsing works. Secondly, the same boxing/unboxing occurs for each property. This could be avoided partially but it's probably not worth it.

Since Javalin MVC is a compile-time annotation processor tool, it makes more sense to try to generate code at compile time. For example, if the route is expecting an int like so:

public ActionResult getGreeting(String name, int age) {
    return new ContentResult("Hello, " + name + "! You are " + age + " years old!");
}

At compile time I can generate a handful of helper methods:

private static int getInteger(String value) {
    try {
        return Integer.parse(value);
    } catch (NumberFormatException ex) {
        return 0;
    }
}

private static String getHeader(HttpRequest request, String key) {
    return request.getHeader(key);
}

// ... and other value source getters

private static String getValue(HttpRequest request, String key) {
    if (request.hasHeader(key)) {
        return request.getHeader(key);
    } /** Other value source getters **/
    else {
        return null;
    }
}

// ... and then call the controller method
ActionResult result = controller.getGreeting(getValue(request, "name"), getInteger(getValue(request, "age")));

This is effectively the same code you'd write by hand. Take special note of the getValue method. If the source of the data isn't specified with a @From* annotation, it will look through each data source one after the other until a match is found. This logic is already present in the current implementation, although it's highly recommended for the absolutely best performance to always specify the source. This is especially important when the value provides security information, like an authentication header - you probably wouldn't want to allow such a value to be overridden with a query string, for example.

One piece of functionality you'd lose using generated code is the ability to set private backing fields. Technically private fields and setters could be set reflectively by generating code to set them reflectively, but this feels like it's a step back. To me, I think dumping this functionality is justifiable.

I am really excited to pursue this idea. It has the potential to make it extremely simple to generate blisteringly fast code.

Update 11/08/2020

I've already made really good progress in just a couple days. At this point all primitives, boxed types, Java 8 date types, BigInteger, BigDecimal, and UUIDs are using compile time code generation to avoid the model binder. I started making similar progress on WebSockets but realized I need significantly better unit test coverage there. I almost want to wait in WebSockets to get binding complex types. I even think there might be a way to support nested complex types without too much work. I feel like I should be done soon.

I also had the idea that I should allow registering arbitrary conversion methods. Each method could be static or a member and would be decorated with an Converter("converter-name") annotation. The processor would check that the method accepted an HttpContext and the parameter name. Then the action method parameter or model field/setter would be decorated using UseConverter("my-converter"). This will allow compete control over the conversion process. For non-static conversion methods, the surrounding class will be instantiated using the injector if necessary.

Clone this wiki locally