springboot2之優雅處理返回值


前言

最近項目組有個老項目要進行前后端分離改造,應前端同學的要求,其后端提供的返回值格式需形如

{
  "status": 0,
  "message": "success",
  "data": {
    
  }
}

方便前端數據處理。要實現前端同學這個需求,其實也挺簡單的,僅需做如下改造,新增一個返回對象,形如

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Result<T> {
    public static final int success = 0;
    public static final int fail = 1;
    private int status = success;
    private String message = "success";
    private T data;


}

然后controller改造成如下


@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {


  @Autowired
  private UserService userService;

  @PostMapping(value="/add")
  public Result<UserDTO> addUser(@Valid UserDTO userDTO, BindingResult bindingResult){
    Result<UserDTO> result = new Result<>();
    if (bindingResult.hasErrors()){
      return getUserFailResult(bindingResult, result);
    }
    saveUser(userDTO, result);

    return result;

  }
}

僅僅需要這么改造就可以滿足前端同學的述求。但這邊存在一個問題就是,這個項目后端接口的contoller之前都是直接返回業務bean對象,形如下

@RestController
@Api(tags = "用戶管理")
@Slf4j
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping(value="/get/{id}")
    @ApiOperation("根據用戶ID查找用戶")
    @ApiImplicitParam(value = "用戶id",name = "id",required = true,paramType = "path")
    public UserDTO getUserById(@PathVariable("id") Long id){
        UserDTO dto = userService.getUserById(id);
        log.info("{}",dto);
        return dto;

    }
    }

如果按上面的思路

把UserDTO改造成Result<UserDTO>

雖然可以滿足需求,但問題是后端這樣的接口有好幾十個,按這種改法很明顯工作量比較大,更重要的不符合開閉原則--對擴展開放,對修改關閉。那有沒有優雅一點的處理方式呢?答案是有的,利用
@RestControllerAdvice+ResponseBodyAdvice就可以滿足我們的需求

改造

1、在改造前,先簡單介紹一下@RestControllerAdvice和ResponseBodyAdvice

@RestControllerAdvice

@RestControllerAdvice這個注解是spring 4.3版本之后新增的注解。用於定義@ExceptionHandler、@InitBinder、@ModelAttribute,並應用到所有@RequestMapping。利用他可以來做異常統一處理。如果使用的spring低於4.3,那可以使用@ControllerAdvice+@ResponseBody。@ControllerAdvice是spring 3.2版本后就提供的注解,其實現的功能和@RestControllerAdvice類似。
其詳細的參考文檔,可以查看鏈接@RestControllerAdvice文檔以及@ControllerAdvice文檔

ResponseBodyAdvice

這個是spring4.1版本之后,新增的接口。其作用是允許在執行@ResponseBody或ResponseEntity控制器方法之后但在使用HttpMessageConverter編寫正文之前自定義響應。可以直接在RequestMappingHandlerAdapter和ExceptionHandlerExceptionResolver中注冊實現,也可以在@ControllerAdvice或者@RestControllerAdvice中注解。其詳細參考文檔可以查看鏈接ResponseBodyAdvice文檔

2、編寫一個通用的響應實體

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Result<T> {
    public static final int success = 0;
    public static final int fail = 1;
    private int status = success;
    private String message = "success";
    private T data;


}

3、編寫一個類上加上@RestControllerAdvice並實現ResponseBodyAdvice接口。用來統一處理響應值

@RestControllerAdvice(basePackages = "com.github.lybgeek")
@Slf4j
public class ResponseAdvice implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if(Objects.isNull(o)){
            return Result.builder().message("success").build();
        }

        if(o instanceof Result){
            return o;
        }

        return Result.builder().message("success").data(o).build();
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<?> exceptionHandler(HttpServletRequest request, Exception e) {
        log.error(e.getMessage(), e);
        return Result.builder().message(e.getMessage()).status(Result.fail).build();
    }

    /**
     * 針對業務異常統一處理
     * @param request
     * @param bizException
     * @return
     */
    @ExceptionHandler(BizException.class)
    @ResponseStatus(code = HttpStatus.EXPECTATION_FAILED)
    public Result<?> bizExceptionHandler(HttpServletRequest request, BizException bizException) {
            int errorCode = bizException.getCode();
            log.error("catch bizException {}", errorCode);
            return Result.builder().message(bizException.getMessage()).status(errorCode).build();
    }


    /**
     * 針對Validate校驗異常統一處理
     * @param request
     * @param methodArgumentNotValidException
     * @return
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    public Result<?> methodArgumentNotValidExceptionExceptionHandler(HttpServletRequest request, MethodArgumentNotValidException methodArgumentNotValidException) {
        Result result = new Result();
        log.error("catch methodArgumentNotValidException :" + methodArgumentNotValidException.getMessage(), methodArgumentNotValidException);
        return ResultUtils.INSTANCE.getFailResult(methodArgumentNotValidException.getBindingResult(),result);
    }

    /**
     * 針對Assert斷言異常統一處理
     * @param request
     * @param illegalArgumentExceptionException
     * @return
     */
    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(code = HttpStatus.EXPECTATION_FAILED)
    public Result<?> illegalArgumentExceptionHandler(HttpServletRequest request, IllegalArgumentException illegalArgumentExceptionException) {
        log.error("illegalArgumentExceptionException:"+illegalArgumentExceptionException.getMessage(), illegalArgumentExceptionException);
        return Result.builder().message(illegalArgumentExceptionException.getMessage()).status(Result.fail).build();
    }


