SpringBoot系列教程之定義介面返回型別的幾種方式

語言: CN / TW / HK

SpringBoot 系列教程之定義介面返回型別的幾種方式

實現一個 web 介面返回 json 資料,基本上是每一個 javaer 非常熟悉的事情了;那麼問題來了,如果我有一個介面,除了希望返回 json 格式的資料之外,若也希望可以返回 xml 格式資料可行麼?

答案當然是可行的,接下來我們將介紹一下,一個介面的返回資料型別,可以怎麼處理

I. 專案搭建

本文建立的例項工程採用 ​ ​SpringBoot 2.2.1.RELEASE​ ​ + ​ ​maven 3.5.3​ ​ + ​ ​idea​ ​ 進行開發

1. pom 依賴

具體的 SpringBoot 專案工程建立就不贅述了,對於 pom 檔案中,需要重點關注下面兩個依賴類

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-xml</artifactId>
    </dependency>
</dependencies>

注意 ​ ​jackson-datafromat-xml​ ​ 這個依賴,加上這個主要時為了支援返回 xml 格式的資料

II. 返回型別設定的多種方式

正常來講,一個 RestController 的介面,預設返回的是 Json 格式資料,當我們引入了上面的 xml 包之後,會怎樣呢?返回的還是 json 麼?

1.通過 produce 設定返回型別

如果一個介面希望返回 json 或者 xml 格式的資料,最容易想到的方式就是直接設定 ​ ​RequestMapping​ ​ 註解中的 produce 屬性

這個值主要就是用來設定這個介面響應頭中的 ​ ​content-type​ ​ ; 如我們現在有兩個介面,一個指定返回 json 格式資料,一個指定返回 xml 格式資料,可以如下寫

@RestController
public class IndexRest{

    @Data
    public static class ResVo<T> {
        private int code;
        private String msg;
        private T data;

        public ResVo(int{
            this.code = code;
            this.msg = msg;
            this.data = data;
        }
    }
    @GetMapping(path = "/xml", produces = {MediaType.APPLICATION_XML_VALUE})
    public ResVo<String> xml(){
        return new ResVo<>(0, "ok", "返回xml");
    }

    @GetMapping(path = "/json", produces = {MediaType.APPLICATION_JSON_VALUE})
    public ResVo<String> json(){
        return new ResVo<>(0, "ok", "返回json");
    }
}

上面的實現中

produces = application/xml
produces = applicatin/json

接下來我們訪問一下看看返回的是否和預期一致

從上面截圖也可以看出,xml 介面返回的是 xml 格式資料;json 介面返回的是 json 格式資料

2. 通過請求頭 accept 設定返回型別

上面的方式,非常直觀,自然我們就會有一個疑問,當介面上不指定 produces 屬性時,直接訪問會怎麼表現呢?

@GetMapping(path = "/")
public ResVo<String> index(){
    return new ResVo<>(0, "ok", "簡單的測試");
}

請注意上面的截圖,兩種訪問方式返回的資料型別不一致

application/xhtml+xml

那麼問題來了,為什麼兩者的表現形式不一致呢?

對著上面的圖再看三秒,會發現主要的差別點就在於請求頭 ​ ​Accept​ ​ 不同;我們可以通過這個請求頭引數,來要求服務端返回我希望的資料型別

如指定返回 json 格式資料

curl 'http://127.0.0.1:8080' -H 'Accept:application/xml' -iv

curl 'http://127.0.0.1:8080' -H 'Accept:application/json'

從上面的執行結果也可以看出,返回的型別與預期的一致;

說明

請求頭可以設定多種 MediaType,用英文逗號分割,後端介面會根據自己定義的 produce 與請求頭希望的 mediaType 取交集,至於最終選擇的順序則以 accept 中出現的順序為準

看一下實際的表現來驗證下上面的說法

通過請求頭來控制返回資料型別的方式可以說是非常經典的策略了,(遵循 html 協議還有什麼好說的呢!)

3. 請求引數來控制返回型別

除了上面介紹的兩種方式之外,還可以考慮為所有的介面,增加一個根據特定的請求引數來控制返回的型別的方式

比如我們現在定義,所有的介面可以選傳一個引數 ​ ​mediaType​ ​ ,如果值為 xml,則返回 xml 格式資料;如果值為 json,則返回 json 格式資料

當不傳時,預設返回 json 格式資料

基於此,我們主要藉助 mvc 配置中的內容協商 ​ ​ContentNegotiationConfigurer​ ​ 來實現

@SpringBootApplication
public class Application implements WebMvcConfigurer{
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer){
        configurer.favorParameter(true)
                // 禁用accept協商方式,即不關心前端傳的accept值
//                .ignoreAcceptHeader(true)
                // 哪個放在前面,哪個的優先順序就高; 當上面這個accept未禁用時,若請求傳的accept不能覆蓋下面兩種,則會出現406錯誤
                .defaultContentType(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)
                // 根據傳參mediaType來決定返回樣式
                .parameterName("mediaType")
                // 當acceptHeader未禁用時,accept的值與mediaType傳參的值不一致時,以mediaType的傳值為準
                // mediaType值可以不傳,為空也行,但是不能是json/xml之外的其他值
                .mediaType("json", MediaType.APPLICATION_JSON)
                .mediaType("xml", MediaType.APPLICATION_XML);
    }

    public static void main(String[] args){
        SpringApplication.run(Application.class);
    }
}

上面的實現中,添加了很多註釋,先別急;我來逐一進行說明

.parameterName("mediaType")
// 當acceptHeader未禁用時,accept的值與mediaType傳參的值不一致時,以mediaType的傳值為準
// mediaType值可以不傳,為空也行,但是不能是json/xml之外的其他值
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML);
複製程式碼</pre>

上面這三行程式碼,主要就是說,現在可以根據傳參 mediaType 來控制返回的型別,我們新增一個介面來驗證一下

<pre class="prettyprint hljs dart">@GetMapping(path = "param")
public ResVo<String> params(@RequestParam(name = "mediaType", required = false) String mediaType) {
    return new ResVo<>(0, "ok", String.format("基於傳參來決定返回型別:%s", mediaType));
}

我們來看下幾個不同的傳參表現

# 返回json格式資料
curl 'http://127.0.0.1:8080/param?mediaType=json' -iv
# 返回xml格式資料
curl 'http://127.0.0.1:8080/param?mediaType=xml' -iv
# 406錯誤
curl 'http://127.0.0.1:8080/param?mediaType=text' -iv
# 走預設的返回型別,json在前,所以返回json格式資料(如果將xml調整到前面,則返回xml格式資料,主要取決於 `.defaultContentType(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)`)
curl 'http://127.0.0.1:8080/param'

疑問:若請求頭中傳遞了 Accept 或者介面上定義了 produce,會怎樣?

當指定了 accept 時,並且傳參中指定了 mediaType,則以傳參為準

  • 如​ ​accept: application/json,application.xml​ ​ , 此時​ ​mediaType=json​ ​ , 返回 json 格式
  • 如​ ​accept: application/json​ ​ , 此時​ ​mediaTyep=xml​ ​ , 返回 xml 格式
  • 如​ ​accept: text/html​ ​ ,此時​ ​mediaType=xml​ ​ ,此時返回的也是 xml 格式
  • 如​ ​accept: text/html​ ​ ,此時​ ​mediaType​ ​ 不傳時 ,因為無法處理​ ​text/html​ ​ 型別,所以會出現 406
  • 如​ ​accept: application/xml​ ​ , 但是​ ​mediaType​ ​ 不傳,雖然預設優先是 json,此時返回的也是 xml 格式,與請求頭希望的保持一致

但是若傳參與 produce 衝突了,那麼就直接 406 異常,不會選擇 mediaType 設定的型別

  • 如​ ​produce = applicatin/json​ ​ , 但是​ ​mediaType=xml​ ​ ,此時就會喜提 406

細心的小夥伴可能發現了上面的配置中,註釋了一行 ​ ​.ignoreAcceptHeader(true)​ ​ ,當我們把它開啟之後,前面說的 Accept 請求頭可以隨意傳,我們完全不 care,當做沒有傳這個引數進行處理即可開

4.小結

本文介紹了三種方式,控制介面返回資料型別

方式一

介面上定義 produce, 如 ​ ​@GetMapping(path = "p2", produces = {"application/xml", "application/json"})​

注意 produces 屬性值是有序的,即先定義的優先順序更高;當一個請求可以同時接受 xml/json 格式資料時,上面這個定義會確保這個介面現有返回 xml 格式資料

方式二

藉助標準的請求頭 accept,控制希望返回的資料型別;但是需要注意的時,使用這種方式時,要求後端不能設定 ​ ​ContentNegotiationConfigurer.ignoreAcceptHeader(true)​

在實際使用這種方式的時候,客戶端需要額外注意,Accept 請求頭中定義的 MediaType 的順序,是優於後端定義的 produces 順序的,因此使用者需要將自己實際希望接受的資料型別放在前面,或者乾脆就只設置一個

方式三

藉助 ​ ​ContentNegotiationConfigurer​ ​ 實現通過請求引數來決定返回型別,常見的配置方式形如

configurer.favorParameter(true)
        // 禁用accept協商方式,即不關心前端傳的accept值
      //                .ignoreAcceptHeader(true)
        // 哪個放在前面,哪個的優先順序就高; 當上面這個accept未禁用時,若請求傳的accept不能覆蓋下面兩種,則會出現406錯誤
        .defaultContentType(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)
        // 根據傳參mediaType來決定返回樣式
        .parameterName("mediaType")
        // 當acceptHeader未禁用時,accept的值與mediaType傳參的值不一致時,以mediaType的傳值為準
        // mediaType值可以不傳,為空也行,但是不能是json/xml之外的其他值
        .mediaType("json", MediaType.APPLICATION_JSON)
        .mediaType("xml", MediaType.APPLICATION_XML);

即新增這個設定之後,最終的表現為:

produce

注意注意:當配置中忽略了 AcceptHeader 時, ​ ​.ignoreAcceptHeader(true)​ ​ ,上面第三條作廢