Spring Boot - 統(tǒng)一數(shù)據(jù)下發(fā)接口格式

[TOC]

前言

當(dāng)前主流的 Web 應(yīng)用開發(fā)通常采用前后端分離模式丰涉,前端和后端各自獨立開發(fā)拓巧,然后通過數(shù)據(jù)接口溝通前后端,完成項目一死。

因此肛度,定義一個統(tǒng)一的數(shù)據(jù)下發(fā)格式,有利于提高項目開發(fā)效率投慈,減少各端開發(fā)溝通成本承耿。

本篇博文主要介紹下在 Spring Boot 中配置統(tǒng)一數(shù)據(jù)下發(fā)格式的搭建步驟。

統(tǒng)一數(shù)據(jù)格式

數(shù)據(jù)的類型多種多樣伪煤,但是可以簡單劃分為以下三種類型:

  • 簡單數(shù)據(jù)類型:比如byte加袋、intdouble等基本數(shù)據(jù)類型抱既。
    :在 Java 中职烧,String屬于Object類型,但是在數(shù)據(jù)層面上防泵,我們通常將其看作是簡單數(shù)據(jù)類型蚀之。

  • 對象數(shù)據(jù)類型:常見的比如說自定義 Java Bean,POJO 等數(shù)據(jù)捷泞。

  • 復(fù)雜/集合數(shù)據(jù)類型:比如List足删、Map等集合類型。

后端下發(fā)的數(shù)據(jù)肯定會包含上述列舉的三種類型數(shù)據(jù)肚邢,通常這些數(shù)據(jù)都作為響應(yīng)體主要內(nèi)容壹堰,用字段data進行表示拭卿,同時我們會附加codemsg字段來描述請求結(jié)果信息,如下表所示:

字段 描述
code 狀態(tài)碼贱纠,標(biāo)志請求是否成功
msg 描述請求狀態(tài)
data 返回結(jié)果

到此峻厚,統(tǒng)一數(shù)據(jù)下發(fā)的格式就確定了,如下代碼所示:

@Getter
@AllArgsConstructor
@ToString
public class ResponseBean<T> {
    private int code;
    private String msg;
    private T data;
}

此時谆焊,數(shù)據(jù)下發(fā)操作如下所示:

@RestController
@RequestMapping("/common")
public class CommonController {

    @GetMapping("/")
    public ResponseBean<String> index() {
        return new ResponseBean<>(200, "操作成功", "Hello World");
    }
}

進階配置

在上文的統(tǒng)一數(shù)據(jù)ResponseBean中惠桃,還可以對其再進行封裝,使代碼更健壯:

  • 抽象codemsgcodemsg用于描述請求結(jié)果信息辖试,直接放置再ResponseBean中辜王,程序員可以隨便設(shè)置這兩個字段,請求結(jié)果一般就是成功罐孝、失敗等常見的幾種結(jié)果呐馆,可以將其再進行封裝,提供常見的請求結(jié)果信息莲兢,縮小權(quán)限:

    @Getter
    @ToString
    public class ResponseBean<T> {
        private int code;
        private String msg;
        private T data;
    
        public ResponseBean(ResultCode result, T data) {
            this.code = result.code;
            this.msg = result.msg;
            this.data = data;
        }
    
        public static enum ResultCode {
            SUCCESS(200, "操作成功"),
            FAILURE(400, "操作失敗");
    
            ResultCode(int code, String msg) {
                this.code = code;
                this.msg = msg;
            }
    
            private final int code;
            private final String msg;
        }
    }
    

    這里使用enum來封裝codemsg汹来,并提供兩個默認(rèn)操作SUCCESSFAILURE。此時調(diào)用方法如下:

    @GetMapping("/")
    public ResponseBean<String> index() {
        return new ResponseBean<>(ResponseBean.ResultCode.SUCCESS, "Hello World");
    }
    
  • 提供默認(rèn)操作:前面的調(diào)用方法還是不太簡潔改艇,這里我們讓ResponseBean直接提供相應(yīng)的默認(rèn)操作收班,方便外部調(diào)用:

    @Getter
    @ToString
    public class ResponseBean<T> {
        private int code;
        private String msg;
        private T data;
    
        // 成功操作
        public static <E> ResponseBean<E> success(E data) {
            return new ResponseBean<E>(ResultCode.SUCCESS, data);
        }
    
        // 失敗操作
        public static <E> ResponseBean<E> failure(E data) {
            return new ResponseBean<E>(ResultCode.FAILURE, data);
        }
    
        // 設(shè)置為 private
        private ResponseBean(ResultCode result, T data) {
            this.code = result.code;
            this.msg = result.msg;
            this.data = data;
        }
    
        // 設(shè)置 private
        private static enum ResultCode {
            SUCCESS(200, "操作成功"),
            FAILURE(400, "操作失敗");
    
            ResultCode(int code, String msg) {
                this.code = code;
                this.msg = msg;
            }
    
            private final int code;
            private final String msg;
        }
    }
    

    我們提供了兩個默認(rèn)操作successfailure,此時調(diào)用方式如下:

    @GetMapping("/")
    public ResponseBean<String> index() {
        return ResponseBean.<String>success("Hello World");
    }
    

    到這里谒兄,數(shù)據(jù)下發(fā)調(diào)用方式就相對較簡潔了摔桦,但是結(jié)合 Spring Boot 還能繼續(xù)進行優(yōu)化,參考下文承疲。

