java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot全局异常处理

SpringBoot全局异常处理方式

作者:骑个小蜗牛

本文详细介绍了Spring Boot中异常处理的多种方案,包括基于请求转发、异常处理器、过滤器等方式,并对其特点、实现方式和适用场景进行了详细介绍,作者推荐了基于请求转发和异常处理器+请求转发补充的方案,认为它们能实现统一的全局异常处理,并能自定义统一响应信息格式

异常处理方案分类

异常处理主要分为三类:

  1. 基于请求转发的方式处理异常;
  2. 基于异常处理器的方式处理异常;
  3. 基于过滤器的方式处理异常。

基于请求转发

基于请求转发的异常处理方式是真正的全局异常处理。

实现方式有:

基于异常处理器

基于异常处理器的异常处理方式其实并不是真正的全局异常处理,因为它处理不了过滤器等抛出的异常。

实现方式有:

基于过滤器

基于过滤器的异常处理方式近似与全局异常处理。它能处理过滤器及之后的环节抛出的异常。

实现方式有:

常见异常处理实现方案

1. BasicExceptionController

这是SpringBoot默认处理异常方式:一旦程序中出现了异常SpringBoot就会请求/error的url,在SpringBoot中提供了一个叫BasicExceptionController的类来处理/error请求,然后跳转到默认显示异常的页面来展示异常信息。显示异常的页面也可以自定义,在目录src/main/resources/templates/下定义一个叫error的文件,可以是jsp也可以是html 。

此种方式是通过请求转发实现的,出现异常时,会转发到请求到/error,该接口对异常进行处理返回。是最符合全局异常处理的。

可以自定义Controller继承BasicErrorController异常处理来实现异常处理的自定义。

@Slf4j
@RestController
public class MyErrorController extends BasicErrorController {

    public MyErrorController() {
        super(new DefaultErrorAttributes(), new ErrorProperties());
    }

    /**
     * produces 设置返回的数据类型:application/json
     * @param request 请求
     * @return 自定义的返回实体类
     */
    @Override
    @RequestMapping(value = "", 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);
        String code = body.get("status").toString();
        String message = body.get("message").toString();
        return new ResponseEntity(ApiUtil.fail(message), HttpStatus.OK);
    }
}

需要注意

1.该种方式能获取到的信息时有限的。一般情况只能获取到下面这几个参数(特殊情况会有补充参数)。

现在一般项目需要的响应信息都是自定义统一格式的JSON(code、msg、data)。对于自定义业务错误码code不好得到,对于错误信息msg有时得到的也不一定是你所想要的(简单说就是一些特殊的异常描述信息不好得到)。

比如:自定义的参数校验信息

@NotNull(message = "主键不能为空")

message参数取到的并不是“主键不能为空”。

2.当出现抛出两次异常,第一次被异常处理器处理,第二次异常转由BasicExceptionController处理。但能取到的异常信息可能是一次的,具体原因下面有分析。

2. @ExceptionHandler

该种方式只能作用于使用@ExceptionHandler注解的Controller的异常,对于其他Controller的异常就无能为力了,所以并不不推荐使用。

此种方式是通过异常处理器实现的,使用HandlerExceptionResolverComposite异常处理器中的ExceptionHandlerExceptionResolver异常处理器处理的。

@RestController
public class TestController {

    @GetMapping("test9")
    public FundInfo test9() throws Exception {
        throw new Exception("test9 error");
    }

    @GetMapping("test10")
    public FundInfo test10() throws Exception {
        throw new IOException("test10 error");
    }

    @ExceptionHandler(Exception.class)
    public ApiResult exceptionHandler(Exception e) {
        return ApiUtil.custom(500, e.getMessage());
    }
}

注意:如果既在具体Controller使用了@ExceptionHandler,也定义了全局异常处理器类(@ControllerAdvice+@ExceptionHandler),优先使用Controller定义的@ExceptionHandler处理。如果处理不了,才会使用全局异常处理器处理。

3. @ControllerAdvice+@ExceptionHandler

使用 @ControllerAdvice+@ExceptionHandler注解能够进行近似全局异常处理,这种方式推荐使用

一般说它只能处理控制器中抛出的异常,这种说法并不准确,其实它能处理DispatcherServlet.doDispatch方法中DispatcherServlet.processDispatchResult方法之前捕捉到的所有异常,包括:拦截器、参数绑定(参数解析、参数转换、参数校验)、控制器、返回值处理等模块抛出的异常。

此种方式是通过异常处理器实现的,使用HandlerExceptionResolverComposite异常处理器中的ExceptionHandlerExceptionResolver异常处理器处理的。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
	......省略代码......
	try {
		ModelAndView mv = null;
		Exception dispatchException = null;

		try {
			......省略代码......
			mappedHandler = getHandler(processedRequest);
			......省略代码......
			HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
			......省略代码......
			if (!mappedHandler.applyPreHandle(processedRequest, response)) {
				return;
			}
			......省略代码......
			mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
			if (asyncManager.isConcurrentHandlingStarted()) {
				return;
			}
			......省略代码......
			mappedHandler.applyPostHandle(processedRequest, response, mv);
		}
		catch (Exception ex) {
			dispatchException = ex;
		}
		catch (Throwable err) {
			dispatchException = new NestedServletException("Handler dispatch failed", err);
		}
		processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
	}
	catch (Exception ex) {
		......省略代码......
	}
	catch (Throwable err) {
		......省略代码......
	}
	finally {
		......省略代码......
	}
}

使用方式

定义一个类,使用@ControllerAdvice注解该类,使用@ExceptionHandler注解方法。@RestControllerAdvice注解是@ControllerAdvice注解的扩展(@RestControllerAdvice=@ControllerAdvice+@ResponseBody),返回值自动为JSON的形式。

/**
 * 全局异常处理器
 */
@Slf4j
@SuppressWarnings("ALL")
@RestControllerAdvice
public class MyGlobalExceptionHandler {

    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.OK)
    public ApiResult bindException(HttpServletRequest request,
                                   HttpServletResponse response,
                                   BindException exception) {
        return ApiUtil.fail(exception.getBindingResult().getFieldError().getDefaultMessage());
    }

    @ExceptionHandler(org.springframework.web.bind.MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.OK)
    public ApiResult methodArgumentNotValidException(HttpServletRequest request,
                                                     HttpServletResponse response,
                                                     MethodArgumentNotValidException exception) {
        return ApiUtil.fail(exception.getBindingResult().getFieldError().getDefaultMessage());
    }

    @ExceptionHandler(MissingServletRequestParameterException.class)
    @ResponseStatus(HttpStatus.OK)
    public ApiResult methodArgumentNotValidException(HttpServletRequest request,
                                                     HttpServletResponse response,
                                                     MissingServletRequestParameterException exception) {
        return ApiUtil.fail(exception.getMessage());
    }

    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.OK)
    public ApiResult methodArgumentNotValidException(HttpServletRequest request,
                                                     HttpServletResponse response,
                                                     ConstraintViolationException exception) {
        System.out.println(exception.getLocalizedMessage());
        Iterator<ConstraintViolation<?>> iterator = exception.getConstraintViolations().iterator();
        if (iterator.hasNext()) {
            ConstraintViolationImpl next = (ConstraintViolationImpl)iterator.next();
            return ApiUtil.fail(next.getMessage());
        }
        return ApiUtil.fail(exception.getMessage());
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.OK)
    public ApiResult exception(HttpServletRequest request,
                               HttpServletResponse response,
                               Exception exception) {
        return ApiUtil.fail(exception.getMessage());
    }
}

@ResponseStatus注解

作用:指定http状态码,正确执行时返回该状态码,但方法执行报错时,该返回啥状态码就是啥状态码,指定的状态码无效。

4. SimpleMappingExceptionResolver

使用简单映射异常处理器处理异常,通过配置SimpleMappingExceptionResolver类也是进行近似全局异常处理,但该种方式不能得到具体的异常信息,且返回的是视图不推荐使用

此种方式是通过异常处理器实现的,使用SimpleMappingExceptionResolver异常处理器处理的。

@Configuration
public class GlobalExceptionConfig {

   @Bean
   public SimpleMappingExceptionResolver getSimpleMappingExceptionResolver(){
       SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
       /**
        * 参数一:异常的类型,这里必须要异常类型的全名
        * 参数二:要跳转的视图名称
        */
       Properties mappings = new Properties();
       mappings.put("java.lang.ArithmeticException", "error1");
       mappings.put("java.lang.NullPointerException", "error1");
       mappings.put("java.lang.Exception", "error1");
       mappings.put("java.io.IOException", "error1");
       // 设置异常与视图的映射信息
       resolver.setExceptionMappings(mappings);
       return resolver;
   }
}

5. HandlerExceptionResolver

实现HandlerExceptionResolver接口来处理异常,该种方式是近似全局异常处理

此种方式是通过异常处理器实现的,使用自定义的异常处理器(实现HandlerExceptionResolver接口)处理的。

public class MyExceptionResolver extends AbstractHandlerExceptionResolver {

    /**
     * 异常解析器的顺序, 数值越小,表示优先级越高
     * @return
     */
    @Override
    public int getOrder() {
        return -999999;
    }

    @Override
    protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        try {
            response.getWriter().write(JSON.toJSONString(ApiUtil.fail(ex.getMessage())));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

AbstractHandlerExceptionResolver类实现了HandlerExceptionResolver接口。

6. Filter

基于过滤器的异常处理方式,比异常处理器处理的范围要大一些(能处理到Filter过滤器抛出的异常),更近似全局异常处理。使用自定义过滤器进行异常处理时,该过滤器应该放到过滤链的第一个位置,这样才能保证能处理到后续过滤器抛出的异常。

    @Bean
    ExceptionFilter exceptionFilter() {
        return new ExceptionFilter();
    }

    @Bean
    public FilterRegistrationBean exceptionFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(exceptionFilter());
        registration.setName("exceptionFilter");
        //此处尽量小,要比其他Filter靠前
        registration.setOrder(-1);
        return registration;
    }
/**
 * 自定义异常过滤器
 * 用于处理Controller外抛出的异常(如Filter抛出的异常)
 */
@Slf4j
public class ExceptionFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException,IOException {
        try {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        } catch (IOException e) {
            httpServletResponse.getWriter().write(JSON.toJSONString(ApiUtil.fail(e.getMessage())));
        }
    }
}

上面的写法其实还是有一定问题的,如果进入了catch就会重复写入httpServletResponse,可能会导致产生一些列的问题。

举例一个问题来说明,通过上面的写法,这样的对响应的写入一般是累加的,可能会导致返回的数据格式有问题,比如:当异常处理器处理了Controller抛出的异常,写入了响应,然后过滤器又抛出了异常,被ExceptionFilter给catch到,这就有一次处理了异常,写入了响应,最后的到的响应数据可能是这样的:

{
    "code": 500,
    "msg": "Controller error"
}{
    "code": 505,
    "msg": "Filter error"
}

这个时候我们一般会使用代理类来再次封装Response,filterChain.doFilter传递的是封装后的代理类。

Response代理类

/**
 * Response代理类
 */
public class ResponseWrapper extends HttpServletResponseWrapper {
    private ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

    private PrintWriter printWriter = new PrintWriter(outputStream);

    public ResponseWrapper(HttpServletResponse response) {
        super(response);
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return new ServletOutputStream() {
            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setWriteListener(WriteListener writeListener) {

            }

            @Override
            public void write(int b) throws IOException {
                outputStream.write(b);
            }

            @Override
            public void write(byte[] b) throws IOException {
                outputStream.write(b);
            }

            @Override
            public void write(byte[] b, int off, int len) throws IOException {
                outputStream.write(b, off, len);
            }

            @Override
            public void flush() throws IOException {
                outputStream.flush();
            }
        };
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        return printWriter;
    }

    public void flush(){
        try {
            printWriter.flush();
            printWriter.close();
            outputStream.flush();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public byte[] getContent() {
        flush();
        return outputStream.toByteArray();
    }
}

自定义过滤器类修改为

@Slf4j
public class ExceptionFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException,IOException {
        try {
            // 封装Response,得到代理对象
            ResponseWrapper responseWrapper = new ResponseWrapper(httpServletResponse);
            // 使用代理对象
            filterChain.doFilter(httpServletRequest, responseWrapper);
            // 读取响应内容
            byte[] bytes = responseWrapper.getContent();
            // 这里可以对响应内容进行修改等操作

            // 模拟Filter抛出异常
            if (true) {
                throw new IOException("Filter error");
            }

            // 内容重新写入原响应对象中
            httpServletResponse.getOutputStream().write(bytes);

        } catch (Exception e) {
            httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
            httpServletResponse.getOutputStream().write(JSON.toJSONString(ApiUtil.fail(e.getMessage())).getBytes());
        }
    }
}

全局异常处理实现方案

要想实现正在的全局异常处理,显然只通过异常处理器的方式处理是不够的,这种方案处理不了过滤器等抛出的异常。

全局异常处理的几种实现方案:

1. 请求转发

该方案貌似不好获取到特殊的异常描述信息(没仔细研究),如参数校验中的message属性信息:

@NotNull(message = "主键不能为空")

本方案通过自定义错误处理Controller继承BasicExceptionController来实现。

具体实现参考:常用异常处理实现方案1

2. 异常处理器+请求转发补充

(1)自定义异常处理Controller实现BasicExceptionController

具体实现参考:常用异常处理实现方案1

(2)异常处理器实现

3. 过滤器

具体实现参考:常用异常处理实现方案6

4. 异常处理器+过滤器补充

创建自定义过滤器bean

    @Bean
    ExceptionFilter exceptionFilter() {
        return new ExceptionFilter();
    }
    
    @Bean
    public FilterRegistrationBean exceptionFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(exceptionFilter());
        registration.setName("exceptionFilter");
        //此处尽量小,要比其他Filter靠前
        registration.setOrder(-1);
        return registration;
    }

方式1:@ControllerAdvice+@ExceptionHandler+Filter(推荐使用

@ControllerAdvice+@ExceptionHandler的实现参考:常用异常处理实现方案3

Filter实现:

@Slf4j
public class ExceptionFilter extends OncePerRequestFilter {

    /**
     * 遇到的坑,ExceptionFilter对象的创建没有交给Spring容器(直接new的),导致@Autowired注入不会生效
     */
    @Autowired
    private HandlerExceptionResolver handlerExceptionResolver;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException,IOException {
        try {
            // 封装Response,得到代理对象
            ResponseWrapper responseWrapper = new ResponseWrapper(httpServletResponse);
            // 使用代理对象
            filterChain.doFilter(httpServletRequest, responseWrapper);
            // 读取响应内容
            byte[] bytes = responseWrapper.getContent();
            // 这里可以对响应内容进行修改等操作

            // 模拟Filter抛出异常
            if (true) {
                throw new IOException("Filter error");
            }

            // 内容重新写入原响应对象中
            httpServletResponse.getOutputStream().write(bytes);

        } catch (Exception e) {
            handlerExceptionResolver.resolveException(httpServletRequest, httpServletResponse, null, e);
        }
    }
}

注入的HandlerExceptionResolver其实是HandlerExceptionResolverComposite异常处理器,最终是使用异常处理器中的ExceptionHandlerExceptionResolver异常处理器处理的。

方式2:HandlerExceptionResolver+Filter

HandlerExceptionResolver的实现参考:常用异常处理实现方案5

Filter的实现:注入的MyExceptionResolver是我们自定义的异常处理器。

@Slf4j
public class ExceptionFilter extends OncePerRequestFilter {

    @Autowired
    private MyExceptionResolver myExceptionResolver;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException,IOException {
        try {
            // 封装Response,得到代理对象
            ResponseWrapper responseWrapper = new ResponseWrapper(httpServletResponse);
            // 使用代理对象
            filterChain.doFilter(httpServletRequest, responseWrapper);
            // 读取响应内容
            byte[] bytes = responseWrapper.getContent();
            // 这里可以对响应内容进行修改等操作

            // 模拟Filter抛出异常
            if (true) {
                throw new IOException("Filter error");
            }

            // 内容重新写入原响应对象中
            httpServletResponse.getOutputStream().write(bytes);

        } catch (Exception e) {
            myExceptionResolver.resolveException(httpServletRequest, httpServletResponse, null, e);
        }
    }
}

注意事项

方案推荐

请求转发推荐)。

异常处理+请求转发补充个人最推荐)。

过滤器不推荐)。

异常处理器+过滤器补充不太推荐)。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文