Spring Web MVC:功能端点(Functional Endpoints)

https://docs.spring.io/spring-framework/reference/web/webmvc-functional.html

Spring Web MVC 包含了 WebMvc.fn,这是一个轻量级的函数式编程模型。在这个模型中,函数被用来路由和处理请求,并且设计了不可变性的规范。它是基于注解的编程模型的一个替代方案,但它仍然运行在同一个 DispatcherServlet 上。

概述

在 WebMvc.fn 中,HTTP 请求通过 HandlerFunction 进行处理:这是一个函数,它接收 ServerRequest 并返回 ServerResponse。请求和响应对象都具有不可变的规范,这些规范提供了对 HTTP 请求和响应的 JDK 8 友好访问方式。HandlerFunction 相当于基于注解的编程模型中 @RequestMapping 方法的主体部分。

传入的请求通过 RouterFunction 路由到相应的处理器函数:RouterFunction 是一个函数,它接收 ServerRequest 并返回一个可选的 HandlerFunction(即 Optional<HandlerFunction>)。当路由器函数匹配时,返回一个处理器函数;否则返回一个空的 Optional。RouterFunction 相当于 @RequestMapping 注解,但主要的区别在于路由器函数不仅提供数据,还提供行为。

RouterFunctions.route() 提供了一个路由器构建器,用于方便地创建路由器,以下示例展示了这一用法:

复制代码
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) {
		// ...
	}
}

如果你将 RouterFunction 注册为一个 bean,例如通过在一个 @Configuration 类中将其暴露出来,它将会被 servlet 自动检测。

HandlerFunction

ServerRequest 和 ServerResponse 是不可变的接口,它们提供了对 HTTP 请求和响应的 JDK 8 友好访问方式,包括头部信息、正文、请求方法和状态码。

ServerRequest

ServerRequest 提供了对 HTTP 方法、URI、头部信息和查询参数的访问,而访问正文内容则通过 body 方法提供。

下示例将请求正文提取为字符串:

复制代码
String string = request.body(String.class);

以下示例将正文提取为 List<Person>,其中 Person 对象是从序列化形式(如 JSON 或 XML)解码而来的:

复制代码
List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});

以下示例展示了如何访问参数:

复制代码
MultiValueMap<String, String> params = request.params();

ServerResponse

ServerResponse 提供了对 HTTP 响应的访问,并且因为它是不可变的,你可以使用构建方法来创建它。你可以使用构建器来设置响应状态码、添加响应头部信息或提供响应正文。以下示例创建了一个包含 JSON 内容的 200(OK)响应:

复制代码
Person person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);

以下示例展示了如何构建一个带有 Location 头部且没有正文的 201(CREATED)响应:

复制代码
URI location = ...
ServerResponse.created(location).build();

你还可以使用异步结果作为正文,具体形式可以是 CompletableFuture、Publisher,或者 ReactiveAdapterRegistry 支持的其他任何类型。例如:

复制代码
Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);

果不仅正文,而且状态码或头部信息也基于异步类型,你可以使用 ServerResponse 上的静态 async 方法,它接受 CompletableFuture<ServerResponse>Publisher<ServerResponse> 或 ReactiveAdapterRegistry 支持的任何其他异步类型。例如:

复制代码
Mono<ServerResponse> asyncResponse = webClient.get().retrieve().bodyToMono(Person.class)
  .map(p -> ServerResponse.ok().header("Name", p.name()).body(p));
ServerResponse.async(asyncResponse);

可以通过 ServerResponse 上的静态 sse 方法提供服务器发送事件(Server-Sent Events)。该方法提供的构建器允许你发送字符串或其他对象作为 JSON。例如:

复制代码
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();

处理器类(Handler Classes)

我们可以将处理器函数编写为 lambda 表达式,如下例所示:

复制代码
HandlerFunction<ServerResponse> helloWorld =
  request -> ServerResponse.ok().body("Hello World");

在实际应用中,我们可能需要多个函数,并且多个内联 lambda 表达式可能会让代码变得混乱。因此,将相关的处理器函数组合成一个处理器类是有用的,这类似于在基于注解的应用程序中使用 @Controller 注解的角色。例如,以下类公开了一个响应式的 Person 存储库:

复制代码
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();
		}
	}

}

验证(Validation)

一个功能端点可以使用 Spring 的验证实用程序来对请求体进行验证。例如,假设我们有一个针对 Person 的自定义 Spring Validator 实现:

复制代码
public class PersonHandler {

	private final Validator validator = new PersonValidator();

	// ...

	public ServerResponse createPerson(ServerRequest request) {
		Person person = request.body(Person.class);
		validate(person);
		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());
		}
	}
}

处理器还可以通过创建并注入一个基于 LocalValidatorFactoryBean 的全局 Validator 实例来使用标准的 Bean 验证 API(JSR-303)

RouterFunction

路由函数用于将请求路由到相应的HandlerFunction。通常,你不会自己编写路由函数,而是使用 RouterFunctions 工具类上的方法来创建一个。RouterFunctions.route()(无参数)提供了一个流畅的构建器来创建路由函数,而 RouterFunctions.route(RequestPredicate, HandlerFunction) 则提供了一种直接创建路由的方式。

通常,推荐使用 route() 构建器,因为它为典型的映射场景提供了方便的快捷方式,而无需使用难以发现的静态导入。例如,路由器函数构建器提供了 GET(String, HandlerFunction) 方法来创建针对 GET 请求的映射,以及 POST(String, HandlerFunction) 方法来创建针对 POST 请求的映射。

除了基于 HTTP 方法的映射外,路由构建器还提供了一种在映射请求时引入额外谓词(predicates )的方式。对于每种 HTTP 方法,都有一个重载的变体,它接受一个 RequestPredicate 作为参数,通过它可以表达额外的约束条件。

Predicates

可以编写自己的RequestPredicate,但RequestPredicates实用类提供了基于请求路径、HTTP方法、内容类型等的常用实现。以下示例使用请求谓词(request predicate )创建了一个基于Accept头的约束:

复制代码
RouterFunction<ServerResponse> route = RouterFunctions.route()
	.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
		request -> ServerResponse.ok().body("Hello World")).build();

可以使用以下方式将多个请求谓词组合在一起:

  • RequestPredicate.and(RequestPredicate) --- 两者都必须匹配。
  • RequestPredicate.or(RequestPredicate) --- 两者之一可以匹配。

RequestPredicates中的许多谓词是组合的。例如,RequestPredicates.GET(String)是由RequestPredicates.method(HttpMethod)和RequestPredicates.path(String)组合而成的。上面显示的示例也使用了两个请求谓词,因为构建器内部使用了RequestPredicates.GET,并将其与accept 谓词组合。

路由(Routes)

路由器函数按顺序进行评估:如果第一个路由不匹配,则评估第二个,依此类推。因此,在声明一般路由之前声明更具体的路由是有意义的。在将路由器函数注册为Spring bean时,这一点也很重要。这种行为与基于注解的编程模型不同,在基于注解的编程模型中,会自动选择"最具体"的控制器方法。

当使用路由器函数构建器时,所有定义的路由都被组合成一个从build()返回的RouterFunction。还有其他方法可以将多个路由器函数组合在一起:

  • 在RouterFunctions.route()构建器上使用add(RouterFunction)
  • RouterFunction.and(RouterFunction)
  • RouterFunction.andRoute(RequestPredicate, HandlerFunction) ---
    对于嵌套的RouterFunctions.route(),这是RouterFunction.and()的快捷方式。

以下示例展示了四条路由的组合:

复制代码
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();

嵌套路由(Nested Routes)

对于一组路由器函数来说,共享一个谓词是常见的,例如共享一个路径。在上面的示例中,共享的谓词将是一个匹配/person的路径谓词,被三个路由使用。当使用注解时,可以通过使用映射到/person的类型级别的@RequestMapping注解来消除这种重复。在WebMvc.fn中,可以通过路由器函数构建器上的path方法共享路径谓词。例如,上面示例的最后几行可以通过使用嵌套路由来改进:

复制代码
RouterFunction<ServerResponse> route = route()
	.path("/person", builder -> builder
		.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
		.GET(accept(APPLICATION_JSON), handler::listPeople)
		.POST(handler::createPerson))
	.build();

尽管基于路径的嵌套是最常见的,但可以通过在构建器上使用nest方法来根据任何类型的谓词进行嵌套。上述示例中仍然包含了一些重复,形式为共享的Accept-header谓词。我们可以通过将nest方法和accept一起使用来进一步改进:

复制代码
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();

提供资源

WebMvc.fn提供了内置的支持,用于提供服务资源。

除了下面描述的功能之外,还可以通过使用RouterFunctions#resource(java.util.function.Function)实现更灵活的资源处理。

重定向到资源

可以将会匹配指定谓词的请求重定向到资源。例如,在单页应用程序中处理重定向时,这可能是有用的。

复制代码
   ClassPathResource index = new ClassPathResource("static/index.html");
List<String> extensions = Arrays.asList("js", "css", "ico", "png", "jpg", "gif");
RequestPredicate spaPredicate = path("/api/**").or(path("/error")).or(pathExtension(extensions::contains)).negate();
RouterFunction<ServerResponse> redirectToIndex = route()
	.resource(spaPredicate, index)
	.build();

从根位置提供服务资源

还可以将会匹配给定模式的请求路由到相对于给定根位置的资源。

复制代码
Resource location = new FileSystemResource("public-resources/");
RouterFunction<ServerResponse> resources = RouterFunctions.resources("/resources/**", location);

运行服务器

通常通过MVC配置在基于DispatcherHandler的设置中运行路由器函数,该配置使用Spring配置声明处理请求所需的组件。MVC Java配置声明以下基础结构组件以支持功能性端点:

  • RouterFunctionMapping:在Spring配置中检测到一个或多个RouterFunction<>
    beans,对它们进行排序,通过RouterFunction.andOther将它们组合起来,并将请求路由到结果组成的RouterFunction。
  • HandlerFunctionAdapter:一个简单的适配器,允许DispatcherHandler调用映射到请求的HandlerFunction。

上述组件让功能性端点能够融入DispatcherServlet的请求处理生命周期中,并且(潜在地)与注解控制器并行运行(如果有声明的话)。这也是Spring Boot Web启动器启用功能性端点的方式。

以下示例展示了一个WebFlux Java配置:

复制代码
@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...
	}
}

过滤处理函数

你可以通过在路由函数构建器上使用before、after或filter方法来过滤处理函数。通过注解,你可以使用@ControllerAdvice、ServletFilter或两者来实现类似的功能。过滤器将应用于构建器构建的所有路由。这意味着在嵌套路由中定义的过滤器不适用于"顶层"路由。例如,考虑以下示例:

复制代码
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)
				.header("X-RequestHeader", "Value")
				.build()))
		.POST(handler::createPerson))
	.after((request, response) -> logResponse(response))
	.build();

路由器构建器上的filter方法接受一个HandlerFilterFunction:这是一个接受ServerRequest和HandlerFunction并返回ServerResponse的函数。处理函数参数表示链中的下一个元素。这通常是被路由到的处理程序,但如果应用了多个过滤器,它也可以是另一个过滤器。

现在我们可以向路由中添加一个简单的安全过滤器,假设我们有一个SecurityManager能够确定特定路径是否被允许。以下示例展示了如何做到这一点:

复制代码
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();

前面的例子表明,调用next.handle(ServerRequest)是可选的。我们只在访问被允许时才运行处理函数。

除了在路由器函数构建器上使用filter方法外,还可以通过RouterFunction.filter(HandlerFilterFunction)将过滤器应用于现有的路由器函数。

对于功能性端点,通过专用的CorsFilter提供了CORS支持。

相关推荐
Carlos_sam3 分钟前
OpenLayers:ol-wind之渲染风场图全解析
前端·javascript
拾光拾趣录12 分钟前
闭包:从“变量怎么还没死”到写出真正健壮的模块
前端·javascript
拾光拾趣录33 分钟前
for..in 和 Object.keys 的区别:从“遍历对象属性的坑”说起
前端·javascript
OpenTiny社区44 分钟前
把 SearchBox 塞进项目,搜索转化率怒涨 400%?
前端·vue.js·github
编程猪猪侠1 小时前
Tailwind CSS 自定义工具类与主题配置指南
前端·css
qhd吴飞1 小时前
mybatis 差异更新法
java·前端·mybatis
YGY Webgis糕手之路2 小时前
OpenLayers 快速入门(九)Extent 介绍
前端·经验分享·笔记·vue·web
患得患失9492 小时前
【前端】【vueDevTools】使用 vueDevTools 插件并修改默认打开编辑器
前端·编辑器
ReturnTrue8682 小时前
Vue路由状态持久化方案,优雅实现记住表单历史搜索记录!
前端·vue.js
UncleKyrie2 小时前
一个浏览器插件帮你查看Figma设计稿代码图片和转码
前端