All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mastfrog.webapi.package.html Maven / Gradle / Ivy

There is a newer version: 2.9.7
Show newest version


    
        Generic Web API API
        
    
    
        

Generic Web API API

There is lots of support for writing REST APIs in Java; there are fewer options for writing REST clients (some existing ones are described here). This library is the result of its author needing to write a REST client one too many times and not being satisfied with the options, and wanting one with an asynchronous, callback-based API - if you are writing an asynchronous web service which needs to call another web service, and calling the remote service is not asynchronous too, then you instantly go from having a massively scalable web API to having one which is blocked doing I/O and scales worse than old-fashioned threaded alternatives.

Calling REST APIs is simple enough that it's commonplace to do it with plain old Java code. But what inevitably happens is that an entire codebase gets littered with boilerplate such as setting content types, literal paths and URLs and similar noise-code. Then one day the API changes, and finding all of the places the code must be updated is a long, expensive and painful job that usually misses some cases.

The idea here is to use some of Guice's injection features under the hood to make it very simple to define elements of a web API and pass arguments to construct a web call. So you deal in objects, and those get translated into an HTTP call in a well-defined way in a well-defined place, but you don't have to write a lot of code to do those calls.

It means that making a web call involves no repetitive boilerplate, and for most common cases, things like interpreting the response are handled. More importantly, it keeps things like URL paths all located in one place, so refactoring of a web api doesn't mean grepping your source code. To call a web api, you simply pass an enum constant representing that call to an Invoker, and some objects to flesh out the call's URL, headers and or body - and you can literally pass as many objects in whatever order as you want - the signature is invoker.call(Enum<?> call, Callback<T> callback, Object... args).

Defining the Web API you want to call

Your web api starts with a Java Enum which defines all of the calls in the API. The enum must implement WebCallEnum which has a single method, get() which returns an instance of WebCall. The enum serves as a handy way to reference API calls, and a way that the system (which uses Guice to bind object types) knows the entire list of types which might be used to construct web calls.

Here's a sample API with two calls, one which says hello and one which echoes back the request body sent to it.

public enum TestAPI implements WebCallEnum {

    HELLO_WORLD(new WebCallBuilder()
                        .method(Method.GET)
                        .addRequiredType(UserId.class)
                        .withDecorator(DisplayName.class, ParameterFromClassNameAndToStringCamelCase.class)
                        .path("/users/{{userid}}/hello").responseType(Map.class)),
    ECHO(new WebCallBuilder()
                        .method(Method.POST)
                        .addRequiredType(String.class)
                        .addRequiredType(UserId.class)
                        .withDecorator(String.class, BodyFromString.class)
                        .path("/users/{{userid}}/echo").responseType(String.class));

    private final WebCall call;
    TestAPI(WebCallBuilder bldr) {
        call = bldr.id(this).build();
    }

    public WebCall get() {
        return call;
    }
}
        

Calling The Web API

How do we use it? Very simply. First, we need an instance of an Invoker; you can either create one using Guice (you need to bind a base URL and an HttpClient), or use a static method to have it do that for you:
            Invoker<TestAPI> invoker = Invoker.create(URL.parse("http://server.example"), 
                TestAPI.class);
        
Then we need a callback which will be called when our call completes - it gets passed the result and can do whatever you need to with it:
Callback<Map> c = new Callback<Map>(Map.class) {
    public void success(Map object) {
        System.out.println("YAY: " + object);
    }
    public void fail(HttpResponseStatus status, ByteBuf bytes) {
        System.out.println("FAILED WITH " + status);
    }
};
        
Then we simply pass the callback, and some arguments, to the invoker:
ResponseFuture f = invoker.call(TestAPI.HELLO_WORLD, c, 
    new DisplayName("Tim Boudreau"), new UserId("tim"));
        
When the request completes, or if it fails, our callback will be called (there are a few other methods, such as exception handling, which can optionally be overridden).

Under the hood, what happens is this:

  1. We look up the path template tied to this web call - in this case /users/{{userid}}/hello. The {{userid}} part will be substituted. Fancy substitutions are possible by writing your own Interpolator, but "userid" matches the lower case name of the class UserId - and we passed one in. So its toString() is called, and "{{userid}}" is replaced with "tim".
  2. When we created the call, you may have noticed the line .withDecorator(DisplayName.class, ParameterFromClassNameAndToStringCamelCase.class). We have a DisplayName class which is also a wrapper for a string. ParameterFromClassNameAndToStringCamelCase is a Decorator which ships with this library. It simply takes the simple class name and lower-cases it, and uses toString() on the value to construct a query parameter. So the actual URL we will call is now http://server.example/users/tim/hello?displayName=Tim+Boudreau
  3. An HTTP request with an empty body is constructed and sent to the server.
  4. Our callback takes a Map as its parameter. So the code will take the response body, interpret it as JSON and convert the content to a Map (using Jackson) and pass that to our callback. We could also have asked for the content as an Image, String, ByteBuf, byte[], InputStream or Image, and it could have be unmarshalled transparently as any of those things; or if it is a type which Jackson can unmarshal from JSON, that can be done automatically. For custom unmarshalling, you simply implement Interpreter and add that to your WebCall.

ResponseFuture

Making an API call returns a ResponseFuture which you can use to cancel the call, or attach listeners to for events such as the response being sent, the headers being sent back, or even individual content chunks arriving.

It also has two await() methods which will block the calling thread until the call has completed. They are useful in unit tests and things like that; please don't use them in non-test code! For an overview of why Future is an antipattern, see this abstract Why There’s No Future in Java Futures .

Essentially, the number one rule of using an asynchronous, message-based API is don't block, ever.

Conclusions

The result is that calling a web api is dirt-simple, scalable, and relatively intuitive; and since the entire definition of the API lives in one place, refactoring is simple - no searching code or guessing required.




© 2015 - 2024 Weber Informatics LLC | Privacy Policy