1. 简介
从 Spring 5 开始,引入了 WebFlux 模块,它允许我们使用响应式编程模型构建 Web 应用。
本教程中,我们将了解如何在 Spring MVC 中使用这种编程模型来实现函数式控制器。
2. Maven 配置
我们将使用 Spring Boot 来演示新的函数式 API。
Spring Boot 支持我们熟悉的基于注解的控制器定义方式,但它也引入了一种新的领域特定语言(DSL),提供了一种更函数式的控制器定义方式。
从 Spring 5.2 开始,这种函数式方式也正式支持 Spring Web MVC 框架。和 WebFlux 一样,RouterFunctions 和 RouterFunction 是这一 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。
我们使用了 EntityResponse(ServerResponse 的子类)来从自定义类型 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 上找到。