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.javaI 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 Optionalthat maps to the class Answerthat creates the response. In case of an error, a class is returned Answerwith an error code and an error message. This interface has a method QueryParamsHandlerFunc::handleRequestinto 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::handleRequestthis interface receives our class Payloadas 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);
  }
}

References



Also popular now: