杨绛先生说:大部分人的问题是,做得不多而想得太多。
你好,我是mickjoust。今天我们要学习的函数式编程可能和Spring Boot本身的关系不太大,但是它很重要!
不仅是因为从Java 7升级到Java 8多了一种新编程语法的支持,更因为这是一种不同的思维模式。同时,今天的内容可能会偏多一点,希望爱学习的你能耐心看完。
Spring 5中的引入了对响应式编程的支持——WebFlux,它基于Reactor类库的基础实现,之前的三篇文章:
- 《Spring Boot 实践折腾记(10):响应式编程支持库Reactor》
- 《Spring Boot 实践折腾记(11):使用 Spring 5的WebFlux快速构建效响应式REST API》
- 《Spring Boot 实践折腾记(13):使用WebFlux构建响应式「推送API 」》
已经详细讲述过Reactor和WebFlux注解模型以及用法,可以点击复习。
注意,目前在Spring Boot中不支持两种范式混用。
本文中我们将一起来实现一个简单的API,是一个使用Java 8 lambda表达式来定义和Spring WebFlux的请求处理方法HandlerFunctions的例子。代码风格偏向于Java 8 lambda,比如:
HandlerFunction<ServerResponse> echoHandlerFn = (request) -> ServerResponse.ok().body(fromObject(request.queryParam("name")));
RequestPredicate predicate = RequestPredicates.GET("/echo");
RouterFunction <ServerResponse> routerFunction = RouterFunctions.route(GET("/echo"), echoHandler::echo);
Mono<ServerResponse> echo = ServerResponse.ok().body(fromObject(request.queryParam("name")));
接下来,先看几个关键组件。
1.HandlerFunction
简单来说,HandlerFunction
是一个接受ServletRequest
并返回ServletResponse
的函数接口。在使用注解模型时,我们会直接使用@RequestMapping("/")
注解来映射请求路径,而HandlerFunction就是起到这个同样的作用。接口定义如下代码:
package org.springframework.web.reactive.function.server;
import reactor.core.publisher.Mono;
@FunctionalInterface
public interface HandlerFunction<T extends ServerResponse> {
Mono<T> handle(ServerRequest var1);
}
这里,我们又看到了@FunctionalInterface
的身影,标明是一个函数式接口,ServerRequest和ServerResponse是在Reactor类型之上构建的接口。我们可以将请求主体body转换为Reactor的Mono或Flux类型,并且可以还发送任何响应的实例流给发布者作为响应主体。
2.ServerRequest
在包org.springframework.web.reactive.function.server
中的ServerRequest
接口是表示的服务器端HTTP请求。 我们可以在HTTP请求中通过各种方法来使用它,这里要注意和注解模型下的ServerHttpRequest区别开,使用的方法如下:
HttpMethod method = request.method();
String path = request.path();
String id = request.pathVariable("id");
Map<String, String> pathVariables = request.pathVariables();
Optional<String> email = request.queryParam("email");
URI uri = request.uri();
由于需要转换为Reactor的响应类型,我们就需要使用bodyToMono()
或bodyToFlux()
方法来将请求body转换为Mono<T>
或Flux<T>
类型,如下:
Mono<User> manMono = request.bodyToMono(Man.class);
Flux<User> mansFlux = request.bodyToFlux(Man.class)
bodyToMono()和bodyToFlux()方法实际上是BodyExtractor
对象的实例,它主要用于提取请求主体内容并将其反序列化为POJO对象。这也就意味着,我们可以使用BodyExtractor类来将请求主体body内容转化为Mono或Flux类型,如下所示:
Mono<User> manMono = request.body(BodyExtractors.toMono(Man.class));
Flux<User> mansFlux = request.body(BodyExtractors.toFlux(Man.class));
如果要将请求主体转换为泛型类型时,还可以使用ParameterizedTypeReference
。
ParameterizedTypeReference<Map<String, List<User>>> typeReference = new Parameterized
TypeReference<Map<String, List<User>>>() {};
Mono<Map<String, List<User>>> mapMono = request.body(BodyExtractors.toMono(typeReference));
3.ServerResponse
同样,在包中org.springframework.web.reactive.function.server
的ServerResponse
接口表示服务器端HTTP的响应式响应。 ServerResponse也是一个唯一的接口,并提供了许多静态构建器方法来构建响应,包括status, contentType, cookies, headers和body等。
以下是如何使用构造器方法,来构建ServerResponse的几个例子:
ServerResponse.ok().contentType(APPLICATION_JSON).body(userMono, User.class);
ServerResponse.ok().contentType(APPLICATION_JSON).body(BodyInserters.fromObject(user));
ServerResponse.created(uri).build();
ServerResponse.notFound().build();
我们还可以使用render()
方法渲染视图模板,如下所示:
Map<String,?> modelAttributes = new HashMap<>();
modelAttributes.put("man",man);
ServerResponse.ok().render("home", modelAttributes);
因此,使用这些ServerResponse的构造器方法,便可以构造HandlerFunction.handle
法的返回值了。
4.RouterFunction
RouterFunction
使用RequestPredicate将传入请求映射到HandlerFunction。我们可以使用RouterFunctions的类静态方法来构建RouterFunction,如下所示:
RouterFunctions.route(GET("/echo"), request -> ok().body(fromObject(request.queryParam("name"))));
还可以将多个路由定义合并到一个新的路由定义中,以便路由到与谓词相匹配的第一个处理函数。
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
RouterFunctions.route(GET("/echo"), request -> ok().body(fromObject(request.queryParam("name"))))
.and(route(GET("/home"), request -> ok().render("home")))
.andRoute(POST("/mans"), request -> ServerResponse.ok().build());
上面,我们将三个路由合成为一个传入给请求,并分别映射处理Handler。假设我们需要编写多个具有相同父级前缀的路由,这时,并不用在每个路由中重复URL路径,使用RouterFunctions.nest()
方法即可实现父级目录和子级目录的映射,如下所示:
RouterFunctions.nest(path("/api/mans"),
nest(accept(APPLICATION_JSON),
route(GET("/{id}"), request -> ServerResponse.ok().build())
.andRoute(method(HttpMethod.GET), request -> ServerResponse.ok().build())));
说明一下,代码中将两个URL映射到其处理函数。一种是GET /api/mans
,它返回所有用户,另一种是GET /api/mans/{id}
返回给定id的用户详细信息。
我们还可以使用RequestPredicates静态方法创建RequestPredicate,以及使用RequestPredicate.and
组合请求达到同样的效果,如下所示:
RouterFunctions.route(path("/api/mans").and(method(HttpMethod.GET)),
request -> ServerResponse.ok().build());
RouterFunctions.route(GET("/api/mans").or(GET("/api/mans/list")),
request -> ServerResponse.ok().build());
5.HandlerFilterFunction
我们如果将基于注解的方法与函数方法进行比较,则RouterFunction与@RequestMapping
注解类似,而HandlerFunction与使用@RequestMapping("/")注解的方法
类似。 WebFlux框架还提供了HandlerFilterFunction
接口,它更类似于Servlet Filter或@ControllerAdvice
方法,接口定义如下:
package org.springframework.web.reactive.function.server;
import java.util.function.Function;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
@FunctionalInterface
public interface HandlerFilterFunction<T extends ServerResponse, R extends ServerResponse> {
Mono<R> filter(ServerRequest var1, HandlerFunction<T> var2);
//其它默认方法
}
比如,我们可以使用HandlerFilterFunction根据用户角色过滤路由,如下示例:
RouterFunction<ServerResponse> route = route(DELETE("/api/mans/{id}"), request -> ok().build());
RouterFunction<ServerResponse> filteredRoute = route.filter((request, next) -> {
if (hasAdminRole()) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
});
private boolean hasAdminRole()
{
//判断是否有管理全权限的逻辑代码
}
当我们向/api/mans/{id}
的URL发出请求时,筛选器将检查用户是否具有管理员角色,并决定执行处理函数,还是返回UNAUTHORIZED响应。
最后,将HandlerFunctions注册为方法引用
我们也可以不使用内联lambda来定义HandlerFunctions,而是将它们定义为方法引用并在路由配置中使用方法引用,如下所示:
@Component
public class EchoHandler {
public Mono<ServerResponse> echo(ServerRequest request) {
return ServerResponse.ok().body(fromObject(request.queryParam("name")));
}
}
@Configuration
public class ManControllerFunc {
@Autowired
private com.hjf.boot.demo.flux.func.EchoHandler echoHandler;
@Bean
public RouterFunction<ServerResponse> echoRouterFunction() {
return RouterFunctions.route(GET("/echo"), echoHandler::echo);
}
}
实战:使用RouterFunction的查询API
现在,有了前面的基础知识受,我们就可以使用功函数式编程模型来构建应用程序了。 我们将创建一个UserHandler来作为HandlerFunctions的操作定义,然后配置一个RouterFunctions的路由Bean来映射路径以处理请求。
第一步,创建示例Bean:ManEntity.class,如下所示:
@Entity
@Table(name="Man")
public class ManEntity {
@Id @GeneratedValue(strategy= GenerationType.AUTO)
private int id;
// @Column(nullable=false)
private String name;
// @Column(nullable=false, unique=true)
private int age;
// 省略get、set
第二步,创建服务组件类:ManHandler.class
@Component
public class ManHandler {
// private ManReactiveRepository manReactiveRepository;//目前Spring-data-jpa并不支持,会报错,要么使用mongodb
//
// @Autowired
// public void UserHandlerFunctions(ManReactiveRepository manReactiveRepository) {
// this.manReactiveRepository = manReactiveRepository;
// }
//
public Mono<ServerResponse> getAllUsers(ServerRequest request)
{
// Flux<ManEntity> allMans = manReactiveRepository.findAll();
Flux<ManEntity> allMans = Flux.fromArray(mockMan(10).toArray(new ManEntity[10]));
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8)
.body(allMans, ManEntity.class);
}
//
public Mono<ServerResponse> getUserById(ServerRequest request) {
// Mono<ManEntity> manMono = manReactiveRepository.findById(Integer.valueOf(request.pathVariable("id")));
Mono<ManEntity> manMono = Mono.just(mockMan(1).get(0));
Mono<ServerResponse> notFount = ServerResponse.notFound().build();
return manMono.flatMap(user -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(fromObject(user)))
.switchIfEmpty(notFount);
}
//
public Mono<ServerResponse> saveUser(ServerRequest request) {
Mono<ManEntity> manMono = request.bodyToMono(ManEntity.class);
// Mono<ManEntity> mono = manMono.flatMap(man -> manReactiveRepository.save(man));
return ServerResponse.ok().body(manMono, ManEntity.class);
}
public Mono<ServerResponse> deleteUser(ServerRequest request) {
Integer id = Integer.valueOf(request.pathVariable("id"));
// Mono<Void> mono = manReactiveRepository.deleteById(id); 删除返回void的空
return ServerResponse.ok().build(Mono.empty());
}
static public List<ManEntity> mockMan(int num){
List<ManEntity> manEntityList = new ArrayList<>();
for (int i = 0; i < num; i++) {
ManEntity man = new ManEntity();
man.setId(i);
man.setName("testname_"+i);
man.setAge(18+i);
manEntityList.add(man);
}
return manEntityList;
}
}
细心的同学已经发现,这里我们应该是要使用JPA的,但是通过实践并查阅官方文档发现,Spring-data-jpa目前暂时还未支持响应式,所以会在启动时报错——
No property saveAll found for type
不过相信在未来,应该是会逐渐受到支持的。这里我们手动写一个mockMan方法来模拟读取数据库数据。这里还可以使用已经受支持的Redis或MongoDB替代也行。
第三步,创建启动类:RunAppFunc .class,并注册RouterFunction,如下
@SpringBootApplication
public class RunAppFunc {
public static void main(String[] args) {
SpringApplication.run(RunAppFunc.class,args);
}
@Autowired
ManHandler manHandler;
@Bean
public RouterFunction<ServerResponse> routerFunctions() {
return nest(path("/api/mans"),
nest(accept(APPLICATION_JSON),
route(GET("/{id}"), manHandler::getUserById)
.andRoute(method(HttpMethod.GET), manHandler::getAllUsers)
.andRoute(DELETE("/{id}"), manHandler::deleteUser)
.andRoute(POST("/"), manHandler::saveUser)));
}
}
代码中,我们对应映射服务类ManHandler的CRUD操作方法,并使用前缀的方式来统一建立路由。再次提醒,SpringBoot目前是不支持两种编程模型的混用的。
第四步,启动应用,再次报错!发现路由没有生效,查询了StackOverflow后发现,是内嵌的tomcat影响,我们只需要排除掉即可,更新pom如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
再次启动,成功!这时,控制台打印会多出以下内容:
Mapped /api/mans => {
Accept: [application/json] => {
(GET && /{id}) -> com.hjf.boot.demo.flux.func.RunAppFunc$$Lambda$236/81355344@552ed807
GET -> com.hjf.boot.demo.flux.func.RunAppFunc$$Lambda$238/1293389141@3971f0fe
(DELETE && /{id}) -> com.hjf.boot.demo.flux.func.RunAppFunc$$Lambda$239/109175108@23940f86
(POST && /) -> com.hjf.boot.demo.flux.func.RunAppFunc$$\Lambda$240/678801430@66153688
}
}
访问http://localhost:8080,输出:
[{“id”:0,“name”:“testname_0”,“age”:18},{“id”:1,“name”:“testname_1”,“age”:19},{“id”:2,“name”:“testname_2”,“age”:20},{“id”:3,“name”:“testname_3”,“age”:21},{“id”:4,“name”:“testname_4”,“age”:22},{“id”:5,“name”:“testname_5”,“age”:23},{“id”:6,“name”:“testname_6”,“age”:24},{“id”:7,“name”:“testname_7”,“age”:25},{“id”:8,“name”:“testname_8”,“age”:26},{“id”:9,“name”:“testname_9”,“age”:27}]
其实,默认情况下,spring-boot-starter-webflux使用reactor-netty作为运行时引擎。 我们可以排除reactor-netty,并使用其他支持反应式非阻塞I/O的服务器,比如,Undertow,Jetty或Tomcat。
小结
本文详细介绍了Spring WebFlux中的函数式编程模型中的各个关键组件,并通过一个实际的例子来讲函数式编程如何与SpringBoot进行了集合。虽然我们可能已经习惯了注解模型的编程方式,但了解一个新的思维模式同样对我们的学习进步有帮助。
希望对你有所帮助。
参考资源
1、Spring Boot官方文档
2、Spring 5 WebFlux
3、注意有关WebFlux自动配置的更多详细,请查看org.springframework.boot.autoconfigure.web.reactive包中的配置类