在springboot应用开发中,面对程序可能出现的各项异常,最好有一个全局的处理。
不然假设后端因为某些原因抛出异常,比如空指针,文件不存在等,会直接返回500
在前后端分离的项目中,前端会拿到internal server error + 后端的一大堆异常堆栈。这对前端是毫无用处的。
使用controlleradvice + exceptionhandler 可以实现后端应用的全局异常处理。
另外还可以通过自定义异常,在需要的时候抛出异常,交给全局异常处理器来返回某些逻辑;通过编码可以实现全局异常处理器对不同类型的异常执行不同的逻辑。
这里我针对一些我自定义的异常,返回特殊提示信息,并对spring的validation产生的各项异常,提取其中前端需要的信息做为message字段返回。
除了使用spring的validation自动校验参数,有时可能需要程序中动态校验来弥补validation不能完成的逻辑,若不符合验证条件就抛出paramerrorexception
另外在业务逻辑中出现一些返回值仅代表成功运行的函数,无法通过返回值区分成功失败的情况,就可以手动抛出tipexception来交给异常处理器处理返回。
@Slf4j
@ControllerAdvice
public class TipControllerAdvice {
/**
* 全局异常处理
*/
@ResponseBody
@ExceptionHandler(value = Exception.class)
public ResponseVo<String> handler(Exception e) {
//default error message
String msg = "系统内部出错";
log.error(msg, e);
return ResponseVo.failure(msg);
}
/**
* 参数校验异常异常处理
*/
@ResponseBody
@ExceptionHandler(value = ConstraintViolationException.class)
public ResponseVo<String> handlerConstraintViolationException(Exception e) {
ConstraintViolationException constraintViolationException = (ConstraintViolationException) e;
String msg = StringUtils.collectionToCommaDelimitedString(
constraintViolationException.getConstraintViolations()
.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.toList()));
return ResponseVo.failure(msg);
}
@ResponseBody
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseVo<String> handlerMethodArgumentNotValidException(Exception e) {
StringBuilder message = new StringBuilder();
MethodArgumentNotValidException exception = (MethodArgumentNotValidException) e;
List<ObjectError> errors = exception.getBindingResult().getAllErrors();
for (ObjectError objectError : errors) {
if (objectError instanceof FieldError) {
FieldError fieldError = (FieldError) objectError;
message.append(StrUtil.toUnderlineCase(fieldError.getField())).append(":").append(fieldError.getDefaultMessage()).append(",");
} else {
message.append(objectError.getDefaultMessage()).append(",");
}
}
return ResponseVo.failure(message.toString());
}
@ResponseBody
@ExceptionHandler(value = BindException.class)
public ResponseVo<String> handlerBindException(Exception e) {
BindException bindException = (BindException) e;
String msg = StringUtils.collectionToCommaDelimitedString(
bindException.getAllErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList()));
return ResponseVo.failure(msg);
}
@ResponseBody
@ExceptionHandler(value = MissingServletRequestParameterException.class)
public ResponseVo<String> handlerMissingServletRequestParameterException(Exception e) {
return ResponseVo.failure("缺少必填参数");
}
@ResponseBody
@ExceptionHandler(value = HttpMessageNotReadableException.class)
public ResponseVo<String> handlerHttpMessageNotReadableException(Exception e) {
return ResponseVo.failure("请求参数异常");
}
@ResponseBody
@ExceptionHandler(value = ParamErrorException.class)
public ResponseVo<String> handlerParamError(Exception e) {
if (StrUtil.isBlank(e.getMessage())) {
return ResponseVo.failure("参数错误");
} else {
return ResponseVo.failure(e);
}
}
@ResponseBody
@ExceptionHandler(value = TipException.class)
public ResponseVo<String> handlerTip(Exception e) {
return ResponseVo.failure(e);
}
}
/**
* 统一响应处理器
* 1 在每个responseBody的响应返回之前进行处理
* 2 全局异常捕捉 统一返回格式
*
* @author wyh
* @date 2020/11/30 17:39
**/
@Slf4j
@ControllerAdvice
public class TipControllerAdvice implements ResponseBodyAdvice<Object> {
private static final Integer STATUS_404 = 404;
public static final String ERROR_MSG_404 = "接口地址不存在";
/**
* 决定是否执行beforeBodyWrite()方法
*/
@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if (o == null) {
return ResponseVo.failure();
}
//String类型需要特殊处理 手动转为json字符串
if (o instanceof String) {
return JsonUtil.toJson(ResponseVo.success(o));
}
if (o instanceof ResponseVo) {
return o;
}
//boolean类型 返回对应的成功或失败
if (o instanceof Boolean) {
return ResponseVo.builder((Boolean) o);
}
//404时 返回特定信息
if (is404(o)) {
return ResponseVo.failure(ERROR_MSG_404);
}
return ResponseVo.success(o);
}
/**
* 全局异常处理
*/
@ResponseBody
@ExceptionHandler(value = Exception.class)
public ResponseVo<String> handler(Exception e) {
//default error message
String msg = "系统内部出错";
log.error(msg, e);
return ResponseVo.failure(msg);
}
/**
* 参数校验异常异常处理
*/
@ResponseBody
@ExceptionHandler(value = ConstraintViolationException.class)
public ResponseVo<String> handlerConstraintViolationException(Exception e) {
ConstraintViolationException constraintViolationException = (ConstraintViolationException) e;
String msg = StringUtils.collectionToCommaDelimitedString(
constraintViolationException.getConstraintViolations()
.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.toList()));
return ResponseVo.failure(msg);
}
@ResponseBody
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseVo<String> handlerMethodArgumentNotValidException(Exception e) {
StringBuilder message = new StringBuilder();
MethodArgumentNotValidException exception = (MethodArgumentNotValidException) e;
List<ObjectError> errors = exception.getBindingResult().getAllErrors();
for (ObjectError objectError : errors) {
if (objectError instanceof FieldError) {
FieldError fieldError = (FieldError) objectError;
message.append(StrUtil.toUnderlineCase(fieldError.getField())).append(":").append(fieldError.getDefaultMessage()).append(",");
} else {
message.append(objectError.getDefaultMessage()).append(",");
}
}
return ResponseVo.failure(message.toString());
}
@ResponseBody
@ExceptionHandler(value = BindException.class)
public ResponseVo<String> handlerBindException(Exception e) {
BindException bindException = (BindException) e;
String msg = StringUtils.collectionToCommaDelimitedString(
bindException.getAllErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList()));
return ResponseVo.failure(msg);
}
@ResponseBody
@ExceptionHandler(value = MissingServletRequestParameterException.class)
public ResponseVo<String> handlerMissingServletRequestParameterException(Exception e) {
return ResponseVo.failure("缺少必填参数");
}
@ResponseBody
@ExceptionHandler(value = HttpMessageNotReadableException.class)
public ResponseVo<String> handlerHttpMessageNotReadableException(Exception e) {
return ResponseVo.failure("请求参数异常");
}
@ResponseBody
@ExceptionHandler(value = ParamErrorException.class)
public ResponseVo<String> handlerParamError(Exception e) {
if (StrUtil.isBlank(e.getMessage())) {
return ResponseVo.failure("参数错误");
} else {
return ResponseVo.failure(e);
}
}
@ResponseBody
@ExceptionHandler(value = TipException.class)
public ResponseVo<String> handlerTip(Exception e) {
return ResponseVo.failure(e);
}
private boolean is404(Object o) {
if (o instanceof Map) {
Map<String, Object> map = Convert.toMap(String.class, Object.class, o);
Integer status = Convert.toInt(map.get("status"));
return STATUS_404.equals(status);
}
return false;
}
}
根据supports方法可以动态决定是否需要执行下面的beforeBodyWrite方法,返回false就不会执行了。
为了满足有些接口还是会返回responseVo的情况,加了层判断,若返回的类已经是responseVo了就直接返回,不进行任何包装。
这里为string类型做了特殊处理,需要手动转一下json,不然会报错。
若返回结果为boolean 则交由responseVo的构造方法,为true则返回success + 0,false 则返回failure + -1 。可根据业务需要随意扩展即可
项目中有了这么个东西,代码写起来就很舒服了。
@RestController
@RequestMapping(Constants.URL_PREFIX + "/system")
public class SystemController {
@Autowired
private SystemService systemService;
/**
* 获取https开关
*/
@GetMapping("/setting_https")
public HttpsSettingVo getHttpsSetting() {
return HttpsSettingVo.builder().web(ConfigConstants.HTTPS_FLAG).build();
}
}