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)
public boolean bodyPostBytes( byte[] body) {
String string = new String(body, StandardCharsets.UTF_8);
return string.equals("foo");
}
@RequestMapping(value = "/body/string", method = RequestMethod.POST)
public boolean 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
public void testNoJSONParseWithBytes() {
final HttpTextResponse httpResponse = httpServerSimulator.sendRequest(
httpRequestBuilder.setUri("/es/body/bytes")
.setMethodPost().setContentType("foo")
.setBody("foo")
.build()
);
assertEquals(200, httpResponse.code());
assertEquals("true", httpResponse.body());
}
@Test
public void testNoJSONParseWithString() {
final HttpTextResponse 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)
public void 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)
public void 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)
public void 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
final Client client = clientBuilder
.setServiceDiscovery(serviceDiscovery, "echo")
.setUri("/echo").setProtocolBatchSize(20).build()
.startClient();
final EchoAsync 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.
final ClientBuilder clientBuilder = ClientBuilder.clientBuilder();
final Client client = clientBuilder.setHost("localhost")
.setPort(8080).setUri("/echo")
.build().startClient();
final EchoAsync 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")
public class HelloWorldJSend {
public static class Hello {
final String hello;
public Hello(String hello) {
this.hello = hello;
}
}
@RequestMapping("/hello")
public String hello() {
return "hello " + System.currentTimeMillis();
}
@RequestMapping("/helloj")
public JSendResponse<String> helloJSend() {
return JSendResponseBuilder.jSendResponseBuilder("hello " + System.currentTimeMillis()).build();
}
@RequestMapping("/helloj2")
public JSendResponse<Hello> helloJSend2() {
return JSendResponseBuilder.jSendResponseBuilder(new Hello("hello " + System.currentTimeMillis())).build();
}
@RequestMapping("/helloj3")
public JSendResponse<List<String>> helloJSend3() {
return JSendResponseBuilder.jSendResponseBuilder(Lists.list("hello " + System.currentTimeMillis())).build();
}
@RequestMapping("/helloj4")
public JSendResponse<List<Hello>> helloJSend4() {
return JSendResponseBuilder.jSendResponseBuilder(Lists.list(new Hello("hello " + System.currentTimeMillis()))).build();
}
@RequestMapping("/helloj5")
public void helloJSend5(Callback<JSendResponse<List<Hello>>> callback) {
callback.returnThis(JSendResponseBuilder.jSendResponseBuilder(Lists.list(new Hello("hello " + System.currentTimeMillis()))).build());
}
@RequestMapping("/helloj6")
public void helloJSend6(Callback<JSendResponse<List<String>>> callback) {
callback.returnThis(JSendResponseBuilder.jSendResponseBuilder(Lists.list(
"hello " + System.currentTimeMillis())).build());
}
@RequestMapping(value = "/helloj7", code = 221)
public void helloJSend7(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
public static void main(final String... args) {
final ManagedServiceBuilder managedServiceBuilder =
ManagedServiceBuilder.managedServiceBuilder().setRootURI("/");
/* Start the service. */
managedServiceBuilder.addEndpointService(new HelloWorldJSend())
.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
final KeyValueStoreService<Todo> todoKVStoreInternal = JsonKeyValueStoreServiceBuilder
.jsonKeyValueStoreServiceBuilder()
.setLowLevelKeyValueStoreService(keyValueStore)
.buildKeyValueStore(Todo.class);
todoKVStore.putWithConfirmation(callback,
"testPutWithConfirmationWrapped", new Todo(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")
public String echoException() {
throw new HttpStatusCodeException(700, "Ouch!");
}
@RequestMapping("/echo4")
public void echoException2(final Callback<String> callback) {
callback.onError(HttpStatusCodeException.httpError(900, "Ouch!!"));
}
@RequestMapping("/echo5")
public void echoException3(final Callback<String> callback) {
try {
throw new IllegalStateException("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.