Java Spring 5 New Feature Functional Web Framework
Give an example
Let's start with some excerpts from the sample application. Below is a response information library that exposes Person objects. Very similar to the traditional, non-responsive information library, except that it returns Flux<Person> and the traditional return List<Person> and the place where Mono<Person> returns Person. Mono<Void> is used as a completion flag: indicates when the save is completed.
public interface PersonRepository { Mono<Person> getPerson(int id); Flux<Person> allPeople(); Mono<Void> savePerson(Mono<Person> person);}Here is how we expose a library with the new functional web framework:
RouterFunction<?> route = route(GET("/person/{id}"), request -> { Mono<Person> person = Mono.justOrEmpty(request.pathVariable("id")) .map(Integer::valueOf) .then(repository::getPerson); return Response.ok().body(fromPublisher(person, Person.class)); }) .and(route(GET("/person"), request -> { Flux<Person> people = repository.allPeople(); return Response.ok().body(fromPublisher(people, Person.class)); })) .and(route(POST("/person"), request -> { Mono<Person> person = request.body(toMono(Person.class)); return Response.ok().build(repository.savePerson(person));}));Here we will introduce how to run, for example in Reactor Netty:
HttpHandler httpHandler = RouterFunctions.toHttpHandler(route);ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);HttpServer server = HttpServer.create("localhost", 8080);server.startAndAwait(adapter);The last thing to do is give it a try:
$ curl 'http://localhost:8080/person/1'{"name":"John Doe","age":42}There are more introductions below, let’s dig deeper!
Core Components
I will introduce the framework by thoroughly explaining the core components: HandlerFunction, RouterFunction, and FilterFunction. These three interfaces, as well as all other types described in the article, can be found in the org.springframework.web.reactive.function package.
HandlerFunction
The starting point of this new framework is HandlerFunction<T>, which is basically Function<Request, Response<T>>, where Request and Response are newly defined, and the unchanging interface is friendly to provide JDK-8 DSL to the underlying HTTP messages. It is a convenient build tool for building Response entities, very similar to what you see in ResponseEntity. Corresponding to the HandlerFunction annotation is a method with @RequestMapping.
Here is a simple example of the "Hello World" processing function, returning a response message with 200 states and a body of String:
HandlerFunction<String> helloWorld = request -> Response.ok().body(fromObject("Hello World"));As we saw in the example above, the handling functions are completely responsive by building on a Reactor basis: they accept Flux, Mono, or any other corresponding stream Publisher as the response type.
One thing to note is that HandlerFunction itself has no side effects because it returns the response instead of treating it as a parameter (see Servlet.service(ServletRequest,ServletResponse), which is essentially BiConsumer<ServletRequest,ServletResponse> ). There are many benefits to no side effects: easy to test, write and optimize.
RouterFunction
The incoming request is routed to the handler function with RouterFunction<T> (i.e. Function<Request, Optional<HandlerFunction<T>>) and is routed to the handler if it matches; otherwise, an empty result is returned. The routing method works similar to the @RequestMapping annotation. However, there is another significant difference: when using annotations, the route will be limited to the range that the annotated value can express, and it is difficult to deal with the overlay of these methods; when using the routing method, the code is there and can be easily overwritten or replaced.
Below is an example of a routing function with an embedded processing function. It looks a bit lengthy, but don't worry: we'll find a way to make it shorter.
RouterFunction<String> helloWorldRoute = request -> { if (request.path().equals("/hello-world"))) { return Optional.of(r -> Response.ok().body(fromObject("Hello World"))); } else { return Optional.empty(); } };Generally, there is no need to write a complete routing method, but instead statically introduce RouterFunctions.route(), so that you can create a routing method using the request judgment formula (RequestPredicate) (i.e. Predicate<Request>) and HandlerFunction). If the judgment is successful, the processing method will be returned, otherwise the empty result will be returned. The following is the example above using the route method:
RouterFunction<String> helloWorldRoute = RouterFunctions.route(request -> request.path().equals("/hello-world"), request -> Response.ok().body(fromObject("Hello World")));You can (statically) import RequestPredicates.* to access commonly used predicates, based on paths, HTTP methods, content types, etc. With it, we can make helloWorldRoute simpler:
RouterFunction<String> helloWorldRoute = RouterFunctions.route(RequestPredicates.path("/hello-world"), request -> Response.ok().body(fromObject("Hello World")));Combination functions
Two routing functions can form a new routing function, route to either processing function: if the first function does not match, then the second one is executed. You can combine two routing functions like this by calling RouterFunction.and():
RouterFunction<?> route = route(path("/hello-world"), request -> Response.ok().body(fromObject("Hello World")))) .and(route(path("/the-answer"), request -> Response.ok().body(fromObject("42"))));If the path matches /hello-world, the above will respond to "Hello World", and if /the-answer matches, it will return "42" at the same time. If neither matches, an empty Optional is returned. Note that the combined routing functions are executed in sequence, so it makes sense to put generic functions before the specific function.
You can also combine request predicates by calling and or or. This works like this: for and, if two given predicates match, the result predicates match, and if one of the two matches, then or matches. For example:
RouterFunction<?> route = route(method(HttpMethod.GET).and(path("/hello-world")), request -> Response.ok().body(fromObject("Hello World")))) .and(route(method(HttpMethod.GET).and(path("/the-answer")), request -> Response.ok().body(fromObject("42"))));In fact, most predicates found in RequestPredicates are combined! For example, RequestPredicates.GET(String) is a composition of RequestPredicates.method(HttpMethod) and RequestPredicates.path(String). Therefore, we can rewrite the above code as:
RouterFunction<?> route = route(GET("/hello-world"), request -> Response.ok().body(fromObject("Hello World"))) .and(route(GET("/the-answer"), request -> Response.ok().body(fromObject(42))));Method reference
By the way: So far we have written all the processing functions as inline lambda expressions. While this performs well in the demo and short examples, it has to be said that there is a tendency to cause "confusion" because you want to mix two concerns: request routing and request processing. So, we want to see if it can make things simpler. First, we create a class that contains processing code:
class DemoHandler { public Response<String> helloWorld(Request request) { return Response.ok().body(fromObject("Hello World")); } /* http://www.manongjc.com/article/1590.html */ public Response<String> theAnswer(Request request) { return Response.ok().body(fromObject("42")); }}Note that both methods have a flag that is compatible with the processing function. This allows us to use method references:
DemoHandler handler = new DemoHandler(); // or obtain via DIRouterFunction<?> route = route(GET("/hello-world"), handler::helloWorld) .and(route(GET("/the-answer"), handler::theAnswer));FilterFunction
The path mapped by the routing function can be filtered by calling RouterFunction.filter(FilterFunction<T, R>), where FilterFunction<T, R> is essentially BiFunction<Request, HandlerFunction<T>, Response<R>>. The handler parameter of the function represents the next item in the entire chain: This is a typical HandlerFunction, but if multiple filters are attached, it can also be another FilterFunction. Let's add a log filter to the route:
// http://www.manongjc.comRouterFunction<?> route = route(GET("/hello-world"), handler::helloWorld) .and(route(GET("/the-answer"), handler::theAnswer)) .filter((request, next) -> { System.out.println("Before handler invocation: " + request.path()); Response<?> response = next.handle(request); Object body = response.body(); System.out.println("After handler invocation: " + body); return response;});It should be noted that whether to call the next handler is optional. This is very useful in security and caching schemes (such as calling next only when the user has sufficient permissions).
Since route is an infinite routing function, we know what type of response information the next handler will return. This is why we ended up with Response<?> in our filter and respond to body with Object. In the handler class, both methods return Response<String>, so it should be possible to have a String response body. We can do this by using RouterFunction.andSame() instead of and(). This combination method requires that the parameter routing function is of the same type. For example, we can make all responses capitalize:
RouterFunction<String> route = route(GET("/hello-world"), handler::helloWorld) .andSame(route(GET("/the-answer"), handler::theAnswer)) .filter((request, next) -> { Response<String> response = next.handle(request); String newBody = response.body().toUpperCase(); return Response.from(response).body(fromObject(newBody)); });Using annotations, similar functions can be implemented using @ControllerAdvice and/or ServletFilter.
Run the server
All this is fine, but one thing I forgot: How can we run these functions in an actual HTTP server? The answer is undoubtedly by calling another function. You can convert the routing function to HttpHandler by using RouterFunctions.toHttpHandler(). HttpHandler is a response abstraction introduced to Spring 5.0 M1: it allows you to run on various response runtimes: Reactor Netty, RxNetty, Servlet 3.1+, and Undertow. In this example, we have shown how it is like running route in Reactor Netty. For Tomcat, it looks like this:
HttpHandler httpHandler = RouterFunctions.toHttpHandler(route);HttpServlet servlet = new ServletHttpHandlerAdapter(httpHandler);Tomcat server = new Tomcat();Context rootContext = server.addContext("", System.getProperty("java.io.tmpdir"));Tomcat.addServlet(rootContext, "servlet", servlet);rootContext.addServletMapping("/", "servlet");tomcatServer.start();One thing to note is that the above code does not depend on the Spring application context. Like JdbcTemplate and other Spring utility classes, using the application context is optional: you can connect the handler and routing functions in the context, but it is not required.
Also note that you can also convert the routing function to HandlerMapping so that it can run in the DispatcherHandler (may require a responsive @Controllers).
in conclusion
Let me draw a conclusion through a brief summary:
To give you a more comprehensive understanding, I have created a simple example project using the functional web framework. Download address
Thank you for reading, I hope it can help you. Thank you for your support for this site!