02-Controller控制器

SpringWebFlux虽然并非基于JavaEE的Servlet API,但它在设计上仍保留了原来SpringMVC的使用方式,我们可以使用@Controller@RestController定义控制器组件,框架会自动通过反射找到这些控制器并用其处理HTTP请求。对于和SpringMVC相同的部分我们这里不会再重复介绍,这篇笔记我们主要了解SpringWebFlux中使用Controller模式编写代码处理HTTP请求时和SpringMVC之间的不同点。

SpringWebFlux的Controller

SpringWebFlux中,我们的代码必须整个都是Reactive的,这也意味着,它和SpringMVC最大的不同点就是Controller返回值的类型也一定是Flux或Mono。

package com.gacfox.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/demo")
public class DemoController {
    @GetMapping("/hello")
    public Mono<String> hello() {
        return Mono.just("Hello, world!");
    }
}

代码中,我们使用@RestController声明这是一个控制器服务端点,它返回Rest响应,@RequestMapping声明该控制器处理/demo路径,@GetMapping声明了一个控制器方法,它处理/hello路径,因此启动服务后我们访问/demo/hello即可查看效果,到此为止其使用方法和SpringMVC完全一致。不一样的地方是方法体内,我们使用just()方法创建了一个简单的Mono类型,它包含Hello, world!字符串信息。

对于POST请求,如果我们需要客户端传入一些内容,我们也需要使用Flux或Mono类型接收,下面是一个例子。

package com.gacfox.demo.controller;

import com.gacfox.demo.model.ApiResult;
import com.gacfox.demo.model.StudentDto;
import com.gacfox.demo.service.StudentService;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;

@RestController
@RequestMapping("/demo")
public class DemoController {
    @Resource
    private StudentService studentService;

    @PostMapping("/saveStudent")
    public Mono<ApiResult<?>> saveStudent(@RequestBody Mono<StudentDto> studentDtoMono) {
        return studentDtoMono
                .flatMap(studentDto -> studentService.saveStudent(studentDto))
                .then(Mono.just(ApiResult.success()));
    }
}

代码中,我们的控制器方法接收Mono<StudentDto>类型的参数,这意味着框架对数据进行反序列化时也会异步的进行。SpringWebFlux控制器方法也支持直接传入非Mono或Flux的DTO类,但注意这样反序列化阶段就会同步阻塞式的进行,因此适用于较小的单个的请求体数据。

此外,由于SpringWebFlux中所有代码都是Reactive的,我们这里的StudentServicesaveStudent()方法也是Reactive的,它接收StudentDto类型参数,操作完成后返回Mono<Void>,这也是比较常见的Reactive写法。

public Mono<Void> saveStudent(StudentDto studentDto) { }

Controller的响应式流程中,我们使用了flatmapthen操作符,先调用saveStudent()方法保存数据,然后返回我们自定义的通用响应体ApiResult.success(),向客户端输出成功信息。

对于Controller,额外我们还需要了解,如果在SpringWebFlux中不使用Mono和Flux会发生什么?这样做其实SpringWebFlux并不会直接报错,框架支持这种写法,但这并不是推荐的方式!如果Controller方法接收非Mono或Flux请求,返回非Mono或Flux响应,本质上SpringWebFlux就会以同步的方式处理这些数据,但这些同步代码会阻塞线程,这与使用SpringWebFlux的初衷是相反的,在高并发环境下这种代码可能造成服务端性能的急剧下降!此外,这类代码也与整个SpringWebFlux的Reactive生态格格不入,给后续维护造成极大困难。

SpringWebFlux支持同步写法的目的仅在于需要兼容部分旧有传统代码的场景,千万不要滥用它们。

读取请求上下文

在传统的SpringMVC中我们可以通过访问HttpServletRequest读取请求上下文,在SpringWebFlux中由于框架并非基于JavaEE Servlet API,因此没有这个对象,在这里我们需要使用的是ServerHttpRequest

package com.gacfox.demo.controller;

import com.gacfox.demo.model.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Slf4j
@RestController
@RequestMapping("/demo")
public class DemoController {

    @GetMapping("/hello")
    public Mono<ApiResult<?>> hello(ServerWebExchange exchange) {
        // 获取Header
        HttpHeaders headers = exchange.getRequest().getHeaders();
        headers.keySet().forEach(key -> log.info("header: {}, value: {}", key, headers.getFirst(key)));
        // 获取Cookie
        MultiValueMap<String, HttpCookie> cookies = exchange.getRequest().getCookies();
        cookies.keySet().forEach(key -> log.info("cookie: {}, value: {}", key, cookies.getFirst(key)));

        return Mono.just(ApiResult.success());
    }
}

