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 和异常拦截,都可以很好的支持

关注我,一起探索新知识新技术

posted @ 2021-06-15 22:45  诸葛小亮  阅读(1142)  评论(0编辑  收藏  举报