Spring Boot 響應(yīng)式 WebFlux(二、GlobalResponse)

在我們提供后端 API 給前端時,我們需要告前端,這個 API 調(diào)用結(jié)果是否成功:

  • 如果成功,成功的數(shù)據(jù)是什么。后續(xù),前端會取數(shù)據(jù)渲染到頁面上。
  • 如果失敗,失敗的原因是什么。一般,前端會將原因彈出提示給用戶。

這樣,我們就需要有統(tǒng)一的返回結(jié)果,而不能是每個接口自己定義自己的風(fēng)格。一般來說,統(tǒng)一的全局返回信息如下:

  • 成功時,返回成功的狀態(tài)碼 + 數(shù)據(jù)。
  • 失敗時,返回失敗的狀態(tài)碼 + 錯誤提示

在標(biāo)準(zhǔn)的 RESTful API 的定義,是推薦使用 HTTP 響應(yīng)狀態(tài)碼 返回狀態(tài)碼。一般來說,我們實(shí)踐很少這么去做,主要有如下原因:

  • 業(yè)務(wù)返回的錯誤狀態(tài)碼很多,HTTP 響應(yīng)狀態(tài)碼無法很好的映射。例如說,活動還未開始、訂單已取消等等。
  • 國內(nèi)開發(fā)者對 HTTP 響應(yīng)狀態(tài)碼不是很了解,可能只知道 200、403、404、500 幾種常見的。這樣,反倒增加學(xué)習(xí)成本。

所以,實(shí)際項(xiàng)目在實(shí)踐時,我們會將狀態(tài)碼放在 Response Body 響應(yīng)內(nèi)容中返回。

在全局統(tǒng)一返回里,我們至少需要定義三個字段:

  • code:狀態(tài)碼。無論是否成功,必須返回。

    • 成功時,狀態(tài)碼為 0 。

    • 失敗時,對應(yīng)業(yè)務(wù)的錯誤碼。

      關(guān)于這一塊,也有團(tuán)隊實(shí)踐時,增加了 success 字段,通過 truefalse 表示成功還是失敗。這個看每個團(tuán)隊的習(xí)慣吧。個人還是偏好基于約定,返回 0 時表示成功。

  • data:數(shù)據(jù)。成功時,返回該字段。

  • message:錯誤提示。失敗時,返回該字段。

那么,讓我們來看兩個示例:

// 成功響應(yīng)
{
 code: 0,
 data: {
 id: 1,
 username: "yudaoyuanma"
 }
}

// 失敗響應(yīng)
{
 code: 233666,
 message: "徐媽太丑了"
}

下面,我們來看一個示例。

2.1 引入依賴

與上篇文章一致。

2.2 Application

與上篇文章一致。

2.3 CommonResult

創(chuàng)建 [CommonResult]類,用于全局統(tǒng)一返回。代碼如下:

package com.erbadagang.springboot.springwebflux.globalresponse.core.vo;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.util.Assert;

import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable;

/**
 * 通用返回結(jié)果
 *
 * @param <T> 結(jié)果泛型
 */
@XmlRootElement
public class CommonResult<T> implements Serializable {

    public static Integer CODE_SUCCESS = 0;

    /**
     * 錯誤碼
     */
    private Integer code;
    /**
     * 錯誤提示
     */
    private String message;
    /**
     * 返回數(shù)據(jù)
     */
    private T data;

    /**
     * 將傳入的 result 對象,轉(zhuǎn)換成另外一個泛型結(jié)果的對象
     *
     * 因?yàn)?A 方法返回的 CommonResult 對象,不滿足調(diào)用其的 B 方法的返回,所以需要進(jìn)行轉(zhuǎn)換。
     *
     * @param result 傳入的 result 對象
     * @param <T> 返回的泛型
     * @return 新的 CommonResult 對象
     */
    public static <T> CommonResult<T> error(CommonResult<?> result) {
        return error(result.getCode(), result.getMessage());
    }

    public static <T> CommonResult<T> error(Integer code, String message) {
        Assert.isTrue(!CODE_SUCCESS.equals(code), "code 必須是錯誤的!");
        CommonResult<T> result = new CommonResult<>();
        result.code = code;
        result.message = message;
        return result;
    }

    public static <T> CommonResult<T> success(T data) {
        CommonResult<T> result = new CommonResult<>();
        result.code = CODE_SUCCESS;
        result.data = data;
        result.message = "";
        return result;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    @JsonIgnore
    public boolean isSuccess() {
        return CODE_SUCCESS.equals(code);
    }

    @JsonIgnore
    public boolean isError() {
        return !isSuccess();
    }

    @Override
    public String toString() {
        return "CommonResult{" +
                "code=" + code +
                ", message='" + message + '\'' +
                ", data=" + data +
                '}';
    }

}

2.4 GlobalResponseBodyHandler

創(chuàng)建 [GlobalResponseBodyHandler]類,全局統(tǒng)一返回的處理器。代碼如下:

package com.erbadagang.springboot.springwebflux.globalresponse.core.web;

import com.erbadagang.springboot.springwebflux.globalresponse.core.vo.CommonResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.function.Function;

public class GlobalResponseBodyHandler extends ResponseBodyResultHandler {

    private static Logger LOGGER = LoggerFactory.getLogger(GlobalResponseBodyHandler.class);

    private static MethodParameter METHOD_PARAMETER_MONO_COMMON_RESULT;

    private static final CommonResult COMMON_RESULT_SUCCESS = CommonResult.success(null);

    static {
        try {
            // 獲得 METHOD_PARAMETER_MONO_COMMON_RESULT 。其中 -1 表示 `#methodForParams()` 方法的返回值
            METHOD_PARAMETER_MONO_COMMON_RESULT = new MethodParameter(
                    GlobalResponseBodyHandler.class.getDeclaredMethod("methodForParams"), -1);
        } catch (NoSuchMethodException e) {
            LOGGER.error("[static][獲取 METHOD_PARAMETER_MONO_COMMON_RESULT 時,找不都方法");
            throw new RuntimeException(e);
        }
    }

    public GlobalResponseBodyHandler(List<HttpMessageWriter<?>> writers, RequestedContentTypeResolver resolver) {
        super(writers, resolver);
    }

    public GlobalResponseBodyHandler(List<HttpMessageWriter<?>> writers, RequestedContentTypeResolver resolver, ReactiveAdapterRegistry registry) {
        super(writers, resolver, registry);
    }

    @Override
    @SuppressWarnings("unchecked")
    public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
        Object returnValue = result.getReturnValue();
        Object body;
        // <1.1>  處理返回結(jié)果為 Mono 的情況
        if (returnValue instanceof Mono) {
            body = ((Mono<Object>) result.getReturnValue())
                    .map((Function<Object, Object>) GlobalResponseBodyHandler::wrapCommonResult)
                    .defaultIfEmpty(COMMON_RESULT_SUCCESS);
        //  <1.2> 處理返回結(jié)果為 Flux 的情況
        } else if (returnValue instanceof Flux) {
            body = ((Flux<Object>) result.getReturnValue())
                    .collectList()
                    .map((Function<Object, Object>) GlobalResponseBodyHandler::wrapCommonResult)
                    .defaultIfEmpty(COMMON_RESULT_SUCCESS);
        //  <1.3> 處理結(jié)果為其它類型
        } else {
            body = wrapCommonResult(returnValue);
        }
        return writeBody(body, METHOD_PARAMETER_MONO_COMMON_RESULT, exchange);
    }

    private static Mono<CommonResult> methodForParams() {
        return null;
    }

    private static CommonResult<?> wrapCommonResult(Object body) {
        // 如果已經(jīng)是 CommonResult 類型,則直接返回
        if (body instanceof CommonResult) {
            return (CommonResult<?>) body;
        }
        // 如果不是,則包裝成 CommonResult 類型
        return CommonResult.success(body);
    }

}
  • 繼承 WebFlux 的 ResponseBodyResultHandler 類,因?yàn)樵擃悓?Response 的 body 寫回給前端。所以,我們通過重寫該類的 #handleResult(ServerWebExchange exchange, HandlerResult result) 方法,將返回結(jié)果進(jìn)行使用 CommonResult 包裝。
  • <1> 處,獲得 METHOD_PARAMETER_MONO_COMMON_RESULT 。其中 -1 表示 #methodForParams() 方法的返回值類型 Mono<CommonResult> 。后續(xù)我們在#handleResult(ServerWebExchange exchange, HandlerResult result) 方法中,會使用到 METHOD_PARAMETER_MONO_COMMON_RESULT 。
  • 重寫 #handleResult(ServerWebExchange exchange, HandlerResult result) 方法,將返回結(jié)果進(jìn)行使用 CommonResult 包裝。
    • <1.1> 處,處理返回結(jié)果為 Mono 的情況。通過調(diào)用 Mono#map(Function<? super T, ? extends R> mapper) 方法,將原返回結(jié)果,進(jìn)行包裝成 CommonResult<?>
    • <1.2> 處,處理返回結(jié)果為 Flux 的情況。先通過調(diào)用 Flux#collectList() 方法,將其轉(zhuǎn)換成 Mono<List<T>> 對象,后續(xù)就是和 <1.1> 相同的邏輯。
    • <1.3> 處,處理結(jié)果為其它類型的情況,直接進(jìn)行包裝成 CommonResult<?>
  • <2> 處,調(diào)用父類方法 #writeBody(Object body, MethodParameter bodyParameter, ServerWebExchange exchange) 方法,實(shí)現(xiàn)將結(jié)果寫回給前端。

在思路上,和 SpringMVC 使用 ResponseBodyAdvice + @ControllerAdvice 注解,是一致的。只是說,WebFlux 暫時沒有提供這樣的方式,所以咱只好通過繼承 ResponseBodyResultHandler 類,重寫其 #handleResult(ServerWebExchange exchange, HandlerResult result) 方法,將返回結(jié)果進(jìn)行使用 CommonResult 包裝。

2.5 WebFluxConfiguration

創(chuàng)建 [WebFluxConfiguration]配置類。代碼如下:

package com.erbadagang.springboot.springwebflux.globalresponse.config;

import com.erbadagang.springboot.springwebflux.globalresponse.core.web.GlobalResponseBodyHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.config.WebFluxConfigurer;

import java.util.Collections;

@Configuration
public class WebFluxConfiguration implements WebFluxConfigurer {

    @Bean
    public GlobalResponseBodyHandler responseWrapper(ServerCodecConfigurer serverCodecConfigurer,
                                                     RequestedContentTypeResolver requestedContentTypeResolver) {
        return new GlobalResponseBodyHandler(serverCodecConfigurer.getWriters(), requestedContentTypeResolver);
    }
}
  • #responseWrapper(serverCodecConfigurer, requestedContentTypeResolver) 方法中,我們創(chuàng)建了 4.4 GlobalResponseBodyHandler Bean 對象,實(shí)現(xiàn)對返回結(jié)果的包裝。

2.6 UserController

創(chuàng)建 [UserController]類。代碼如下:

package com.erbadagang.springboot.springwebflux.globalresponse.controller;

import com.erbadagang.springboot.springwebflux.globalresponse.constants.ServiceExceptionEnum;
import com.erbadagang.springboot.springwebflux.globalresponse.core.exception.ServiceException;
import com.erbadagang.springboot.springwebflux.globalresponse.core.vo.CommonResult;
import com.erbadagang.springboot.springwebflux.globalresponse.vo.UserVO;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.List;

/**
 * 用戶 Controller
 */
@RestController
@RequestMapping("/users")
//@CrossOrigin(value = "*")
public class UserController {

    /**
     * 查詢用戶列表
     *
     * @return 用戶列表
     */
    @GetMapping("/list")
    public Flux<UserVO> list() {
        // 查詢列表
        List<UserVO> result = new ArrayList<>();
        result.add(new UserVO().setId(1).setUsername("trek"));
        result.add(new UserVO().setId(2).setUsername("specialized"));
        result.add(new UserVO().setId(3).setUsername("look"));
        // 返回列表
        return Flux.fromIterable(result);
    }

    /**
     * 獲得指定用戶編號的用戶
     *
     * @param id 用戶編號
     * @return 用戶
     */
    @GetMapping("/get")
//    @PostMapping("/get")
    public Mono<UserVO> get(@RequestParam("id") Integer id) {
        // 查詢用戶
        UserVO user = new UserVO().setId(id).setUsername("username:" + id);
        // 返回
        return Mono.just(user);
    }

    /**
     * 獲得指定用戶編號的用戶
     *
     * @param id 用戶編號
     * @return 用戶
     */
    @GetMapping("/get2")
    public Mono<CommonResult<UserVO>> get2(@RequestParam("id") Integer id) {
        // 查詢用戶
        UserVO user = new UserVO().setId(id).setUsername("username:" + id);
        // 返回
        return Mono.just(CommonResult.success(user));
    }

    /**
     * 獲得指定用戶編號的用戶
     *
     * @param id 用戶編號
     * @return 用戶
     */
    @GetMapping("/get3")
    public UserVO get3(@RequestParam("id") Integer id) {
        // 查詢用戶
        UserVO user = new UserVO().setId(id).setUsername("username:" + id);
        // 返回
        return user;
    }

    /**
     * 獲得指定用戶編號的用戶
     *
     * @param id 用戶編號
     * @return 用戶
     */
    @GetMapping("/get4")
    public CommonResult<UserVO> get4(@RequestParam("id") Integer id) {
        // 查詢用戶
        UserVO user = new UserVO().setId(id).setUsername("username:" + id);
        // 返回
        return CommonResult.success(user);
    }

    /**
     * 測試拋出 NullPointerException 異常
     */
    @GetMapping("/exception-01")
    public UserVO exception01() {
        throw new NullPointerException("沒有粗面魚丸");
    }

    /**
     * 測試拋出 ServiceException 異常
     */
    @GetMapping("/exception-02")
    public UserVO exception02() {
        throw new ServiceException(ServiceExceptionEnum.USER_NOT_FOUND);
    }

//    @PostMapping(value = "/add",
//            // ↓ 增加 "application/xml"、"application/json" ,針對 Content-Type 請求頭
//            consumes = {MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE},
//            // ↓ 增加 "application/xml"、"application/json" ,針對 Accept 請求頭
//            produces = {MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE}
//    )

    @PostMapping(value = "/add",
            // ↓ 增加 "application/xml"、"application/json" ,針對 Content-Type 請求頭
            consumes = {MediaType.APPLICATION_XML_VALUE},
            // ↓ 增加 "application/xml"、"application/json" ,針對 Accept 請求頭
            produces = {MediaType.APPLICATION_XML_VALUE}
    )
//    @PostMapping(value = "/add")
    public Mono<UserVO> add(@RequestBody Mono<UserVO> user) {
        return user;
    }

}

API 接口雖然比較多,但是我們可以先根據(jù)返回結(jié)果的類型,分成 Flux 和 Mono 兩類。然后,艿艿這里又創(chuàng)建了 Mono 分類的四種情況的接口,就是 /users/get、/users/get2、/users/get3、/users/get4 四個。
在 #get(Integer id) 方法,返回的結(jié)果是 UserVO 類型。這樣,結(jié)果會被 GlobalResponseBodyHandler 攔截,包裝成 CommonResult 類型返回。請求結(jié)果如下:

{
    "code": 0,
    "message": "",
    "data": {
        "id": 10,
        "username": "username:10"
    }
}

會有"message": ""的返回的原因是,我們使用 SpringMVC 提供的 Jackson 序列化,對于CommonResult此時的message = null的情況下,會序列化它成"message": ""返回。實(shí)際情況下,不會影響前端處理。
# get2(Integer id)方法,返回的結(jié)果是Mono<Common<UserVO>>類型。結(jié)果雖然也會被GlobalResponseBodyHandler處理,但是不會二次再重復(fù)包裝成 CommonResult類型返回。

訪問http://localhost:8080/users/list,測試結(jié)果展示:

返回統(tǒng)一響應(yīng)消息格式

底線


本文源代碼使用 Apache License 2.0開源許可協(xié)議,這里是本文源碼Gitee地址,可通過命令git clone+地址下載代碼到本地,也可直接點(diǎn)擊鏈接通過瀏覽器方式查看源代碼。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容