多数情况下,我们在使用Spring的Controller时,会使用@RequestMapping的形式把请求按照URL路由到指定方法上。Spring还提供了一种编程的方式去实现请求和路由方法之间的路由关系,这种关系在Spring启动时确定,运行过程中不可变。编程式路由和注解式路由可以使用同一个DispatcherServlet。本文会对Spring编程式Endpoint进行介绍,本文主要参考了Spring官方文档

总览

在Spring MVC编程式路由中一次请求会被一个处理方法进行处理,处理方法在Spring中用HandlerFunction表示,函数的入参为ServerRequest,返回值为ServerResponse。Spring可以通过编程的方式定义路由规则RouterFunction,RouterFunction等价于@RequestMapping注解。我们可以按照如下方式去配置路由规则,并且可以通过@Configuration中的@Bean来将路由规则RouterFunction注册到Servlet中。

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()
.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) {
// ...
}
}

处理函数的定义

在编程式路由中,一个请求最终要交给一个处理函数去处理,这就是HandlerFunction。这个函数的入参是ServerRequest和ServerResponse,分别绑定了请求的Request和Response,并且包含了请求的header、Body、状态码等信息。

ServerRequest

ServerRequest包含了请求中的所有信息,如请求方式、请求URL、请求的Header和请求参数等信息,并且提供了请求体相关的访问方法。

  • 如果请求体是String类型的数据,我们可以通过如下示例获取请求体数据:

        String string = request.body(String.class);
  • 如果需要把请求转为对应的Bean,如List,Spring会把Json或xml数据反序列化为对应的对象:

        List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});
  • 我们可以通过如下方式获取请求中的参数信息:

        MultiValueMap<String, String> params = request.params();

ServerResponse

ServerResponse用于向响应中写入数据,可以通过建造者模式生成对应的响应,

  • 如下例子会返回响应为200的Json数据:

    Person person = ...
    ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
  • 如下的例子可以生成一个Created的响应,状态码是201:

    URI location = ...
    ServerResponse.created(location).build();
  • 返回的数据也可以是异步的结果:

    Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
    ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
  • Spring甚至允许Header和状态码也是异步的结果

    Mono<ServerResponse> asyncResponse = webClient.get().retrieve().bodyToMono(Person.class).map(p -> ServerResponse.ok().header("Name", p.name()).body(p));
    ServerResponse.async(asyncResponse);
  • Spring还支持Server-Sent Events(和WebSocket类似),使用方法如下示例:

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

处理类的定义

处理方法可以用Lambda来表示,但是如果处理方法很多或者处理方法有共享的状态,如果继续使用Lambda就会使程序很乱。这种情况下可以按照功能把这些类封装到不用的类中,示例如下所示:

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

参数校验

如果需要对请求中的参数进行校验,我们就需要通过编程的方式进行校验了,校验的示例如下所示,校验结束会返回校验结果,用户可以根据校验结果自定义处理逻辑。

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

路由函数的定义

路由函数的作用是把请求绑定到对应的处理方法之上,Spring提供了RouterFunctions工具以建造者模式的方法创建路由规则,建造者模式创建以RouterFunctions.route(RequestPredicate, HandlerFunction)格式创建路由函数。

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

Predicates

SpringMVC中的RequestPredicate用于判断一次请求是否会命中指定的规则,用户可以自定义RequestPredicate的实现,也可以使用RequestPredicates中的工具类去构建RequestPredicate,下面的例子通过工具类满足GET方法和参数类型为MediaType.TEXT_PLAIN的数据。RequestPredicatest提供了请求方法、请求头等常用的RequestPredicate,RequestPredicate之间还支持与或关系。

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

路由规则

我们可以向DistpatcherServlet中注册多个RouterFunction,这些RouterFunction之间应该有顺序,每个RouteFunction又允许定义多个路由规则,这些路由规则之间是有顺序的。如果请求匹配到了前面的路由规则匹配,那么它就不会再继续匹配后面的路由规则,会直接使用第一个匹配到的规则。

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

嵌套路由

如果一系列路由规则包含了相同的条件,比如相同前缀的URL等,这种条件下推荐使用嵌套路由,嵌套路由的使用方法如下所示:

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

路由配置

上文中介绍了如何定义路由规则,定义好的路由规则往往需要注册到Spring的容器中,我们可以通过实现WebMvcConfigurer接口向容器中添加配置信息,并且根据配置信息生成DispatcherServlet。

@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...
}
}

路由过滤器

