
Create a simple RESTful API with the Spark Framework
What will you learn
You will learn how to define generic controllers using the Java 8 functional interfaces. Sample code on GitHub .
EchoApplication.java
This is the class that ties your application together. When you open this class, you should immediately understand how everything works:
public class EchoApplication {
private static final Logger LOG = Logger.getLogger(EchoApplication.class);
// Declare dependencies
public static EchoService echoService;
static {
echoService = new EchoService();
}
public static void main(String[] args) {
port(4567);
start();
}
public static void start() {
JsonUtils.registerModules();
LOG.info("Initializing routes");
establishRoutes();
}
private static void establishRoutes() {
path("/api", () ->
path("/v1", () -> {
get(Path.ECHO, EchoController.echo);
post(Path.PONG, EchoController.pong);
})
);
}
}
Static dependencies?
Static dependencies are not what you're used to seeing in Java
, but statics can be better than injected dependencies when it comes to controllers in web applications. Moreover, in our microservice, which performs only one function, there will not be many services. Of the advantages, it should be noted that rejecting DI speeds up the launch of the application, and unit tests for one or two services can be written without DI.
Path and Controller.field
In the class, Path.java
I hold the REST API entry points as constants. In this application, there are only two request handlers that are placed in one controller EchoController
:
public class EchoController {
public static Route echo = (req, res) ->
((QueryParamsHandlerFunc) echoQuery ->
echoService
.echo(echoQuery.value("echo"))
.map(Answer::ok)
.orElse(Answer.error(HTTP_BAD_REQUEST, ErrorMessages.UNABLE_TO_ECHO + req.body()))
).handleRequest(req, res);
public static Route pong = (req, res) ->
((ModelHandlerFunc) pongRequest ->
echoService
.pong(pongRequest)
.map(Answer::ok)
.orElse(Answer.error(HTTP_BAD_REQUEST, ErrorMessages.UNABLE_TO_PONG + req.body()))
).handleRequest(req, res, Ping.class);
}
The first handler is a lambda function whose body is a functional interface QueryParamsHandlerFunc
. The input parameter of this interface is the GET request data - a dictionary with a part of the requests from the link. (For example, /echo?echo=message
). In the body of this interface, our handler service is called. Those. the service receives an already assembled object and does not depend on the controller in any way, which facilitates testing. The service returns Optional
that maps to the class Answer
that creates the response. In case of an error, a class is returned Answer
with an error code and an error message. This interface has a method QueryParamsHandlerFunc::handleRequest
into which the request and response are transferred from the controller-handler Func::handlerRequest(req, res)
.
The second handler returns a result that forms about the same interface as above, only the class inheriting the interface is specified as a template parameter Payload
. The processing of this request is no different from the one described above. The only difference is that the method of ModelHandlerFunc::handleRequest
this interface receives our class Payload
as a parameter .ModelHandlerFunc
QueryParamsHandlerFunc.java
This is a functional interface that inherits the basic interface into which actions common to the GET request handler are issued. This interface defines the default method, QueryParamsHandlerFunc :: handleRequest, which accepts request and response objects as input. Performs some checks, for example, of headers, calling the base interface method BaseHandlerFunc :: commonCheck (request). Next, it takes the query dictionary from the request (/ echo? Echo = message), passes them to the method defined in the QueryParamsHandlerFunc :: process interface, after processing the request, indicates the response code and serializes this response in Json.
@FunctionalInterface
public interface QueryParamsHandlerFunc extends BaseHandlerFunc {
default String handleRequest(Request request, Response response) {
String check = commonCheck(request);
if (StringUtils.isNotBlank(check)) {
return check;
}
QueryParamsMap queryParamsMap = request.queryMap();
Answer processed = process(queryParamsMap);
response.status(processed.getCode());
return dataToJson(processed);
}
Answer process(QueryParamsMap data);
}
ModelHandlerFunc.java
This interface works the same as described above, but we only distinguish that it processes POST requests. The converted class, as well as when processing request parameters, is passed to the interface method ModelHandlerFunc::process
.
@FunctionalInterface
public interface ModelHandlerFunc extends BaseHandlerFunc {
default String handleRequest(Request request, Response response, Class clazz) {
String check = commonCheck(request);
if (StringUtils.isNotBlank(check)) {
return check;
}
String json = request.body();
T data = jsonToData(json, clazz);
Answer processed = process(data);
response.status(processed.getCode());
return dataToJson(processed);
}
Answer process(T data);
}
BaseHandlerFunc.java
This is an interface that aggregates common methods.
public interface BaseHandlerFunc {
default String commonCheck(Request request) {
// do your smart check here
return null;
}
}
Jackson Polymorphic Type Handling Annotations
To serialize the Answer class and its load in JSON, anthration polymorphism was applied @JsonTypeInfo
. More details on the links.
Controller test
To test the controller, use the Spark Test library . Sample code testing.
public class EchoControllerTest {
private static String echoUrl = "/api/v1";
private static Integer randomPort = 1000 + new Random().nextInt(60000);
public static class BoardBoxControllerTestSparkApplication implements SparkApplication {
@Override
public void init() {
EchoApplication.start();
}
}
@ClassRule
public static SparkServer testServer = new SparkServer<>(BoardBoxControllerTestSparkApplication.class, randomPort);
@Test
public void should_echo() throws HttpClientException {
String echoMsg = "echo";
Echo echo = (Echo) get("/echo?echo" + "=" + echoMsg).getBody();
assertEquals(echoMsg, echo.getEcho());
}
@Test
public void should_pong() throws HttpClientException {
Pong pong = (Pong) post("/ping", new Ping("PING")).getBody();
assertEquals("PING PONG", pong.getPong());
}
private Answer post(String path, Object payload) throws HttpClientException {
PostMethod resp = testServer.post(echoUrl + path, dataToJson(payload), false);
HttpResponse execute = testServer.execute(resp);
return jsonToData(new String(execute.body()), Answer.class);
}
private Answer get(String params) throws HttpClientException {
GetMethod resp = testServer.get(echoUrl + "/" + params, false);
HttpResponse execute = testServer.execute(resp);
return jsonToData(new String(execute.body()), Answer.class);
}
}