Spring 5 MVC 中的 Router Function 使用

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

posted @ 2021-10-27 14:46  大卫小东(Sheldon)  阅读(3390)  评论(2编辑  收藏  举报