測試驗證

1、編寫業務DTO

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ApiModel
public class UserDTO implements Serializable {

  @NotNull(message = "編號不能為空",groups = {Update.class, Delete.class})
  @ApiModelProperty(value = "編號",name = "id",example = "1")
  private Long id;

  @NotBlank(message = "用戶名不能為空",groups = {Add.class})
  @ApiModelProperty(value = "用戶名",name = "userName",example = "zhangsan")
  private String userName;

  @NotBlank(message = "姓名不能為空",groups = {Add.class})
  @ApiModelProperty(value = "姓名",name = "realName",example = "張三")
  private String realName;

  @NotBlank(message = "密碼不能為空",groups = {Add.class})
  @Size(max=32,min=6,message = "密碼長度要在6-32之間",groups = {Add.class})
  @ApiModelProperty(value = "密碼",name = "password",example = "123456")
  private String password;

  @NotNull(message = "性別不能為空",groups = {Add.class})
  @ApiModelProperty(value = "性別",name = "gender",example = "1")
  @EnumValid(target = Gender.class, message = "性別取值必須為0或者1",groups = {Add.class,Update.class})
  private Integer gender;

  @ApiModelProperty(value = "郵箱",name = "email",example = "zhangsan@qq.com")
  @Pattern(regexp = "^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*\\.[a-zA-Z0-9]{2,6}$",message = "不滿足郵箱正則表達式",groups = {Add.class,Update.class})
  private String email;



}

2、編寫業務controller

@RestController
@Api(tags = "用戶管理")
@Slf4j
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping(value="/get/{id}")
    @ApiOperation("根據用戶ID查找用戶")
    @ApiImplicitParam(value = "用戶id",name = "id",required = true,paramType = "path")
    public UserDTO getUserById(@PathVariable("id") Long id){
        UserDTO dto = userService.getUserById(id);
        log.info("{}",dto);
        return dto;

    }

    @PostMapping(value="/add")
    @ApiOperation("添加用戶")
    public UserDTO add(@RequestBody @Validated({Add.class}) UserDTO userDTO){
        log.info("{}",userDTO);
        return userService.save(userDTO);
    }

    @PostMapping(value="/update")
    @ApiOperation("更新用戶")
    public UserDTO update(@RequestBody @Validated({Update.class}) UserDTO userDTO){
        log.info("{}",userDTO);
        return userService.save(userDTO);
    }

    @DeleteMapping(value="/detele")
    @ApiOperation("刪除用戶")
    public boolean delete(@Validated({Delete.class}) UserDTO userDTO){
        log.info("id:{}",userDTO.getId());
        return userService.delete(userDTO.getId());
    }
}

注: 業務service就不貼了和文章內容關系不大。如果感興趣的朋友,可以從文末提供的鏈接進行查看

3、利用swagger在線接口文檔進行測試

a:正常響應時,返回值形如下

{
  "status": 0,
  "message": "success",
  "data": {
    "id": 1,
    "userName": "zhangsan",
    "realName": "張三",
    "password": "123456",
    "gender": 1,
    "email": "zhangsan@qq.com"
  }
}

b:當數據校驗異常時,返回值形如下

{
  "status": 1,
  "message": "姓名不能為空;",
  "data": null
}

c:當業務異常時,返回值形如下

{
  "status": 1,
  "message": "user is not found by id :3",
  "data": null
}

總結

本文主要介紹了如何利用@RestControllerAdvice和ResponseBodyAdvice來統一處理返回值。本文代碼示例還實現了分組校驗,自定義校驗,利用mdc traceId日志埋點,如果對這些內容感興趣的朋友,可以查看文末項目鏈接

demo鏈接

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-unit-resp


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM