1) Support for non-JSON bodies from REST end-points
Added support for String and byte[] to be passed without JSON parsing.
@RequestMapping(value ="/body/bytes", method =RequestMethod.POST)
publicboolean bodyPostBytes( byte[] body) {
String string =newString(body, StandardCharsets.UTF_8);
return string.equals("foo");
}
@RequestMapping(value ="/body/string", method =RequestMethod.POST)
publicboolean bodyPostString(String body) {
return body.equals("foo");
}
If the Content-Type of the request is
null
or is application/json
then we will parse the body as JSON. If the Content-Type is set and is not application/json
then we will pass the raw String or raw bytes. This allows you to handle non-JSON content from REST. It does not do any auto-conversion. You will get the raw bytes or raw UTF-8 string. @Test
publicvoid testNoJSONParseWithBytes() {
finalHttpTextResponse httpResponse = httpServerSimulator.sendRequest(
httpRequestBuilder.setUri("/es/body/bytes")
.setMethodPost().setContentType("foo")
.setBody("foo")
.build()
);
assertEquals(200, httpResponse.code());
assertEquals("true", httpResponse.body());
}
@Test
publicvoid testNoJSONParseWithString() {
finalHttpTextResponse httpResponse = httpServerSimulator.sendRequest(
httpRequestBuilder.setUri("/es/body/string")
.setMethodPost().setContentType("foo")
.setBody("foo")
.build()
);
assertEquals(200, httpResponse.code());
assertEquals("true", httpResponse.body());
}
2) Added HttpProxy support
You can proxy backend services from a single endpoint. This also allows you to do actions before sending the request, and to not forward the request based on a Predicate.
3) Added ability to return different response codes for success
By default QBit sends a 200 (OK) for a non-void call (a call that has a return or a
Callback
). If the REST operation has no return or no callback then QBit sends a 202 (Accepted). There may be times when you want to send a 201 (Created) or some other code that is not an Exception. You can do that by setting code
on @RequestMapping
. By default the code is -1 which means use the default behavior. @RequestMapping(value ="/helloj7", code =221)
publicvoid helloJSend7(Callback<JSendResponse<List<String>>> callback) {
callback.returnThis(JSendResponseBuilder.jSendResponseBuilder(Lists.list(
"hello "+System.currentTimeMillis())).build());
}
4) Working with non JSON responses
You do not have to return JSON form rest calls. You can return any binary or any text.
4.1) Returning non JSON from REST call
@RequestMapping(method =RequestMethod.GET)
publicvoid ping2(Callback<HttpTextResponse> callback) {
callback.returnThis(HttpResponseBuilder.httpResponseBuilder()
.setBody("hello mom").setContentType("mom")
.setCode(777)
.buildTextResponse());
}
4.2) Returning binary from REST call
@RequestMapping(method =RequestMethod.GET)
publicvoid ping2(Callback<HttpBinaryResponse> callback) {
callback.returnThis(HttpResponseBuilder.httpResponseBuilder()
.setBody("hello mom").setContentType("mom")
.setCode(777)
.buildBinaryResponse());
}
5) Create websocket service client that is ServiceDiscovery aware
ServiceDiscovery-aware websocket service client
finalClient client = clientBuilder
.setServiceDiscovery(serviceDiscovery, "echo")
.setUri("/echo").setProtocolBatchSize(20).build()
.startClient();
finalEchoAsync echoClient = client.createProxy(EchoAsync.class, "echo");
Currently the
clientBuilder
will load all service endpoints that are registered under the service name, and randomly pick one.In the future we can RoundRobin calls or shard calls to websocket service and/or provide auto fail over if the connection is closed. We do this for the event bus that uses service discovery but it is not baked into WebSocket based client stubs yet.
For comparison here is a non-ServiceDiscovery version.
finalClientBuilder clientBuilder =ClientBuilder.clientBuilder();
finalClient client = clientBuilder.setHost("localhost")
.setPort(8080).setUri("/echo")
.build().startClient();
finalEchoAsync echoClient = client.createProxy(EchoAsync.class, "echo");
Recall
ServiceDiscovery
includes Consul based, watching JSON files on disk, and DNS SVR records. It is easy to write your own service discovery as well and plug it into QBit.6) JSend support for serialization and swagger
We started to add JSend support. The JSend is supported for marshaling JSend objects, and via our Swagger support.
@RequestMapping("/hw")
publicclassHelloWorldJSend {
publicstaticclassHello {
finalString hello;
publicHello(Stringhello) {
this.hello = hello;
}
}
@RequestMapping("/hello")
publicStringhello() {
return"hello "+System.currentTimeMillis();
}
@RequestMapping("/helloj")
publicJSendResponse<String>helloJSend() {
returnJSendResponseBuilder.jSendResponseBuilder("hello "+System.currentTimeMillis()).build();
}
@RequestMapping("/helloj2")
publicJSendResponse<Hello>helloJSend2() {
returnJSendResponseBuilder.jSendResponseBuilder(newHello("hello "+System.currentTimeMillis())).build();
}
@RequestMapping("/helloj3")
publicJSendResponse<List<String>>helloJSend3() {
returnJSendResponseBuilder.jSendResponseBuilder(Lists.list("hello "+System.currentTimeMillis())).build();
}
@RequestMapping("/helloj4")
publicJSendResponse<List<Hello>>helloJSend4() {
returnJSendResponseBuilder.jSendResponseBuilder(Lists.list(newHello("hello "+System.currentTimeMillis()))).build();
}
@RequestMapping("/helloj5")
publicvoidhelloJSend5(Callback<JSendResponse<List<Hello>>>callback) {
callback.returnThis(JSendResponseBuilder.jSendResponseBuilder(Lists.list(newHello("hello "+System.currentTimeMillis()))).build());
}
@RequestMapping("/helloj6")
publicvoidhelloJSend6(Callback<JSendResponse<List<String>>>callback) {
callback.returnThis(JSendResponseBuilder.jSendResponseBuilder(Lists.list(
"hello "+System.currentTimeMillis())).build());
}
@RequestMapping(value="/helloj7", code=221)
publicvoidhelloJSend7(Callback<JSendResponse<List<String>>>callback) {
callback.returnThis(JSendResponseBuilder.jSendResponseBuilder(Lists.list(
"hello "+System.currentTimeMillis())).build());
}
Hitting the above
# String respnose
curl http://localhost:8080/hw/hello | jq .
"hello 1446088919561"
# JSend wrapping a a string
$ curl http://localhost:8080/hw/helloj | jq .
{
"data": "hello 1446088988074",
"status": "success"
}
#JSend wrapping a domain Object Hello
$ curl http://localhost:8080/hw/helloj2 | jq .
{
"data": {
"hello": "hello 1446089041902"
},
"status": "success"
}
#JSend wrapping a list of domain objects
$ curl http://localhost:8080/hw/helloj5 | jq .
{
"data": [
{
"hello": "hello 1446089152089"
}
],
"status": "success"
}
Use jq to get pretty print JSON from QBit.
In this example we setup the admin interface as well so we can query swagger gen.
Starting up admin
publicstaticvoid main(finalString... args) {
finalManagedServiceBuilder managedServiceBuilder =
ManagedServiceBuilder.managedServiceBuilder().setRootURI("/");
/* Start the service. */
managedServiceBuilder.addEndpointService(newHelloWorldJSend())
.getEndpointServerBuilder()
.build().startServer();
/* Start the admin builder which exposes health end-points and meta data. */
managedServiceBuilder.getAdminBuilder().build().startServer();
System.out.println("Servers started");
managedServiceBuilder.getSystemManager().waitForShutdown();
}
Showing swagger support for JSend
$ curl http://localhost:7777/__admin/meta/ | jq .
{
"swagger":"2.0",
"info": {
"title":"application title goes here",
"description":"Description not set",
"contact": {
"name":"ContactName not set",
"url":"Contact URL not set",
"email":"no.contact.email@set.me.please.com"
},
"version":"0.1-NOT-SET",
"license": {
"name":"licenseName not set",
"url":"http://www.license.url.com/not/set/"
}
},
"host":"localhost:8080",
"basePath":"/",
"schemes": [
"http",
"https",
"wss",
"ws"
],
"consumes": [
"application/json"
],
"definitions": {
"jsend-array-String": {
"properties": {
"data": {
"type":"string"
},
"status": {
"type":"string",
"description":"Status of return, this can be 'success', 'fail' or 'error'"
}
},
"description":"jsend standard response"
},
"Hello": {
"properties": {
"hello": {
"type":"string"
}
}
},
"jsend-Hello": {
"properties": {
"data": {
"$ref":"#/definitions/Hello"
},
"status": {
"type":"string",
"description":"Status of return, this can be 'success', 'fail' or 'error'"
}
},
"description":"jsend standard response"
},
"jsend-String": {
"properties": {
"data": {
"type":"string"
},
"status": {
"type":"string",
"description":"Status of return, this can be 'success', 'fail' or 'error'"
}
},
"description":"jsend standard response"
},
"jsend-array-Hello": {
"properties": {
"data": {
"type":"array",
"items": {
"$ref":"#/definitions/Hello"
}
},
"status": {
"type":"string",
"description":"Status of return, this can be 'success', 'fail' or 'error'"
}
},
"description":"jsend standard response"
}
},
"produces": [
"application/json"
],
"paths": {
"/hw/helloj7": {
"get": {
"operationId":"helloJSend7",
"summary":"no summary",
"description":"no description",
"produces": [
"application/json"
],
"responses": {
"221": {
"description":"no return description",
"schema": {
"$ref":"#/definitions/jsend-array-String"
}
}
}
}
},
"/hw/helloj6": {
"get": {
"operationId":"helloJSend6",
"summary":"no summary",
"description":"no description",
"produces": [
"application/json"
],
"responses": {
"200": {
"description":"no return description",
"schema": {
"$ref":"#/definitions/jsend-array-String"
}
}
}
}
},
"/hw/helloj5": {
"get": {
"operationId":"helloJSend5",
"summary":"no summary",
"description":"no description",
"produces": [
"application/json"
],
"responses": {
"200": {
"description":"no return description",
"schema": {
"$ref":"#/definitions/jsend-array-Hello"
}
}
}
}
},
"/hw/helloj4": {
"get": {
"operationId":"helloJSend4",
"summary":"no summary",
"description":"no description",
"produces": [
"application/json"
],
"responses": {
"200": {
"description":"no return description",
"schema": {
"$ref":"#/definitions/jsend-array-Hello"
}
}
}
}
},
"/hw/helloj3": {
"get": {
"operationId":"helloJSend3",
"summary":"no summary",
"description":"no description",
"produces": [
"application/json"
],
"responses": {
"200": {
"description":"no return description",
"schema": {
"$ref":"#/definitions/jsend-array-String"
}
}
}
}
},
"/hw/helloj2": {
"get": {
"operationId":"helloJSend2",
"summary":"no summary",
"description":"no description",
"produces": [
"application/json"
],
"responses": {
"200": {
"description":"no return description",
"schema": {
"$ref":"#/definitions/jsend-Hello"
}
}
}
}
},
"/hw/helloj": {
"get": {
"operationId":"helloJSend",
"summary":"no summary",
"description":"no description",
"produces": [
"application/json"
],
"responses": {
"200": {
"description":"no return description",
"schema": {
"$ref":"#/definitions/jsend-String"
}
}
}
}
},
"/hw/hello": {
"get": {
"operationId":"hello",
"summary":"no summary",
"description":"no description",
"produces": [
"application/json"
],
"responses": {
"200": {
"description":"no return description",
"schema": {
"type":"string"
}
}
}
}
}
}
}
To learn more about swagger see swagger.
More work is needed to support JSend error and failures.
7) Add kv store / cache support part of core
finalKeyValueStoreService<Todo> todoKVStoreInternal =JsonKeyValueStoreServiceBuilder
.jsonKeyValueStoreServiceBuilder()
.setLowLevelKeyValueStoreService(keyValueStore)
.buildKeyValueStore(Todo.class);
todoKVStore.putWithConfirmation(callback,
"testPutWithConfirmationWrapped", newTodo(value));
8) Custom exception error codes
You can use
HttpStatusCodeException
to send custom HTTP error codes and to wrap exceptions.Custom http error code exceptions
@RequestMapping("/echo3")
publicString echoException() {
thrownewHttpStatusCodeException(700, "Ouch!");
}
@RequestMapping("/echo4")
publicvoid echoException2(finalCallback<String> callback) {
callback.onError(HttpStatusCodeException.httpError(900, "Ouch!!"));
}
@RequestMapping("/echo5")
publicvoid echoException3(finalCallback<String> callback) {
try {
thrownewIllegalStateException("Shoot!!");
}catch (Exception ex) {
callback.onError(HttpStatusCodeException.httpError(666, ex.getMessage(), ex));
}
}
10) Improved speed for HTTP Rest services that have only a few client
QBit was originally written for high-end, high-traffic, high-volume services. It needed to be tuned to work with just a few clients as well as high-end.
11) Bug fixes
- Issue DELETE is allowed to have a body but QBit does not support it
- Issue with Vertx 3 integration
- Issue with Spring AOP and QBit on the same project
- Refactoring core Queue lib caused an issue with CPU which was fixed
- defaultValue not working for @RequestParam
- RequestParam optional not working
- Default port not getting set for ManageServiceBuilder
- Fixed issue with wrong exception getting sent for Callback handler and improved exception handling output overall
There were quite a few potential issue that end up not being issue, but we wrote better test cases to prove (if only to ourselves) that these were not issues.