Spring Boot 实践折腾记(17):Spring WebFlux中的函数式编程模型

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/mickjoust/article/details/80324318

杨绛先生说:大部分人的问题是,做得不多而想得太多。

今天要讲的函数式编程可能和Spring Boot本身的关系不太大,但是它很重要!不仅是因为从Java 7升级到Java 8多了一种新编程语法的支持,更因为这是一种不同的思维模式。同时,今天的内容可能会偏多一点,希望爱学习的你能耐心看完。

Spring 5中的引入了对响应式编程的支持——WebFlux,它基于Reactor类库的基础实现,之前的三篇文章:

已经详细讲述过Reactor和WebFlux注解模型以及用法,可以点击复习。目前在Spring Boot中不支持两种范式混用。

本文我们将实现一个使用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")));

接下来,我们先看几个关键组件。

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类型,并且可以还发送任何响应的实例流给发布者作为响应主体。

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));

ServerResponse

同样,在包中org.springframework.web.reactive.function.serverServerResponse接口表示服务器端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法的返回值了。

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());

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包中的配置类

猜你喜欢

转载自blog.csdn.net/mickjoust/article/details/80324318
今日推荐