-
Notifications
You must be signed in to change notification settings - Fork 48
Kontraktor 4 Async REST
The main issue with synchronous/multithreaded request processing is the limit in the number of threads. Usually a rest service needs to talk to other services via network (e.g. database or other services (microservice or SOA)), the overall system throughput is determined by the maximum number of concurrent requests (in-flight), not by raw processing power. Therefore async processing (does not require a thread-per-request) excels on throughput, even if raw single client performance tests suggest a different result (see nodejs vs java servlet based apps).
Kontraktor provides a simple convention based mapping of REST requests to actor methods to @reduce @annotation @mess.
A REST Actor can be published on a base path which then is automatically removed from REST requests.
MyRESTActor act = AsActor(MyRESTActor.class);
Http4K.Build("localhost", 8080)
.restAPI("/api", act )
.build();
The method name is derived from the http request method ("GET","PUT","POST", ..) in combination with the first path element given.
E.g. an API published under "/api" the request
curl -i -X GET http://localhost:8080/api/books/234234
will result in the method name getBooks(..)
.
if no matching method has been found, an attempt is made to look for a plain 'get' / 'post' .. method named like the http method of the incoming request.
E.g.:
public IPromise get(String[] path, @RequestPath String rpath ) {
Log.Info(this,"GET "+rpath);
return resolve(404);
}
public IPromise post(String[] path, @RequestPath String rpath, byte[] data ) {
Log.Info(this,"POST "+rpath);
return resolve(404);
}
Once the method name is determined, kontraktor tries to map arguments of the matching method to remaining path of a REST request (query params and some types are handled different see below).
E.g. public IPromise getBooks( int id )
will try to parse an int (234234) from the request path http://localhost:8080/api/books/234234
.
The following types can be parsed: int, long, double, String
A http-request also has a header, optionally a request body, the url might contain 'query parameters'.
Request header
The request header is injected into parameters of type HeaderMap
(if present).
Query params
All query parameters are injected into parameters of type Map<String,Deque<String>>
(if present).
Additionally its possible to parse single query params using the @FromQuery
annotation.
request path (>=version 4.20.1)
Gets injected into arguments of type String[]
or annotated with "@RequestPath"
E.g.:
//curl -i -X PUT --data "{ \"name\": \"satoshi\", \"nkey\": 13345 }" 'http://localhost:8080/api/user/nakamoto/?x=simple&something=pokpokpok&x=13&age=111'
public IPromise putUser(String name, @FromQuery("age") int age, JsonObject body, Map<String,Deque<String>> queryparms)
Request Body
The request body is injected into parameters of type byte[]
(utf-8) or JsonObject
(parsed json).
Responses are returned async (IPromise). Check docs on how kontraktor's Actors/Promises work.
-
prom.resolve("A String response")
results in a 200 response with given String -
prom.resolve( someSerializablePojo )
results in a json-serialized (fst style) kson response -
prom.resolve( 400 )
results in an empty response with given http status code -
prom.resolve( new Pair(403), "not this time" )
results in a response with given status code and given String content
An Actor is single threaded, therefore you should not do any blocking operations or heavy computing on the actor thread.
To execute blocking calls (e.g. database query) use a kontraktor's threadpool which enables thread safe delivery of a result object:
public class RESTActor<T extends RESTActor> extends Actor<T> {
public IPromise getBooks( int id ) {
Promise res = promise();
execInThreadPool( () -> {
// simulate blocking operation (e.g. database query)
return new Book().title("Title "+id).id(""+id).author("kontraktor");
})
.then(res);
return res;
}
}
see examples/REST for a full source example project.
public class RESTActor<T extends RESTActor> extends Actor<T> {
// curl -i -X GET http://localhost:8080/api/books/234234
public IPromise getBooks( int id ) {
Promise res = promise();
// simulate blocking operation (e.g. database query)
execInThreadPool( () -> new Book().title("Title "+id).id(""+id).author("kontraktor") ).
then(res);
return res;
}
//curl -i -X POST --data "param1=value1¶m2=value2" http://localhost:8080/api/stuff
public IPromise postStuff( byte[] body ) {
Log.Info(this,"posted:"+new String(body));
return resolve(200);
}
//curl -i -X POST --data "{ \"key\": \"value\", \"nkey\": 13 }" http://localhost:8080/api/stuff1
public IPromise postStuff1(JsonObject body, HeaderMap headerValues ) {
headerValues.forEach( hv -> {
Log.Info(this,""+hv.getHeaderName());
hv.forEach( s -> {
Log.Info(this," "+s);
});
});
Log.Info(this,""+body);
return resolve(new Pair(202,body.toString()));
}
//curl -i -X PUT --data "{ \"name\": \"satoshi\", \"nkey\": 13345 }" 'http://localhost:8080/api/user/nakamoto/?x=simple&something=pokpokpok&x=13&age=111'
@ContentType("application/json")
public IPromise putUser(String name, @FromQuery("age") int age, JsonObject body, Map<String,Deque<String>> queryparms) {
Log.Info(this,"name:"+name+" age:"+age);
queryparms.forEach( (k,v) -> {
Log.Info(this,""+k+"=>");
v.forEach( s -> {
Log.Info(this," "+s);
});
});
return resolve(body);
}
public static void main(String[] args) {
RESTActor act = AsActor(RESTActor.class);
Http4K.Build("localhost", 8080)
.restAPI("/api", act )
.build();
}
}