Appearance
Spring Web Servlet:MVC - Part3
Tip:基于 Spring Core 5.3.30 版本。
1. 函数式端点
Spring Web MVC 包含了 WebMvc.fn,这是一个轻量级的函数式编程模型,其中的函数被用来路由和处理请求,而契约则被设计为不可变的。它是基于注解的编程模型的一种替代,但在其他方面,它运行在相同的 DispatcherServlet 上。
1.1. 概述
在 WebMvc.fn 中,HTTP 请求是由 HandlerFunction 处理的:一个接收 ServerRequest 并返回 ServerResponse 的函数。请求和响应对象都有不可变的契约,提供了对 HTTP 请求和响应的 JDK 8 友好的访问。HandlerFunction 相当于基于注解的编程模型中 @RequestMapping 方法的主体。
传入的请求通过 RouterFunction 路由到处理函数:一个接收 ServerRequest 并返回可选的 HandlerFunction(即 Optional<HandlerFunction>)的函数。当路由函数匹配时,返回一个处理函数,否则返回一个空的 Optional。RouterFunction 相当于 @RequestMapping 注解,但主要区别在于路由函数不仅提供数据,还提供行为。
RouterFunctions.route() 提供了一个路由构建器,便于创建路由,如下例所示:
Java
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Java
public class PersonHandler {
// ...
public ServerResponse listPeople(ServerRequest request) {
// ...
}
public ServerResponse createPerson(ServerRequest request) {
// ...
}
public ServerResponse getPerson(ServerRequest request) {
// ...
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
如果你将 RouterFunction 注册为一个 Bean,例如在 @Configuration 类中暴露它,那么它将会被 Servlet 自动检测到,如运行服务器中所解释的那样。
1.2. HandlerFunction
ServerRequest 和 ServerResponse 是不可变的接口,它们提供了对 HTTP 请求和响应的 JDK 8 友好的访问方式,包括头部、主体、方法和状态码。
1.2.1. ServerRequest
ServerRequest 提供了对 HTTP 方法、URI、头部和查询参数的访问,而对主体的访问则通过 body 方法提供。
下面的示例将请求主体提取为一个字符串:
Java
String string = request.body(String.class);下面的示例将主体提取为 List<Person>,其中 Person 对象是从序列化的形式(如 JSON 或 XML)解码的:
Java
List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});以下示例展示了如何访问参数:
Java
MultiValueMap<String, String> params = request.params();1.2.2. ServerResponse
ServerResponse 提供了对 HTTP 响应的访问,由于它是不可变的,你可以使用 build 方法来创建它。你可以使用构建器来设置响应状态,添加响应头,或者提供一个主体。以下示例创建了一个带有 JSON 内容的 200(OK)响应:
Java
Person person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);1
2
2
以下示例展示了如何构建一个带有 Location 头部且无主体的 201(CREATED)响应:
Java
URI location = ...
ServerResponse.created(location).build();1
2
2
你也可以使用异步结果作为主体,形式可以是 CompletableFuture、Publisher 或者任何其他 ReactiveAdapterRegistry 支持的类型。例如:
Java
Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);1
2
2
如果不仅是主体,而且状态或头部也基于异步类型,你可以使用 ServerResponse 上的静态 async 方法,它接受 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);1
2
3
2
3
可以通过 ServerResponse 上的静态 sse 方法提供 Server-Sent Events。该方法提供的构建器允许你发送字符串,或者以 JSON 的形式发送其他对象。例如:
Java
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();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1.2.3. 处理器类
我们可以将处理函数写成一个 lambda 表达式,如下例所示:
Java
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().body("Hello World");1
2
2
这确实很方便,但在应用程序中,我们需要多个函数,多个内联 lambda 表达式可能会变得混乱。因此,将相关的处理函数组合到一个处理类中是很有用的,这个处理类在基于注解的应用程序中的角色类似于 @Controller。例如,以下类公开了一个响应式的 Person 仓库:
Java
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) {
List<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people);
}
public ServerResponse createPerson(ServerRequest request) throws Exception {
Person person = request.body(Person.class);
repository.savePerson(person);
return ok().build();
}
public ServerResponse getPerson(ServerRequest request) {
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1.2.4. 验证
一个功能性的端点可以使用 Spring 的验证设施对请求主体进行验证。例如,给定一个针对 Person 的自定义 Spring Validator 实现:
Java
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());
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
处理器也可以通过创建和注入基于 LocalValidatorFactoryBean 的全局 Validator 实例来使用标准的 Bean 验证 API(JSR-303)。请参阅 Spring 验证。
1.3. RouterFunction
路由函数用于将请求路由到相应的 HandlerFunction。通常,你不需要自己编写路由函数,而是使用 RouterFunctions 实用类上的方法来创建一个。RouterFunctions.route()(无参数)为你提供了一个 Fluent 的构建器来创建路由函数,而 RouterFunctions.route(RequestPredicate, HandlerFunction) 提供了一种直接创建路由的方式。
一般来说,建议使用 route() 构建器,因为它为典型的映射场景提供了方便的快捷方式,而不需要难以发现的静态导入。例如,路由函数构建器提供了 GET(String, HandlerFunction) 方法来为 GET 请求创建映射,以及 POST(String, HandlerFunction) 用于 POST 请求。
除了基于 HTTP 方法的映射,路由构建器还提供了一种在映射请求时引入额外谓词的方式。对于每个 HTTP 方法,都有一个接受 RequestPredicate 作为参数的重载变体,通过这种方式可以表达额外的约束。
1.3.1. 谓词
你可以编写自己的 RequestPredicate,但 RequestPredicates 实用类提供了常用的实现,基于请求路径、HTTP 方法、内容类型等。以下示例使用一个请求谓词来创建一个基于 Accept 头部的约束:
Java
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().body("Hello World")).build();1
2
3
2
3
你可以通过以下方式组合多个请求谓词:
RequestPredicate.and(RequestPredicate):两者都必须匹配;RequestPredicate.or(RequestPredicate):任一可以匹配;
RequestPredicates 中的许多谓词都是组合的。例如,RequestPredicates.GET(String) 是由 RequestPredicates.method(HttpMethod) 和 RequestPredicates.path(String) 组合而成的。上面显示的示例也使用了两个请求谓词,因为构建器内部使用了 RequestPredicates.GET,并将其与 accept 谓词组合。
1.3.2. 路由
路由函数是按顺序评估的:如果第一个路由不匹配,那么就评估第二个,依此类推。因此,最好在通用路由之前声明更具体的路由。这在将路由函数注册为 Spring Bean 时也很重要,稍后将进行描述。请注意,这种行为与基于注解的编程模型不同,在那里会自动选择 “最具体” 的控制器方法。
使用路由函数构建器时,所有定义的路由都会组合成一个从 build() 返回的 RouterFunction。还有其他方式可以将多个路由函数组合在一起:
在
RouterFunctions.route()构建器上添加add(RouterFunction)。RouterFunction.and(RouterFunction)。RouterFunction.andRoute(RequestPredicate, HandlerFunction)——RouterFunction.and()的快捷方式,内嵌RouterFunctions.route()。
以下示例显示了四个路由的组合:
Java
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)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.add(otherRoute)
.build();1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
- 第 10 行:
GET /person/{id},当Accept头匹配 JSON 时,会被路由到PersonHandler.getPerson; - 第 11 行:
GET /person,当Accept头匹配 JSON 时,会被路由到PersonHandler.listPeople; - 第 12 行:
POST /person,没有额外的谓词,会映射到PersonHandler.createPerson; - 第 13 行:
otherRoute是在其他地方创建的路由函数,并被添加到构建的路由中;
1.3.3. 嵌套路由
对于一组路由函数来说,拥有一个共享的谓词是很常见的,例如一个共享的路径。在上面的例子中,共享的谓词将是一个匹配 /person 的路径谓词,被三个路由使用。当使用注解时,你可以通过使用映射到 /person 的类型级别的 @RequestMapping 注解来消除这种重复。在 WebMvc.fn 中,路径谓词可以通过路由函数构建器上的 path 方法来共享。例如,上面例子的最后几行可以通过使用嵌套路由的方式进行改进:
Java
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();1
2
3
4
5
6
2
3
4
5
6
虽然基于路径的嵌套是最常见的,但你可以通过在构建器上使用 nest 方法来进行任何类型的谓词嵌套。上述内容中仍然包含一些重复,形式为共享的 Accept 头谓词。我们可以通过结合 accept 使用 nest 方法来进一步改进:
Java
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();1
2
3
4
5
6
7
2
3
4
5
6
7
1.4. 运行服务器
你通常会在基于 DispatcherHandler 的设置中通过 MVC 配置运行路由函数,该配置使用 Spring 配置来声明处理请求所需的组件。MVC Java 配置声明了以下基础设施组件以支持函数式端点:
RouterFunctionMapping:在 Spring 配置中检测一个或多个RouterFunction<?>Bean,对它们进行排序,通过RouterFunction.andOther将它们组合起来,并将请求路由到所组成的RouterFunction。HandlerFunctionAdapter:简单的适配器,让DispatcherHandler调用映射到请求的HandlerFunction。
前述组件让函数式端点适应 DispatcherServlet 请求处理生命周期,并且也(可能)与声明的任何带注解的控制器并行运行。这也是 Spring Boot Web starter 启用函数式端点的方式。
以下示例显示了一个 WebFlux Java 配置:
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...
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1.5. 过滤处理函数
你可以通过在路由函数构建器上使用 before、after 或 filter 方法来过滤处理函数。通过注解,你可以使用 @ControllerAdvice、ServletFilter 或两者都用来实现类似的功能。过滤器将应用于构建器构建的所有路由。这意味着在嵌套路由中定义的过滤器不适用于 “顶级” 路由。例如,考虑以下示例:
Java
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();1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
- 第 6 行:添加自定义请求头的
before过滤器仅应用于两个 GET 路由; - 第 10 行:记录响应的
after过滤器应用于所有路由,包括嵌套的路由;
路由构建器上的 filter 方法接受一个 HandlerFilterFunction:一个接受 ServerRequest 和 HandlerFunction 并返回 ServerResponse 的函数。处理函数参数代表链中的下一个元素。这通常是路由到的处理器,但如果应用了多个过滤器,它也可以是另一个过滤器。
现在我们可以在我们的路由中添加一个简单的安全过滤器,假设我们有一个 SecurityManager 可以确定是否允许特定的路径。以下示例展示了如何做到这一点:
Java
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();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
前面的示例表明,调用 next.handle(ServerRequest) 是可选的。我们只在允许访问时才运行处理函数。
除了在路由函数构建器上使用 filter 方法外,还可以通过 RouterFunction.filter(HandlerFilterFunction) 将过滤器应用到现有的路由函数。
Note:对于函数式端点,CORS 支持是通过专用的
CorsFilter提供的。
2. URI 链接
这一部分描述了在 Spring 框架中处理 URI 的各种可用选项。
2.1. UriComponents
UriComponentsBuilder 帮助从带有变量的 URI 模板构建 URI,如下面的示例所示:
Java
UriComponents uriComponents = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.build();
URI uri = uriComponents.expand("Westin", "123").toUri();1
2
3
4
5
6
7
2
3
4
5
6
7
前面的示例可以被整合成一个链,并通过 buildAndExpand 进行缩短,如下面的示例所示:
Java
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("Westin", "123")
.toUri();1
2
3
4
5
6
2
3
4
5
6
你可以通过直接转到 URI(这暗含编码)来进一步缩短它,如下面的示例所示:
Java
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");1
2
3
4
2
3
4
你还可以通过使用完整的 URI 模板来进一步缩短它,如下面的示例所示:
Java
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
.build("Westin", "123");1
2
3
2
3
2.2. UriBuilder
UriComponentsBuilder 实现了 UriBuilder。你可以使用 UriBuilderFactory 创建一个 UriBuilder。UriBuilderFactory 和 UriBuilder 一起提供了一个可插拔的机制,基于共享配置(如基础 URL、编码偏好和其他细节)从 URI 模板构建 URI。
你可以使用 UriBuilderFactory 配置 RestTemplate 和 WebClient,以自定义 URI 的准备。DefaultUriBuilderFactory 是 UriBuilderFactory 的默认实现,它内部使用 UriComponentsBuilder 并公开共享配置选项。
下面的示例展示了如何配置 RestTemplate:
Java
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
下面的示例展示了如何配置 WebClient:
Java
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();1
2
3
4
5
6
7
2
3
4
5
6
7
此外,你也可以直接使用 DefaultUriBuilderFactory。它与使用 UriComponentsBuilder 类似,但不是静态工厂方法,而是一个实际的实例,该实例持有配置和偏好,如下面的示例所示:
Java
String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);
URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");1
2
3
4
5
6
2
3
4
5
6
2.3. URI 编码
UriComponentsBuilder 在两个层面上暴露了编码选项:
UriComponentsBuilder#encode():首先预编码 URI 模板,然后在扩展时严格编码 URI 变量;UriComponents#encode():在 URI 变量扩展后编码 URI 组件;
这两个选项都会将非 ASCII 和非法字符替换为转义的八进制数。然而,第一个选项还会替换出现在 URI 变量中的具有保留含义的字符。
Note:考虑
;,它在路径中是合法的,但具有保留含义。第一个选项会将 URI 变量中的;替换为%3B,但不会在 URI 模板中替换。相比之下,第二个选项永远不会替换;,因为它是路径中的合法字符。
对于大多数情况,第一个选项可能会给出预期的结果,因为它将 URI 变量视为需要完全编码的不透明数据,而第二个选项在 URI 变量确实包含保留字符时很有用。当完全不扩展 URI 变量时,第二个选项也很有用,因为它也会编码任何偶然看起来像 URI 变量的东西。
下面的示例使用了第一个选项:
Java
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("New York", "foo+bar")
.toUri();
// Result is "/hotel%20list/New%20York?q=foo%2Bbar"1
2
3
4
5
6
7
2
3
4
5
6
7
你可以通过直接转到 URI(这暗含编码)来缩短前面的示例,如下面的示例所示:
Java
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar");1
2
3
2
3
你还可以通过使用完整的 URI 模板来进一步缩短它,如下面的示例所示:
Java
URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
.build("New York", "foo+bar");1
2
2
WebClient 和 RestTemplate 通过 UriBuilderFactory 策略在内部扩展和编码 URI 模板。两者都可以配置自定义策略,如下面的示例所示:
Java
String baseUrl = "https://example.com";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
// Customize the RestTemplate..
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
// Customize the WebClient..
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
DefaultUriBuilderFactory 实现使用 UriComponentsBuilder 在内部扩展和编码 URI 模板。作为一个工厂,它提供了一个单一的地方来配置编码的方法,基于以下的编码模式之一:
TEMPLATE_AND_VALUES:使用UriComponentsBuilder#encode(),对应于前面列表中的第一个选项,预先编码 URI 模板,并在扩展时严格编码 URI 变量。VALUES_ONLY:不对 URI 模板进行编码,而是通过UriUtils#encodeUriVariables对 URI 变量进行严格编码,然后将它们扩展到模板中。URI_COMPONENT:使用UriComponents#encode(),对应于前面列表中的第二个选项,在 URI 变量扩展后编码 URI 组件值。NONE:不应用任何编码。
出于历史原因和向后兼容性,RestTemplate 被设置为 EncodingMode.URI_COMPONENT。WebClient 依赖于 DefaultUriBuilderFactory 中的默认值,该值在 5.0.x 版本中从 EncodingMode.URI_COMPONENT 更改为 EncodingMode.TEMPLATE_AND_VALUES。
2.4. 相对 Servlet 请求
你可以使用 ServletUriComponentsBuilder 创建相对于当前请求的 URI,如下面的示例所示:
Java
HttpServletRequest request = ...
// Re-uses scheme, host, port, path, and query string...
URI uri = ServletUriComponentsBuilder.fromRequest(request)
.replaceQueryParam("accountId", "{id}")
.build("123");1
2
3
4
5
6
7
2
3
4
5
6
7
你可以创建相对于上下文路径的 URI,如下面的示例所示:
Java
HttpServletRequest request = ...
// Re-uses scheme, host, port, and context path...
URI uri = ServletUriComponentsBuilder.fromContextPath(request)
.path("/accounts")
.build()
.toUri();1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
你可以创建相对于 Servlet 的 URI(例如,/main/*),如下面的示例所示:
Java
HttpServletRequest request = ...
// Re-uses scheme, host, port, context path, and Servlet mapping prefix...
URI uri = ServletUriComponentsBuilder.fromServletMapping(request)
.path("/accounts")
.build()
.toUri();1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Note:从 5.1 版本开始,
ServletUriComponentsBuilder忽略了来自Forwarded和X-Forwarded-*头部的信息,这些信息指定了客户端发起的地址。你可以考虑使用ForwardedHeaderFilter来提取和使用或丢弃这些头部信息。
2.5. 链接到控制器
Spring MVC 提供了一种机制来准备指向控制器方法的链接。例如,下面的 MVC 控制器允许创建链接:
Java
@Controller
@RequestMapping("/hotels/{hotel}")
public class BookingController {
@GetMapping("/bookings/{booking}")
public ModelAndView getBooking(@PathVariable Long booking) {
// ...
}
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
你可以通过按名称引用方法来准备一个链接,如下面的示例所示:
Java
UriComponents uriComponents = MvcUriComponentsBuilder
.fromMethodName(BookingController.class, "getBooking", 21).buildAndExpand(42);
URI uri = uriComponents.encode().toUri();1
2
3
4
2
3
4
在前面的示例中,我们提供了实际的方法参数值(在这种情况下,是 long 值:21)来作为路径变量并插入到 URL 中。此外,我们提供了值 42 来填充任何剩余的 URI 变量,例如从类型级请求映射继承的 hotel 变量。如果方法有更多的参数,我们可以为不需要用于 URL 的参数提供 null。一般来说,只有 @PathVariable 和 @RequestParam 参数对于构造 URL 是相关的。
还有其他方式可以使用 MvcUriComponentsBuilder。例如,你可以使用类似于通过代理进行模拟测试的技术来避免按名称引用控制器方法,如下面的示例所示:
Java
UriComponents uriComponents = MvcUriComponentsBuilder
.fromMethodCall(MvcUriComponentsBuilder.on(BookingController.class).getBooking(21)).buildAndExpand(42);
URI uri = uriComponents.encode().toUri();1
2
3
4
2
3
4
Note:当控制器方法签名被设计为可用于
fromMethodCall创建链接时,其设计受到了限制。除了需要适当的参数签名外,还有一个技术限制在于返回类型(即,为链接构建器调用生成运行时代理),因此返回类型不能是final的。特别是,常见的用于视图名称的String返回类型在这里不起作用。你应该使用ModelAndView或者甚至是纯Object(带有String返回值)。
先前的示例使用了 MvcUriComponentsBuilder 中的静态方法。在内部,它们依赖 ServletUriComponentsBuilder 从当前请求的 Scheme、Host、Port、上下文路径和 Servlet 路径来准备基础 URL。这在大多数情况下都能很好地工作。然而,有时候,这可能是不够的。例如,你可能在请求的上下文之外(比如一个准备链接的批处理过程)或者你可能需要插入一个路径前缀(如从请求路径中移除的 Local 前缀,需要重新插入到链接中)。
对于这样的情况,你可以使用接受一个 UriComponentsBuilder 的静态 fromXxx 重载方法来使用基础 URL。或者,你可以用一个基础 URL 创建一个 MvcUriComponentsBuilder 的实例,然后使用基于实例的 withXxx 方法。例如,下面的列表使用了 withMethodCall:
Java
UriComponentsBuilder base = ServletUriComponentsBuilder.fromCurrentContextPath().path("/en");
MvcUriComponentsBuilder builder = MvcUriComponentsBuilder.relativeTo(base);
builder.withMethodCall(MvcUriComponentsBuilder.on(BookingController.class).getBooking(21)).buildAndExpand(42);
URI uri = uriComponents.encode().toUri();1
2
3
4
5
2
3
4
5
Note:从 5.1 版本开始,
MvcUriComponentsBuilder忽略了来自Forwarded和X-Forwarded-*头部的信息,这些信息指定了客户端发起的地址。你可以考虑使用ForwardedHeaderFilter来提取、使用或丢弃这些头部信息。
2.6. 视图中的链接
在诸如 Thymeleaf、FreeMarker 或 JSP 这样的视图中,你可以通过引用每个请求映射隐式或显式分配的名称,构建到带注解的控制器的链接。
请参考以下示例:
Java
@RequestMapping("/people/{id}/addresses")
public class PersonAddressController {
@RequestMapping("/{country}")
public HttpEntity<PersonAddress> getAddress(@PathVariable String country) { ... }
}1
2
3
4
5
2
3
4
5
考虑到前面的控制器,你可以按照以下方式从 JSP 准备一个链接:
jsp
<%@ taglib uri="http://www.springframework.org/tags" prefix="s" %>
...
<a href="${s:mvcUrl('PAC#getAddress').arg(0,'US').buildAndExpand('123')}">Get Address</a>1
2
3
2
3
前面的示例依赖于在 Spring 标签库(即,META-INF/spring.tld)中声明的 mvcUrl 函数,但是你可以轻松定义自己的函数,或者为其他模板技术准备类似的函数。
下面是它的工作方式。在启动时,每个 @RequestMapping 通过 HandlerMethodMappingNamingStrategy 分配一个默认名称,其默认实现使用类名大写字母和方法名(例如,ThingController 中的 getThing 方法变成 TC#getThing)。如果出现名称冲突,你可以使用 @RequestMapping(name="..") 来分配一个显式名称,或者实现你自己的 HandlerMethodMappingNamingStrategy。
3. 异步请求
Spring MVC 与 Servlet 3.0 的异步请求处理有着广泛的集成:
- 控制器方法中的
DeferredResult和Callable返回值为单个异步返回值提供了基本支持; - 控制器可以流式传输多个值,包括 SSE 和原始数据;
- 控制器可以使用响应式客户端,并返回响应式类型进行响应处理;
3.1. DeferredResult
一旦在 Servlet 容器中启用了异步请求处理功能,控制器方法可以使用 DeferredResult 封装任何支持的控制器方法返回值,如下面的示例所示:
Java
@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
DeferredResult<String> deferredResult = new DeferredResult<String>();
// Save the deferredResult somewhere..
return deferredResult;
}
// From some other thread...
deferredResult.setResult(result);1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
控制器可以从不同的线程异步地生成返回值,例如,响应外部事件(JMS 消息)、计划任务或其他事件。
3.2. Callable
控制器可以使用 java.util.concurrent.Callable 封装任何支持的返回值,如下面的示例所示:
Java
@PostMapping
public Callable<String> processUpload(final MultipartFile file) {
return new Callable<String>() {
public String call() throws Exception {
// ...
return "someView";
}
};
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
然后,可以通过运行配置的 TaskExecutor 中的给定任务来获取返回值。
3.3. 处理
以下是关于 Servlet 异步请求处理的非常简洁的概述:
通过调用
request.startAsync(),可以将ServletRequest放入异步模式。这样做的主要效果是,Servlet(以及任何过滤器)可以退出,但响应保持开放,以便稍后完成处理。调用
request.startAsync()返回AsyncContext,你可以使用它来进一步控制异步处理。例如,它提供了dispatch方法,该方法类似于 Servlet API 的 Forward,只是它让应用程序在 Servlet 容器线程上恢复请求处理。ServletRequest提供了对当前DispatcherType的访问,你可以使用它来区分处理初始请求、异步调度、Forward 和其他调度器类型。
DeferredResult 的处理如下:
控制器返回一个
DeferredResult并将其保存在某个内存队列或列表中,以便可以访问。Spring MVC 调用
request.startAsync()。与此同时,
DispatcherServlet和所有配置的过滤器退出请求处理线程,但响应保持开放。应用程序从某个线程设置
DeferredResult,然后 Spring MVC 将请求调度回 Servlet 容器。DispatcherServlet再次被调用,处理过程恢复,使用异步生成的返回值。
Callable 的处理如下:
控制器返回一个
Callable。Spring MVC 调用
request.startAsync()并将Callable提交给TaskExecutor在单独的线程中处理。与此同时,
DispatcherServlet和所有过滤器退出 Servlet 容器线程,但响应保持开放。最终,
Callable生成一个结果,然后 Spring MVC 将请求调度回 Servlet 容器以完成处理。DispatcherServlet再次被调用,处理过程恢复,使用Callable异步生成的返回值。
有关更多背景和上下文,你也可以阅读介绍了 Spring MVC 3.2 中异步请求处理支持的博客文章。
3.3.1. 异常处理
当你使用 DeferredResult 时,你可以选择是否调用 setResult 或者带有异常的 setErrorResult。在这两种情况下,Spring MVC 都会将请求调度回 Servlet 容器以完成处理。然后,它会被处理,就好像控制器方法返回了给定的值,或者就好像它产生了给定的异常。然后,异常会通过常规的异常处理机制(例如,调用 @ExceptionHandler 方法)。
当你使用 Callable 时,会发生类似的处理逻辑,主要的区别在于结果是从 Callable 返回的,或者是由它引发的异常。
3.3.2. 拦截
HandlerInterceptor 实例可以是 AsyncHandlerInterceptor 类型,以在启动异步处理的初始请求上接收 afterConcurrentHandlingStarted 回调(而不是 postHandle 和 afterCompletion)。
HandlerInterceptor 的实现也可以注册 CallableProcessingInterceptor 或 DeferredResultProcessingInterceptor,以更深入地与异步请求的生命周期集成(例如,处理超时事件)。有关更多详细信息,请参见 AsyncHandlerInterceptor。
DeferredResult 提供了 onTimeout(Runnable) 和 onCompletion(Runnable) 回调。有关更多详细信息,请参见 DeferredResult 的 JavaDoc。Callable 可以被替换为 WebAsyncTask,它公开了用于超时和完成回调的额外方法。
3.3.3. 与 WebFlux 的比较
Servlet API 最初是为了通过 Filter-Servlet 链进行单次传递而构建的。在 Servlet 3.0 中添加的异步请求处理,让应用程序可以退出 Filter-Servlet 链,但保留响应以进行进一步处理。Spring MVC 的异步支持就是围绕这种机制构建的。当控制器返回一个 DeferredResult 时,Filter-Servlet 链被退出,Servlet 容器线程被释放。稍后,当 DeferredResult 被设置时,会进行一个 ASYNC 调度(到相同的 URL),在此过程中,控制器再次被映射,但是,而不是调用它,DeferredResult 的值被使用(就像控制器返回它一样)来恢复处理。
相比之下,Spring WebFlux 既不是基于 Servlet API 构建的,也不需要这样的异步请求处理特性,因为它是异步设计的。异步处理被构建到所有框架合同中,并在请求处理的所有阶段得到本质上的支持。
从编程模型的角度来看,Spring MVC 和 Spring WebFlux 都支持在控制器方法中返回异步和响应式类型。Spring MVC 甚至支持流,包括响应式背压(Reactive Back Pressure)。然而,对响应的单个写入仍然是阻塞的(并且在一个单独的线程上执行),这与 WebFlux 不同,WebFlux 依赖于非阻塞 I/O,不需要为每次写入增加一个线程。
另一个基本的区别是,Spring MVC 不支持在控制器方法参数中的异步或响应式类型(例如,@RequestBody,@RequestPart 等),也没有对异步和响应式类型作为模型属性的明确支持。Spring WebFlux 支持所有这些。
3.4. HTTP 流
你可以使用 DeferredResult 和 Callable 来返回一个异步的值。那么,如果你想产生多个异步的值,并将这些值写入到响应中,该怎么做呢?本节将对此进行描述。
3.4.1. Objects
你可以使用 ResponseBodyEmitter 返回值来产生一个对象流,其中每个对象都通过 HttpMessageConverter 进行序列化,并写入到响应中,如下面的示例所示:
Java
@GetMapping("/events")
public ResponseBodyEmitter handle() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter();
// Save the emitter somewhere..
return emitter;
}
// In some other thread
emitter.send("Hello once");
// and again later on
emitter.send("Hello again");
// and done at some point
emitter.complete();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
你也可以使用 ResponseBodyEmitter 作为 ResponseEntity 的主体,这样你就可以自定义响应的状态和头部。
当 emitter 抛出一个 IOException(例如,如果远程客户端已经离开),应用程序不负责清理连接,也不应调用 emitter.complete 或 emitter.completeWithError。相反,Servlet 容器会自动启动一个 AsyncListener 错误通知,在这个通知中,Spring MVC 会调用 completeWithError。这个调用反过来会对应用程序进行一次最后的 ASYNC 调度,在这个过程中,Spring MVC 会调用配置的异常解析器并完成请求。
3.4.2. SSE
SseEmitter(ResponseBodyEmitter 的子类)提供了对服务器发送事件(Server-Sent Events)的支持,其中从服务器发送的事件按照 W3C SSE 规范进行格式化。要从控制器产生一个 SSE 流,返回 SseEmitter,如下面的示例所示:
Java
@GetMapping(path="/events", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handle() {
SseEmitter emitter = new SseEmitter();
// Save the emitter somewhere..
return emitter;
}
// In some other thread
emitter.send("Hello once");
// and again later on
emitter.send("Hello again");
// and done at some point
emitter.complete();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
虽然 SSE 是流入浏览器的主要选项,但请注意,Internet Explorer 不支持服务器发送事件。考虑使用 Spring 的 WebSocket 消息传递,配合 SockJS 回退传输(包括 SSE),以覆盖广泛的浏览器。
也请参见前一节关于异常处理的说明。
3.4.3. 原始数据
有时,直接绕过消息转换并直接流到响应的 OutputStream 是很有用的(例如,用于文件下载)。你可以使用 StreamingResponseBody 返回值类型来实现这一点,如下面的示例所示:
Java
@GetMapping("/download")
public StreamingResponseBody handle() {
return new StreamingResponseBody() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
// write...
}
};
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
你可以使用 StreamingResponseBody 作为 ResponseEntity 的主体来自定义响应的状态和头部。
3.5. 响应式类型
Spring MVC 支持在控制器中使用响应式客户端库(也请阅读 WebFlux 部分的响应式库)。这包括来自 spring-webflux 的 WebClient 和其他的,如 Spring Data 响应式数据仓库。在这种情况下,能够从控制器方法返回响应式类型是很方便的。
响应式返回值的处理如下:
单值 Promise 被适配,类似于使用
DeferredResult。示例包括Mono(Reactor)或Single(RxJava)。具有流媒体类型(如
application/x-ndjson或text/event-stream)的多值流被适配,类似于使用ResponseBodyEmitter或SseEmitter。示例包括Flux(Reactor)或Observable(RxJava)。应用程序也可以返回Flux<ServerSentEvent>或Observable<ServerSentEvent>。具有任何其他媒体类型(如
application/json)的多值流被适配,类似于使用DeferredResult<List<?>>。
Note:Spring MVC 通过来自
spring-core的ReactiveAdapterRegistry支持 Reactor 和 RxJava,这使得它可以从多个响应式库进行适配。
对于流到响应(streaming to the response),支持响应式背压,但是对响应的写入仍然是阻塞的,并且通过配置的 TaskExecutor 在一个单独的线程上运行,以避免阻塞上游源(如从 WebClient 返回的 Flux)。默认情况下,SimpleAsyncTaskExecutor 被用于阻塞写入,但是在负载下不适合。如果你计划使用响应式类型进行流,你应该使用 MVC 配置来配置任务执行器。
3.6. 断开连接
Servlet API 不提供当远程客户端离开时的任何通知。因此,在流到响应时,无论是通过 SseEmitter 还是响应式类型,定期发送数据是很重要的,因为如果客户端已断开连接,写入将失败。发送可以采取空的(仅注释)SSE 事件的形式,或者其他任何其他方必须解释为心跳并忽略的数据。
或者,考虑使用具有内置心跳机制的网络消息解决方案(如 STOMP over WebSocket 或 WebSocket with SockJS)。
3.7. 配置
异步请求处理特性必须在 Servlet 容器级别启用。MVC 配置还公开了一些与异步请求处理相关的选项。
3.7.1. Servlet 容器
Filter 和 Servlet 声明有一个 asyncSupported 标志,需要设置为 true 以启用异步请求处理。此外,应声明 Filter 映射以处理 ASYNC javax.servlet.DispatchType。
在 Java 配置中,当你使用 AbstractAnnotationConfigDispatcherServletInitializer 来初始化 Servlet 容器时,这会自动完成。
在 web.xml 配置中,你可以添加 <async-supported>true</async-supported> 到 DispatcherServlet 和 Filter 声明,并添加 <dispatcher>ASYNC</dispatcher> 到 filter 映射。
3.7.2. Spring MVC
MVC 配置公开了以下与异步请求处理相关的选项:
- Java 配置:在
WebMvcConfigurer上使用configureAsyncSupport回调; - XML 命名空间:在
<mvc:annotation-driven>下使用<async-support>元素;
你可以配置以下内容:
异步请求的默认超时值,如果未设置,则取决于底层的 Servlet 容器。
在流式传输响应式类型以及执行从控制器方法返回的
Callable实例时,AsyncTaskExecutor用于进行阻塞写入。我们强烈建议配置此属性,如果你使用响应式类型进行流,或者有控制器方法返回Callable,因为默认情况下,它是一个SimpleAsyncTaskExecutor。DeferredResultProcessingInterceptor实现和CallableProcessingInterceptor实现。
请注意,你也可以在 DeferredResult,ResponseBodyEmitter 和 SseEmitter 上设置默认的超时值。对于 Callable,你可以使用 WebAsyncTask 提供一个超时值。
4. CORS
Spring MVC 允许你处理 CORS(Cross-Origin Resource Sharing,跨源资源共享)。本节将描述如何进行操作。
4.1. 介绍
出于安全原因,浏览器禁止对当前源之外的资源进行 AJAX 调用。例如,你可能在一个标签页中打开了你的银行账户,而在另一个标签页中打开了 evil.com。evil.com 的脚本不应该能够使用你的凭证向你的银行 API 发送 AJAX 请求 —— 例如从你的账户中提款!
跨源资源共享(CORS)是一项由大多数浏览器实现的 W3C 规范,它让你可以指定哪种类型的跨域请求被授权,而不是使用基于 IFRAME 或 JSONP 的较不安全且功能较弱的变通方法。
4.2. 处理
CORS 规范区分了预检请求、简单请求和实际请求。要了解 CORS 是如何工作的,你可以阅读这篇文章,或者查看规范以获取更多详细信息。
Spring MVC 的 HandlerMapping 实现提供了对 CORS 的内置支持。在成功将请求映射到处理器后,HandlerMapping 实现会检查给定请求和处理器的 CORS 配置,并采取进一步的操作。预检请求直接处理,而简单和实际的 CORS 请求则被拦截、验证,并设置所需的 CORS 响应头。
为了启用跨源请求(即,Origin 头存在并与请求的主机不同),你需要有一些明确声明的 CORS 配置。如果找不到匹配的 CORS 配置,预检请求将被拒绝。简单和实际的 CORS 请求的响应中不添加 CORS 头,因此,浏览器会拒绝它们。
每个 HandlerMapping 可以单独配置基于 URL 模式的 CorsConfiguration 映射。在大多数情况下,应用程序使用 MVC Java 配置或 XML 命名空间来声明这些映射,这将导致一个全局映射被传递给所有 HandlerMapping 实例。
你可以将 HandlerMapping 级别的全局 CORS 配置与更细粒度的处理器级别 CORS 配置结合使用。例如,带注解的控制器可以使用类级别或方法级别的 @CrossOrigin 注解(其他处理器可以实现 CorsConfigurationSource)。
组合全局和本地配置的规则通常是累加的 —— 例如,所有全局和所有本地源。对于只能接受单个值的属性,例如 allowCredentials 和 maxAge,本地值会覆盖全局值。有关更多详细信息,请参见 CorsConfiguration#combine(CorsConfiguration)。
Note
要从源码中学习更多信息或进行高级定制,请查看以下代码:
CorsConfigurationCorsProcessor、DefaultCorsProcessorAbstractHandlerMapping
4.3. 凭证请求
使用 CORS 处理带有凭证的请求需要启用 allowedCredentials。请注意,这个选项与配置的域建立了高度的信任,并且通过暴露敏感的用户特定信息(如 Cookies 和 CSRF 令牌)增加了 Web 应用程序的攻击面。
启用凭证也会影响如何处理配置的 "*" CORS 通配符:
在
allowOrigins中不允许使用通配符,但可以替代地使用allowOriginPatterns属性来匹配一组动态的源。当在
allowedHeaders或allowedMethods上设置时,Access-Control-Allow-Headers和Access-Control-Allow-Methods响应头是通过复制 CORS 预检请求中指定的相关头和方法来处理的。当在
exposedHeaders上设置时,Access-Control-Expose-Headers响应头被设置为配置的头列表或通配符字符。虽然 CORS 规范在Access-Control-Allow-Credentials设置为true时不允许使用通配符字符,但大多数浏览器支持它,并且在 CORS 处理过程中并非所有的响应头都可用,因此,无论allowCredentials属性的值如何,当指定时,通配符字符都是用作头值。
Note:虽然这样的通配符配置可能很方便,但建议在可能的情况下配置一组有限的值,以提供更高级别的安全性。
4.4. @CrossOrigin
@CrossOrigin 注解能够在被注解的控制器方法上启用跨源请求,如下面的示例所示:
Java
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
默认情况下,@CrossOrigin 允许:
- 所有的源;
- 所有的头;
- 所有映射到控制器方法的 HTTP 方法;
默认情况下,allowCredentials 是未启用的,因为这会建立一个信任级别,暴露敏感的用户特定信息(如 Cookies 和 CSRF 令牌),只有在适当的地方才应使用。当它启用时,必须将 allowOrigins 设置为一个或多个特定的域(但不能是特殊值 "*"),或者可以使用 allowOriginPatterns 属性来匹配一组动态的源。
maxAge 设置为 30 分钟。
@CrossOrigin 也支持在类级别,并且被所有方法继承,如下面的示例所示:
Java
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
你可以在类级别和方法级别都使用 @CrossOrigin,如下面的示例所示:
Java
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin("https://domain2.com")
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
4.5. 全局配置
除了精细的控制器方法级别的配置外,你可能也想定义一些全局的 CORS 配置。你可以在任何 HandlerMapping 上单独设置基于 URL 的 CorsConfiguration 映射。然而,大多数应用程序使用 MVC Java 配置或 MVC XML 命名空间来完成这个任务。
默认情况下,全局配置启用以下内容:
- 所有的源;
- 所有的头;
GET、HEAD和POST方法;
默认情况下,allowCredentials 是未启用的,因为这会建立一个信任级别,暴露敏感的用户特定信息(如 Cookies 和 CSRF 令牌),只有在适当的地方才应使用。当它启用时,必须将 allowOrigins 设置为一个或多个特定的域(但不能是特殊值 "*"),或者可以使用 allowOriginPatterns 属性来匹配一组动态的源。
maxAge 设置为 30 分钟。
4.5.1. Java 配置
要在 MVC Java 配置中启用 CORS,你可以使用 CorsRegistry 回调,如下面的示例所示:
Java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);
// Add more mappings...
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
4.5.2. XML 配置
要在 XML 命名空间中启用 CORS,你可以使用 <mvc:cors> 元素,如下面的示例所示:
XML
<mvc:cors>
<mvc:mapping path="/api/**"
allowed-origins="https://domain1.com, https://domain2.com"
allowed-methods="GET, PUT"
allowed-headers="header1, header2, header3"
exposed-headers="header1, header2" allow-credentials="true"
max-age="123" />
<mvc:mapping path="/resources/**"
allowed-origins="https://domain1.com" />
</mvc:cors>1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
4.6. CORS 过滤器
你可以通过内置的 CorsFilter 应用 CORS 支持。
Note:如果你尝试将
CorsFilter与 Spring Security 一起使用,请记住,Spring Security 有内置的 CORS 支持。
要配置过滤器,将 CorsConfigurationSource 传递给它的构造函数,如下面的示例所示:
Java
CorsConfiguration config = new CorsConfiguration();
// Possibly...
// config.applyPermitDefaultValues()
config.setAllowCredentials(true);
config.addAllowedOrigin("https://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
CorsFilter filter = new CorsFilter(source);1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14