前言
最近項目組有個老項目要進行前后端分離改造,應前端同學的要求,其后端提供的返回值格式需形如
{
"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