• SpringMVC 解析(四)编程式路由


    多数情况下,我们在使用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();
    

    qrcode_for_gh_83670e17bbd7_344-2021-09-04-10-55-16

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

  • 相关阅读:
    计算机毕业设计 基于SpringBoot智慧养老中心管理系统的设计与实现 Javaweb项目 Java实战项目 前后端分离 文档报告 代码讲解 安装调试
    三视角:定位、自省与多维
    一个简单的dw网页制作作业,学生个人html静态网页制作成品代码——怪盗基德动漫主题网页成品(15页)
    DevOps 学习
    Java笔试题总结
    【云原生】一文讲透Kubevela addon如何添加腾讯Crane
    arm-linux 字符设备IO控制
    【数值计算】基于四阶Runge-Kutta方法实现Mackey_Glass混沌时间序列生成以及相位时差的绘制附matlab代码
    mall-2-后台开发环境配置
    玩转PyCharm
  • 原文地址:https://www.cnblogs.com/yuhushen/p/16098420.html