In this article, we’ll talk about a new concept in the upcoming Spring Framework 5, which is called a functional web framework, and see how it can help in developing lightweight applications and microservices.
You may be surprised to see Spring and the microframe in one sentence. But that's right, Spring may well become your next Java microform. To avoid misunderstandings, let's define what they mean by micro :
Although some of these items are relevant when using Spring Boot , it alone adds additional magic over the Spring Framework itself . Even basic annotations such as @Controller
not quite straightforward, what can we say about auto-configuration and scanning of components. In general, for large-scale applications, it is simply indispensable that Spring takes care of DI, routing, configuration, etc. However, in the world of microservices, where applications are just gears in one big machine, the whole power of Spring Boot can be a bit unnecessary.
To solve this problem, the Spring team introduced a new feature, which is called a functional web framework - and we will talk about it. Overall, this is part of the larger Spring WebFlux sub-project, formerly called Spring Reactive Web .
First, let's go back to the basics and see what a web application is and what components we expect to have in it. Undoubtedly, there is a basic thing - a web server . To avoid manual processing of requests and calling application methods, a router is useful to us. And finally, we need a handler - a piece of code that accepts the request and responds. In fact, that's all you need! And it is these components that Spring provides a functional web framework , removing all magic and focusing on a fundamental minimum. I note that this does not mean at all that Spring abruptly changes direction and moves away from Spring MVC , the functional web just gives another opportunity to create applications on Spring .
Let's take an example. First of all, let's go to 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 accepts the request and responds.
HandlerFunction hello = new HandlerFunction() { @Override public Mono handle(ServerRequest request) { return ServerResponse.ok().body(fromObject("Hello")); } };
So, our handler is just an implementation of the HandlerFunction
interface that accepts a request
parameter (of type ServerRequest
) and returns an object of type ServerResponse
with the text "Hello". Spring also provides convenient builders to create a response from the server. In our case, we use ok()
which automatically return the HTTP response code 200. To return the answer, we need another helper fromObject
to form the response from the provided object.
We can also make the code a bit more concise and use lambdas from Java 8 and since HandlerFunction
is a single method interface (single abstract method interface, SAM), we can write our function as:
HandlerFunction hello = request -> ServerResponse.ok().body(fromObject("Hello"));
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 GET
method. To achieve this, we define an object of type RouterFunction
which maps the function handler, to the route:
RouterFunction router = route(GET("/"), hello);
route
and GET
are static methods from the RequestPredicates
and RouterFunctions
, they allow you to create a so-called RouterFunction
. Such a function accepts the request, checks whether it matches all predicates (URL, method, content type, etc) and calls the required handler function. In this case, the predicate is the http method GET and the URL '/' , and the handler function is hello
, which is defined above.
And now it's time to put everything together in a single application. We use a lightweight and simple Reactive Netty
server. To integrate our router with a web server, you need to turn it into a HttpHandler
. After that you can start the server:
HttpServer .create("localhost", 8080) .newHandler(new ReactorHttpHandlerAdapter(httpHandler)) .block();
ReactorHttpHandlerAdapter
is a class provided by Netty, which accepts HttpHandler
, the rest of the code, I think, needs no explanation. We create a new web server tied to the localhost
host and on port 8080
and provide an httpHandler
created from our router.
And that's all, the app 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 needed only to keep the JVM process alive, because HttpServer itself does not block it. You may immediately notice that the application starts instantly - there is neither a component scan, nor an auto-configuration.
We can also run this application as a regular Java application, no application containers and other things are required.
To package the deployment application, we can take advantage of the Maven Spring plugin and just call
./mvnw package
This command will create a so-called fat JAR with all dependencies included in the JAR. This file can be terminated and run without anything but the installed JRE.
java -jar target/functional-web-0.0.1-SNAPSHOT.jar
Also, if we check the memory usage of the application, we will see that it keeps around 32 MB - 22 MB used on metaspace (classes) and about 10 MB is occupied directly in the heap. Of course, our application doesn’t do anything - but nevertheless, it’s just an indicator that the framework and runtime themselves require a minimum of system resources.
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 features from the Lombok project, the @Data
annotation. The presence of this annotation will automatically create getters, setters, equals
and hashCode
methods, so we don’t have to manually release them.
@Data class Hello { private final String name; }
Now, we need to expand our router to return the JSON response when accessing the URL /json
. This can be done by calling the andRoute(...)
method on the existing route. Also, let's move the code of the router into a separate function in order 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 restarting, the application will return { "name": "world" }
when accessing the URL /json
when requesting content with the type application/json
.
You may have noticed that we have not defined the context of the application - we just do not need it! Despite the fact that we can declare RouterFunction
as a bin (bean) in the context of the Spring WebFlux application, and it will also process requests for specific URLs in the same way, the router can be run just over the Netty Server to create simple and lightweight JSON services.
To test reactive applications, Spring provides a new client called WebTestClient
(like MockMvc
). You can create it 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
includes a number of WebTestClient
that can be applied to the received response in order to validate the HTTP code, the content of the response, the type of response, etc.
Spring 5 introduces a new paradigm for developing small and lightweight microservice-style web applications. Such applications can work without application context, autoconfiguration, and generally use the microframe approach, when the router and handler functions and the web server are explicitly defined in the application body.
Available on GitHub
I am also the author of the original article, so questions can be asked in the comments.
Source: https://habr.com/ru/post/337604/
All Articles