Spring 5 新功能:函数式 Web 框架 已翻译 100%

oschina 投递于 2016/09/23 15:42 (共 6 段, 翻译完成于 09-26)
阅读 27120
收藏 86
11
加载中

As mentioned yesterday in Juergen’s blog post, the second milestone of Spring Framework 5.0 introduced a new functional web framework. In this post, I will give more information about the framework.

Keep in mind the functional web framework is built on the same reactive foundation that we provided in M1 and on which we also support annotation-based (i.e. @Controller,@RequestMapping) request handling, see the M1 blog post for more on that.

Example

We start with some excerpts from our sample application. Below is a reactive repository that exposes Person objects. It is quite similar to a traditional, non-reactive repository, except that it returns Flux<Person> where you would return a List<Person> traditionally, andMono<Person> where you would return a PersonMono<Void> is used as a completion signal: to indicate when the save has been completed. For more information on these Reactor types, refer to Dave’s blog post.

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

Here is how we expose that 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));
    }));

And here is how we would run it, for instance in Reactor Netty:

HttpHandler httpHandler = RouterFunctions.toHttpHandler(route);
ReactorHttpHandlerAdapter adapter =
  new ReactorHttpHandlerAdapter(httpHandler);
HttpServer server = HttpServer.create("localhost", 8080);
server.startAndAwait(adapter);

The final thing to do is to give it a try:

$ curl ' 
{"name":"John Doe","age":42}

There is a lot to cover here, so let’s dig in deeper!

已有 1 人翻译此段
我来翻译

Key Components

I will explain the framework by going over its key components: HandlerFunction,RouterFunction, and FilterFunction. These three interfaces, and all other types described in this post, can be found in the org.springframework.web.reactive.function package.

HandlerFunction

The starting point of this new framework is the HandlerFunction<T>, which is essentially aFunction<Request, Response<T>>, where Request and Response are newly-defined, immutable interfaces that offer a JDK-8 friendly DSL to the underlying HTTP messages. There is a convenient builder for building Response instances, quite similar to the one found inResponseEntity. The annotation counterpart to HandlerFunction would be a method with@RequestMapping.

Here is an example of a simple “Hello World” handler function, that returns a response with a 200 status and a body based on a String:

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

As we saw in the example above, handler functions are fully reactive by building on top of Reactor: they accept FluxMono, or any other Reactive Streams Publisher as response type.

It is important to note that HandlerFunction itself is side-effect free, because it returns the response, as opposed to taking it as a parameter (cf.Servlet.service(ServletRequest,ServletResponse), which essentially is aBiConsumer<ServletRequest,ServletResponse>). Side-effect free functions have many benefits: they are easier to test, compose, and optimize.

已有 1 人翻译此段
我来翻译

RouterFunction

Incoming requests are routed to handler functions with a RouterFunction<T> (i.e.Function<Request, Optional<HandlerFunction<T>>). A router function evaluates to a handler function if it matches; otherwise it returns an empty result. The RouterFunction has a similar purpose as a @RequestMapping annotation. However, there is an important distinction: with the annotation your route is limited to what can be expressed through the annotation values, and the processing of those is not trivial to override; with router functions the processing code is right in front of you: you can override or replace it quite easily.

Here is an example of a router function with an inlined handler function. It does look a bit verbose, but do not worry about that: we will find ways to make it shorter below.

RouterFunction<String> helloWorldRoute = 
  request -> {
    if (request.path().equals("/hello-world")) {
      return Optional.of(r -> Response.ok().body(fromObject("Hello World")));
    } else {
      return Optional.empty();
    }
  };

Typically, you do not write complete router functions, but rather (statically) importRouterFunctions.route(), which allows you to create a RouterFunction using aRequestPredicate (i.e. Predicate<Request>) and a HandlerFunction. If the predicate applies, the handler function is returned; otherwise an empty result. Using route, we can rewrite the above to the following:

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

You can (statically) import RequestPredicates.* to get access to commonly used predicates, such matching based on path, HTTP method, content-type, etc. With it, we can make ourhelloWorldRoute even simpler:

RouterFunction<String> helloWorldRoute =
  RouterFunctions.route(RequestPredicates.path("/hello-world"),
    request -> Response.ok().body(fromObject("Hello World")));
已有 1 人翻译此段
我来翻译

Composing Functions

Two router functions can be composed into a new router function that routes to either handler function: if the first function does not match, the second is evaluated. You can compose two router functions by calling RouterFunction.and(), like so:

RouterFunction<?> route =
  route(path("/hello-world"),
    request -> Response.ok().body(fromObject("Hello World")))
  .and(route(path("/the-answer"),
    request -> Response.ok().body(fromObject("42"))));

The above will respond with “Hello World” if the path matches /hello-world, and “42” if it matches /the-answer. If neither match, an empty Optional is returned. Note that the composed router functions are evaluated in order, so it makes sense to put generic functions before specific ones.

You can also compose request predicates, by calling and or or. These work as expected: forand the resulting predicate matches if both given predicates match, or matches if eitherpredicate does. For instance:

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 compositions! For instance,RequestPredicates.GET(String) is a composition of RequestPredicates.method(HttpMethod)and RequestPredicates.path(String). So we can rewrite the above to:

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 References

As an aside: so far we have written all handler functions as inline lambda’s. While this is fine for demo’s and short examples, it does have the tendency to get “messy”, as you are mixing two concerns: request routing and request handling. So let’s see if we can make things cleaner. First we create a class that contains the handling code:

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

Note that both methods have a signature that is compatible with a handler function. This allows us to use method references:

DemoHandler handler = new DemoHandler(); // or obtain via DI
RouterFunction<?> route =
  route(GET("/hello-world"), handler::helloWorld)
  .and(route(GET("/the-answer"), handler::theAnswer));
已有 1 人翻译此段
我来翻译

FilterFunction

Routes mapped by a router function can be filtered by callingRouterFunction.filter(FilterFunction<T, R>), where FilterFunction<T,R> is essentially aBiFunction<Request, HandlerFunction<T>, Response<R>>. The handler function parameter represents the next item in the chain: this is typically a HandlerFunction, but can also be another FilterFunction if multiple filters are applied. Let’s add a logging filter to our route:

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 invoking the next handler is optional. This is useful in security or caching scenarios (eg. invoke nextonly if the user has sufficient rights).

Because route is an unbounded router function, we do know what type of response the next handler is going to return. This is why we end up with a Response<?> in our filter, and with anObject response body. In our handler class, both methods return a Response<String>, so it should be possible to have a String response body. We can accomplish this by usingRouterFunction.andSame() instead of and(). This composition method requires the parameter router function to be of the same type. For example, we can make all responses upper case:

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

With annotations, similar functionality can be achieved using @ControllerAdvice and/or aServletFilter.

Running a Server

All this is well and good, but there is one piece missing: how can we actually run these functions in a HTTP server? The answer, unsurprisingly, comes by calling another function. You can convert a router function into a HttpHandler by using RouterFunctions.toHttpHandler(). The HttpHandler is a reactive abstraction that was introduced in Spring 5.0 M1: it allows you to run on a wide variety of reactive runtimes: Reactor Netty, RxNetty, Servlet 3.1+, and Undertow. In the example, we already showed what running a route in Reactor Netty looks like. 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 does not rely on a Spring application context. Just likeJdbcTemplate and other Spring utility classes, using a application context is optional: you can wire up your handler and router functions in a context, but it is not required.
Also note that you can also convert a router function into a HandlerMapping, so that it can run in a DispatcherHandler (possibly side-by-side with reactive @Controllers).

已有 2 人翻译此段
我来翻译

Conclusion

This ends the introduction to Spring’s new functional style web framework. Let me conclude by giving a short summary:

  • Handler functions handle request by returning a response,

  • Router functions route to handler functions, and can be composed with other router functions,

  • Router functions can be filtered by filter functions,

  • Router functions can be run in a reactive web runtime.

已有 1 人翻译此段
我来翻译
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(61)

xuezhongyu01
xuezhongyu01
这个是不是不支持mysql数据库?
三两记忆
三两记忆

引用来自“邹海彬”的评论

其实java要引入函数式编程,我是反对的
你反对有屁用呀
N
NoAfter
说看不懂函数式编程的是纯java程序员吧,函数式编程的好处是使开发流程简化,如果说看不懂函数式编程,那是不是grooy,scala什么的都不用活了,不是看不懂,只是你个人看不懂而已
niniwei
niniwei
感觉还可以哇、支持
xiaour
xiaour
感觉Spring 5 在玩儿火。你好好的把SpringBoot弄好就可以了,函数式编程看着真蛋疼啊!阅读性太差。以后出个公式式编程,a+~b->c.print(a)
叁
看完完全懵逼。。。。。感觉函数式可读性有点差啊,也许自己不太熟
webgaojun
webgaojun
猴子捞月一样,是不是可读性不好。
robortly
robortly

引用来自“abcijkxyz”的评论

这是要炫技吗?
不接受别人的思想,可不可以不要有偏见?
robortly
robortly
又在跟着C#的节奏走了。C#老早就有这东西了......
suemi94
suemi94
@gakaki 楼上正解,这个确实适合用来做微服务,搭配好的服务治理方案,做网站就扯淡了。对于传统网站,这种写法并不能好的拆分业务,不要忘了对业务无与伦比的建模和分割能力才是java在企业级编程成功的核心。上面一群喊着时代淘汰的也是醉了,搞个语法糖就是淘汰了。。。这到底改变了什么核心技术
返回顶部
顶部