1. 程式人生 > 其它 >Spring Boot Mvc 統一返回結果

Spring Boot Mvc 統一返回結果

背景

在 spring boot 專案中,使用@RestController / @RequestMapping / @GetMapping / @PostMapping 等註解提供api的功能,但是每個Mapping返回的型別各不相同,有的是void,有的是基礎型別如strping /integer,有的是dto。
在前後端分離的專案中,返回格式不統一,使得前端處理返回結果也不能統一,會導致寫很多程式碼。

原始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方法統一返回型別ApiResult

新建統一返回類

@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");


    }

缺點

每個方法統一返回ApiResult型別,但是有一個缺點,就是需要程式設計師自身關注這件事情,如果忘記返回了,會影響使用。

方式二,使用攔截器

spring mvc 提供了一個介面ResponseBodyAdvice, 用來攔截響請求響應,可以通過自定義攔截器完成統一結果返回

定義攔截器


/**
 * 通過結果返回攔截器,只攔截 @RestController 標識的類
 */
@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;
    }

}

測試程式碼不用修改,執行測試,發現測試是通過,說明通過攔截器,可以統一返回型別,並且不需要強制Controller方法返回ApiResult型別

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

定義註解

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

在Controller方法或類上,添加註解@DontWrapResult, 擴充套件 controller 方法

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

        return  result;
    }

修改攔截器,是的@DontWrapResult 註解的方法或類直接返回結果


    @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");


    }

異常

統一返回型別後,全域性異常也要包裝到型別ApiResult

定義友好的業務異常類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;
    }


}

修改攔截器,進行異常攔截


    @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 新增 除法運算


   @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 新增 加法運算

@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");
    }

總結

在spring boot專案中,讓controller返回統一結果有兩種實現方式:

  1. 方法程式碼寫死返回型別,弊端是沒有有效的檢測機制,如果方法沒有返回,會影響使用一致性
  2. 繼承ResponseBodyAdvice<Object> 介面自定義攔截器,不強制要求方法返回統一型別,並且針對個性化要求,比如DontWrapResult 和異常攔截,都可以很好的支援

關注我,一起探索新知識新技術