推荐阅读:
47天时间,洒热血复习,我成功“挤进”了字节跳动(附Java面试题+学习笔记+算法刷题)zhuanlan.zhihu.com
图标 面试字节跳动三轮凉凉,内推4面终拿下抖音offer(Java后台研发)zhuanlan.zhihu.com 图标本篇主要是记录如何使用 SpringBoot 所提供的 ErrorController 这个接口能力;其内置了一个 BasicErrorController 对异常进行统一的处理,当在 Controller 发生异常的时候会自动把请求 forward 到 /error 这个请求 path 下(/error 是 SpringBoot 提供的一个默认的mapping)。BasicErrorController 提供两种返回错误:1、页面返回;2、json 返回。
背景
开发中遇到的一个问题:项目中所有的 rest 请求均是通过 json 形式返回,且自定义了一个统一的数据结构对象,如下:
public class Response<T> { // 数据 private T data; // success 标记 private boolean success; // 异常信息 private String error; // 省略 get set}
这个结构非常常见,相信很多开发者都这么玩过。项目中 rest 请求返回的所有结果都是以 Response 对象形式返回,如下:
@RequestMapping("test")public Response<String> testApi(){ Response<String> result = new Response<>(); result.setData("this is glmapper blog"); result.setSuccess(true); return result;}
这基本是最简化版的一个模型;出于安全考虑,现在有个需求是需要对每个请求做校验,比如校验请求中是否携带 token 这种。思路很简单就是通过拦截器或者过滤器的方式来对请求做拦截检验。
其实不管是拦截器还是过滤器,需要考虑的一个问题是,在校验不通过或者校验时产生异常的情况下,怎么把异常信息以项目中规定的统一数据格式返回,即返回 Response。
直接将 Response 写回去
利用 ServletResponse 中提供的 PrintWriter,将 Response 以 json 格式直接 print 回去。大概代码如下:
@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; String requestURI = request.getRequestURI(); // mock 测试异常请求 if (requestURI.contains("testTokenError")) { Response<String> response = new Response<>(); response.setError("token validation fails"); // 回写异常信息 returnResponse((HttpServletResponse)servletResponse,JSONObject.toJSONString(response)); // 返回 return; } chain.doFilter(servletRequest, servletResponse);}private void returnResponse(HttpServletResponse response, String data) { PrintWriter writer = null; response.setCharacterEncoding("UTF-8"); response.setContentType("text/html; charset=utf-8"); try { writer = response.getWriter(); // 通过 PrintWriter 将 data 数据直接 print 回去 writer.print(data); } catch (IOException e) { } finally { if (writer != null) writer.close(); }复制代码
这种方式比较简单和直接,print 异常数据之后直接 return,不再继续过滤器链。
抛出异常,通过 BasicErrorController 方式处理
这种方式是利用了 SpringBoot 本身提供的能力,可以更优雅的处理错误信息。代码大致如下:
1、是在 Filter 中就直接抛出一个异常
@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; String requestURI = request.getRequestURI(); // mock 测试异常请求 if (requestURI.contains("testTokenError")) { // 直接返回一个自定义的异常 throw new ValidationException("token validation fails"); } chain.doFilter(servletRequest, servletResponse);}
2、定义一个异常处理的 Controller
这里定义一个 TokenErrorController ,继承自 SpringBoot 提供的 BasicErrorController 这个类,然后重写 error 这个方法(如果是页面的话,重写 errorHtml 这个方法),用于返回自定义的 Response 数据。代码如下:
@RestControllerpublic class TokenErrorController extends BasicErrorController { // 重写 error 方法 @Override @RequestMapping(produces = { MediaType.APPLICATION_JSON_VALUE }) public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); HttpStatus status = getStatus(request); // 拿到 body 中的异常 message String message = body.get("message").toString(); // 构建 Response 对象 Response response = new Response(); // 将 message 的 设置到 response response.setError(message); // 返回 return new ResponseEntity(response, status); } // 省略其他无关代码}
这样就可以实现在不改动之前工程任何代码的情况下只处理额外 Filter 中抛出的异常了。需要注意的是,上述是通过 BasicErrorController 来接受了 Filter 抛出的异常信息,然后再通过 BasicErrorController 将异常信息进行包装并且返回。为什么要提一下这个呢?主要是为了和 SpringBoot 中基于 REST 请求层所提供的两个用于处理全局异常的注解区分,这两个注解分别是 @ControllerAdvice 和 @RestControllerAdvice,通过注解的名字其实就能看出,SpringBoot 中,可以通过这两个注解来实现对 @Controller 和 @RestController 标注的类进行全局拦截,因为是 Controller 层面的 AOP 拦截,所以对于 Filter 中抛出的异常,通过 @ControllerAdvice 和 @RestControllerAdvice 两个注解定义的全局异常处理器是没法处理的。
下面就简单介绍下 @ControllerAdvice 和 @RestControllerAdvice 这两个注解的使用。
全局异常处理
自定义一个 OtherExcepetion ,然后再使用基于 @RestControllerAdvice 注解编写一个全局异常处理器。
@RestControllerAdvicepublic class OtherExceptionHandler { // 这里只处理 OtherException 异常类型 @ExceptionHandler(value = OtherException.class) public Response<String> otherExceptionHandler(HttpServletRequest req, OtherException e){ Response response = new Response(); response.setError(e.getMessage()); return response; } // 当然你也可以定义处理其他异常的 @ExceptionHandler}
这种方式是没法处理 Filter 中异常的,只能处理 Controller 里面抛出的异常。
小结
本篇主要记录了在 SpringBoot 中如何保证 Filter 中抛出的异常能和业务一样以指定类型的对象返回,并对 SpringBoot 中提供的基于 Controller 层异常捕获处理进行简单介绍。两者处理异常的思路是不同的:
- BasicErrorController:接受来自 /error 的异常请求处理,Filter 中抛出的异常先 forward 到 /error,然后处理。
- @RestControllerAdvice:通过对于所有 @Controller 注解所标注的类进行 AOP 拦截,能够根据异常类型匹配具体的 ExceptionHandler 进行处理。
水平有限,文章如果表述错误的地方,希望各位大佬给予指正~
作者:glmapper
链接:https://juejin.im/post/5e47f5a8e51d4526ff02484a
欢迎关注专栏:Java架构技术进阶。里面有大量batj面试题集锦,还有各种技术分享,如有好文章也欢迎投稿哦。
网友评论