就像在昨天Juergen发布的博客的一样,Spring 5.0框架第二个里程碑版本中介绍了一个新的函数式web框架。在这篇文章中,我将更详细的介绍这个框架。
紧记该函数式web框架是在Spring5.0第一个里程碑版本基础上构建的。并且我们依旧提供基于注解的请求处理(例如@Controller,@RequestMapping),关于基于注解的请求处理部分的相关信息请查阅关于Spring5.0第一个里程碑版本的博客。
我将通过依次介绍HandlerFunction,RouterFunction以及FilterFunction 等核心组件来介绍整个框架。这三个接口以及本文中其他类型都可以在org.springframework.web.reactive.function包中找到。
新框架的起点是HandlerFunction
如下是“Hello World”的处理方法,它返回了状态为200,body为字符串的消息。
1
2
|
HandlerFunction
request -> Response.ok().body(fromObject(
"Hello World"
));
|
如上,构建于Reactor之上的处理方法是完全的响应式的(reactive),它们可以接受Flux、Mono或者其他相应流(Reactive Streams)的发布者作为返回类型的参数。
需要注意的是处理方法本身是没有副作用的,因为它将response作为返回值,而不是作为参数(对比Servlet.service(ServletRequest,ServletResponse),其实质是BiConsumer
入站请求是由RouterFunction
如下是一个路由方法的例子,包含了一个行内的处理方法。这里看起来有一点冗余,不必担心,因为后面我们将会将它变得精简。
1
2
3
4
5
6
7
8
|
RouterFunction
request -> {
if
(request.path().equals(
"/hello-world"
)) {
return
Optional.of(r -> Response.ok().body(fromObject(
"Hello World"
)));
}
else
{
return
Optional.empty();
}
};
|
一般不用写完整的路由方法,而是静态引入RouterFunctions.route(),这样就可以用请求判断式(RequestPredicate) (即 Predicate
1
2
3
|
RouterFunction
RouterFunctions.route(request -> request.path().equals(
"/hello-world"
),
request -> Response.ok().body(fromObject(
"Hello World"
)));
|
静态引入RequestPredicates.*后就可以使用那些常用的判断式了,如匹配路径、HTTP方法、content-type等。这样上面的例子将会变得更精简:
1
2
3
|
RouterFunction
RouterFunctions.route(RequestPredicates.path(
"/hello-world"
),
request -> Response.ok().body(fromObject(
"Hello World"
)));
|
两个路由方法可以被组合成一个新的路由方法,可以路由任意处理方法:如果第一个路由不匹配则执行第二个。可以通过调用RouterFunction.and()方法实现,如下:
1
2
3
4
5
|
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则表示任意判断式满足。如下:
1
2
3
4
5
|
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)的组合。所以上面的例子可以重写为:
1
2
3
4
5
|
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表达式。尽管这样很适合于实例和简短的例子,但是当结合请求路由和请求处理两个关注点时,可能就有变“混乱”的趋势了。所以我们将尝试将他们简化。首先,创建一个包含处理逻辑的类:
1
2
3
4
5
6
7
|
class
DemoHandler {
public
Response
return
Response.ok().body(fromObject(
"Hello World"
));
}
public
Response
return
Response.ok().body(fromObject(
"42"
));
}}
|
注意,这两个方法的签名都是和处理方法兼容的。这样就可以方法引用了:
1
2
3
4
|
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
1
2
3
4
5
6
7
8
9
10
|
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() 来完成这件事情。这个组合方法要求路由器函数参数是同一个类型。例如,我们可以让所有的响应消息变成小写的文本形式:
1
2
3
4
5
6
7
8
|
RouterFunction
route(GET(
"/hello-world"
), handler::helloWorld)
.andSame(route(GET(
"/the-answer"
), handler::theAnswer))
.filter((request, next) -> {
Response
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 来说则是像下面这个样子:
1
2
3
4
5
6
7
8
|
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 并行)中运行了。