A library for defining a REST web API as a Java enum
and calling
that API using POJOs for arguments and return values. Uses an
asynchronous Netty-based HTTP library
for HTTP communication, so few threads are used.
To define a web API, simply create an enum which implements WebCallEnum
as
described below; use a builder to define what the call should look like.
To call a web API, you invoke that web API, passing in the arguments it needs as a free-form array of objects plus a callback which will be invoked with the result. E.g.
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)),
...
}
That defines an HTTP call which issues a GET
request; a portion of the URL
is templated - it will be gotten from calling toString()
on a UserId
object
(i.e. some application specific class). The API wants a URL parameter called
"displayName" - we map that to toString()
on an instance of DisplayName
-
this could be any type of our own creation (and we could unmarshal it some other
way than toString()
). The response will be unmarshalled from JSON into a Map -
but we could use any type Jackson can handle here.
So to call it, we do
Invoker<TestAPI> invoker = Invoker.create(URL.parse("http://server.example"),
TestAPI.class);
To receive a call back with the response, we implement the Callback
interface,
and then call
ResponseFuture f = invoker.call(TestAPI.HELLO_WORLD, myCallback,
new DisplayName("Tim Boudreau"), new UserId("tim"));
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)</code>.
##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:
<pre>
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:
-
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 ownInterpolator
, but "userid" matches the lower case name of the classUserId
- and we passed one in. So itstoString()
is called, and "{{userid}}" is replaced with "tim". -
When we created the call, you may have noticed the line
.withDecorator(DisplayName.class, ParameterFromClassNameAndToStringCamelCase.class)
. We have aDisplayName
class which is also a wrapper for a string.ParameterFromClassNameAndToStringCamelCase
is aDecorator
which ships with this library. It simply takes the simple class name and lower-cases it, and usestoString()
on the value to construct a query parameter. So the actual URL we will call is nowhttp://server.example/users/tim/hello?displayName=Tim+Boudreau
-
An HTTP request with an empty body is constructed and sent to the server.
-
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 aMap
(using Jackson) and pass that to our callback. We could also have asked for the content as anImage
,String
,ByteBuf
(from Netty),byte[]
orInputStream
, 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 implementInterpreter
and add that to yourWebCall
.
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.
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.