This guide explains how to build your own HTTP microservices using the Gumdrop framework. Unlike the servlet container, file server, or WebSocket service—which are ready-to-use implementations—this page shows you how to create custom services from scratch using the HTTP Server API. The examples use streaming, event-driven parsing with Gonzalez (XML) and jsonparser (JSON) for maximum efficiency.
A Gumdrop microservice is an HTTPService with a custom
HTTPRequestHandlerFactory. The factory creates
HTTPRequestHandler instances for each request. Handlers receive
events in sequence:
headers() // request headers (:method, :path, content-type, etc.) startRequestBody() // body is starting (POST, PUT, etc.) requestBodyContent() // body data chunks (may be called many times) endRequestBody() // body complete requestComplete() // stream closed
For efficient parsing, feed each requestBodyContent(ByteBuffer)
chunk directly into a streaming parser. Do not accumulate the entire
body in memory first. Both Gonzalez (XML) and jsonparser (JSON) support
receive(ByteBuffer) for incremental parsing.
This example parses a small application/xml request body and
responds with XML. It uses the Gonzalez push parser with a SAX
ContentHandler to extract data as it arrives.
The handler receives parsing events. For a simple request like
<echo>Hello</echo>, we capture the text content:
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
class EchoContentHandler extends DefaultHandler {
private final StringBuilder text = new StringBuilder();
private String rootElement;
@Override
public void startElement(String uri, String localName, String qName,
Attributes atts) throws SAXException {
if (rootElement == null) {
rootElement = localName;
}
text.setLength(0);
}
@Override
public void characters(char[] ch, int start, int length) {
text.append(ch, start, length);
}
String getRootElement() { return rootElement; }
String getText() { return text.toString().trim(); }
}
The handler feeds each body chunk to the Gonzalez parser. Create the parser
once, call receive(data) for each chunk, and close()
when the body is complete:
import org.bluezoo.gonzalez.Parser;
import org.bluezoo.gumdrop.http.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import static java.nio.charset.StandardCharsets.UTF_8;
class XmlEchoHandler extends DefaultHTTPRequestHandler {
private final Parser parser = new Parser();
private final EchoContentHandler contentHandler = new EchoContentHandler();
private boolean bodyStarted;
@Override
public void headers(HTTPResponseState state, Headers headers) {
if (!"POST".equals(headers.getMethod())) {
sendError(state, 405, "Method Not Allowed");
return;
}
String ct = headers.getValue("content-type");
if (ct == null || !ct.startsWith("application/xml")) {
sendError(state, 415, "Unsupported Media Type");
return;
}
bodyStarted = false;
parser.setContentHandler(contentHandler);
try {
parser.reset();
} catch (Exception e) {
sendError(state, 500, e.getMessage());
}
}
@Override
public void startRequestBody(HTTPResponseState state) {
bodyStarted = true;
}
@Override
public void requestBodyContent(HTTPResponseState state, ByteBuffer data) {
if (!bodyStarted) return;
try {
parser.receive(data);
} catch (Exception e) {
sendError(state, 400, "Invalid XML: " + e.getMessage());
}
}
@Override
public void endRequestBody(HTTPResponseState state) {
if (!bodyStarted) return;
try {
parser.close();
sendXmlResponse(state);
} catch (Exception e) {
sendError(state, 400, "Invalid XML: " + e.getMessage());
}
}
private void sendXmlResponse(HTTPResponseState state) throws IOException {
String root = contentHandler.getRootElement();
String text = contentHandler.getText();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
org.bluezoo.gonzalez.XMLWriter xml = new org.bluezoo.gonzalez.XMLWriter(baos);
xml.writeStartElement(root != null ? root : "response");
xml.writeDefaultNamespace("urn:example:echo");
if (text != null && !text.isEmpty()) {
xml.writeCharacters(text);
}
xml.writeEndElement();
xml.close();
Headers response = new Headers();
response.status(HTTPStatus.OK);
response.add("content-type", "application/xml");
state.headers(response);
state.startResponseBody();
state.responseBodyContent(ByteBuffer.wrap(baos.toByteArray()));
state.endResponseBody();
state.complete();
}
private void sendError(HTTPResponseState state, int code, String message) {
Headers response = new Headers();
response.status(HTTPStatus.fromCode(code));
response.add("content-type", "text/plain");
state.headers(response);
state.startResponseBody();
state.responseBodyContent(ByteBuffer.wrap(message.getBytes(UTF_8)));
state.endResponseBody();
state.complete();
}
}
Extend HTTPService and provide a handler factory. The factory
routes requests (here, all POST requests to /echo get the
XML handler):
import org.bluezoo.gumdrop.http.*;
public class XmlEchoService extends HTTPService {
@Override
protected void initService() {
// No resources to initialise
}
@Override
protected HTTPRequestHandlerFactory getHandlerFactory() {
return new XmlEchoHandlerFactory();
}
private static class XmlEchoHandlerFactory implements HTTPRequestHandlerFactory {
@Override
public HTTPRequestHandler createHandler(HTTPResponseState state,
Headers headers) {
String path = headers.getValue(":path");
if ("/echo".equals(path) && "POST".equals(headers.getMethod())) {
return new XmlEchoHandler();
}
return null; // 404
}
}
}
Add the service to Gumdrop and configure a listener. See Configuration for XML-based setup.
Gumdrop gumdrop = Gumdrop.getInstance();
XmlEchoService service = new XmlEchoService();
HTTPListener listener = new HTTPListener();
listener.setPort(9090);
service.addListener(listener);
gumdrop.addService(service);
gumdrop.start();
// Block until shutdown (Ctrl+C); JVM shutdown hook calls gumdrop.shutdown()
try {
gumdrop.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
This example extends the first: the service receives a JSON request, calls a downstream microservice (also JSON), and returns the aggregated response. It demonstrates:
JSONParser.receive(ByteBuffer)
Extend JSONDefaultHandler to capture the fields you need. The
parser receives body chunks incrementally:
import org.bluezoo.json.JSONDefaultHandler;
import org.bluezoo.json.JSONException;
class LookupRequestHandler extends JSONDefaultHandler {
private String currentKey;
private String query;
@Override
public void key(String key) throws JSONException {
currentKey = key;
}
@Override
public void stringValue(String value) throws JSONException {
if ("query".equals(currentKey)) {
query = value;
}
}
@Override
public void endObject() throws JSONException {
currentKey = null;
}
String getQuery() { return query; }
}
The handler parses the JSON request in responseBodyContent, then
in endRequestBody initiates an outbound HTTP call. Use
state.getSelectorLoop() for SelectorLoop affinity so the client
connection shares the same I/O thread as the server connection:
import org.bluezoo.gumdrop.SelectorLoop;
import org.bluezoo.gumdrop.SecurityInfo;
import org.bluezoo.gumdrop.http.*;
import org.bluezoo.gumdrop.http.client.*;
import org.bluezoo.json.*;
import java.nio.ByteBuffer;
import static java.nio.charset.StandardCharsets.UTF_8;
class LookupHandler extends DefaultHTTPRequestHandler {
private final String downstreamHost;
private final int downstreamPort;
private final JSONParser jsonParser = new JSONParser();
private final LookupRequestHandler requestHandler = new LookupRequestHandler();
private boolean parserInitialised;
private HTTPResponseState pendingState;
LookupHandler(String host, int port) {
this.downstreamHost = host;
this.downstreamPort = port;
}
@Override
public void headers(HTTPResponseState state, Headers headers) {
if (!"POST".equals(headers.getMethod())) {
sendError(state, 405, "Method Not Allowed");
return;
}
pendingState = state;
parserInitialised = false;
jsonParser.setContentHandler(requestHandler);
try {
jsonParser.reset();
} catch (Exception e) {
sendError(state, 500, e.getMessage());
}
}
@Override
public void startRequestBody(HTTPResponseState state) {
// Body starting
}
@Override
public void requestBodyContent(HTTPResponseState state, ByteBuffer data) {
if (!parserInitialised) {
jsonParser.setContentHandler(requestHandler);
parserInitialised = true;
}
try {
jsonParser.receive(data);
} catch (JSONException e) {
sendError(state, 400, "Invalid JSON: " + e.getMessage());
}
}
@Override
public void endRequestBody(HTTPResponseState state) {
try {
jsonParser.close();
} catch (JSONException e) {
sendError(state, 400, "Invalid JSON: " + e.getMessage());
return;
}
String query = requestHandler.getQuery();
if (query == null || query.isEmpty()) {
sendError(state, 400, "Missing 'query' field");
return;
}
callDownstream(state, query);
}
private void callDownstream(HTTPResponseState state, String query) {
SelectorLoop loop = state.getSelectorLoop();
HTTPClient client = (loop != null)
? new HTTPClient(loop, downstreamHost, downstreamPort)
: new HTTPClient(downstreamHost, downstreamPort);
// Propagate trace context so the distributed trace remains connected
client.setTrace(state.getTrace());
client.connect(new HTTPClientHandler() {
@Override
public void onConnected(Endpoint endpoint) {
HTTPRequest request = client.post("/lookup");
request.header("Content-Type", "application/json");
request.header("Accept", "application/json");
DefaultHTTPResponseHandler responseHandler =
new DefaultHTTPResponseHandler() {
private final JSONParser responseParser = new JSONParser();
private final LookupResponseHandler jsonHandler =
new LookupResponseHandler();
private boolean parserInit;
@Override
public void ok(HTTPResponse response) {
responseParser.setContentHandler(jsonHandler);
try { responseParser.reset(); } catch (Exception e) { /* ignore */ }
parserInit = true;
}
@Override
public void responseBodyContent(ByteBuffer data) {
if (parserInit) {
try {
responseParser.receive(data);
} catch (JSONException e) {
// Log and continue
}
}
}
@Override
public void close() {
try {
if (parserInit) responseParser.close();
} catch (Exception e) { /* ignore */ }
String result = jsonHandler.getResult();
sendJsonResponse(pendingState, result);
}
@Override
public void failed(Exception ex) {
sendError(pendingState, 502, "Downstream error: " + ex.getMessage());
}
};
String body = "{\"query\":\"" + escapeJson(query) + "\"}";
request.startRequestBody(responseHandler);
request.requestBodyContent(ByteBuffer.wrap(body.getBytes(UTF_8)));
request.endRequestBody();
}
@Override
public void onError(Exception cause) {
sendError(pendingState, 502, "Connection failed: " + cause.getMessage());
}
@Override
public void onSecurityEstablished(SecurityInfo info) { }
@Override
public void onDisconnected() { }
});
}
private void sendJsonResponse(HTTPResponseState state, String result) {
String json = "{\"result\":\"" + escapeJson(result != null ? result : "") + "\"}";
Headers response = new Headers();
response.status(HTTPStatus.OK);
response.add("content-type", "application/json");
state.headers(response);
state.startResponseBody();
state.responseBodyContent(ByteBuffer.wrap(json.getBytes(UTF_8)));
state.endResponseBody();
state.complete();
}
private void sendError(HTTPResponseState state, int code, String message) {
Headers response = new Headers();
response.status(HTTPStatus.fromCode(code));
response.add("content-type", "text/plain");
state.headers(response);
state.startResponseBody();
state.responseBodyContent(ByteBuffer.wrap(message.getBytes(UTF_8)));
state.endResponseBody();
state.complete();
}
private static String escapeJson(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\"");
}
}
class LookupResponseHandler extends JSONDefaultHandler {
private String currentKey;
private String result;
@Override
public void key(String key) throws JSONException {
currentKey = key;
}
@Override
public void stringValue(String value) throws JSONException {
if ("result".equals(currentKey)) {
result = value;
}
}
@Override
public void endObject() throws JSONException {
currentKey = null;
}
String getResult() { return result; }
}
public class LookupService extends HTTPService {
private String downstreamHost = "localhost";
private int downstreamPort = 9091;
public void setDownstreamHost(String host) { downstreamHost = host; }
public void setDownstreamPort(int port) { downstreamPort = port; }
@Override
protected HTTPRequestHandlerFactory getHandlerFactory() {
return (state, headers) -> {
if ("/lookup".equals(headers.getPath())) {
return new LookupHandler(downstreamHost, downstreamPort);
}
return null;
};
}
}
When creating the HTTPClient, pass state.getSelectorLoop()
so the outbound connection uses the same worker thread as the inbound request.
This avoids context switches and keeps I/O on a single thread. If
getSelectorLoop() returns null (e.g. in tests), the client falls
back to Gumdrop's default worker assignment.
Microservices can be configured via gumdroprc like any other
service. Example for the XML echo service:
<service id="echo" class="com.example.XmlEchoService">
<listener class="org.bluezoo.gumdrop.http.HTTPListener">
<property name="port">9090</property>
</listener>
</service>
For the lookup service with downstream configuration:
<service id="lookup" class="com.example.LookupService">
<property name="downstream-host">api.internal</property>
<property name="downstream-port">9091</property>
<listener class="org.bluezoo.gumdrop.http.HTTPListener">
<property name="port">9092</property>
</listener>
</service>
See Configuration for property naming
conventions (downstream-host maps to setDownstreamHost)
and the full dependency injection framework.
HTTPService and implement getHandlerFactory()requestBodyContent(ByteBuffer) chunks directly to
Parser.receive() (Gonzalez) or JSONParser.receive()
(jsonparser) — never buffer the entire bodyparser.close() in endRequestBody() to
finalise parsingstate.getSelectorLoop() when creating HTTP clients for
SelectorLoop affinityclient.setTrace(state.getTrace()) before
connect() to propagate the distributed trace to downstream
services (traceparent header is added automatically)state.responseBodyContent(ByteBuffer)
and always call state.complete()gumdrop.join() after start() so the main
thread blocks until shutdown (Ctrl+C)For more on the HTTP API, see HTTP Server & Client. For the HTTP client in detail, see the HTTP Client section.
Gumdrop