SpringBoot 实践-Filter 中的异常处理和 Controller 中的异常处理

https://juejin.im/post/5e47f5a8e51d4526ff02484ahtml


本篇主要是记录如何使用 SpringBoot 所提供的 ErrorController 这个接口能力;其内置了一个 BasicErrorController 对异常进行统一的处理,当在 Controller 发生异常的时候会自动把请求 forward 到 /error 这个请求 path 下(/error 是 SpringBoot 提供的一个默认的mapping)。BasicErrorController 提供两种返回错误:一、页面返回;二、json 返回。java

背景

开发中遇到的一个问题:项目中全部的 rest 请求均是经过 json 形式返回,且自定义了一个统一的数据结构对象,以下: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。app

直接将 Response 写回去

利用 ServletResponse 中提供的 PrintWriter,将 Response 以 json 格式直接 print 回去。大概代码以下:ide

@Override
public 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,再也不继续过滤器链。post

抛出异常,经过 BasicErrorController 方式处理

这种方式是利用了 SpringBoot 自己提供的能力,能够更优雅的处理错误信息。代码大体以下:测试

一、是在 Filter 中就直接抛出一个异常this

@Override
public 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);
}
复制代码










二、定义一个异常处理的 Controller

这里定义一个 TokenErrorController ,继承自 SpringBoot 提供的 BasicErrorController 这个类,而后重写 error 这个方法(若是是页面的话,重写 errorHtml 这个方法),用于返回自定义的 Response 数据。代码以下:

@RestController
public 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 注解编写一个全局异常处理器。

@RestControllerAdvice
public 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 进行处理。

水平有限,文章若是表述错误的地方,但愿各位大佬给予指正~

相关文章
相关标签/搜索