前言
最近项目组有个老项目要进行前后端分离改造,应前端同学的要求,其后端提供的返回值格式需形如
{
"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