|
对于最新的稳定版本,请使用 Spring Framework 7.0.6! |
函数式端点
Spring Web MVC 包括 WebMvc.fn,这是一种轻量级的函数式编程模型,其中使用函数来路由和处理请求,并且契约设计为不可变性。它是基于注解的编程模型的替代方案,但同样运行在相同的 DispatcherServlet 上。
概述
在WebMvc.fn中,一个HTTP请求是通过HandlerFunction来处理的:一个接受ServerRequest并返回ServerResponse的函数。
请求和响应对象都有不可变的契约,提供了对HTTP请求和响应的JDK 8友好的访问。
HandlerFunction相当于基于注解编程模型中的@RequestMapping方法的主体。
Incoming requests are routed to a handler function with a RouterFunction: a function that
takes ServerRequest and returns an optional HandlerFunction (i.e. Optional<HandlerFunction>).
When the router function matches, a handler function is returned; otherwise an empty Optional.
RouterFunction is the equivalent of a @RequestMapping annotation, but with the major
difference that router functions provide not just data, but also behavior。
RouterFunctions.route() 提供了一个路由器构建器,便于创建路由器,
如下例所示:
-
Java
-
Kotlin
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
import static org.springframework.web.servlet.function.RouterFunctions.route;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route = route() (1)
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
public class PersonHandler {
// ...
public ServerResponse listPeople(ServerRequest request) {
// ...
}
public ServerResponse createPerson(ServerRequest request) {
// ...
}
public ServerResponse getPerson(ServerRequest request) {
// ...
}
}
| 1 | 使用route()创建路由器。 |
import org.springframework.web.servlet.function.router
val repository: PersonRepository = ...
val handler = PersonHandler(repository)
val route = router { (1)
accept(APPLICATION_JSON).nest {
GET("/person/{id}", handler::getPerson)
GET("/person", handler::listPeople)
}
POST("/person", handler::createPerson)
}
class PersonHandler(private val repository: PersonRepository) {
// ...
fun listPeople(request: ServerRequest): ServerResponse {
// ...
}
fun createPerson(request: ServerRequest): ServerResponse {
// ...
}
fun getPerson(request: ServerRequest): ServerResponse {
// ...
}
}
| 1 | 使用路由器DSL创建路由器。 |
如果您将 RouterFunction 注册为一个 bean,例如通过在 @Configuration 类中公开它,它将被 servlet 自动检测到,如 运行服务器 所述。
处理器函数
ServerRequest 和 ServerResponse 是不可变的接口,它们提供了 JDK 8 友好的访问 HTTP 请求和响应的功能,包括头信息、正文、方法和状态码。
服务器请求
ServerRequest 提供对 HTTP 方法、URI、标头和查询参数的访问,
而主体的访问则通过 body 方法提供。
以下示例将请求正文提取到 String:
-
Java
-
Kotlin
String string = request.body(String.class);
val string = request.body<String>()
以下示例将正文提取到 List<Person>,
其中 Person 对象从序列化形式(如 JSON 或 XML)解码:
-
Java
-
Kotlin
List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});
val people = request.body<Person>()
以下示例展示了如何访问参数:
-
Java
-
Kotlin
MultiValueMap<String, String> params = request.params();
val map = request.params()
服务器响应
ServerResponse 提供对 HTTP 响应的访问,并且由于它是不可变的,您可以使用 build 方法来创建它。您可以使用构建器设置响应状态、添加响应头或提供正文。以下示例创建了一个 200 (OK) 响应,带有 JSON 内容:
-
Java
-
Kotlin
Person person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)
以下示例展示了如何构建一个包含Location标头且没有正文的201(已创建)响应:
-
Java
-
Kotlin
URI location = ...
ServerResponse.created(location).build();
val location: URI = ...
ServerResponse.created(location).build()
您也可以使用异步结果作为正文,格式为 CompletableFuture,
Publisher,或 ReactiveAdapterRegistry 支持的任何其他类型。例如:
-
Java
-
Kotlin
Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
val person = webClient.get().retrieve().awaitBody<Person>()
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)
如果不仅仅是正文,状态或标头也基于异步类型,
您可以使用 async 上的静态方法 ServerResponse,该方法
接受 CompletableFuture<ServerResponse>、Publisher<ServerResponse> 或
任何 ReactiveAdapterRegistry 支持的其他异步类型。例如:
-
Java
Mono<ServerResponse> asyncResponse = webClient.get().retrieve().bodyToMono(Person.class)
.map(p -> ServerResponse.ok().header("Name", p.name()).body(p));
ServerResponse.async(asyncResponse);
服务器发送事件可以通过sse类上的静态ServerResponse方法提供。该方法提供的构建器允许您发送字符串或其他对象作为JSON。例如:
-
Java
-
Kotlin
public RouterFunction<ServerResponse> sse() {
return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> {
// Save the sseBuilder object somewhere..
}));
}
// In some other thread, sending a String
sseBuilder.send("Hello world");
// Or an object, which will be transformed into JSON
Person person = ...
sseBuilder.send(person);
// Customize the event by using the other methods
sseBuilder.id("42")
.event("sse event")
.data(person);
// and done at some point
sseBuilder.complete();
fun sse(): RouterFunction<ServerResponse> = router {
GET("/sse") { request -> ServerResponse.sse { sseBuilder ->
// Save the sseBuilder object somewhere..
}
}
// In some other thread, sending a String
sseBuilder.send("Hello world")
// Or an object, which will be transformed into JSON
val person = ...
sseBuilder.send(person)
// Customize the event by using the other methods
sseBuilder.id("42")
.event("sse event")
.data(person)
// and done at some point
sseBuilder.complete()
处理器类
我们可以将处理器函数写成 lambda 表达式,如下例所示:
-
Java
-
Kotlin
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().body("Hello World");
val helloWorld: (ServerRequest) -> ServerResponse =
{ ServerResponse.ok().body("Hello World") }
这很方便,但在应用程序中我们需要多个函数,并且多个内联lambda可能会变得混乱。因此,将相关的处理器函数组合到一个处理器类中是有用的,这个处理器类的作用类似于注解应用中的 @Controller。
例如,以下类公开了一个响应式Person存储库:
-
Java
-
Kotlin
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
public class PersonHandler {
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
public ServerResponse listPeople(ServerRequest request) { (1)
List<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people);
}
public ServerResponse createPerson(ServerRequest request) throws Exception { (2)
Person person = request.body(Person.class);
repository.savePerson(person);
return ok().build();
}
public ServerResponse getPerson(ServerRequest request) { (3)
int personId = Integer.parseInt(request.pathVariable("id"));
Person person = repository.getPerson(personId);
if (person != null) {
return ok().contentType(APPLICATION_JSON).body(person);
}
else {
return ServerResponse.notFound().build();
}
}
}
| 1 | listPeople 是一个处理器函数,它返回存储库中找到的所有 Person 对象的 JSON。 |
| 2 | createPerson 是一个处理器函数,用于存储包含在请求体中的新 Person。 |
| 3 | getPerson 是一个处理器函数,它根据 id 路径变量返回一个单独的人员信息。我们从仓库中检索该 Person 并创建 JSON 响应,如果找到的话。如果没有找到,则返回 404 Not Found 响应。 |
class PersonHandler(private val repository: PersonRepository) {
fun listPeople(request: ServerRequest): ServerResponse { (1)
val people: List<Person> = repository.allPeople()
return ok().contentType(APPLICATION_JSON).body(people);
}
fun createPerson(request: ServerRequest): ServerResponse { (2)
val person = request.body<Person>()
repository.savePerson(person)
return ok().build()
}
fun getPerson(request: ServerRequest): ServerResponse { (3)
val personId = request.pathVariable("id").toInt()
return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).body(it) }
?: ServerResponse.notFound().build()
}
}
| 1 | listPeople 是一个处理器函数,它返回存储库中找到的所有 Person 对象的 JSON。 |
| 2 | createPerson 是一个处理器函数,用于存储包含在请求体中的新 Person。 |
| 3 | getPerson 是一个处理器函数,它根据 id 路径变量返回一个单独的人员信息。我们从仓库中检索该 Person 并创建 JSON 响应,如果找到的话。如果没有找到,则返回 404 Not Found 响应。 |
验证
-
Java
-
Kotlin
public class PersonHandler {
private final Validator validator = new PersonValidator(); (1)
// ...
public ServerResponse createPerson(ServerRequest request) {
Person person = request.body(Person.class);
validate(person); (2)
repository.savePerson(person);
return ok().build();
}
private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw new ServerWebInputException(errors.toString()); (3)
}
}
}
| 1 | 创建 Validator 实例。 |
| 2 | 应用验证。 |
| 3 | 对于400响应抛出异常。 |
class PersonHandler(private val repository: PersonRepository) {
private val validator = PersonValidator() (1)
// ...
fun createPerson(request: ServerRequest): ServerResponse {
val person = request.body<Person>()
validate(person) (2)
repository.savePerson(person)
return ok().build()
}
private fun validate(person: Person) {
val errors: Errors = BeanPropertyBindingResult(person, "person")
validator.validate(person, errors)
if (errors.hasErrors()) {
throw ServerWebInputException(errors.toString()) (3)
}
}
}
| 1 | 创建 Validator 实例。 |
| 2 | 应用验证。 |
| 3 | 对于400响应抛出异常。 |
Handlers can also use the standard bean validation API (JSR-303) by creating and injecting
a global Validator instance based on LocalValidatorFactoryBean.
See Spring Validation.
RouterFunction
Router functions are used to route the requests to the corresponding HandlerFunction.
Typically, you do not write router functions yourself, but rather use a method on the
RouterFunctions utility class to create one.
RouterFunctions.route() (no parameters) provides you with a fluent builder for creating a router function,
whereas RouterFunctions.route(RequestPredicate, HandlerFunction) offers a direct way
to create a router.
通常,建议使用route()构建器,因为它为典型的映射场景提供了方便的快捷方式,而不需要难以发现的静态导入。
例如,路由器函数构建器提供了方法GET(String, HandlerFunction)来创建GET请求的映射;以及POST(String, HandlerFunction)用于POST请求。
除了基于HTTP方法的映射,路由构建器还提供了一种在映射请求时引入其他条件的方法。
对于每个HTTP方法,都有一个重载的变体,它将RequestPredicate作为参数,通过该参数可以表达其他约束条件。
谓词
你可以编写自己的 RequestPredicate,但是RequestPredicates工具类提供了常用的实现,这些实现基于请求路径、HTTP方法、内容类型等。
以下示例使用请求谓词来根据Accept头创建约束:
-
Java
-
Kotlin
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().body("Hello World")).build();
import org.springframework.web.servlet.function.router
val route = router {
GET("/hello-world", accept(TEXT_PLAIN)) {
ServerResponse.ok().body("Hello World")
}
}
你可以通过以下方式组合多个请求谓词:
-
RequestPredicate.and(RequestPredicate)— 两者都必须匹配。 -
RequestPredicate.or(RequestPredicate)— 两者都可以匹配。
许多来自RequestPredicates的谓词是组合的。
例如,RequestPredicates.GET(String)是由RequestPredicates.method(HttpMethod)
和RequestPredicates.path(String)组合而成的。
上面显示的例子还使用了两个请求谓词,因为构建器内部使用
RequestPredicates.GET,并将其与accept谓词组合。
路由
路由函数是按顺序进行评估的:如果第一个路由不匹配,将评估第二个,依此类推。 因此,先声明更具体的路由再声明一般的路由是有意义的。 当以后将路由函数注册为Spring bean时,这一点也很重要。 请注意,这种行为与基于注解的编程模型不同,在基于注解的编程模型中,会自动选择“最具体”的控制器方法。
在使用路由器功能构建器时,所有定义的路由都将组合成一个RouterFunction,该RouterFunction从build()返回。
还有其他方法可以将多个路由器功能组合在一起:
-
add(RouterFunction)在RouterFunctions.route()构建器上 -
RouterFunction.and(RouterFunction) -
RouterFunction.andRoute(RequestPredicate, HandlerFunction)—RouterFunction.and()的快捷方式,其中包含嵌套的RouterFunctions.route()。
以下示例展示了四个路由的组成:
-
Java
-
Kotlin
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> otherRoute = ...
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
.POST("/person", handler::createPerson) (3)
.add(otherRoute) (4)
.build();
| 1 | GET /person/{id} 带有匹配JSON的 Accept 标头被路由到
PersonHandler.getPerson |
| 2 | GET /person 带有匹配JSON的 Accept 标头被路由到
PersonHandler.listPeople |
| 3 | POST /person 没有附加谓词时映射到
PersonHandler.createPerson,并且 |
| 4 | otherRoute 是一个在其他地方创建并添加到路由构建中的路由器函数。 |
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.web.servlet.function.router
val repository: PersonRepository = ...
val handler = PersonHandler(repository);
val otherRoute = router { }
val route = router {
GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
POST("/person", handler::createPerson) (3)
}.and(otherRoute) (4)
| 1 | GET /person/{id} 带有匹配JSON的 Accept 标头被路由到
PersonHandler.getPerson |
| 2 | GET /person 带有匹配JSON的 Accept 标头被路由到
PersonHandler.listPeople |
| 3 | POST /person 没有附加谓词时映射到
PersonHandler.createPerson,并且 |
| 4 | otherRoute 是一个在其他地方创建并添加到路由构建中的路由器函数。 |
嵌套路由
一组路由器函数通常会有一个共享的谓词,例如一个共享的路径。
在上面的例子中,共享的谓词将是一个匹配/person的路径谓词,被三条路由使用。
当使用注解时,你可以通过使用类型级别的@RequestMapping注解来消除这种重复。
在WebMvc.fn中,可以通过路由器函数构建器上的path方法来共享路径谓词。
例如,上面的最后几行可以通过使用嵌套路由来改进:
-
Java
-
Kotlin
RouterFunction<ServerResponse> route = route()
.path("/person", builder -> builder (1)
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET(accept(APPLICATION_JSON), handler::listPeople)
.POST(handler::createPerson))
.build();
| 1 | 请注意,第二个参数path是一个接受路由器构建器的消费者。 |
import org.springframework.web.servlet.function.router
val route = router {
"/person".nest { (1)
GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
GET(accept(APPLICATION_JSON), handler::listPeople)
POST(handler::createPerson)
}
}
| 1 | 使用 nest DSL。 |
尽管基于路径的嵌套是最常见的,但你可以通过使用构建器上的nest方法来根据任何类型的谓词进行嵌套。
上述代码中仍然包含一些重复的部分,即共享的Accept-header谓词。
我们可以通过结合使用nest方法和accept来进行进一步优化:
-
Java
-
Kotlin
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.build();
import org.springframework.web.servlet.function.router
val route = router {
"/person".nest {
accept(APPLICATION_JSON).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST(handler::createPerson)
}
}
}
运行服务器
您通常在基于DispatcherHandler的设置中通过MVC配置运行路由功能,该配置使用Spring配置来声明处理请求所需的组件。MVC Java配置声明以下基础设施组件以支持功能性端点:
-
RouterFunctionMapping: 检测Spring配置中的一个或多个RouterFunction<?>bean,对其进行排序,通过RouterFunction.andOther将其组合,并将请求路由到生成的组合RouterFunction。 -
HandlerFunctionAdapter: 简单的适配器,允许DispatcherHandler调用 一个被映射到请求的HandlerFunction。
上述组件让功能端点适应DispatcherServlet请求处理生命周期,并且(潜在地)与注解控制器并行运行,如果声明了任何注解控制器的话。这也是Spring Boot WebStarters如何启用功能端点的方式。
以下示例展示了一个WebFlux Java配置:
-
Java
-
Kotlin
@Configuration
@EnableMvc
public class WebConfig implements WebMvcConfigurer {
@Bean
public RouterFunction<?> routerFunctionA() {
// ...
}
@Bean
public RouterFunction<?> routerFunctionB() {
// ...
}
// ...
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// configure message conversion...
}
@Override
public void addCorsMappings(CorsRegistry registry) {
// configure CORS...
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// configure view resolution for HTML rendering...
}
}
@Configuration
@EnableMvc
class WebConfig : WebMvcConfigurer {
@Bean
fun routerFunctionA(): RouterFunction<*> {
// ...
}
@Bean
fun routerFunctionB(): RouterFunction<*> {
// ...
}
// ...
override fun configureMessageConverters(converters: List<HttpMessageConverter<*>>) {
// configure message conversion...
}
override fun addCorsMappings(registry: CorsRegistry) {
// configure CORS...
}
override fun configureViewResolvers(registry: ViewResolverRegistry) {
// configure view resolution for HTML rendering...
}
}
过滤处理程序函数
你可以通过使用路由函数构建器上的 before、after 或 filter 方法来过滤处理器函数。通过注解,你可以使用 @ControllerAdvice、一个 ServletFilter 或两者结合来实现类似的功能。过滤器将应用于由构建器构建的所有路由。这意味着在嵌套路由中定义的过滤器不会应用到“顶级”路由上。
例如,考虑以下示例:
-
Java
-
Kotlin
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople)
.before(request -> ServerRequest.from(request) (1)
.header("X-RequestHeader", "Value")
.build()))
.POST(handler::createPerson))
.after((request, response) -> logResponse(response)) (2)
.build();
| 1 | 添加自定义请求头的before过滤器仅应用于两个GET路由。 |
| 2 | The after 过滤器会记录响应,它被应用于所有路由,包括嵌套的路由。 |
import org.springframework.web.servlet.function.router
val route = router {
"/person".nest {
GET("/{id}", handler::getPerson)
GET(handler::listPeople)
before { (1)
ServerRequest.from(it)
.header("X-RequestHeader", "Value").build()
}
}
POST(handler::createPerson)
after { _, response -> (2)
logResponse(response)
}
}
| 1 | 添加自定义请求头的before过滤器仅应用于两个GET路由。 |
| 2 | The after 过滤器会记录响应,它被应用于所有路由,包括嵌套的路由。 |
The filter 方法在路由构建器上接受一个 HandlerFilterFunction: 一个接受 ServerRequest 和 HandlerFunction 并返回一个 ServerResponse 的函数。该处理器函数参数表示链中的下一个元素。这通常是被路由到的处理器,但也可能是应用了多个过滤器时的另一个过滤器。
现在我们可以为我们的路由添加一个简单的安全过滤器,假设我们有一个SecurityManager可以判断特定路径是否允许。
以下示例展示了如何实现这一点:
-
Java
-
Kotlin
SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();
import org.springframework.web.servlet.function.router
val securityManager: SecurityManager = ...
val route = router {
("/person" and accept(APPLICATION_JSON)).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST(handler::createPerson)
filter { request, next ->
if (securityManager.allowAccessTo(request.path())) {
next(request)
}
else {
status(UNAUTHORIZED).build();
}
}
}
}
上一个示例演示了调用 next.handle(ServerRequest) 是可选的。
我们只在允许访问时才运行处理器函数。
除了使用路由器功能构建器上的 filter 方法,还可以通过 RouterFunction.filter(HandlerFilterFunction) 将过滤器应用于现有的路由器功能。
CORS支持通过专门的CorsFilter为功能性端点提供。 |