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)碼 + 錯誤提示靶草。

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

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

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

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

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

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

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

      關(guān)于這一塊举娩,也有團隊實踐時,增加了 success 字段构罗,通過 truefalse 表示成功還是失敗铜涉。這個看每個團隊的習(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é)果的對象
     *
     * 因為 A 方法返回的 CommonResult 對象症昏,不滿足調(diào)用其的 B 方法的返回随闽,所以需要進行轉(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 類鼠次,因為該類將 Response 的 body 寫回給前端。所以芋齿,我們通過重寫該類的 #handleResult(ServerWebExchange exchange, HandlerResult result) 方法腥寇,將返回結(jié)果進行使用 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é)果進行使用 CommonResult 包裝职辅。
    • <1.1> 處棒呛,處理返回結(jié)果為 Mono 的情況。通過調(diào)用 Mono#map(Function<? super T, ? extends R> mapper) 方法域携,將原返回結(jié)果,進行包裝成 CommonResult<?> 鱼喉。
    • <1.2> 處秀鞭,處理返回結(jié)果為 Flux 的情況。先通過調(diào)用 Flux#collectList() 方法扛禽,將其轉(zhuǎn)換成 Mono<List<T>> 對象锋边,后續(xù)就是和 <1.1> 相同的邏輯。
    • <1.3> 處编曼,處理結(jié)果為其它類型的情況豆巨,直接進行包裝成 CommonResult<?>
  • <2> 處掐场,調(diào)用父類方法 #writeBody(Object body, MethodParameter bodyParameter, ServerWebExchange exchange) 方法往扔,實現(xiàn)將結(jié)果寫回給前端贩猎。

在思路上,和 SpringMVC 使用 ResponseBodyAdvice + @ControllerAdvice 注解萍膛,是一致的吭服。只是說,WebFlux 暫時沒有提供這樣的方式蝗罗,所以咱只好通過繼承 ResponseBodyResultHandler 類艇棕,重寫其 #handleResult(ServerWebExchange exchange, HandlerResult result) 方法,將返回結(jié)果進行使用 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 對象桩匪,實現(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": ""返回悠抹。實際情況下珠月,不會影響前端處理。
# 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+地址下載代碼到本地掏觉,也可直接點擊鏈接通過瀏覽器方式查看源代碼。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末值漫,一起剝皮案震驚了整個濱河市澳腹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌杨何,老刑警劉巖酱塔,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異危虱,居然都是意外死亡羊娃,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進店門埃跷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蕊玷,“玉大人,你說我怎么就攤上這事弥雹±В” “怎么了?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵剪勿,是天一觀的道長贸诚。 經(jīng)常有香客問我,道長窗宦,這世上最難降的妖魔是什么赦颇? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮赴涵,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘订讼。我一直安慰自己髓窜,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著寄纵,像睡著了一般鳖敷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上程拭,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天定踱,我揣著相機與錄音,去河邊找鬼恃鞋。 笑死崖媚,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的恤浪。 我是一名探鬼主播畅哑,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼水由!你這毒婦竟也來了荠呐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤砂客,失蹤者是張志新(化名)和其女友劉穎泥张,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鞠值,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡媚创,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了齿诉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片筝野。...
    茶點故事閱讀 39,932評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖粤剧,靈堂內(nèi)的尸體忽然破棺而出歇竟,到底是詐尸還是另有隱情,我是刑警寧澤抵恋,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布焕议,位于F島的核電站,受9級特大地震影響弧关,放射性物質(zhì)發(fā)生泄漏盅安。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一世囊、第九天 我趴在偏房一處隱蔽的房頂上張望别瞭。 院中可真熱鬧,春花似錦株憾、人聲如沸蝙寨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽墙歪。三九已至听系,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間虹菲,已是汗流浹背靠胜。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留毕源,地道東北人浪漠。 一個月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像脑豹,于是被迫代替她去往敵國和親郑藏。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,884評論 2 354