Spring Boot 中如何統(tǒng)一 API 接口響應(yīng)格式螟碎?

今天又要給大家介紹一個(gè) Spring Boot 中的組件--HandlerMethodReturnValueHandler最仑。

今天要和大家介紹一種更加靈活的方案--HandlerMethodReturnValueHandler矢洲,我們一起來(lái)看看下绅喉。

1.HandlerMethodReturnValueHandler

HandlerMethodReturnValueHandler 的作用是對(duì)處理器的處理結(jié)果再進(jìn)行一次二次加工,這個(gè)接口里邊有兩個(gè)方法:

public interface HandlerMethodReturnValueHandler {
  boolean supportsReturnType(MethodParameter returnType);
  void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
          ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}
  • supportsReturnType:這個(gè)處理器是否支持相應(yīng)的返回值類型颈抚。
  • handleReturnValue:對(duì)方法返回值進(jìn)行處理踩衩。

HandlerMethodReturnValueHandler 有很多默認(rèn)的實(shí)現(xiàn)類,我們來(lái)看下:

image

接下來(lái)我們來(lái)把這些實(shí)現(xiàn)類的作用捋一捋:

ViewNameMethodReturnValueHandler

這個(gè)處理器用來(lái)處理返回值為 void 和 String 的情況贩汉。如果返回值為 void驱富,則不做任何處理。如果返回值為 String匹舞,則將 String 設(shè)置給 mavContainer 的 viewName 屬性褐鸥,同時(shí)判斷這個(gè) String 是不是重定向的 String,如果是赐稽,則設(shè)置 mavContainer 的 redirectModelScenario 屬性為 true叫榕,這是處理器返回重定向視圖的標(biāo)志。

ViewMethodReturnValueHandler

這個(gè)處理器用來(lái)處理返回值為 View 的情況姊舵。如果返回值為 View晰绎,則將 View 設(shè)置給 mavContainer 的 view 屬性,同時(shí)判斷這個(gè) View 是不是重定向的 View括丁,如果是荞下,則設(shè)置 mavContainer 的 redirectModelScenario 屬性為 true,這是處理器返回重定向視圖的標(biāo)志史飞。

MapMethodProcessor

這個(gè)處理器用來(lái)處理返回值類型為 Map 的情況尖昏,具體的處理方案就是將 map 添加到 mavContainer 的 model 屬性中。

StreamingResponseBodyReturnValueHandler

這個(gè)用來(lái)處理 StreamingResponseBody 或者 ResponseEntity<StreamingResponseBody> 類型的返回值构资。

DeferredResultMethodReturnValueHandler

這個(gè)用來(lái)處理 DeferredResult抽诉、ListenableFuture 以及 CompletionStage 類型的返回值,用于異步請(qǐng)求吐绵。

CallableMethodReturnValueHandler

處理 Callable 類型的返回值掸鹅,也是用于異步請(qǐng)求。

HttpHeadersReturnValueHandler

這個(gè)用來(lái)處理 HttpHeaders 類型的返回值拦赠,具體處理方式就是將 mavContainer 中的 requestHandled 屬性設(shè)置為 true,該屬性是請(qǐng)求是否已經(jīng)處理完成的標(biāo)志(如果處理完了葵姥,就到此為止荷鼠,后面不會(huì)再去找視圖了),然后將 HttpHeaders 添加到響應(yīng)頭中榔幸。

ModelMethodProcessor

這個(gè)用來(lái)處理返回值類型為 Model 的情況允乐,具體的處理方式就是將 Model 添加到 mavContainer 的 model 上矮嫉。

ModelAttributeMethodProcessor

這個(gè)用來(lái)處理添加了 @ModelAttribute 注解的返回值類型,如果 annotaionNotRequired 屬性為 true牍疏,也可以用來(lái)處理其他非通用類型的返回值蠢笋。

ServletModelAttributeMethodProcessor

同上,該類只是修改了參數(shù)解析方式鳞陨。

ResponseBodyEmitterReturnValueHandler

這個(gè)用來(lái)處理返回值類型為 ResponseBodyEmitter 的情況昨寞。

ModelAndViewMethodReturnValueHandler

這個(gè)用來(lái)處理返回值類型為 ModelAndView 的情況,將返回值中的 Model 和 View 分別設(shè)置到 mavContainer 的相應(yīng)屬性上去厦滤。

