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)​ ​ ,上面第三條作廢