SpringMVC 解析(四)程式設計式路由

御狐神發表於2022-04-04

多數情況下,我們在使用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

本文最先發布至微信公眾號,版權所有,禁止轉載!

相關文章