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

[TOC]

前言

當前主流的 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ù)類型:比如ListMap等集合類型。

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

字段 描述
code 狀態(tài)碼,標志請求是否成功
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,并提供兩個默認操作SUCCESSFAILURE。此時調(diào)用方法如下:

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

    我們提供了兩個默認操作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>,當控制器方法被@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)換,當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)當是一個 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 默認配置,這樣所有的配置都需要程序員手動進行控制,會很麻煩。大多數(shù)配置 Spring Boot 都提供了對應(yīng)的配置方法,比如,我們可以配置HttpMessageConverter,去除StringHttpMessageConverter等默認填充的轉(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"}]}

最后,當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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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