SpringBoot写的后端API接口如何写得更优雅

妖狐艹你老母 2023-10-02 19:55 78阅读 0赞

日常工作中,我们开发接口时,一般都会涉及到参数校验、异常处理、封装结果返回等处理。如果每个后端开发在参数校验、异常处理等都是各写各的,没有统一处理的话,代码就不优雅,也不容易维护。所以,作为一名合格的后端开发工程师,我们需要统一校验参数,统一异常处理、统一结果返回,让代码更加规范、可读性更强、更容易维护。

一、使用注解,统一参数校验

假设需要实现一个注册用户的功能,在controller 层,他会先进行校验参数,如下:

  1. @RestController
  2. @RequestMapping
  3. public class UserController {
  4. @RequestMapping("addUser")
  5. public String addUser(UserParam userParam) {
  6. if (StringUtils.isEmpty(userParam.getUserName())) {
  7. return "用户名不能为空";
  8. }
  9. if (StringUtils.isEmpty(userParam.getPhone())) {
  10. return "手机号不能为空";
  11. }
  12. if (userParam.getPhone().length() > 11) {
  13. return "手机号不能超过11";
  14. }
  15. if (StringUtils.isEmpty(userParam.getEmail())) {
  16. return "邮箱不能为空";
  17. }
  18. //省略其他参数校验
  19. //todo 插入用户信息表
  20. return "SUCCESS";
  21. }
  22. }

以上代码有什么问题嘛?其实没什么问题,就是校验有点辣眼睛。正常的添加用户业务还没写,参数校验就一大堆啦。假设后来,小田螺又接了一个需求:编辑用户信息。实现编辑用户信息前,也是先校验信息,如下:

  1. @RequestMapping("editUser")
  2. public String editUser(UserParam userParam) {
  3. if (StringUtils.isEmpty(userParam.getUserName())) {
  4. return "用户名不能为空";
  5. }
  6. if (StringUtils.isEmpty(userParam.getPhone())) {
  7. return "手机号不能为空";
  8. }
  9. if (userParam.getPhone().length() > 11) {
  10. return "手机号不能超过11";
  11. }
  12. if (StringUtils.isEmpty(userParam.getEmail())) {
  13. return "邮箱不能为空";
  14. }
  15. //省略其他参数校验
  16. //todo 编辑用户信息表
  17. return "SUCCESS";
  18. }

我们可以使用注解的方式,来进行参数校验,这样代码更加简洁,也方便统一管理。实际上, spring boot有个validation的组件,我们可以拿来即用。引入这个包即可:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-validation</artifactId>
  4. </dependency>

