Spring Boot統一返回體的踩坑記錄

前言

在Spring Boot項目中我們可以通過RestControllerAdvice配合實現ResponseBodyAdvice<T>接口來保證Spring MVC接口具有統一的返回格式,以保證前端同學能夠封裝統一的數據接收工具。但是很多網上的文章並沒有對實際開發中的細節作出更多的講解。今天胖哥就來分享一下我的采坑經歷,也算作一個總結。

控制作用范圍

我記得在前面關於Swagger3的文章中提過,如果我們不指定范圍將導致Swagger無法識別接口的元信息。因此如果你使用瞭Swagger必須指定其范圍,這裡你可以通過指定掃描包來指定其作用域:

@RestControllerAdvice("cn.felord.controller")

如果你的Spring MVC控制器有統一的父類控制器的話,

@RestController
@RequestMapping("/foo")
public class FooController extends BaseController {
 //todo 省略
}    

也可以這樣:

@RestControllerAdvice(assignableTypes = BaseController.class)

白名單

有些接口可能根據業務需要或者協議需要不能使用統一返回體,例如支付的通知應答。這就需要一個類似白名單的機制來繞過統一返回體控制器通知類。我們可以借助於ResponseBodyAdvice<T>的下列方法實現:

boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

這個方法如果返回false就表示不執行統一返回體的封裝邏輯。這裡我推薦註解實現。定義一個標記註解,可以定義在類上或者方法上:

@Documented
@Inherited
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreRestBody {
}

然後上面的supports方法這樣實現:

@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
    return !returnType.hasMethodAnnotation(IgnoreRestBody.class);
}

如果某個Controller下所有的方法都繞過,就把這個註解標記在控制器類上;如果隻想忽略某個方法上就把它標記在該方法上即可。

返回獨立字符串的問題

有些接口我們會返回一個字符串:

@GetMapping("/get")
public String getStr(){
    //返回瞭一個字符串
    return "felord.cn";
}

我們希望這個字符串被統一返回體處理,類似這樣:

{
    code: 200,
    data: "felord.cn",
    msg: "返回成字符串",
}

但是你會發現並沒有達到期望的效果,會拋出類型轉換異常。這是因為當我們的Spring MVC接口返回數據時,會根據Content-Type來選擇一個HttpMessageConverter來處理,而字符串在不聲明Content-Type的情況下優先使用StringHttpMessageConverter ,就導致瞭轉換異常,需要設定成MappingJackson2HttpMessageConverter用Jackson來處理,Spring MVC的對應配置如下:

@Configuration(proxyBeanMethods = false)
public class SpringMvcConfiguration implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 解決 String 統一封裝RestBody的問題
        converters.add(0, new MappingJackson2HttpMessageConverter());
    }
}

嗯,這樣就起效瞭!你以為這樣就完瞭?你會發現你的JSON序列化不按照你設置的策略執行瞭。因為你new瞭一個而不是采用系統初始化的那個。解決方法為,將Spring IoC中的ObjectMapper註入到MappingJackson2HttpMessageConverter中去。或者你使用Debug調試出系統默認的MappingJackson2HttpMessageConverter的位置,比如我的索引為7,就可以這樣配置:

@Configuration(proxyBeanMethods = false)
public class SpringMvcConfiguration implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 解決 String 統一封裝RestBody的問題
        HttpMessageConverter<?> httpMessageConverter = converters.get(7);
        if (!(httpMessageConverter instanceof MappingJackson2HttpMessageConverter)) {
            // 確保正確,如果有改動就重新debug
            throw new RuntimeException("MappingJackson2HttpMessageConverter is not here");
        }
        converters.add(0, httpMessageConverter);
    }
}

Data的類型問題

曾經一個安卓開發同學說,你這統一結構中的data如果是數組:

{
    code: 200,
    data: ['a','b'],
    msg: "返回成字符串",
}

後續如果data添加其它與數組沒有關系的屬性就不兼容瞭,你應該保證這個data是個Map。是的,這也是問題,實際中發現不僅僅是數組,如果是int、long等原始類型或者String類型都面臨這種情況,需要加一個額外的判斷body是不是可能改變data類型的類型:

private boolean checkPrimitive(Object body) {
    Class<?> clazz = body.getClass();
    return clazz.isPrimitive()
            || clazz.isArray()
            || Collection.class.isAssignableFrom(clazz)
            || body instanceof Number
            || body instanceof Boolean
            || body instanceof Character
            || body instanceof String;
}

然後我們在ResponseBodyAdvice<T>實現中增加一個判斷:

      // 增強擴展性
        if (checkPrimitive(body)) {
            return RestBody.okData(Collections.singletonMap("result", body));
        }

就解決問題瞭。

總結

今天對Spring Boot中統一返回體的一些細節問題進行瞭分享,希望能夠幫助你解決一些實際開發中遇到的同樣問題。

到此這篇關於Spring Boot統一返回體踩坑記錄的文章就介紹到這瞭,更多相關Spring Boot統一返回體內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: