java Spring 5 new features functional Web framework

  • 2020-05-24 05:32:21
  • OfStack

java Spring 5 new features functional Web framework

For example,

Let's start with some excerpts from the sample application. Below is the response library that exposes the Person object. Very similar to a traditional, non-responsive library, except that it returns Flux < Person > The traditional return is List < Person > , and return Mono < Person > The place to return to Person. Mono < Void > Used as a completion identifier: indicates when the save was completed.


public interface PersonRepository {
 Mono<Person> getPerson(int id);
 Flux<Person> allPeople();
 Mono<Void> savePerson(Mono<Person> person);
}

Here's how we expose the repository 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's how to do it, 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 to try 1:


$ curl 'http://localhost:8080/person/1'
{"name":"John Doe","age":42}

More on that below, let's dig deeper!

Core components

I'll introduce the framework by thoroughly explaining the core components: HandlerFunction, RouterFunction, and FilterFunction. The three interfaces and all the other types can be described in org. springframework. web. reactive. function found in the package.

HandlerFunction

The starting point for this new framework is HandlerFunction < T > Function, basically < Request, Response < T > > , where Request and Response are newly defined, and 1 percent of the unchanged interface is friendly to provide JDK-8 DSL to the underlying HTTP messages. This is a convenient build tool for building Response entities, very similar to what you saw in ResponseEntity. The corresponding annotation to HandlerFunction is a method with @RequestMapping.

Here is an example of a simple "Hello World" handler that returns a response message with a state of 200 and body as String:


HandlerFunction<String> helloWorld =
 request -> Response.ok().body(fromObject("Hello World"));

As we saw in the example above, the handlers are fully responsive by building on Reactor: they accept Flux, Mono, or any other corresponding stream Publisher as the response type.

Note 1 that HandlerFunction itself has no side effects because it returns the response rather than treating it as a parameter (see Servlet.service (ServletRequest,ServletResponse), which is essentially BiConsumer < ServletRequest,ServletResponse > ). The absence of side effects has many benefits: easy to test, write, and optimize.

RouterFunction

Incoming requests are routed to RouterFunction < T > Is the processing function (Function) < Request, Optional < HandlerFunction < T > > ) route to the handler, if it matches; Otherwise, an empty result is returned. The routing method is similar to the @RequestMapping annotation. However, there is one significant difference: routing with annotations is limited to what the annotation's value can express, and it is difficult to handle the coverage of these methods; When using the routing method, the code is there and can be easily overwritten or replaced.

Here is an example of a routing function with an embedded handler. It looks a bit wordy, 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();
  }
 };

Instead of writing the entire routing method, RouterFunctions.route () is introduced statically, so that the request judgment formula (RequestPredicate) (Predicate) can be used < Request > ) and the processing method (HandlerFunction) create the routing method. If the judgment is successful, it returns the processing method; otherwise, it returns an empty result. The following example is rewritten 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 common predicates, based on path, HTTP method, content type, and so on. With it, we can make helloWorldRoute much simpler:


RouterFunction<String> helloWorldRoute =
 RouterFunctions.route(RequestPredicates.path("/hello-world"),
  request -> Response.ok().body(fromObject("Hello World")));
 

Combination function

Two routing functions can form a new routing function, routing to either of the processing functions: if the first function does not match, the second 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 it matches/the-answer, it will also return "42". If both do not match, an empty Optional is returned. Note that the combined routing functions are executed in sequence, so it makes sense to put generic functions in front of concrete functions.

You can also combine demand predicates by calling and or or. Here's how it works: for and, if two given predicates match, the resulting predicates match, and if one of the two predicates matches, or matches. Such as:


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 of the 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("/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));
}));
0

Method references

BTW: so far, we have written all the handlers as inline lambda expressions. While this works well in demos and short examples, it has to be said that this has a tendency to lead to "chaos" because you are mixing two concerns: request routing and request handling. So let's see if we can make things simpler. First, we create a class that contains the processing code:


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));
}));
1

Notice that both methods have a flag that is compatible with the handler. This allows us to use method references:


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));
}));
2

FilterFunction

The path mapped by the routing function can be done by calling RouterFunction.filter (FilterFunction) < T, R > ) are filtered, FilterFunction among them < T,R > It's essentially BiFunction < Request, HandlerFunction < T > , Response < R > > . The function's processor (handler) parameter represents the next item in the chain: this is a typical HandlerFunction, but it can also be another FilterFunction if multiple filters are attached. Let's add a log filter to the route:


// http://www.manongjc.com
RouterFunction<?> 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;
});

Note that it is optional to call the next handler or not. This is useful in security and caching scenarios (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. Is that why we end up using Response in our filters < ? > End and reason for Object in response to body. In the handler class, both methods return Response < String > , so it should be possible to have an String response body. We can do this by using RouterFunction.andSame () instead of and(). This combination requires that the parameter routing function be of the same type. For example, we can make all responses uppercase:


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));
}));
4

With annotations, similar functionality can be implemented using @ControllerAdvice and/or ServletFilter.

Run server

All of these one-cuts are good, but one thing is forgotten: how do we run these functions on a real HTTP server? The answer, of course, is to call 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 a variety of response runtimes: Reactor Netty, RxNetty, Servlet 3.1+, and Undertow. In this example, we have shown what it is like to run route in Reactor Netty. For Tomcat, it looks like this:


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));
}));
5

One thing to note is that the above code does not depend on the Spring application context. Just like the JdbcTemplate and other Spring utility classes, using the application context is optional: you can plug in handlers 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 runs in DispatcherHandler (perhaps with a responsive @Controllers).

conclusion

Let me conclude with a brief summary:

The handler handles the request by returning the response. Routing functions route to processing functions and can be combined with other routing functions. Routing functions can be filtered through filters. The routing function can be run in the response web runtime.

To give you a more complete picture, I've created a simple example project using the functional web framework. Download address

Thank you for reading, I hope to help you, thank you for your support of this site!


Related articles: