Spring 5 MVC 中的 Router Function 使用

山不在高水不在深發表於2021-10-27

Spring 5 發行已經好幾年了,裡面提出了好幾個新點子。其中一個就是 RouterFunction,這是個什麼東西呢?

Spring框架給我們提供了兩種http端點暴露方式來隱藏servlet原理,一種就是這多年大家都在使用的基於註解的形式@Controller@RestController 以及其他的註解如@RequestMapping@GetMapping等等。另外一種是基於路由配置RouterFunctionHandlerFunction的,稱為“函式式WEB”。這篇文章我們就是來介紹後面這種函式式web的。

為什要說這個東西呢?老老實實用註解不好嗎?一個原因是它既然存在,我們就該學習 ? 。第二個原因是WebFlux推薦使用這個方式,而Spring在將來有可能推薦使用WebFlux而非MVC(Spring mvc可能會被廢棄)。所以我們需要提早掌握。

wait...你不是來宣傳WebFlux的吧?放心,這篇文章裡再也不會出現WebFlux了

既然基於註解的MVC和函式式開發是等效的,那我們就先看下他們的對比。下面分別是用兩種風格實現的程式碼:

@RestController
@RequestMapping("/model/building")
@Slf4j
public class ModelBuildingController {

    @Autowired
    private IModelBuildingService modelBuildingService;

    @GetMapping("/{entId}/stations")
    public PagedResult<StationVO> getStations(@PathVariable("entId") Long entId) {
        List<StationBO> stationBoList = modelBuildingService.getStations(entId);
        List<StationVO> stationVoList = TransformUtils.transformList(stationBoList, StationVO.class);
        return PagedResult.success(stationVoList);
    }
}

再看函式式風格

import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerResponse;

@Configuration
public class ModelBuildingRouting {

    @Autowired
    private IModelBuildingService modelBuildingService;

    @Bean
    public RouterFunction<ServerResponse> getModelBuildingRouters() {
        return RouterFunctions.route(GET("/model/building/{entId}/stations"),
                request -> {
                    Long entId = Long.valueOf(request.pathVariable("entId"));
                    List<StationBO> stationBoList = modelBuildingService.getStations(entId);
                    return ServerResponse.ok().body(PagedResult.success(stationVoList));
                }
        );
    }
}

我們可以稍作修改,來看下效果:

@Configuration
public class ModelBuildingRouting {

    @Autowired
    private IModelBuildingService modelBuildingService;

    @Bean
    public RouterFunction<ServerResponse> getModelBuildingRouters() {
        return RouterFunctions.route(GET("/model/building/{entId}/stations"),
                request -> {
                    Long entId = Long.valueOf(request.pathVariable("entId"));
//                    List<StationBO> stationBoList = modelBuildingService.getStations(entId);
                    List<StationVO> stationVoList = new ArrayList<>(1);
                    StationVO e = new StationVO();
                    e.setCode("123");
                    e.setName("xyz");
                    e.setAliasCode("AA");
                    e.setType("TT");
                    stationVoList.add(e);
                    return ServerResponse.ok().body(PagedResult.success(stationVoList));
                }
        );
    }
}

返回值當然是一樣的了

如果你複製這段程式碼後編譯報錯,可能是引入了webflux依賴,我們這裡使用的是web依賴,注意看一下import的類

路由巢狀

在驚喜之餘,可能你在上面的程式碼中發現有一點小問題:使用Controller的時候,類上面是可以定義公共url字首的,比如/model/building。但是使用函式式,貌似每個Url都要自己拼上這一段。

其實,這兩種東西都是spring自己搞的,它不可能削弱新東西的表達能力。那應該怎麼用呢?

RouterFunctions提供了一個方法nest,可以把路由組織起來。修改上面的程式碼為

import static org.springframework.web.servlet.function.RequestPredicates.GET;
import static org.springframework.web.servlet.function.RequestPredicates.path;

@Bean
public RouterFunction<ServerResponse> getModelBuildingRouters() {
    return RouterFunctions.nest(path("/model/building"),
            RouterFunctions.route(GET("/{entId}/stations"),
                    request -> {
                        Long entId = Long.valueOf(request.pathVariable("entId"));
                        List<StationBO> stationBoList = modelBuildingService.getStations(entId);
                        return ServerResponse.ok().body(PagedResult.success(stationVoList));
                    }
            ));
}

增加路由

在controller中可以任意增加新的Action方法,只要使用RequestMapping標註就行,這樣釋出就能立即生效。那在RouterFunction中怎麼增加更多路由呢?

RouterFunctions提供了一個方法andRoute,可以新增更多的路由。修改上面的程式碼為

@Bean
public RouterFunction<ServerResponse> getModelBuildingRouters() {
    return RouterFunctions.nest(path("/model/building"),
            RouterFunctions.route(GET("/{entId}/stations"),
                    request -> {
                        Long entId = Long.valueOf(request.pathVariable("entId"));
                        List<StationBO> stationBoList = modelBuildingService.getStations(entId);
                        return ServerResponse.ok().body(PagedResult.success(stationVoList));
                    }
            ).andRoute(GET("/{stationId}/device-types"),
                    request -> {
                      String stationId = request.pathVariable("stationId");
                      List<DeviceTypeBO> deviceTypeBoList = modelBuildingService.getDeviceTypes(stationId);
                      List<DeviceTypeVO> deviceTypeVoList = TransformUtils.transformList(deviceTypeBoList, DeviceTypeVO.class);
                      return ServerResponse.ok().body(deviceTypeVoList);
                    }
                ));
}

