Spring 实现 RESTful 统一返回值和错误处理

Posted on May 9, 2020

背景

最近在折腾一些有的没的, 实现一些接口, 而且我最近有些完美主义, 明明能花几分钟写完的代码却是喜欢花更多是时间去看看别人怎么写的, 然后再借鉴~~(ctrl+v)~~ 至于 RESTful 的介绍可以看 维基百科 上的介绍

接口设计

我的设计目前还是比较简单

1
2
3
4
/user/register
/user/delete
/user/{id}/detail
/user/changePassword

总的来说就是增删查改
这个增删查改我们用 4 种提交方式来对应他们
分别是POST, DELETE, GET, PUT
而我这里采用的状态码分别是

1
2
3
4
5
6
200 OK
201 CREATE
204 NO CONTENT
400 BAD REQUEST
404 NOT FOUND
500 INTERNAL ERROR

更多的状态码可以看 MDN 的 这篇文档

前置要求

我这里使用的是Spring Boot
web 是 Spring Boot Web.

省略的代码

下面是我们的一个 User 类, 具体意思就不表了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Setter
@Data
@Document(collection = "user")
public class User {
    @Id
    private String id;
    private String username;
    private String password;
    private String email;
}

统一的返回值封装

我们希望得到的数据都有一个统一是格式, 这样比较方便处理, 而且也比较的美观
通过这个类 得到的 json 就类似于 {"message": "qwertyuiop", "data": null}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Getter
@Setter
@Data
public class CommonResult<T> {
    private String message;
    private Object data;

    public CommonResult(String message) {
        this.message = message;
    }

    public CommonResult(T data) {
        if (data instanceof String) {
            message = (String) data;
        } else {
            this.data = data;
        }
    }
}

全局返回处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@EnableWebMvc
@Configuration
public class GlobalResultResolver {

    @RestControllerAdvice
    static class CommonResultConfigAdvice implements ResponseBodyAdvice<Object> {
        @Override
        public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?> converterType) {
            return true;
        }

        @Override
        public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
            if (body instanceof CommonResult) {
                return body;
            }
            return new CommonResult<>(body);
        }
    }
}

全局异常处理

我们希望异常也是可以和普通版本一样的输出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@ControllerAdvice
@ResponseBody
public class GlobalExceptionResolver {
    @ExceptionHandler(ResourceConflictException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public ResultBody<?> handleResourceConflict(ResourceConflictException e) {
        return new ResultBody<>(e.getMessage());
    }

    @ExceptionHandler(CommonException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResultBody<?> handleCommon(CommonException e) {
        return new ResultBody<>(e.getMessage());
    }

    @ExceptionHandler(BadRequestException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResultBody<?> handleBadRequest(BadRequestException e) {
        return new ResultBody<>(e.getMessage());
    }

    @ExceptionHandler(ResourcesNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ResultBody<?> handleNotFound(ResourcesNotFoundException e) {
        return new ResultBody<>(e.getMessage());
    }
}

这里举个异常的例子, 其他就不表了

1
2
3
4
5
public class CommonException extends RuntimeException {
    public CommonException(String message) {
        super(message);
    }
}

其他那些 UserRepository extend balabala, UserService 的我这里就不写了, 大家都是一样的

现在, 我们来写 Controller

Controller

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@RestController
@AllArgsConstructor
@RequestMapping("/user")
public class UserController {
    private final UserService userService;

    @DeleteMapping("/delete")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteUser(HttpServletRequest servletRequest) {
        if (!userService.delete(servletRequest.getSession().getAttribute("ID"))) {
            throw new CommonException("用户删除失败");
        }
    }

    @PostMapping("/register")
    @ResponseStatus(HttpStatus.CREATED)
    public User register(@RequestParam Map<String, String> paramMap) {
        if (!userService.addUser(user)) {
            throw new ResourceConflictException("用户名或邮箱已存在");
        }
    }

    @PutMapping("/changePassword")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void changePassword(@RequestParam String oldPassword, @RequestParam String newPassword, HttpServletRequest servletRequest) {
        if (!userService.changePassword((String) servletRequest.getSession().getAttribute("ID"), oldPassword, newPassword)) {
            throw new BadRequestException("更改密码失败, 请检查原密码是否正确");
        }
    }
}

代码到这里就差不多了, 注意这里的代码并不能直接复制粘贴运行, 是需要修改一下的

演示

注意一下右边的 status 返回值

注册用户(成功):
注册用户(失败):
更改密码(成功):
更改密码(失败):

相关阅读

RESTful API 最佳实践(阮一峰)