在前后端分离的场景中, 一般会以前端约定通信格式, 即后端返回统一的格式, 前端只需按照这种数据结构进行一致性的解析操作. 无论是正常请求还是服务器端异常, 都应该通过这种一致的格式反馈给前端.
本文探讨如何通过 @ControllerAdvice
和 BasicErrorController
处理所有异常并通过 FastJson 的 HttpMessageConverter
给前端返回统一的消息.
准备工作
简单创建一个 SpringBoot WEB 工程, 先约定前后端通信的数据结构如下:
{
"httpStatus": 500,
"timestamp": "yyyy-MM-dd HH:mm:ss",
"message": "exception-message",
"data": "{}"
}
@Getter
@Setter
@Builder
@AllArgsConstructor
public class ApiResponse {
@JSONField(serialize = false)
private final String PATTERN = "yyyy-MM-dd HH:mm:ss";
/**
* {@link HttpStatus}
*/
@JSONField(defaultValue = "200", ordinal = 1)
private int httpStatus;
/**
* 消息
*/
@JSONField(ordinal = 3)
private String message;
/**
* 时间戳
*/
@JSONField(format = "yyyy-MM-dd HH:mm:ss", ordinal = 2)
private LocalDateTime timestamp;
/**
* 数据
*/
@JSONField(defaultValue = "{}", ordinal = 4)
private Object data;
}
全局异常处理 - @ControllerAdvice
@ControllerAdvice
是 Spring 提供的 Controller 级的异常拦截注解. 通过它可以实现全局异常拦截…
正如 @RestController
(Spring 4.0 引入) 是 @Controller
+ @ResponseBody
, @RestControllerAdvice
也是 @ControllerAdvice
+ @ResponseBody
的语法糖.
在前后端分离的场景中一般都会用到 @RestController
, 所以本文中我们也会采用 @RestControllerAdvice
.
来看看代码实现,
新建一个简单的 Controller 抛出一个简单的异常:
@RestController
public class SimpleController {
@GetMapping("/access")
public String access() {
throw new ControllerException("ControllerException@SimpleController");
}
}
然后编写一个标注有 @RestControllerAdvice
的异常处理类:
@RestControllerAdvice
public class RestControllerExceptionHandler {
@ExceptionHandler(value = ControllerException.class)
public ApiResponse simpleControllerException(ControllerException controllerException) {
return
ApiResponse
.builder()
.httpStatus(HttpStatus.INTERNAL_SERVER_ERROR.value())
.timestamp(LocalDateTime.now())
.message(controllerException.getMessage())
.build();
}
}
启动程序直接访问 /access 端点, 可以看到前端拿到的数据是:
{
"httpStatus": 500,
"message": "ControllerException@SimpleController",
"timestamp": "2020-05-25T20:30:20.142",
"data": null,
"pattern": "yyyy-MM-dd HH:mm:ss"
}
可以看到, 由于我们自己定义了返回的数据结构, 也就是 ApiResponse, SpringBoot 默认使用 Jackson 序列化标记有 @ResponseBody
的请求, 我们能不能用 FastJson 自定义序列化的方式?
另外, 我们通过 @RestControllerAdvice
和 @ExceptionHandler
可以精确处理任意在 Controller 层即以下抛出的异常. 但是对于 Filter 级的异常, 或是访问一个不存在的端点, 这时该如何处理?
请继续往下看 …
BasicErrorController
就目前的处理, 如果我们访问一个根本不存在的端点, 响应消息是:
{
"timestamp": "2020-05-25T12:29:35.418+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/"
}
这显然不是我们约定的格式. 在浏览器上随便访问某个不存在的端点, 看到如下提示
看起来, 如果程序抛出了未能被 @ControllerAdvice
处理的异常, SpringBoot 会把请求转向 /error 这个端点, 自然的, 我们会期望有这么一个 Controller 用户处理这个端点的请求, SpringBoot 有一个默认的 Controller 用于处理这类请求, 它就是 BasicErrorController
BasicErrorController
中有两个标注了 @RequestMapping
的方法, 当请求头的 Accept 中包含 text/html
时, 会调用 public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response)
, 其他情况则会调用 public ResponseEntity<Map<String, Object>> error(HttpServletRequest request)
, 通过断点调试可以看到, 这个 request 对象的 arrtibute 中封装了异常对象本身, HTTP 状态码, requestURI 这类非常有用的信息. 我们完全可以利用这些信息来定义自己的信息体.
为了便于测试我们先写一个简单 Filter, 里面当请求携带指定参数时, 抛出自定义异常:
@Component
public class SimplePerRequestFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.info("SimplePerRequestFilter#doFilterInternal");
if (Boolean.parseBoolean(request.getParameter("shouldThrowAnException"))) {
throw new FilterException("FilterException@SimplePerRequestFilter");
}
filterChain.doFilter(request, response);
}
}
继承 BasicErrorController
, 将 error 方法体用我们自己的逻辑覆盖:
@RestController
public class CustomErrorController extends BasicErrorController {
private static final TypeReference<Map<String, Object>> mapTypeReference = new TypeReference<Map<String, Object>>() {
};
public CustomErrorController(ErrorAttributes errorAttributes) {
super(errorAttributes, new ErrorProperties());
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
final Map<String, Object> body = getErrorAttributes(request, false);
final HttpStatus status = getStatus(request);
return
new ResponseEntity<>(
JSON.parseObject(JSON.toJSONString(
ApiResponse.builder().httpStatus(status.value()).timestamp(LocalDateTime.now()).message((String) body.get("message")).build()),
mapTypeReference
),
status
);
}
}
由于在这个方法中, 我们手动调用了 FastJson 方法序列化 ApiResponse, 所以用 Postman 请求可以看到期望的结果:
{
"data": "{}",
"httpStatus": 500,
"message": "FilterException@SimplePerRequestFilter",
"timestamp": "2020-05-25 21:17:27"
}
再访问一个不存在的资源, 可以看到, 404 的请求也被处理到了:
{
"data": "{}",
"httpStatus": 404,
"message": "No message available",
"timestamp": "2020-05-25 21:22:36"
}
统一的数据格式 - HttpMessageConverter
简介: Message Converter 能将对象转换成不同的数据形式, 如 JSON, XML 等. SpringMVC 使用 HttpMessageConverter
转换 (读写) HTTP 请求和响应中的数据对象, 也提供了很多"开箱即用"的转换器. 如 ByteArrayHttpMessageConverter
, StringHttpMessageConverter
等.
到目前为止, 我们几乎已经处理了所有的异常. 还有一个问题, 被 @RestControllerAdvice
中标记了 @ExceptionHandler
的方法返回的是 ApiReponse, 默认会通过 Jackson 序列化, 自然是不会响应我们在 ApiResponse 中用 FastJson 的注解做的自定义序列化的设定, 所以需要修改序列化的方式, 通过 HttpMessageConverter
:
@Configuration
public class HttpMessageConvertConfiguration {
@Bean
public HttpMessageConverter<?> httpMessageConvertConfigurer() {
final FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
final FastJsonConfig config = new FastJsonConfig();
config.setSerializerFeatures(
// 保留 Map 空的字段
SerializerFeature.WriteMapNullValue,
// 将 String类型的 null 转成 ""
SerializerFeature.WriteNullStringAsEmpty,
// 将 Number类型的 null 转成 0
SerializerFeature.WriteNullNumberAsZero,
// 将 List类型的 null 转成 []
SerializerFeature.WriteNullListAsEmpty,
// 将 Boolean 类型的 null 转成 false
SerializerFeature.WriteNullBooleanAsFalse,
// 避免循环引用
SerializerFeature.DisableCircularReferenceDetect);
converter.setFastJsonConfig(config);
converter.setDefaultCharset(StandardCharsets.UTF_8);
List<MediaType> mediaTypeList = new ArrayList<>(1);
// 相当于在 Controller 上的 @RequestMapping 中的 produces = "application/json"
mediaTypeList.add(MediaType.APPLICATION_JSON);
converter.setSupportedMediaTypes(mediaTypeList);
return converter;
}
}
FastJsonHttpMessageConverter
为 FastJson 实现的 HttpMessageConverter
, 默认能处理所有类型 (supports 方法永远返回 true):
重启服务之后, 再次访问 /access 端点:
{
"httpStatus": 500,
"timestamp": "2020-05-25 21:29:30",
"message": "ControllerException@SimpleController",
"data": "{}"
}
总结
本文简单介绍了通过 @ControllerAdvice
和 BasicErrorController
来处理全局异常, 并用 FastJson 来统一序列化服务给前端返回的对象. 达到一致性.
相关源码也比较简单, 无需作额外说明.
- END -