現在就有兩個url了:/model/building/{entId}/stations/model/building/{stationId}/device-types

你可能會說:這不是沒有必要嗎,我也可以再增加一個Bean,變成下面這樣:

@Configuration
public class ModelBuildingRouting {

    @Bean
    public RouterFunction<ServerResponse> getModelBuildingRouters(IModelBuildingService modelBuildingService) {
        return RouterFunctions.nest(path("/model/building"),
                RouterFunctions.route(GET("/{entId}/stations"),
                        request -> {
                            Long entId = Long.valueOf(request.pathVariable("entId"));
                            System.out.println(entId);
                            List<StationBO> stationBoList = modelBuildingService.getStations(entId);
                            return ServerResponse.ok().body(PagedResult.success(stationBoList));
                        }
                ));
    }

    @Bean
    public RouterFunction<ServerResponse> getModelBuildingRouters1(IModelBuildingService modelBuildingService) {
        return RouterFunctions.nest(path("/model/building"),
                RouterFunctions.route(GET("/{stationId}/device-types"),
                        request -> {
                            String stationId = request.pathVariable("stationId");
                            List<DeviceTypeBO> deviceTypeBoList = modelBuildingService.getDeviceTypes(stationId);
                            List<DeviceTypeVO> deviceTypeVoList = TransformUtils.transformList(deviceTypeBoList, DeviceTypeVO.class);
                            return ServerResponse.ok().body(deviceTypeVoList);
                        }
                ));
    }
}

的確,這樣也是可以的。甚至可以建多個@Configuration類,每個類分一些路由都行。但是,我們是通過類、方法、組織來管理路由系統的。我們當然期望儘量通過一個類、幾個方法來管理全部的路由。

HandlerFunction

如果你留意一下route()方法,可以看到這個方法的第二個引數型別是org.springframework.web.servlet.function.HandlerFunction。從前面的邏輯也可以看出來,這個函式式介面中方法的入參是請求request,返回是業務資料。所以很明顯,這個就是網路請求的處理器。

為了風格簡潔,通常我們不會把業務邏輯寫在Routing這個Configuration中。因為前面說了,我們的所有路由維護都在一起,如果連邏輯也寫在這,那這個類的大小就不可控了。另外還有一個問題是,業務邏輯寫在路由定義處,就會導致大量注入Service。不論是通過屬性注入到類還是通過方法引數傳入進來,數量上來都會比較醜陋。
所以和Controller的拆分一樣,我們通過拆分Handler來組織業務邏輯。

新建Handler類:

@Component
public class ModelBuildingHandler {
    @Autowired
    private IModelBuildingService modelBuildingService;

    public ServerResponse getStations(ServerRequest req) {
        Long entId = Long.valueOf(req.pathVariable("endId"));
        List<StationBO> stationBoList = modelBuildingService.getStations(entId);
        List<StationVO> stationVoList = TransformUtils.transformList(stationBoList, StationVO.class);
        return ok().body(PagedResult.success(stationVoList));
    }

    public ServerResponse getDeviceTypes(ServerRequest req) {
        String stationId = req.pathVariable("stationId");
        List<DeviceTypeBO> deviceTypeBoList = modelBuildingService.getDeviceTypes(stationId);
        List<DeviceTypeVO> deviceTypeVoList = TransformUtils.transformList(deviceTypeBoList, DeviceTypeVO.class);
        return ok().body(PagedResult.success(deviceTypeVoList));
    }
}

可以看到,裡面的方法和原來(long long ago)最初的controller中的邏輯幾乎一樣,只是引數和返回值固定成了ServerRequestServerResponse型別。

然後改造路由定義類,來使用這些handler:

@Configuration
public class RoutingConfig {

    @Bean
    public RouterFunction<ServerResponse> getModelBuildingRouters(ModelBuildingHandler modelBuildingHandler) {
        return RouterFunctions.nest(path("/model/building"),
                RouterFunctions.route(GET("/{entId}/stations"), modelBuildingHandler::getStations)
                        .andRoute(GET("/{stationId}/device-types"), modelBuildingHandler::getDeviceTypes)
        );
    }
}

可以看到,這個類變得簡潔多了,這樣每個方法可以對應一個Handler,將其通過引數傳入即可。

當然如果嫌模板程式碼太多可以建立父類,比如

public abstract class BaseHandler {
    protected ServerResponse body(Object body) {
        return ServerResponse.ok().body(body);
    }

    protected String path(ServerRequest req, String path) {
        return req.pathVariable(path);
    }
}

Swagger

如果你開開心心改造完,發下原來大家都喜歡的swagger 文件再也不見了,是不是哭完還要把程式碼改回來?

其實不用哭了,不是有程式碼庫版本管理的嘛

SpringFox的swagger版本是2。要想讓swagger掃描到RouterFunction配置,需要升級到版本3。具體方法請看下一篇《Spring 5 中函式式web開發中的swagger文件》。

參考文件

Spring Functional Web Framework Guide
Functional Controllers in Spring MVC
Spring Boot RouterFunction
Migrating from SpringFox
Spring-webflux/WebMvc.fn with Functional Endpoints

相關文章