Home > Article > Web Front-end > New in Spring 5: Functional Web Framework
Example
We choose a sample program to start. Below is a reactive resource library for exposing Person objects. This responsive resource library is similar to the traditional non-responsive resource library, except that Flux
public interface PersonRepository { Mono<Person> getPerson(int id); Flux<Person> allPeople(); Mono<Void> savePerson(Mono<Person> person);}
Here we introduce how to use the new functional web framework to expose the resource library:
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 introduce how to run Here is an example of 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 make a try request:
$ curl ' {"name":"John Doe","age":42}
The above introduction covers a lot of content, let’s dig deeper!
Core Components
I will introduce the entire framework by introducing core components such as HandlerFunction, RouterFunction and FilterFunction in sequence. These three interfaces, as well as other types in this article, can be found in the org.springframework.web.reactive.function package.
Processing function
The starting point of the new framework is HandlerFunction
The following is the processing method of "Hello World", which returns a message with status 200 and body as string.
HandlerFunction<String> helloWorld = request -> Response.ok().body(fromObject("Hello World"));
As mentioned above, the processing methods built on Reactor are completely reactive, and they can accept publishers of Flux, Mono or other corresponding streams (Reactive Streams) as return type parameters.
It should be noted that the processing method itself has no side effects, because it uses response as a return value, not as a parameter (compare Servlet.service (ServletRequest, ServletResponse), which is essentially BiConsumer
Routing function
Inbound requests are routed to HandlerFunction by RouterFunction
The following is an example of a routing method, including an inline processing method. This seems a bit redundant, don't worry because we will streamline it later.
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, you do not need to write a complete routing method, but statically introduce RouterFunctions.route(), so that you can use the request predicate (ie Predicate
RouterFunction<String> helloWorldRoute = RouterFunctions.route(request -> request.path().equals("/hello-world"), request -> Response.ok().body(fromObject("Hello World")));
After statically introducing RequestPredicates.*, you can use commonly used judgment expressions, such as matching paths, HTTP methods, content-type, etc. This way the above example will become more streamlined:
RouterFunction<String> helloWorldRoute = RouterFunctions.route(RequestPredicates.path("/hello-world"), request -> Response.ok().body(fromObject("Hello World")));
组合功能
两个路由方法可以被组合成一个新的路由方法,可以路由任意处理方法:如果第一个路由不匹配则执行第二个。可以通过调用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"))));
上面的例子如果路径匹配/hello-world会返回“Hello World”,如果匹配/the-answer则返回“42”。如果都不匹配则返回一个空的Optional对象。注意,组合的路由是按顺序执行的,所以应该将更通用的方法放到更明确的方法的前面。
请求判断式也是可以组合的,通过调研and或者or方法。正如预期的一样:and表示给定的两个判断式同时满足则组合判断式满足,or则表示任意判断式满足。如下:
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"))));
实际上,RequestPredicates中的大部分判断式都是组合的!比如RequestPredicates.GET(String)是RequestPredicates.method(HttpMethod)和RequestPredicates.path(String)的组合。所以上面的例子可以重写为:
RouterFunction<?> route = route(GET("/hello-world"), request -> Response.ok().body(fromObject("Hello World"))) .and(route(GET("/the-answer"), request -> Response.ok().body(fromObject(42))));
方法引用
此外,目前为止我们的处理方法都是行内的lambda表达式。尽管这样很适合于实例和简短的例子,但是当结合请求路由和请求处理两个关注点时,可能就有变“混乱”的趋势了。所以我们将尝试将他们简化。首先,创建一个包含处理逻辑的类:
class DemoHandler { public Response<String> helloWorld(Request request) { return Response.ok().body(fromObject("Hello World")); } public Response<String> theAnswer(Request request) { return Response.ok().body(fromObject("42")); }}
注意,这两个方法的签名都是和处理方法兼容的。这样就可以方法引用了:
DemoHandler handler = new DemoHandler(); // or obtain via DI RouterFunction<?> route = route(GET("/hello-world"), handler::helloWorld) .and(route(GET("/the-answer"), handler::theAnswer));
过滤功能
由路由器函数进行映射的路由可以通过调用 RouterFunction.filter(FilterFunction
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; });
注意这里对下一个处理器的调用时可选的。这个在安全或者缓存的场景中是很有用的 (例如只在用户拥有足够的权限时才调用 next)。
因为 route 是一个没有被绑定的路由器函数,我们就得知道接下来的处理会返回什么类型的响应消息。这就是为什么我们在过滤器中要以一个 Response> 结束, 那样它就会可能有一个 String 类型的响应消息体。我们可以通过使用 RouterFunction.andSame() 而不是 and() 来完成这件事情。这个组合方法要求路由器函数参数是同一个类型。例如,我们可以让所有的响应消息变成小写的文本形式:
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)); });
使用注解的话,类似的功能可以使用 @ControllerAdvice 或者是一个 ServletFilter 来实现。
运行一个服务端
所有这些都很不错,不过仍然有一块欠缺:我们如何实际地将这些函数在一个 HTTP 服务器中跑起来呢? 答案毋庸置疑,那就是通过调用另外的一个函数。 你可以通过使用 RouterFunctions.toHttpHandler() 来将一个路由器函数转换成 HttpHandler。HttpHandler 是 Spring 5.0 M1 中引入的一个响应式抽象: 它能让你运行许多的响应式运行时: Reactor Netty, RxNetty, Servlet 3.1+, 以及 Undertow。在本示例中,我们已经展示了在 Reactor Netty 中运行一个路由会是什么样子的。对于 Tomcat 来说则是像下面这个样子:
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();
需要注意的意见事情就是上面的东西并不依赖于一个 Spring 应用程序上下文。就跟 JdbcTemplate 以及其它 Spring 的工具类那样, 要不要使用应用程序上下文是可以选的: 你可以将你的处理器和路由器函数在一个上下文中进行绑定,但并不是必须的。
还要注意的就是你也可以将一个路由器函数转换到一个 HandlerMapping中去,那样就它可以在一个 DispatcherHandler (可能是跟响应式的 @Controllers 并行)中运行了。
结论
This concludes the introduction to Spring’s new functional web framework. Let me briefly summarize:
A processing function handles a request by making a response,
A router function can be connected to a processing function and can be shared with other router functions,
A router function can be filtered by a filter function,
Router Features can run on a reactive web operating mechanism.