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

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