引入包后,参数校验就非常简洁啦,如下:

  1. public class UserParam {
  2. @NotNull(message = "用户名不能为空")
  3. private String userName;
  4. @NotNull(message = "手机号不能为空")
  5. @Max(value = 11)
  6. private String phone;
  7. @NotNull(message = "邮箱不能为空")
  8. private String email;

然后在UserParam参数对象中,加入@Validated注解哈,把错误信息接收到BindingResult对象,代码如下:

  1. @RequestMapping("addUser")
  2. public String addUser(@Validated UserParam userParam, BindingResult result) {
  3. List<FieldError> fieldErrors = result.getFieldErrors();
  4. if (!fieldErrors.isEmpty()) {
  5. return fieldErrors.get(0).getDefaultMessage();
  6. }
  7. //todo 插入用户信息表
  8. return "SUCCESS";
  9. }

二、接口统一响应对象返回

如果你在你们项目代码中,看到controller 层报文返回结果,有这样的:

  1. @RequestMapping("/hello")
  2. public String getStr(){
  3. return "hello,捡田螺的小男孩";
  4. }
  5. //返回
  6. hello,捡田螺的小男孩

也有这样的:

  1. @RequestMapping("queryUser")
  2. public UserVo queryUser(String userId) {
  3. return new UserVo("666", "捡田螺的小男孩");
  4. }
  5. //返回:
  6. {"userId":"666","name":"捡田螺的小男孩"}

显然,如果接口返回结果不统一,前端处理就不方便,我们代码也不好维护。再比如有的人喜欢用Result处理结果,有的人喜欢用Response处理结果,可以想象一下,这些代码有多乱。

所以作为后端开发,我们项目的响应结果,需要统一标准的返回格式。一般一个标准的响应报文对象,都有哪些属性呢?

  • code :响应状态码
  • message :响应结果描述
  • data:返回的数据

响应状态码一般用枚举表示:

  1. public enum CodeEnum {
  2. /**操作成功**/
  3. SUCCESS("0000","操作成功"),
  4. /**操作失败**/
  5. ERROR("9999","操作失败"),;
  6. /**
  7. * 自定义状态码
  8. **/
  9. private String code;
  10. /**自定义描述**/
  11. private String message;
  12. CodeEnum(String code, String message){
  13. this.code = code;
  14. this.message = message;
  15. }
  16. public String getCode() {
  17. return code;
  18. }
  19. public String getMessage() {
  20. return message;
  21. }
  22. }

因为返回的数据类型不是确定的,我们可以使用泛型,如下:

  1. /**
  2. * @author 捡田螺的小男孩
  3. * @param <T>
  4. */
  5. public class BaseResponse<T> {
  6. /**
  7. * 响应状态码(0000表示成功,9999表示失败
  8. */
  9. private String code;
  10. /**
  11. * 响应结果描述
  12. */
  13. private String message;
  14. /**
  15. * 返回的数据
  16. */
  17. private T data;
  18. /**
  19. * 成功返回
  20. * @param data
  21. * @param <T>
  22. * @return
  23. */
  24. public static <T> BaseResponse<T> success(T data) {
  25. BaseResponse<T> response= new BaseResponse<>();
  26. response.setCode(CodeEnum.SUCCESS.getCode());
  27. response.setMessage(CodeEnum.SUCCESS.getMessage());
  28. response.setData(data);
  29. return response;
  30. }
  31. /**
  32. * 失败返回
  33. * @param code
  34. * @param message
  35. * @param <T>
  36. * @return
  37. */
  38. public static <T> BaseResponse<T> fail(String code, String message) {
  39. BaseResponse<T> response = new BaseResponse<>();
  40. response.setCode(code);
  41. response.setMessage(message);
  42. return response;
  43. }
  44. public void setCode(String code) {
  45. this.code = code;
  46. }
  47. public void setMessage(String message) {
  48. this.message = message;
  49. }
  50. public void setData(T data) {
  51. this.data = data;
  52. }
  53. }

有了统一的响应体,我们就可以优化一下controller 层的代码:

  1. @RequestMapping("/hello")
  2. public BaseResponse<String> getStr(){
  3. return BaseResponse.success("hello,捡田螺的小男孩");
  4. }
  5. //output
  6. {"code":"0000","message":"操作成功","data":"hello,捡田螺的小男孩"}
  7. @RequestMapping("queryUser")
  8. public BaseResponse<UserVo> queryUser(String userId) {
  9. return BaseResponse.success(new UserVo("666", "捡田螺的小男孩"));
  10. }
  11. //output
  12. {"code":"0000","message":"操作成功","data":{"userId":"666","name":"捡田螺的小男孩"}}

三、统一异常处理

日常开发中,我们一般都是自定义统一的异常类,如下:

  1. public class BizException extends RuntimeException {
  2. private String retCode;
  3. private String retMessage;
  4. public BizException() {
  5. super();
  6. }
  7. public BizException(String retCode, String retMessage) {
  8. this.retCode = retCode;
  9. this.retMessage = retMessage;
  10. }
  11. public String getRetCode() {
  12. return retCode;
  13. }
  14. public String getRetMessage() {
  15. return retMessage;
  16. }
  17. }

在controller 层,很可能会有类似代码:

  1. @RequestMapping("/query")
  2. public BaseResponse<UserVo> queryUserInfo(UserParam userParam) {
  3. try {
  4. return BaseResponse.success(userService.queryUserInfo(userParam));
  5. } catch (BizException e) {
  6. //doSomething
  7. } catch (Exception e) {
  8. //doSomething
  9. }
  10. return BaseResponse.fail(CodeEnum.ERROR.getCode(),CodeEnum.ERROR.getMessage());
  11. }

这块代码,没什么问题哈,但是如果try...catch太多,不是很优雅。

可以借助注解@RestControllerAdvice,让代码更优雅。@RestControllerAdvice是一个应用于Controller层的切面注解,它一般配合@ExceptionHandler注解一起使用,作为项目的全局异常处理。我们来看下demo代码哈。

还是原来的UserController,和一个会抛出异常的userService的方法,如下:

  1. @RestController
  2. public class UserController {
  3. @Autowired
  4. private UserService userService;
  5. @RequestMapping("/query")
  6. public BaseResponse<UserVo> queryUserInfo1(UserParam userParam) {
  7. return BaseResponse.success(userService.queryUserInfo(userParam));
  8. }
  9. }
  10. @Service
  11. public class UserServiceImpl implements UserService {
  12. //抛出异常
  13. @Override
  14. public UserVo queryUserInfo(UserParam userParam) throws BizException {
  15. throw new BizException("6666", "测试异常类");
  16. }
  17. }

我们再定义一个全局异常处理器,用@RestControllerAdvice注解,如下:

  1. @RestControllerAdvice(annotations = RestController.class)
  2. public class ControllerExceptionHandler {
  3. }

我们有想要拦截的异常类型,比如想拦截BizException类型,就新增一个方法,使用@ExceptionHandler注解修饰,如下:

  1. @RestControllerAdvice(annotations = RestController.class)
  2. public class ControllerExceptionHandler {
  3. @ExceptionHandler(BizException.class)
  4. @ResponseBody
  5. public BaseResponse<Void> handler(BizException e) {
  6. System.out.println("进入业务异常"+e.getRetCode()+e.getRetMessage());
  7. return BaseResponse.fail(CodeEnum.ERROR.getCode(), CodeEnum.ERROR.getMessage());
  8. }
  9. }

四、最后

本文大家学到了哪些知识呢?

  1. 为了写出更优雅、更简洁、更容易维护的代码,我们需要统一参数校验、统一响应对象返回、统一异常处理
  2. 参数校验更简洁,可以使用注解实现。
  3. 如何统一响应对象返回,一般要包括状态码、描述信息、返回数据。
  4. Controller层如何统一全局异常处理?@RestControllerAdvice+@ExceptionHandler
  5. 进阶篇?大家可以自己实现自定义注解哈,也建议去看看@RestControllerAdvice实现原理,它其实就是一个切面注解,看下它的源码即可。

发表评论

表情:
评论列表 (有 0 条评论,78人围观)

还没有评论,来说两句吧...

相关阅读