ModelAndViewResolverMethodReturnValueHandler

這個(gè)的 supportsReturnType 方法返回 true援岩,即可以處理所有類型的返回值,這個(gè)一般放在最后兜底掏导。

AbstractMessageConverterMethodProcessor

這是一個(gè)抽象類享怀,當(dāng)返回值需要通過(guò) HttpMessageConverter 進(jìn)行轉(zhuǎn)化的時(shí)候會(huì)用到它的子類。這個(gè)抽象類主要是定義了一些工具方法趟咆。

RequestResponseBodyMethodProcessor

這個(gè)用來(lái)處理添加了 @ResponseBody 注解的返回值類型添瓷。

HttpEntityMethodProcessor

這個(gè)用來(lái)處理返回值類型是 HttpEntity 并且不是 RequestEntity 的情況。

AsyncHandlerMethodReturnValueHandler

這是一個(gè)空接口值纱,暫未發(fā)現(xiàn)典型使用場(chǎng)景鳞贷。

AsyncTaskMethodReturnValueHandler

這個(gè)用來(lái)處理返回值類型為 WebAsyncTask 的情況。

HandlerMethodReturnValueHandlerComposite

看 Composite 就知道计雌,這是一個(gè)組合處理器悄晃,沒(méi)啥好說(shuō)的。

這個(gè)就是系統(tǒng)默認(rèn)定義的 HandlerMethodReturnValueHandler凿滤。

那么在上面的介紹中妈橄,大家看到反復(fù)涉及到一個(gè)組件 mavContainer,這個(gè)我也要和大家介紹一下翁脆。

2.ModelAndViewContainer

ModelAndViewContainer 就是一個(gè)數(shù)據(jù)穿梭巴士眷蚓,在整個(gè)請(qǐng)求的過(guò)程中承擔(dān)著數(shù)據(jù)傳送的工作,從它的名字上我們可以看出來(lái)它里邊保存著 Model 和 View 兩種類型的數(shù)據(jù)反番,但是實(shí)際上可不止兩種沙热,我們來(lái)看下 ModelAndViewContainer 的定義:

public class ModelAndViewContainer {
    private boolean ignoreDefaultModelOnRedirect = false;
    @Nullable
    private Object view;
    private final ModelMap defaultModel = new BindingAwareModelMap();
    @Nullable
    private ModelMap redirectModel;
    private boolean redirectModelScenario = false;
    @Nullable
    private HttpStatus status;
    private final Set<String> noBinding = new HashSet<>(4);
    private final Set<String> bindingDisabled = new HashSet<>(4);
    private final SessionStatus sessionStatus = new SimpleSessionStatus();
    private boolean requestHandled = false;
}

把這幾個(gè)屬性理解了,基本上也就整明白 ModelAndViewContainer 的作用了:

  • defaultModel:默認(rèn)使用的 Model罢缸。當(dāng)我們?cè)诮涌趨?shù)重使用 Model篙贸、ModelMap 或者 Map 時(shí),最終使用的實(shí)現(xiàn)類都是 BindingAwareModelMap枫疆,對(duì)應(yīng)的也都是 defaultModel爵川。
  • redirectModel:重定向時(shí)候的 Model,如果我們?cè)诮涌趨?shù)中使用了 RedirectAttributes 類型的參數(shù)息楔,那么最終會(huì)傳入 redirectModel寝贡。

可以看到扒披,一共有兩個(gè) Model,兩個(gè) Model 到底用哪個(gè)呢圃泡?這個(gè)在 getModel 方法中根據(jù)條件返回合適的 Model:

public ModelMap getModel() {
    if (useDefaultModel()) {
        return this.defaultModel;
    }
    else {
        if (this.redirectModel == null) {
            this.redirectModel = new ModelMap();
        }
        return this.redirectModel;
    }
}
private boolean useDefaultModel() {
    return (!this.redirectModelScenario || (this.redirectModel == null && !this.ignoreDefaultModelOnRedirect));
}

這里 redirectModelScenario 表示處理器是否返回 redirect 視圖碟案;ignoreDefaultModelOnRedirect 表示是否在重定向時(shí)忽略 defaultModel,所以這塊的邏輯是這樣:

  1. 如果 redirectModelScenario 為 true颇蜡,即處理器返回的是一個(gè)重定向視圖价说,那么使用 redirectModel。如果 redirectModelScenario 為 false澡匪,即處理器返回的不是一個(gè)重定向視圖熔任,那么使用 defaultModel。
  2. 如果 redirectModel 為 null唁情,并且 ignoreDefaultModelOnRedirect 為 false疑苔,則使用 redirectModel,否則使用 defaultModel甸鸟。

