Spring 5 發行已經好幾年了,裡面提出了好幾個新點子。其中一個就是 RouterFunction,這是個什麼東西呢?
Spring框架給我們提供了兩種http端點暴露方式來隱藏servlet原理,一種就是這多年大家都在使用的基於註解的形式@Controller
或@RestController
以及其他的註解如@RequestMapping
、@GetMapping
等等。另外一種是基於路由配置RouterFunction
和HandlerFunction
的,稱為“函式式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中的邏輯幾乎一樣,只是引數和返回值固定成了ServerRequest
和ServerResponse
型別。
然後改造路由定義類,來使用這些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