在定义一条路由规则的时候,我们可以对指定规则添加执行前方法、执行后方法和过滤器。我们也可以再ControllerAdvice中添加全局的执行前方法、执行后方法和过滤器规则,所有的编程式路由规则都会使用这些方法。如下为执行前方法、执行后方法和过滤器的使用示例:

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("/person", handler::createPerson))
.after((request, response) -> logResponse(response))
.build(); SecurityManager securityManager = ... RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST("/person", handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();

本文最先发布至微信公众号,版权所有,禁止转载!

SpringMVC 解析(四)编程式路由的更多相关文章

  1. VueJs(10)---vue-router(动态路由,嵌套式路由,编程式路由)

    vue-router(动态路由,嵌套式路由,编程式路由) 本文是基于官网学习,官网具体学习目录:vue-router 一.安装 基于vue-cli脚手架安装还是蛮简单的:在文件当前目录下运行: npm ...

  2. 在React中使用 react-router-dom 编程式路由导航的正确姿势【含V5.x、V6.x】

    ## react-router-dom 编程式路由导航 (v5) ###### 1.push跳转+携带params参数 ```jsx props.history.push(`/b/child1/${i ...

  3. vue_VueRouter 路由_路由器管理n个路由_并向路由组件传递数据_新标签路由_编程式路由导航

    路由:就是一个 key 与 value 的映射关系.key 就是 pathh 前台路由的 value 是 Component 组件对象 后台路由的 value 是一个 回调函数 普通链接: 会发送请求 ...

  4. vue.js编程式路由导航 --- 由浅入深

    编程式路由导航 实例中定义一个方法,这个方法绑定在标签上 然后就设置路由跳转 语法 this.$router.history.push('要跳转路由的地址') <!DOCTYPE html> ...

  5. vue-router 编程式路由

    $route -> 使用它的属性 $router-> 使用它的方法 编程式的导航,即js控制跳转 //声明式:<router-link :to="..."> ...

  6. vue编程式路由实现新窗口打开

    一. 标签实现新窗口打开: 官方文档中说 v-link 指令被 组件指令替代,且 不支持 target=”_blank” 属性,如果需要打开一个新窗口必须要用标签,但事实上vue2版本的 是支持 ta ...

  7. Vue编程式路由跳转传递参数

    Vue 有时在路由跳转时需要用到一些原页面里的数据,用以下方法: 1.在跳转页的方法里写下query参数 TableChange(scope){ this.$router.push({ path:'d ...

  8. [Vue 牛刀小试]:第十四章 - 编程式导航与实现组件与 Vue Router 之间的解耦

    一.前言 在上一章的学习中,通过举例说明,我们了解了 Vue Router 中命名路由.命名视图的使用方法,以及如何通过 query 查询参数传参,或者是采用 param 传参的方式实现路由间的参数传 ...

  9. VueRouter爬坑第四篇-命名路由、编程式导航

    VueRouter系列的文章示例编写时,项目是使用vue-cli脚手架搭建. 项目搭建的步骤和项目目录专门写了一篇文章:点击这里进行传送 后续VueRouter系列的文章的示例编写均基于该项目环境. ...

随机推荐

  1. laravel 访问器 和修改器的使用

    对于访问器我是这样定义的,就是将数据库中的数据被访问时可以变成我们想要的数据类型(例如:数据库中的时间字段是int类型,要将她变成data(Y-m-d H:i:s),格式类型) 参看博客 https: ...

  2. 从刘维尔方程到Velocity-Verlet算法

    技术背景 我们说分子动力学模拟是一个牛顿力学的过程,在使用量子化学的手段或者深度学习的方法或者传统的力场方法,去得到某个时刻某个位置的受力之后,就可以获取下一步的整个系统的状态信息.这个演化的过程所使 ...

  3. BSOJ7526口胡

    直觉告诉我一般情况下,询问古怪的题都是分块,但是这一类题不太一样. 思考一个奇怪的暴力,每次询问的时候询问 \(f(1,k),f(2,k+1),f(3,k+2),...f(n-k+1,n)\),然后加 ...

  4. H3C三层交换机之IRF虚拟化技术详解及配置

    一.IRF是什么? 目前,网络中主要存在两种结构的通信设备,固定盒式设备和模块框式分布式设备. 固定盒式设备成本低廉,但没有高可用性支持:模块框式分布式设备具有高可用性.高性能.高端口密度的优点,但投 ...

  5. HIve的基本使用

    WHERE从表中筛选行: SELECT从表中查询指定的列: group by在列上做聚合. -- 假设数据文件的内容,字段之间以ASCII 001(ctrl-A)分隔,行之间以换行分隔. CREATE ...

  6. Python函数-5 生成器

    生成器有时候,序列或集合内的元素的个数非常巨大,如果全制造出来并放入内存,对计算机的压力是非常大的.比如,假设需要获取一个10**20次方如此巨大的数据序列,把每一个数都生成出来,并放在一个内存的列表 ...

  7. ZYNQ SGI、PPI、SPI三种中断的实例(含代码)

    ZYNQ中断分为3类: SGI(Software Generated Interrupts)软件中断 PPI(Private Peripheral Interrupts)私有外设中断 SPI(Shar ...

  8. 使用pyautogui自动在某网站投票的脚本

    网页自动投票: # !/usr/bin/env python # -*- coding:utf-8 -*- # Author:Hiuhung Wan import re import pypercli ...

  9. virtualenv和virtualenvwrapper pipenv安装

    virtualenv和virtualenvwrapper pipenv安装 virtualenvwrapper工具 virtualenvwrapper是用来管理virtualenv的扩展包,用着很方便 ...

  10. 5月31日 python学习总结 JavaScript概述

    JavaScript概述 ECMAScript和JavaScript的关系 1996年11月,JavaScript的创造者--Netscape公司,决定将JavaScript提交给国际标准化组织ECM ...