Spring Boot2.x-11 使用@ControllerAdvice和@ExceptionHandler实现自定义全局异常
我们搞个boot工程 ,来看下为什么以及如何来实现统一格式封装及高阶全局异常处理
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>jakarta.validationgroupId>
<artifactId>jakarta.validation-apiartifactId>
dependency>
dependencies>
@RestController // 返回JSON
@RequestMapping("/v1")
public class ArtisanV1Controller {
/**
* 返回字符串
*
* @return
*/
@GetMapping("/getString")
public String getStr() {
return "OOOOOOOK";
}
/**
* 返回自定义对象
*
* @return
*/
@GetMapping("/getArtisan")
public Artisan getArt() {
Artisan artisan = new Artisan();
artisan.setJob("ArtisanJob");
artisan.setAge(18);
return artisan;
}
/**
* 接口异常
*
* @return
*/
@GetMapping("/getMockError")
public int getMockError() {
int i = 1 / 0;
return i;
}
}
分别测试下
这混乱的格式, 前端同学怎么想
一个合格的标准的返回格式至少包含3部分:
如果需要可以加入其他节点,比如在返回对象中添加了接口调用时间 (timestamp: 接口调用时间)
package com.artisan.resp;
import lombok.Data;
/**
* @author 小工匠
* @version 1.0
* @description: 公共结果
* @mark: show me the code , change the world
*/
@Data
public class ResponseData<T> {
/**
* 结果状态 ,具体状态码参见ResponseCode
*/
private int status;
/**
* 响应消息
**/
private String message;
/**
* 响应数据
**/
private T data;
/**
* 接口请求时间
**/
private long timestamp;
/**
* 初始化,增加接口请求事件
*/
public ResponseData() {
this.timestamp = System.currentTimeMillis();
}
/**
* 成功
*
* @param
* @return
*/
public static <T> ResponseData<T> success() {
ResponseData<T> resultData = new ResponseData<>();
resultData.setStatus(ResponseCode.RC100.getCode());
resultData.setMessage(ResponseCode.RC100.getMessage());
return resultData;
}
/**
* 成功
*
* @param message
* @param
* @return
*/
public static <T> ResponseData<T> success(String message) {
ResponseData<T> resultData = new ResponseData<>();
resultData.setStatus(ResponseCode.RC100.getCode());
resultData.setMessage(message);
return resultData;
}
/**
* 成功
*
* @param data
* @param
* @return
*/
public static <T> ResponseData<T> success(T data) {
ResponseData<T> resultData = new ResponseData<>();
resultData.setStatus(ResponseCode.RC100.getCode());
resultData.setMessage(ResponseCode.RC100.getMessage());
resultData.setData(data);
return resultData;
}
/**
* 失败
*
* @param message
* @param
* @return
*/
public static <T> ResponseData<T> fail(String message) {
ResponseData<T> resultData = new ResponseData<>();
resultData.setStatus(ResponseCode.RC999.getCode());
resultData.setMessage(message);
return resultData;
}
/**
* 失败
*
* @param code
* @param message
* @param
* @return
*/
public static <T> ResponseData<T> fail(int code, String message) {
ResponseData<T> resultData = new ResponseData<>();
resultData.setStatus(code);
resultData.setMessage(message);
return resultData;
}
/**
* 失败
*
* @param
* @return
*/
public static <T> ResponseData<T> fail() {
ResponseData<T> resultData = new ResponseData<>();
resultData.setStatus(ResponseCode.RC999.getCode());
resultData.setMessage(ResponseCode.RC999.getMessage());
return resultData;
}
}
package com.artisan.resp;
import lombok.Getter;
/**
* @author 小工匠
* @version 1.0
* @description: 状态码集合
* @mark: show me the code , change the world
*/
public enum ResponseCode {
/**
* 操作成功
**/
RC100(100, "操作成功"),
/**
* 操作失败
**/
RC999(999, "操作失败"),
/**
* access_denied
**/
RC403(403, "无访问权限,请联系管理员授予权限"),
/**
* access_denied
**/
RC401(401, "匿名用户访问无权限资源时的异常"),
/**
* 服务异常
**/
RC500(500, "系统异常,请稍后重试"),
ILLEGAL_ARGUMENT(3001, "非法参数"),
INVALID_TOKEN(2001, "访问令牌不合法"),
ACCESS_DENIED(2003, "没有权限访问该资源"),
CLIENT_AUTHENTICATION_FAILED(1001, "客户端认证失败"),
USERNAME_OR_PASSWORD_NOTMATCH(1002, "用户名或密码错误");
/**
* 自定义状态码
**/
@Getter
private final int code;
/**
* 自定义描述
**/
@Getter
private final String message;
ResponseCode(int code, String message) {
this.code = code;
this.message = message;
}
}
package com.artisan.controller;
import com.artisan.entity.Artisan;
import com.artisan.resp.ResponseData;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 小工匠
* @version 1.0
* @description: 版本2
* @mark: show me the code , change the world
*/
@RestController
@RequestMapping("/v2")
public class ArtisanV2Controller {
@GetMapping("/getString")
public ResponseData<String> getStr() {
return ResponseData.success("OOOOOOK");
}
@GetMapping("/getArtisan")
public ResponseData<Artisan> getArt() {
Artisan artisan = new Artisan();
artisan.setJob("CodeMonkey");
artisan.setAge(18);
return ResponseData.success(artisan);
}
@GetMapping("/getMockError")
public ResponseData<Integer> getMockError() {
int i = 1 / 0;
return ResponseData.success(i);
}
}
好像部分实现了统一格式返回,确实也是有很多项目在Controller层通过ResponseData.success()对返回结果进行包装后返回给前端。
但是这个抛异常的这么玩还是不行呀? ------------------------> 全局异常处理
package com.artisan.resp;
import com.artisan.exception.BaseException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import java.util.stream.Collectors;
/**
* @author 小工匠
* @version 1.0
* @description: 全局异常处理
* @mark: show me the code , change the world
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 默认全局异常处理。
*
* @param e e
* @return ResponseData
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseData<String> exception(Exception e) {
log.error("兜底异常信息 ex={}", e.getMessage());
return ResponseData.fail(ResponseCode.RC500.getCode(), e.getMessage());
}
/**
* Assert异常
*/
@ExceptionHandler({IllegalArgumentException.class, IllegalStateException.class})
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseData<String> exception(IllegalArgumentException e) {
return ResponseData.fail(ResponseCode.ILLEGAL_ARGUMENT.getCode(), e.getMessage());
}
/**
* 抓取自定义异常 BaseException
*/
@ExceptionHandler(BaseException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseData<String> exception(BaseException e) {
return ResponseData.fail(e.getErrorCode(), e.getMessage());
}
}
重新验证下
V2版本有缺陷么?
我们不难发现每写一个接口都需要调用ResponseData.success()对结果进行包装 ,程序猿懒啊, 能不写吗
ResponseBodyAdvice的作用一般是用于拦截Controller方法的返回值,统一处理返回值/响应体, 加解密,签名等
package com.artisan.resp.v3;
import com.artisan.resp.ResponseData;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* @author 小工匠
* @version 1.0
* @description: 拦截Controller方法的返回值,统一处理返回值/响应体
* @mark: show me the code , change the world
*/
@RestControllerAdvice
public class CustomResponseAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
// 处理String类型
if (o instanceof String) {
return objectMapper.writeValueAsString(ResponseData.success(o));
}
// 若是统一返回类型,则不用再此封装
// if (o instanceof ResponseData) {
// return o;
// }
return ResponseData.success(o);
}
}
接入@RestControllerAdvice后, Controller就正常写就可以了,不用统一格式去包装了,如下
@RestController
@RequestMapping("/v3")
public class ArtisanV3Controller {
@GetMapping("/getString")
public String getStr() {
return "OOOOOOK";
}
@GetMapping("/getArtisan")
public Artisan getArt() {
Artisan artisan = new Artisan();
artisan.setJob("CodeMonkey");
artisan.setAge(18);
return artisan;
}
@GetMapping("/getMockError")
public int getMockError() {
int i = 1 / 0;
return i;
}
}
测试下
看到问题了吧,当我们同时启用统一标准格式封装功能ResponseAdvice和RestExceptionHandler全局异常处理器时,统一格式增强功能会给返回的异常结果再次封装,所以跟前端的接口响应又迷糊了
因为全局异常处理器已经帮我们封装好了标准格式,我们只需要直接返回给客户端即可。
// 若是统一返回类型,则不用再此封装
if (o instanceof ResponseData) {
return o;
}
如果返回的结果是ResponseData对象,直接返回即可。
重新测试 ,