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 和异常拦截,都可以很好的支持
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容