接下來(lái)還剩下如下一些參數(shù):

@ResponseBody

這個(gè) ModelAndViewContainer 小伙伴們權(quán)且做一個(gè)了解惦费,松哥在后面的源碼分析中,還會(huì)和大家再次聊到這個(gè)組件抢韭。

接下來(lái)我們也來(lái)自定義一個(gè) HandlerMethodReturnValueHandler薪贫,來(lái)感受一下 HandlerMethodReturnValueHandler 的基本用法。

3.API 接口數(shù)據(jù)包裝

假設(shè)我有這樣一個(gè)需求:我想在原始的返回?cái)?shù)據(jù)外面再包裹一層刻恭,舉個(gè)簡(jiǎn)單例子瞧省,本來(lái)接口是下面這樣:

@RestController
public class UserController {
    @GetMapping("/user")
    public User getUserByUsername(String username) {
        User user = new User();
        user.setUsername(username);
        user.setAddress("www.javaboy.org");
        return user;
    }
}

返回的數(shù)據(jù)格式是下面這樣:

{"username":"javaboy","address":"www.javaboy.org"}

現(xiàn)在我希望返回的數(shù)據(jù)格式變成下面這樣:

{"status":"ok","data":{"username":"javaboy","address":"www.javaboy.org"}}

就這樣一個(gè)簡(jiǎn)單需求,我們一起來(lái)看下怎么實(shí)現(xiàn)鳍贾。

3.1 RequestResponseBodyMethodProcessor

在開(kāi)始定義之前鞍匾,先給大家介紹一下 RequestResponseBodyMethodProcessor,這是 HandlerMethodReturnValueHandler 的實(shí)現(xiàn)類之一骑科,這個(gè)主要用來(lái)處理返回 JSON 的情況橡淑。

我們來(lái)稍微看下:

@Override
public boolean supportsReturnType(MethodParameter returnType) {
    return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
            returnType.hasMethodAnnotation(ResponseBody.class));
}
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
        ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
        throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
    mavContainer.setRequestHandled(true);
    ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
    ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
    writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
@ResponseBody

有了上面的知識(shí)儲(chǔ)備之后,接下來(lái)我們就可以自己實(shí)現(xiàn)了咆爽。

3.2 具體實(shí)現(xiàn)

首先自定義一個(gè) HandlerMethodReturnValueHandler:

public class MyHandlerMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
    private HandlerMethodReturnValueHandler handler;

    public MyHandlerMethodReturnValueHandler(HandlerMethodReturnValueHandler handler) {
        this.handler = handler;
    }

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return handler.supportsReturnType(returnType);
    }

    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        Map<String, Object> map = new HashMap<>();
        map.put("status", "ok");
        map.put("data", returnValue);
        handler.handleReturnValue(map, returnType, mavContainer, webRequest);
    }
}

由于我們要做的功能其實(shí)是在 RequestResponseBodyMethodProcessor 基礎(chǔ)之上實(shí)現(xiàn)的梁棠,因?yàn)橹С?@ResponseBody ,輸出 JSON 那些東西都不變斗埂,我們只是在輸出之前修改一下數(shù)據(jù)而已符糊。所以我這里直接定義了一個(gè)屬性 HandlerMethodReturnValueHandler,這個(gè)屬性的實(shí)例就是 RequestResponseBodyMethodProcessor呛凶,supportsReturnType 方法就按照 RequestResponseBodyMethodProcessor 的要求來(lái)濒蒋,在 handleReturnValue 方法中,我們先對(duì)返回值進(jìn)行一個(gè)預(yù)處理,然后調(diào)用 RequestResponseBodyMethodProcessor#handleReturnValue 方法繼續(xù)輸出 JSON 即可沪伙。

接下來(lái)就是配置 MyHandlerMethodReturnValueHandler 使之生效了。由于 SpringMVC 中 HandlerAdapter 在加載的時(shí)候已經(jīng)配置了 HandlerMethodReturnValueHandler(這塊松哥以后會(huì)和大家分析相關(guān)源碼)县好,所以我們可以通過(guò)如下方式對(duì)已經(jīng)配置好的 RequestMappingHandlerAdapter 進(jìn)行修改围橡,如下:

@Configuration
public class ReturnValueConfig implements InitializingBean {
    @Autowired
    RequestMappingHandlerAdapter requestMappingHandlerAdapter;
    @Override
    public void afterPropertiesSet() throws Exception {
        List<HandlerMethodReturnValueHandler> originHandlers = requestMappingHandlerAdapter.getReturnValueHandlers();
        List<HandlerMethodReturnValueHandler> newHandlers = new ArrayList<>(originHandlers.size());
        for (HandlerMethodReturnValueHandler originHandler : originHandlers) {
            if (originHandler instanceof RequestResponseBodyMethodProcessor) {
                newHandlers.add(new MyHandlerMethodReturnValueHandler(originHandler));
            }else{
                newHandlers.add(originHandler);
            }
        }
        requestMappingHandlerAdapter.setReturnValueHandlers(newHandlers);
    }
}

自定義 ReturnValueConfig 實(shí)現(xiàn) InitializingBean 接口,afterPropertiesSet 方法會(huì)被自動(dòng)調(diào)用缕贡,在該方法中翁授,我們將 RequestMappingHandlerAdapter 中已經(jīng)配置好的 HandlerMethodReturnValueHandler 拎出來(lái)挨個(gè)檢查,如果類型是 RequestResponseBodyMethodProcessor晾咪,則重新構(gòu)建收擦,用我們自定義的 MyHandlerMethodReturnValueHandler 代替它,最后給 requestMappingHandlerAdapter 重新設(shè)置 HandlerMethodReturnValueHandler 即可谍倦。

最后再提供一個(gè)測(cè)試接口:

@RestController
public class UserController {
    @GetMapping("/user")
    public User getUserByUsername(String username) {
        User user = new User();
        user.setUsername(username);
        user.setAddress("www.javaboy.org");
        return user;
    }
}
public class User {
    private String username;
    private String address;
    //省略其他
}

配置完成后塞赂,就可以啟動(dòng)項(xiàng)目啦。

項(xiàng)目啟動(dòng)成功后昼蛀,訪問(wèn) /user 接口宴猾,如下:

image

完美。

4.小結(jié)

其實(shí)統(tǒng)一 API 接口響應(yīng)格式辦法很多叼旋,

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末仇哆,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子夫植,更是在濱河造成了極大的恐慌讹剔,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,376評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件详民,死亡現(xiàn)場(chǎng)離奇詭異延欠,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)阐斜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)衫冻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人谒出,你說(shuō)我怎么就攤上這事隅俘。” “怎么了笤喳?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,966評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵为居,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我杀狡,道長(zhǎng)蒙畴,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,432評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮膳凝,結(jié)果婚禮上碑隆,老公的妹妹穿的比我還像新娘。我一直安慰自己蹬音,他們只是感情好上煤,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,519評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著著淆,像睡著了一般劫狠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上永部,一...
    開(kāi)封第一講書(shū)人閱讀 49,792評(píng)論 1 290
  • 那天独泞,我揣著相機(jī)與錄音,去河邊找鬼苔埋。 笑死懦砂,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的讲坎。 我是一名探鬼主播孕惜,決...
    沈念sama閱讀 38,933評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼晨炕!你這毒婦竟也來(lái)了衫画?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,701評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤瓮栗,失蹤者是張志新(化名)和其女友劉穎削罩,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體费奸,經(jīng)...
    沈念sama閱讀 44,143評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡弥激,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,488評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了愿阐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片微服。...
    茶點(diǎn)故事閱讀 38,626評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖缨历,靈堂內(nèi)的尸體忽然破棺而出以蕴,到底是詐尸還是另有隱情,我是刑警寧澤辛孵,帶...
    沈念sama閱讀 34,292評(píng)論 4 329
  • 正文 年R本政府宣布丛肮,位于F島的核電站,受9級(jí)特大地震影響魄缚,放射性物質(zhì)發(fā)生泄漏宝与。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,896評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望习劫。 院中可真熱鬧咆瘟,春花似錦、人聲如沸诽里。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,742評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)须肆。三九已至,卻和暖如春桩皿,著一層夾襖步出監(jiān)牢的瞬間豌汇,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工泄隔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拒贱,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓佛嬉,卻偏偏與公主長(zhǎng)得像逻澳,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子暖呕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,494評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容