Spring: your next Java microframework

Original author: Alexey Nesterov
  • Transfer

In this article, we will talk about a new concept in the upcoming Spring Framework 5 called the functional web framework and see how it can help with the development of lightweight applications and microservices.


You may be surprised to see Spring and the microframework in one sentence. But that's right, Spring may well be your next Java microframework. To avoid misunderstandings, let's define what they mean by micro :


  • Laconic - minimum boilerplate, minimum setting
  • Simple - no magic
  • Easy to deploy - one artifact for deployment
  • Easy to run - no extra dependencies
  • Lightweight - minimal memory / CPU usage
  • Non-blocking - for writing competitive non-blocking applications

Although some of these points are relevant when using Spring Boot , it itself adds extra magic on top of the Spring Framework itself . Even such basic annotations as are @Controllernot quite straightforward, let alone auto-configuration and component scanning. In general, for large-scale applications, it is simply irreplaceable that Spring takes care of DI, routing, configuration, etc. However, in the world of microservices, where applications are just gears in one big machine, all the power of Spring Boot may be a little redundant.


To solve this problem, the Spring team introduced a new feature called a functional web framework - and this is what we will talk about. In general, this is part of a larger sub-project of Spring WebFlux , formerly called Spring Reactive Web .


To get started, let's go back to the basics and see what a web application is and what components we expect to have in it. Sure, there is a basic thing - a web server . To avoid manual processing of requests and calling application methods, we need a router . And finally, we need a handler - a piece of code that accepts a request and gives an answer. In fact, that’s all you need! And it is these components that the functional Spring web framework provides , removing all the magic and focusing on the fundamental minimum. I note that this does not mean that Spring abruptly changes direction and leaves Spring MVC , the functional web just gives one morethe ability to create applications on Spring .


Handler


Let's look at an example. To get started, let's go to the Spring Initializr and create a new project using Spring Boot 2.0 and Reactive Web as the only dependency. Now we can write our first handler - a function that receives a request and gives an answer.


HandlerFunction hello = new HandlerFunction() {
    @Override
    public Mono handle(ServerRequest request) {
        return ServerResponse.ok().body(fromObject("Hello"));
    }
};

So, our handler is just an implementation of an interface HandlerFunctionthat takes a parameter request(of type ServerRequest) and returns an object of type ServerResponsewith the text "Hello". Spring also provides convenient builders to create a response from the server. In our case, we use ok()those that automatically return an HTTP response code of 200. To return a response, we need another helper - fromObjectto form a response from the provided object.


We can also make the code a bit more concise and use lambdas from Java 8 and so on. HandlerFunctionthis is a single abstract method interface (SAM), we can write our function as:


HandlerFunction hello = request -> ServerResponse.ok().body(fromObject("Hello"));

Router


Now that we have a handler, it's time to define a router . For example, we want to call our handler when the URL "/" was called using the HTTP method GET. To achieve this, we define an object of the type RouterFunctionthat maps the handler function to the route:


RouterFunction router = route(GET("/"), hello);

routeand GETthese are static methods from classes RequestPredicatesand RouterFunctions, they allow you to create the so-called RouterFunction. Such a function takes a request, checks to see if it matches all the predicates (URL, method, content type, etc) and calls the desired handler function. In this case, the predicate is the http GET method and the URL '/' , and the handler function is the helloone defined above.


Web server


And now it's time to put everything together in a single application. We use a lightweight and simple server Reactive Netty. To integrate our router with a web server, you need to turn it into HttpHandler. After that, you can start the server:


HttpServer
    .create("localhost", 8080)
    .newHandler(new ReactorHttpHandlerAdapter(httpHandler))
    .block();

ReactorHttpHandlerAdapterthis is a class provided by Netty that accepts HttpHandler, the rest of the code, I think, requires no explanation. We create a new web server bound to the host localhostand port 8080and provide a httpHandlercreated one from our router.


And that’s it, the application is ready! And its full code:


public static void main(String[] args)
          throws IOException, LifecycleException, InterruptedException {
    HandlerFunction hello = request -> ServerResponse.ok().body(fromObject("Hello"));
    RouterFunction router = route(GET("/"), hello);
    HttpHandler httpHandler = RouterFunctions.toHttpHandler(router);
    HttpServer
            .create("localhost", 8080)
            .newHandler(new ReactorHttpHandlerAdapter(httpHandler))
            .block();
    Thread.currentThread().join();
}

The last line is only needed to keep the JVM process alive, as HttpServer itself does not block it. You may immediately notice that the application starts instantly - there is no scanning of components or auto-configuration.


We can also run this application as a regular Java application, no application containers are required and so on.


To package the deployment application, we can take advantage of the Maven Spring plugin and simply call


./mvnw package


This command will create the so-called fat JAR with all the dependencies included in the JAR. This file can be deployed and run without anything other than the JRE installed.


java -jar target/functional-web-0.0.1-SNAPSHOT.jar


Also, if we check the application's memory usage, we will see that it stays in the region of 32 MB - 22 MB is used on metaspace (classes) and about 10 MB is taken directly on the heap. Of course, our application does not do anything - but nevertheless, it is just an indicator that the framework and runtime alone require a minimum of system resources.


JSON Support


In our example, we returned a string, but returning a JSON response is just as easy. Let's extend our application with a new endpoint that will return JSON. Our model will be very simple - just one string field called name. To avoid unnecessary boilerplate code, we will use the features from the Lombok project , annotation @Data. Having this annotation will automatically create getters, setters, equalsand methods hashCode, so we don’t have to release them manually.


@Data
class Hello {
    private final String name;
}

Now, we need to expand our router to return a JSON response when accessing the URL /json. This can be done by calling the andRoute(...)method on an existing route. Also, let's put the router code in a separate function to separate it from the application code and allow it to be used later in tests.


static RouterFunction getRouter() {
    HandlerFunction hello = request -> ok().body(fromObject("Hello"));
    return
        route(
            GET("/"), hello)
        .andRoute(
            GET("/json"), req ->
                ok()
                .contentType(APPLICATION_JSON)
                .body(fromObject(new Hello("world")));
}

After a restart, the application will return { "name": "world" }when accessing the URL /jsonwhen requesting content with a type application/json.


Application context


You may have noticed that we did not define the application context - we simply do not need it! Despite the fact that we can declare a RouterFunctionbean in the context of the Spring WebFlux application, and it will process requests to specific URLs in the same way, the router can be launched simply on top of Netty Server to create simple and lightweight JSON services.


Testing


To test reactive applications, Spring provides a new client called WebTestClient(similar MockMvc). It can be created for an existing application context, but you can also define it for RouterFunction.


public class FunctionalWebApplicationTests {
    private final WebTestClient webTestClient =
            WebTestClient
                .bindToRouterFunction(
                    FunctionalWebApplication.getRouter())
                .build();
    @Test
    public void indexPage_WhenRequested_SaysHello() {
        webTestClient.get().uri("/").exchange()
                .expectStatus().is2xxSuccessful()
                .expectBody(String.class)
                .isEqualTo("Hello");
    }
    @Test
    public void jsonPage_WhenRequested_SaysHello() {
        webTestClient.get().uri("/json").exchange()
                .expectStatus().is2xxSuccessful()
                .expectHeader().contentType(APPLICATION_JSON)
                .expectBody(Hello.class)
                .isEqualTo(new Hello("world"));
    }
}

WebTestClient включает ряд assert-ов, которые можно применить к полученному ответу, чтобы провалидировать HTTP код, содержимое ответа, тип ответа и т.п.


В заключение


Spring 5 представляет новую парадигму для разработки маленьких и легковесных microservice-style веб-приложений. Такие приложения могут работать без контекста приложений, автоконфигурации и в целом использовать подход микрофреймворков, когда роутер и функции-обработчики и веб-сервер опеределены явно в теле приложения.


Код


Доступен на GitHub


Ссылки



From translator


I am also the author of the original article, so questions can be asked in the comments.


Also popular now: