Spring Boot Mvc 統(tǒng)一返回結(jié)果

背景

在 spring boot 項(xiàng)目中欺嗤,使用@RestController / @RequestMapping / @GetMapping / @PostMapping 等注解提供api的功能,但是每個(gè)Mapping返回的類型各不相同,有的是void艇纺,有的是基礎(chǔ)類型如strping /integer,有的是dto。
在前后端分離的項(xiàng)目中毁欣,返回格式不統(tǒng)一躲叼,使得前端處理返回結(jié)果也不能統(tǒng)一,會(huì)導(dǎo)致寫很多代碼垄提。

原始controller

例子的代碼如下

t org.springframework.web.bind.annotation.RestController;

@RestController()
@RequestMapping
public class NoResultWarpperController {

    @PostMapping("hello")
    public HelloDto hello(@RequestBody HelloCmd name){
        HelloDto result = new HelloDto();
        result.setResult("hello,"+name);
        return result;
    }


    @Data
    public class HelloCmd{
        private String name;
    }

    @Data
    public class HelloDto{
        private String result;
    }
}

測試代碼如下


@SpringBootTest
@AutoConfigureMockMvc
public class NoResultWarpperControllerTest {


    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testHello() throws Exception{

        ObjectMapper map = new ObjectMapper();

        NoResultWarpperController.HelloCmd cmd = new NoResultWarpperController.HelloCmd();
        cmd.setName("zhangsan");

        String body = map.writeValueAsString(cmd);
        MvcResult mvcResult = mockMvc.perform(
                MockMvcRequestBuilders.post("/hello")
                    .contentType(MediaType.APPLICATION_JSON)
                .content(body)
        ).andReturn();

        assertThat(mvcResult.getResponse().getStatus()).isEqualTo(200);
        NoResultWarpperController.HelloDto dto = map.readValue(mvcResult.getResponse().getContentAsString(), NoResultWarpperController.HelloDto.class);
        assertThat(dto).isNotNull();
        assertThat(dto.getResult()).isEqualTo("hello,zhangsan");


    }

}

方式一,Controller方法統(tǒng)一返回類型ApiResult

新建統(tǒng)一返回類

@Data
public class ApiResult<T> {
    private T result;
    private boolean success;
    private String errorCode;
    private String errorMessage;
    private String errorDetail;
}

修改上面例子的Controller周拐, 方法返回ApiResult


@RestController()
@RequestMapping
public class NoResultWarpperController {

    @PostMapping("hello")
    public ApiResult<HelloDto> hello(@RequestBody HelloCmd cmd){
        HelloDto result = new HelloDto();
        result.setResult("hello,"+ cmd.getName());

        return  new ApiResult<>(result);
    }
}

測試代碼


    @Test
    public void testHello() throws Exception{

        ObjectMapper map = new ObjectMapper();

        NoResultWarpperController.HelloCmd cmd = new NoResultWarpperController.HelloCmd();
        cmd.setName("zhangsan");

        String body = map.writeValueAsString(cmd);
        MvcResult mvcResult = mockMvc.perform(
                MockMvcRequestBuilders.post("/hello")
                    .contentType(MediaType.APPLICATION_JSON)
                .content(body)
        ).andReturn();

        assertThat(mvcResult.getResponse().getStatus()).isEqualTo(200);
        ApiResult<NoResultWarpperController.HelloDto> dto = map.readValue(mvcResult.getResponse().getContentAsString(),
                new TypeReference<ApiResult<NoResultWarpperController.HelloDto>>(){});
        assertThat(dto).isNotNull();
        assertThat(dto.isSuccess()).isTrue();
        assertThat(dto.getResult().getResult()).isEqualTo("hello,zhangsan");


    }

缺點(diǎn)

每個(gè)方法統(tǒng)一返回ApiResult類型铡俐,但是有一個(gè)缺點(diǎn),就是需要程序員自身關(guān)注這件事情妥粟,如果忘記返回了审丘,會(huì)影響使用。

方式二勾给,使用攔截器

spring mvc 提供了一個(gè)接口ResponseBodyAdvice, 用來攔截響請求響應(yīng)滩报,可以通過自定義攔截器完成統(tǒng)一結(jié)果返回

定義攔截器


/**
 * 通過結(jié)果返回?cái)r截器,只攔截 @RestController 標(biāo)識的類
 */
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@RestControllerAdvice(annotations = RestController.class)
public class RequestResponseAdvice  implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {

        ObjectMapper mapper = new ObjectMapper();
        if (body instanceof ApiResult){
            return body;
        }

        // 包裝 string 類型
        if(body instanceof String){

            return mapper.writeValueAsString(new ApiResult<>(body));
        }

        return new ApiResult<>(body);
    }
}

修改方法一的方法播急,去掉返回類型ApiResult


@RestController()
@RequestMapping
public class NoResultWarpperController {

    @PostMapping("hello")
    public HelloDto hello(@RequestBody HelloCmd cmd){
        HelloDto result = new HelloDto();
        result.setResult("hello,"+ cmd.getName());

        return  result;
    }

}

測試代碼不用修改脓钾,運(yùn)行測試,發(fā)現(xiàn)測試是通過桩警,說明通過攔截器可训,可以統(tǒng)一返回類型,并且不需要強(qiáng)制Controller方法返回ApiResult類型

過濾器中指定方法不使用ApiResult

定義注解

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DontWrapResult {
}

在Controller方法或類上捶枢,添加注解@DontWrapResult, 擴(kuò)展 controller 方法

@PostMapping("helloNoWrap")
    @DontWrapResult
    public HelloDto helloNoWrap(@RequestBody HelloCmd cmd){
        HelloDto result = new HelloDto();
        result.setResult("hello,"+ cmd.getName());

        return  result;
    }

修改攔截器沉噩,是的@DontWrapResult 注解的方法或類直接返回結(jié)果


    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {


        if (methodParameter.hasMethodAnnotation(DontWrapResult.class)){
            return body;
        }

        if (AnnotationUtils.findAnnotation(methodParameter.getDeclaringClass(),DontWrapResult.class)!=null){
            return body;
        }

        ObjectMapper mapper = new ObjectMapper();
        if (body instanceof ApiResult){
            return body;
        }

        // 包裝 string 類型
        if(body instanceof String){

            return mapper.writeValueAsString(new ApiResult<>(body));
        }

        return new ApiResult<>(body);
    }

添加測試代碼


    @Test
    public void testHelloNoWrap() throws Exception{

        ObjectMapper map = new ObjectMapper();

        NoResultWarpperController.HelloCmd cmd = new NoResultWarpperController.HelloCmd();
        cmd.setName("liubei");

        String body = map.writeValueAsString(cmd);
        MvcResult mvcResult = mockMvc.perform(
                MockMvcRequestBuilders.post("/helloNoWrap")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(body)
        ).andReturn();

        assertThat(mvcResult.getResponse().getStatus()).isEqualTo(200);
        NoResultWarpperController.HelloDto dto = map.readValue(mvcResult.getResponse().getContentAsString(),
                NoResultWarpperController.HelloDto.class);
        assertThat(dto).isNotNull();
        assertThat(dto.getResult()).isEqualTo("hello,liubei");


    }

異常

統(tǒng)一返回類型后,全局異常也要包裝到類型ApiResult

定義友好的業(yè)務(wù)異常類UserFriendlyException

public class UserFriendlyException  extends Exception{

    private int code;

    public int errorCode(){
        return code;
    }

    public UserFriendlyException(){}

    public UserFriendlyException(String msg){
        super(msg);
    }

    public UserFriendlyException(int code, String msg){
        this(msg);
        this.code= code;
    }


}

修改攔截器柱蟀,進(jìn)行異常攔截


    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ApiResult<Object> exceptionHandler(
            HttpServletRequest request,
            HttpServletResponse serverHttpResponse, Exception e) {
        serverHttpResponse.setStatus(500);
        return error(500, e);
    }

    private ApiResult<Object> error(int code,Exception ex){

        ApiResult<Object> result = new ApiResult<>();
        if (ex instanceof UserFriendlyException){
            result.setErrorCode(((UserFriendlyException) ex).errorCode());
        }
        else{
            result.setErrorCode(code);
        }
        result.setSuccess(false);
        result.setErrorMessage(ex.getMessage());
        result.setResult(null);
        return result;
    }

普通異常測試

Controller 添加 除法運(yùn)算


   @GetMapping("div")
    public Double div(){
        throw new RuntimeException("b is zero");
    }

測試


    @Test
    public void testDiv() throws Exception {
        ObjectMapper map = new ObjectMapper();
        MvcResult mvcResult = mockMvc.perform(
                MockMvcRequestBuilders.get("/div")
        ).andReturn();

        assertThat(mvcResult.getResponse().getStatus()).isEqualTo(500);
        assertThat(mvcResult.getResponse().getContentAsString()).isNotNull();
        ApiResult<Object> errorInfo = map.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<ApiResult<Object>>(){});
        assertThat(errorInfo).isNotNull();
        assertThat(errorInfo.getErrorCode()).isEqualTo(500);
        assertThat(errorInfo.getErrorMessage()).isEqualTo("b is zero");
    }

友好異常

Controller 添加 加法運(yùn)算

@GetMapping("add")
    public void add() throws UserFriendlyException {
        throw new UserFriendlyException(10000, "no method");
    }

測試代碼


    @Test
    public void testAdd() throws Exception {
        ObjectMapper map = new ObjectMapper();
        MvcResult mvcResult = mockMvc.perform(
                MockMvcRequestBuilders.get("/add")
        ).andReturn();

        assertThat(mvcResult.getResponse().getStatus()).isEqualTo(500);
        assertThat(mvcResult.getResponse().getContentAsString()).isNotNull();
        ApiResult<Object> errorInfo = map.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<ApiResult<Object>>(){});
        assertThat(errorInfo).isNotNull();
        assertThat(errorInfo.getErrorCode()).isEqualTo(10000);
        assertThat(errorInfo.getErrorMessage()).isEqualTo("no method");
    }

總結(jié)

在spring boot項(xiàng)目中川蒙,讓controller返回統(tǒng)一結(jié)果有兩種實(shí)現(xiàn)方式:

  1. 方法代碼寫死返回類型,弊端是沒有有效的檢測機(jī)制长已,如果方法沒有返回畜眨,會(huì)影響使用一致性
  2. 繼承ResponseBodyAdvice<Object> 接口自定義攔截器,不強(qiáng)制要求方法返回統(tǒng)一類型术瓮,并且針對個(gè)性化要求康聂,比如DontWrapResult 和異常攔截,都可以很好的支持
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末胞四,一起剝皮案震驚了整個(gè)濱河市恬汁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌辜伟,老刑警劉巖氓侧,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脊另,死亡現(xiàn)場離奇詭異,居然都是意外死亡约巷,警方通過查閱死者的電腦和手機(jī)偎痛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來独郎,“玉大人踩麦,你說我怎么就攤上這事∶グ” “怎么了谓谦?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長贪婉。 經(jīng)常有香客問我茁计,道長,這世上最難降的妖魔是什么谓松? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮践剂,結(jié)果婚禮上鬼譬,老公的妹妹穿的比我還像新娘。我一直安慰自己逊脯,他們只是感情好优质,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著军洼,像睡著了一般巩螃。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上匕争,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天避乏,我揣著相機(jī)與錄音,去河邊找鬼甘桑。 笑死拍皮,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的跑杭。 我是一名探鬼主播铆帽,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼德谅!你這毒婦竟也來了爹橱?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤窄做,失蹤者是張志新(化名)和其女友劉穎愧驱,沒想到半個(gè)月后慰技,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡冯键,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年惹盼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片惫确。...
    茶點(diǎn)故事閱讀 40,146評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡手报,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出改化,到底是詐尸還是另有隱情掩蛤,我是刑警寧澤,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布陈肛,位于F島的核電站揍鸟,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏句旱。R本人自食惡果不足惜阳藻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望谈撒。 院中可真熱鬧腥泥,春花似錦、人聲如沸啃匿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽溯乒。三九已至夹厌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間裆悄,已是汗流浹背矛纹。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留光稼,地道東北人崖技。 一個(gè)月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像钟哥,于是被迫代替她去往敵國和親迎献。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評論 2 356

推薦閱讀更多精彩內(nèi)容