1. 简介

从 Spring 5 开始,引入了 WebFlux 模块,它允许我们使用响应式编程模型构建 Web 应用。

本教程中,我们将了解如何在 Spring MVC 中使用这种编程模型来实现函数式控制器。

2. Maven 配置

我们将使用 Spring Boot 来演示新的函数式 API。

Spring Boot 支持我们熟悉的基于注解的控制器定义方式,但它也引入了一种新的领域特定语言(DSL),提供了一种更函数式的控制器定义方式。

从 Spring 5.2 开始,这种函数式方式也正式支持 Spring Web MVC 框架。和 WebFlux 一样,RouterFunctionsRouterFunction 是这一 API 的核心抽象。

我们先从引入 spring-boot-starter-web 依赖开始:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

3. RouterFunction vs @Controller

在函数式编程模型中,一个 Web 服务被称为“路由(route)”。传统的 @Controller@RequestMapping 注解被 RouterFunction 所取代。

为了创建第一个服务,我们先来看一个基于注解的控制器示例,并将其转换为函数式写法。

这是一个返回产品列表的控制器:

@RestController
public class ProductController {

    @RequestMapping("/product")
    public List<Product> productListing() {
        return ps.findAll();
    }
}

对应的函数式写法如下:

@Bean
public RouterFunction<ServerResponse> productListing(ProductService ps) {
    return route().GET("/product", req -> ok().body(ps.findAll()))
      .build();
}

3.1 路由定义

需要注意的是,在函数式方式中,productListing() 方法返回的是一个 RouterFunction,而不是响应体。它是一个路由的定义,而不是请求的执行。

RouterFunction 包括路径、请求头、处理函数(用于生成响应体和响应头),它可以包含一个或一组 Web 服务。

我们会在“嵌套路由”部分详细讨论多个服务的组合。

在这个例子中,我们使用了 RouterFunctions 类中的静态方法 route() 来创建 RouterFunction。该方法可以用于定义路由的所有请求和响应属性。

3.2 请求谓词

在示例中,我们使用了 GET() 方法来指定这是一个 GET 请求,并传入了路径作为字符串。

我们也可以使用 RequestPredicate 来更精确地定义请求条件。

例如,上面的路径也可以这样写:

RequestPredicates.path("/product")

这里我们使用了静态工具类 RequestPredicates 来创建一个 RequestPredicate 实例。

3.3 响应构建

类似地,ServerResponse 提供了静态方法用于构建响应对象。

在示例中,我们使用 ok() 方法设置 HTTP 状态码为 200,然后使用 body() 方法指定响应体。

此外,ServerResponse 还支持通过 EntityResponse 构建自定义数据类型的响应,也可以通过 RenderingResponse 使用 Spring MVC 的 ModelAndView

3.4 注册路由

接下来,我们需要通过 @Bean 注解将该路由注册到 Spring 应用上下文中:

@SpringBootApplication
public class SpringBootMvcFnApplication {

    @Bean
    RouterFunction<ServerResponse> productListing(ProductController pc, ProductService ps) {
        return pc.productListing(ps);
    }
}

接下来,我们继续实现一些常见的 Web 服务开发场景。

4. 嵌套路由

在开发 Web 服务时,通常会将多个服务按功能或实体进行分组。例如,所有与产品相关的接口都以 /product 为前缀。

我们来为 /product 添加一个按名称查找产品的接口:

public RouterFunction<ServerResponse> productSearch(ProductService ps) {
    return route().nest(RequestPredicates.path("/product"), builder -> {
        builder.GET("/name/{name}", req -> ok().body(ps.findByName(req.pathVariable("name"))));
    }).build();
}

在传统的注解方式中,我们会使用 @Controller 注解并传入路径。而在函数式方式中,我们可以使用 nest() 方法来实现路由分组。

这里我们传入了主路径 /product,然后通过 builder 添加子路由,方式与之前一致。

nest() 方法会自动将 builder 中的路由合并到主路由中。

5. 异常处理

另一个常见的需求是自定义异常处理逻辑。我们可以通过 onError() 方法定义异常处理器。

这与注解方式中的 @ExceptionHandler 类似,但更加灵活,因为它可以为不同的路由组定义不同的异常处理逻辑。

我们为上面的产品搜索接口添加一个异常处理器,用于处理找不到产品时抛出的自定义异常:

public RouterFunction<ServerResponse> productSearch(ProductService ps) {
    return route()...
      .onError(ProductService.ItemNotFoundException.class,
         (e, req) -> EntityResponse.fromObject(new Error(e.getMessage()))
           .status(HttpStatus.NOT_FOUND)
           .build())
      .build();
}

onError() 方法接受一个异常类对象,并期望返回一个 ServerResponse

我们使用了 EntityResponseServerResponse 的子类)来从自定义类型 Error 创建响应对象,并设置状态码,最后通过 build() 返回 ServerResponse

6. 过滤器

认证、日志、审计等横切关注点通常通过过滤器实现。过滤器用于决定是否继续处理请求或中断处理。

我们来看一个添加产品接口的例子:

public RouterFunction<ServerResponse> adminFunctions(ProductService ps) {
    return route().POST("/product", req -> ok().body(ps.save(req.body(Product.class))))
      .onError(IllegalArgumentException.class, 
         (e, req) -> EntityResponse.fromObject(new Error(e.getMessage()))
           .status(HttpStatus.BAD_REQUEST)
           .build())
        .build();
}

这是一个管理员接口,我们需要对其进行身份验证。

我们可以通过 filter() 方法添加认证逻辑:

public RouterFunction<ServerResponse> adminFunctions(ProductService ps) {
   return route().POST("/product", req -> ok().body(ps.save(req.body(Product.class))))
     .filter((req, next) -> authenticate(req) ? next.handle(req) : 
       status(HttpStatus.UNAUTHORIZED).build())
     ....;
}

filter() 方法接收请求和下一个处理器。我们在这里进行了一个简单的认证判断,认证通过则继续处理请求,否则返回 401。

7. 横切关注点处理

有时候我们需要在请求处理前后执行一些操作,比如记录请求信息和响应结果。

我们可以在匹配到请求时记录日志,使用 before() 方法:

@Bean
RouterFunction<ServerResponse> allApplicationRoutes(ProductController pc, ProductService ps) {
    return route()...
      .before(req -> {
          LOG.info("Found a route which matches " + req.uri()
            .getPath());
          return req;
      })
      .build();
}

同样地,我们可以在请求处理完成后使用 after() 方法记录日志:

@Bean
RouterFunction<ServerResponse> allApplicationRoutes(ProductController pc, ProductService ps) {
    return route()...
      .after((req, res) -> {
          if (res.statusCode() == HttpStatus.OK) {
              LOG.info("Finished processing request " + req.uri()
                  .getPath());
          } else {
              LOG.info("There was an error while processing request" + req.uri());
          }
          return res;
      })          
      .build();
    }

8. 总结 ✅

本教程中,我们介绍了 Spring MVC 中函数式控制器的基本概念,并对比了注解式和函数式的写法。

接着我们实现了一个返回产品列表的简单 Web 服务,并逐步扩展了以下功能:

  • 嵌套路由:使用 nest() 实现路由分组
  • 异常处理:使用 onError() 自定义异常响应
  • 认证过滤:通过 filter() 添加权限控制
  • 日志记录:使用 before()after() 实现请求前后处理

这些功能使得函数式控制器在保持代码简洁的同时,也具备强大的灵活性和可组合性。

完整的示例代码可以在 GitHub 上找到。


原始标题:Functional Controllers in Spring MVC