數(shù)據(jù)下發(fā)攔截修改

Spring 框架提供了一個接口:ResponseBodyAdvice<T>邻耕,當(dāng)控制器方法被@ResponseBody注解或返回一個ResponseEntity時,該接口允許我們在HttpMessageConverter寫入響應(yīng)體前纪隙,攔截響應(yīng)體并進行自定義修改赊豌。

因此,要攔截Controller響應(yīng)數(shù)據(jù)绵咱,只需實現(xiàn)一個自定義ResponseBodyAdvice,并將其注冊到RequestMappingHandlerAdapterExceptionHandlerExceptionResolver熙兔,或者直接使用@ControllerAdvice注解進行激活悲伶。如下所示:

@RestControllerAdvice
public class FormatResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    /**
     * @param returnType 響應(yīng)的數(shù)據(jù)類型
     * @param converterType 最終將會使用的消息轉(zhuǎn)換器
     * @return true: 執(zhí)行 beforeBodyWrite 方法,修改響應(yīng)體
               false: 不執(zhí)行 beforeBodyWrite 方法
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        boolean isResponseBeanType = ResponseBean.class.equals(returnType.getParameterType());
        // 如果返回的是 ResponseBean 類型住涉,則無需進行攔截修改麸锉,直接返回即可
        // 其他類型則攔截,并進行 beforeBodyWrite 方法進行修改
        return !isResponseBeanType;
    }

    /**
     * @param body 響應(yīng)的數(shù)據(jù)舆声,也就是響應(yīng)體
     * @param returnType 響應(yīng)的數(shù)據(jù)類型
     * @param selectedContentType 響應(yīng)的ContentType
     * @param selectedConverterType 最終將會使用的消息轉(zhuǎn)換器
     * @param request
     * @param response
     * @return 被修改后的響應(yīng)體花沉,可以為null柳爽,表示沒有任何響應(yīng)
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        return ResponseBean.success(body);
    }
}

這里需要注意的一個點是,僅僅實現(xiàn)一個自定義ResponseBodyAdvice碱屁,對其他類型的數(shù)據(jù)是可以成功進行攔截并轉(zhuǎn)換磷脯,但是對于直接返回String類型的方法,這里會拋出一個異常:

java.lang.ClassCastException: class com.yn.common.entity.ResponseBean cannot be cast to class java.lang.String

這是因為請求體在返回給客戶端前娩脾,會被一系列HttpMessageConverter進行轉(zhuǎn)換赵誓,當(dāng)Controller返回一個String時,beforeBodyWrite方法中的第四個參數(shù)selectedConverterType就是一個StringHttpMessageConverter柿赊,因此俩功,我們在beforeBodyWrite中將String響應(yīng)攔截并轉(zhuǎn)換為ResponseBean類型,然后StringHttpMessageConverter就會轉(zhuǎn)換我們的ResponseBean類型碰声,這樣轉(zhuǎn)換就會失敗诡蜓,因為類型不匹配。解決這個問題的方法大致有如下三種胰挑,任選其一即可:

  1. 轉(zhuǎn)換為String類型:由于采用的是StringHttpMessageConverter万牺,因此,我們需要將ResponseBean轉(zhuǎn)換為String洽腺,這樣StringHttpMessageConverter就可以處理了:

    @RestControllerAdvice
    public class GlobalExceptionHandler implements ResponseBodyAdvice<Object> {
    
        @Override
        public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
            ResponseBean bean = ResponseBean.success(body);
            try {
                if (body instanceof String) {
                    response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
                    // String 類型則將 bean 轉(zhuǎn)化為 JSON 字符串
                    return new ObjectMapper().writeValueAsString(bean);
                }
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
            return bean;
        }
    }
    
  2. 前置 JSON 轉(zhuǎn)換器:能轉(zhuǎn)換我們自定義的ResponseBean應(yīng)當(dāng)是一個 JSON 轉(zhuǎn)換器脚粟,比如MappingJackson2HttpMessageConverter,因此蘸朋,這里我們可以配置一下核无,讓MappingJackson2HttpMessageConverter轉(zhuǎn)換器優(yōu)先級比StringHttpMessageConverter高,這樣轉(zhuǎn)換就能成功藕坯,如下所示:

    @Configuration
    @EnableWebMvc
    public class WebConfiguration implements WebMvcConfigurer {
    
        @Override
        public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            converters.add(0, new MappingJackson2HttpMessageConverter());
        }
    }
    

    其實就是在轉(zhuǎn)換器集合中將MappingJackson2HttpMessageConverter排列到StringHttpMessageConverter前面团南。

  3. 配置 JSON 轉(zhuǎn)換器:如果是 Spring Boot 項目時,通常不建議在配置類上使用@EnableWebMvc注解炼彪,因為該注解會失效 Spring Boot 自動加載 SpringMVC 默認(rèn)配置吐根,這樣所有的配置都需要程序員手動進行控制,會很麻煩辐马。大多數(shù)配置 Spring Boot 都提供了對應(yīng)的配置方法拷橘,比如,我們可以配置HttpMessageConverter喜爷,去除StringHttpMessageConverter等默認(rèn)填充的轉(zhuǎn)換器冗疮,只注入 JSON 轉(zhuǎn)換器即可(因為前后端分離項目,只需 JSON 轉(zhuǎn)換即可):

    @SpringBootApplication
    public class Application {
    
        @Bean
        public HttpMessageConverters converters() {
            return new HttpMessageConverters(
                    false, Arrays.asList(new MappingJackson2HttpMessageConverter()));
        }
    }
    

現(xiàn)在檩帐,Controller可以直接返回任意類型數(shù)據(jù)术幔,最終都會被ResponseBodyAdvice攔截并更改為ResponseBean類型,如下所示:

@RestController
@RequestMapping("/common")
public class CommonController {

    // 簡單類型
    @GetMapping("/basic")
    public int basic() {
        return 3;
    }

    // 字符串
    @GetMapping("/string")
    public String basicType() {
        return "Hello World";
    }

    // 對象類型
    @GetMapping("/obj")
    public User user() {
        return new User("Whyn", "whyncai@gmail.com");
    }

    // 復(fù)雜/集合類型
    @GetMapping("/complex")
    public List<User> users() {
        return Arrays.asList(
                new User("Why1n", "Why1n@qq.com"),
                new User("Why1n", "Why1n@qq.com")
        );
    }

    @Data
    @AllArgsConstructor
    private static class User {
        private String name;
        private String email;
    }

}

請求上述接口湃密,結(jié)果如下:

$ curl -X GET localhost:8080/common/basic
{"code":200,"msg":"操作成功","data":3}

$ curl -X GET localhost:8080/common/string
{"code":200,"msg":"操作成功","data":"Hello World"}

$ curl -X GET localhost:8080/common/obj
{"code":200,"msg":"操作成功","data":{"name":"Whyn","email":"whyncai@gmail.com"}}

$ curl -X GET localhost:8080/common/complex
{"code":200,"msg":"操作成功","data":[{"name":"Why1n","email":"Why1n@qq.com"},{"name":"Why1n","email":"Why1n@qq.com"}]}

最后诅挑,當(dāng)Controller拋出異常時四敞,異常信息也會被我們自定義的RestControllerAdvice攔截到,但是data字段是系統(tǒng)的異常信息拔妥,因此最好還是手動對全局異常進行捕獲忿危,比如:

@RestControllerAdvice
public class FormatResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        boolean isResponseBeanType = ResponseBean.class.equals(returnType.getParameterType());
        // 如果返回的是 ResponseBean 類型,則無需進行攔截修改毒嫡,直接返回即可
        // 其他類型則攔截癌蚁,并進行 beforeBodyWrite 方法進行修改
        return !isResponseBeanType;
    }
    //...
    @ExceptionHandler(Throwable.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseBean<String> handleException() {
        return ResponseBean.failure("Error occured");
    }
}

剛好ResponseBodyAdvice需要@RestControllerAdvice進行驅(qū)動,而@RestControllerAdvice又能全局捕獲Controller異常兜畸,所以這里簡單地將異常捕獲放置到自定義ResponseBodyAdvice中努释,一個需要注意的點就是:這里我們對異常手動返回ResponseBean對象,因為在自定義ResponseBodyAdvice中咬摇,supports方法內(nèi)我們設(shè)置了對ResponseBean數(shù)據(jù)類型不進行攔截伐蒂,而如果這里異常處理返回其他類型,最終都都會被自定義ResponseBodyAdvice攔截到肛鹏,這里需要注意一下逸邦。

更多異常處理詳情,可查看本人的另一篇博客:Spring Boot - 全局異常捕獲

附錄

上述內(nèi)容的完整配置代碼如下所示:

  • 數(shù)據(jù)統(tǒng)一下發(fā)實體
    @Getter
    @ToString
    public class ResponseBean<T> {
        private int code;
        private String msg;
        private T data;
    
        // 成功操作
        public static <E> ResponseBean<E> success(E data) {
            return new ResponseBean<E>(ResultCode.SUCCESS, data);
        }
    
        // 失敗操作
        public static <E> ResponseBean<E> failure(E data) {
            return new ResponseBean<E>(ResultCode.FAILURE, data);
        }
    
        // 設(shè)置為 private
        private ResponseBean(ResultCode result, T data) {
            this.code = result.code;
            this.msg = result.msg;
            this.data = data;
        }
    
        // 設(shè)置 private
        private static enum ResultCode {
            SUCCESS(200, "操作成功"),
            FAILURE(400, "操作失敗");
    
            ResultCode(int code, String msg) {
                this.code = code;
                this.msg = msg;
            }
    
            private final int code;
            private final String msg;
        }
    }
    
  • 轉(zhuǎn)換器配置類
    @Configuration
    @EnableWebMvc
    public class WebConfiguration implements WebMvcConfigurer {
    
        @Override
        public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            converters.add(0, new MappingJackson2HttpMessageConverter());
        }
    }
    
  • 數(shù)據(jù)下發(fā)攔截器
    @RestControllerAdvice
    public class FormatResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    
        @Override
        public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
            boolean isResponseBeanType = ResponseBean.class.equals(returnType.getParameterType());
            // 如果返回的是 ResponseBean 類型在扰,則無需進行攔截修改缕减,直接返回即可
            // 其他類型則攔截,并進行 beforeBodyWrite 方法進行修改
            return !isResponseBeanType;
        }
    
        @Override
        public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
            return ResponseBean.success(body);
        }
    
        @ExceptionHandler(Throwable.class)
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
        public ResponseBean<String> handleException() {
            return ResponseBean.failure("Error occured");
        }
    }
    

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末桥狡,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子皱卓,更是在濱河造成了極大的恐慌裹芝,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件娜汁,死亡現(xiàn)場離奇詭異嫂易,居然都是意外死亡,警方通過查閱死者的電腦和手機掐禁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門怜械,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人穆桂,你說我怎么就攤上這事宫盔。” “怎么了享完?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長有额。 經(jīng)常有香客問我般又,道長彼绷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任茴迁,我火速辦了婚禮寄悯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘堕义。我一直安慰自己猜旬,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布倦卖。 她就那樣靜靜地躺著洒擦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪怕膛。 梳的紋絲不亂的頭發(fā)上熟嫩,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機與錄音褐捻,去河邊找鬼掸茅。 笑死,一個胖子當(dāng)著我的面吹牛柠逞,可吹牛的內(nèi)容都是我干的昧狮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼板壮,長吁一口氣:“原來是場噩夢啊……” “哼逗鸣!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起个束,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤慕购,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后茬底,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體沪悲,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年阱表,在試婚紗的時候發(fā)現(xiàn)自己被綠了殿如。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡最爬,死狀恐怖涉馁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情爱致,我是刑警寧澤烤送,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站糠悯,受9級特大地震影響帮坚,放射性物質(zhì)發(fā)生泄漏妻往。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一试和、第九天 我趴在偏房一處隱蔽的房頂上張望讯泣。 院中可真熱鬧,春花似錦阅悍、人聲如沸好渠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拳锚。三九已至,卻和暖如春肴茄,著一層夾襖步出監(jiān)牢的瞬間晌畅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工寡痰, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留抗楔,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓拦坠,卻偏偏與公主長得像连躏,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子贞滨,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354