代码中我们首先注入了ServerWebExchange对象,它是SpringWebFlux框架中的核心接口,用于在应用中表示服务器端HTTP请求和响应的上下文。exchange.getRequest()方法能够获取到SpringWebFlux的ServerHttpRequest对象,其中包含了大量的请求上下文信息,我们可以从其中取出Header或Cookie键值对。

此外SpringWebFlux其实也可以直接注入ServerHttpRequest对象,如果我们只操作请求上下文而不用处理响应信息,像下面这样写也是可以的。

@GetMapping("/hello")
public Mono<ApiResult<?>> hello(ServerHttpRequest serverHttpRequest) { }

设置响应信息

和获取请求上下文类似,处理响应信息时我们需要通过ServerHttpResponse对象实现,它包含了设置响应状态码、响应头、响应Cookie等方法。下面例子代码中我们使用ServerHttpResponse设置了响应头信息。

package com.gacfox.demo.controller;

import com.gacfox.demo.model.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Slf4j
@RestController
@RequestMapping("/demo")
public class DemoController {

    @GetMapping("/hello")
    public Mono<ApiResult<?>> hello(ServerWebExchange exchange) {
        exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
        return Mono.just(ApiResult.failure("You are not allowed to access!"));
    }
}

Controller控制器中,设置响应信息另一种更好的方式是使用ResponseEntity。统一使用ResponseEntity封装状态码等信息和响应体,语义更清晰,代码可读性更好。

package com.gacfox.demo.controller;

import com.gacfox.demo.model.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;

@Slf4j
@RestController
@RequestMapping("/demo")
public class DemoController {

    @GetMapping("/hello")
    public Mono<ResponseEntity<ApiResult<?>>> hello() {
        return Mono.just(ResponseEntity
                .status(HttpStatus.FORBIDDEN)
                .body(ApiResult.failure("You are not allowed to access!"))
        );
    }
}

返回流式响应

前面我们提到过,SpringWebFlux的主要用途就是开发那些高并发、长连接、实时性强的服务,输出SSE流式响应就是一个非常常见的使用场景。下面例子中,我们输出了一组Flux类型的SSE响应信息到客户端。

package com.gacfox.demo.controller;

import com.gacfox.demo.model.ChatEventResponseData;
import com.gacfox.demo.service.ChatService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;

import javax.annotation.Resource;

@Slf4j
@RestController
@RequestMapping("/demo")
public class DemoController {
    @Resource
    private ChatService chatService;

    @GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<ChatEventResponseData>> chat() {
        return chatService.chat()
                .map(data -> ServerSentEvent.<ChatEventResponseData>builder()
                        .event("message")
                        .data(data)
                        .build());
    }
}

代码中,我们的ChatService能够返回Flux类型数据流,Controller内我们指定HTTP响应类型是MediaType.TEXT_EVENT_STREAM_VALUE,SpringWebFlux框架会自动处理Flux,每当数据流中有数据元素生成时,框架都会将其推送给客户端,这样客户端就能实时接收服务端返回的消息,而不必等待整个响应全部生成后再处理。SSE虽然可以使用任何文本类型,但一般还是遵循W3C SSE规范,SpringWeb提供了ServerSentEvent封装SSE响应,因此我们没必要手动拼接SSE响应格式字符串,控制器方法返回Flux<ServerSentEvent<>>类型即可。如果你对HTML5中的SSE不熟悉,可以参考Web前端/HTML5/SSE服务端推送章节。

总体上来看,这显然是一个长连接服务,但和SpringMVC相比,我们是采用异步非阻塞的方式开发的,在这里SpringWebFlux能够以更少的服务器资源支持更大的并发连接数。

ControllerAdvice控制器切面

SpringMVC中我们经常使用ControllerAdvice定义控制器的切面逻辑,比如十分常用的通用异常处理机制,SpringWebFlux同样支持ControllerAdvice,我们使用@ControllerAdvice@RestControllerAdvice定义,但它的返回值通常应当是Mono或Flux。

package com.gacfox.demo.controller;

import com.gacfox.demo.model.ApiResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import reactor.core.publisher.Mono;

@RestControllerAdvice
public class GlobalExceptionAdvice {
    @ExceptionHandler(Exception.class)
    public Mono<ApiResult<?>> handleException(Exception e) {
        return Mono.just(ApiResult.failure(e.getMessage()));
    }
}

代码中,如果我们的其它控制器调用流程中抛出异常,ControllerAdvice对应的处理方法将执行,并返回一个通用的异常响应。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap