@ExceptionHandler or HandlerExceptionResolver?如何优雅处理全局异常?【享学Spring MVC】

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

本文链接:https://blog.csdn.net/f641385712/article/details/102294670

每篇一句

你现在多学一样本事,将来就少说一句求人的话

前言

阅读上文,了解到了可以通过自定义HandlerExceptionResolver实现来处理程序异常,当然Spring MVC也内置了一些实现来对异常处理进行支持。但是作为新时代的程序员,我估计已经很少人知道HandlerExceptionResolver这个异常处理器接口(更有甚者连ModelAndView都没听说过也大有人在啊),虽然这不应该,但存在即合理。因此从现象上可以认为使用自定义HandlerExceptionResolver实现的方式去处理异常已经out了,它已经被新的方式所取代:@ExceptionHandler方式,这就是本章节的核心议题,来探讨它的使用以及原理。

回忆上篇文章讲述HandlerExceptionResolver,你是否疑问过这个问题:通过HandlerExceptionResolver如何返回一个json串呢?其实这个问题雷同于:源生Servlet如何给前端返回一个json串呢?因为上文的示例都是返回的一个ModelAndView页面,so本文在最开头先解决这个疑问,为下面内容做个铺垫吧。

HandlerExceptionResolver如何返回JSON格式数据?

基于上篇文章案例自定义了一个异常处理器来处理Handler抛出的异常,示例中返回的是一个页面ModelAndView。但是通常情况下我们的应用都是REST应用,我们的接口返回的都是一个JSON串,那么若接口抛出异常的话我们处理好后也同样的返回一个JSON串比返回一个页面更为合适。 这时若你项目较老,使用的仍旧是HandlerExceptionResolver方式处理异常的话,我在本处提供两种处理方式,供以参考:

方式一:response直接输出json

自定义异常处理器(匿名实现):

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
        // 自定义异常处理器一般请放在首位
        exceptionResolvers.add(0, new AbstractHandlerExceptionResolver() {
            @Override
            protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
                response.setContentType("application/json;charset=UTF-8");
                response.setCharacterEncoding("UTF-8");
                try {
                    String jsonStr = "";
                    if (ex instanceof BusinessException) {
                        response.setStatus(HttpStatus.OK.value());
                        jsonStr = "{'code':100001,'message':'业务异常,请联系客服处理'}";
                    } else {
                        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
                        jsonStr = "{'code':500,'message':'服务器未知异常'}";
                    }
                    response.getWriter().print(jsonStr);
                    response.getWriter().flush();
                    response.getWriter().close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            }
        });
    }
    
}

访问截图如下:

注意事项:

  1. 因为return null,所以后面若还有处理器将继续执行。但因为本处已把response close了,因此请确保后面不会再使用此response
  2. 若所有Resolver处理完后还是return null,那Spring MVC将直接throw ex,因此你看到的效果是:控制台上有异常栈,但是前段页面上显示是友好的json串。
  3. 因为木有ModelAndView(值为null),所以不会有渲染步骤,因此后续步骤Spring MVC也不会再使用到response(自定义的拦截器除外~)。
方式二:借助MappingJackson2JsonView

自定义异常处理器,借助MappingJackson2JsonView这个json视图实现:

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
        // 自定义异常处理器一般请放在首位
        exceptionResolvers.add(0, new AbstractHandlerExceptionResolver() {
            @Override
            protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
                ModelAndView mv = new ModelAndView();
                MappingJackson2JsonView view = new MappingJackson2JsonView();
                view.setJsonPrefix("fsxJson"); // 设置JSON前缀,有的时候很好用的哦
                //view.setModelKey(); // 让只序列化指定的key
                mv.setView(view);

                // 这样添加key value就非常方便
                mv.addObject("code", "100001");
                mv.addObject("message", "业务异常,请联系客服处理");
                return mv;
            }
        });
    }
    
}

访问截图如下:

显然这种使用JsonView的方式代码看起来更加舒服,使用起来更加的面向对象。

这两种方式都是基于自定义HandlerExceptionResolver实现类的方式来处理异常,最终给前端返回一个json串。虽然方式二看起来步骤也不麻烦,也够面向对象,但接下来的@ExceptionHandler方式可谓是杀手级的应用~

@ExceptionHandler

此注解是Spring 3.0后提供的处理异常的注解,整个Spring3.0+中新增了大量的能力来对REST应用提供支持,此注解便是其中之一。 它(只能)标注在方法上,可以使得这个方法成为一个异常处理器,处理指定的异常类型。

// @since 3.0
@Target(ElementType.METHOD) // 只能标注在方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
	// 指定异常类型,可以多个
	Class<? extends Throwable>[] value() default {};
}

上篇讲解HandlerExceptionResolver的原理部分讲到了,DispatcherServlet对异常的处理最终都是无一例外的交给了HandlerExceptionResolver异常处理器,因此很容易想到@ExceptionHandler它的底层实现原理其实也是一个异常处理器,它便是:ExceptionHandlerExceptionResolver

在分析它之前,需要先前置介绍两个类:AbstractHandlerMethodExceptionResolverExceptionHandlerMethodResolver

AbstractHandlerMethodExceptionResolver

它是ExceptionHandlerExceptionResolver的抽象父类,服务于处理器类型是HandlerMethod类型的抛出的异常,它并不规定实现方式必须是@ExceptionHandler。它复写了抽象父类AbstractHandlerExceptionResolvershouldApplyTo方法:

// @since 3.1 专门处理HandlerMethod类型是HandlerMethod类型的异常
public abstract class AbstractHandlerMethodExceptionResolver extends AbstractHandlerExceptionResolver {
	
	// 只处理HandlerMethod这种类型的处理器抛出的异常~~~~~~
	@Override
	protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) {
		if (handler == null) {
			return super.shouldApplyTo(request, null);
		} else if (handler instanceof HandlerMethod) {
			HandlerMethod handlerMethod = (HandlerMethod) handler;
			// 可以看到最终getBean表示最终哪去验证的是它所在的Bean类,而不是方法本身
			// 所以异常的控制是针对于Controller这个类的~
			handler = handlerMethod.getBean(); 
			return super.shouldApplyTo(request, handler);
		} else {
			return false;
		}
	}

	@Override
	@Nullable
	protected final ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
		return doResolveHandlerMethodException(request, response, (HandlerMethod) handler, ex);
	}

	@Nullable
	protected abstract ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception ex);
}

此抽象类非常简单:规定了只处理HandlerMethod抛出的异常。

ExceptionHandlerMethodResolver(重要)

它是一个会在Class及Class的父类中找出带有@ExceptionHandler注解的类,该类带有key为Throwable,value为Method缓存属性,提供匹配效率。

// @since 3.1
public class ExceptionHandlerMethodResolver {

	// A filter for selecting {@code @ExceptionHandler} methods.
	public static final MethodFilter EXCEPTION_HANDLER_METHODS = method -> AnnotatedElementUtils.hasAnnotation(method, ExceptionHandler.class);

	// 两个缓存:key:异常类型   value:目标方法Method
	private final Map<Class<? extends Throwable>, Method> mappedMethods = new HashMap<>(16);
	private final Map<Class<? extends Throwable>, Method> exceptionLookupCache = new ConcurrentReferenceHashMap<>(16);

	// 唯一构造函数
	// detectExceptionMappings:传入method,找到这个Method可以处理的所有的异常类型们(注意此方法的逻辑)
	// addExceptionMapping:把异常类型和Method缓存进mappedMethods里
	public ExceptionHandlerMethodResolver(Class<?> handlerType) {
		for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
			for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
				addExceptionMapping(exceptionType, method);
			}
		}
	}

	// 找到此Method能够处理的所有的异常类型
	// 1、detectAnnotationExceptionMappings:本方法或者父类的方法上标注有ExceptionHandler注解,然后读取出其value值就是它能处理的异常们
	// 2、若value值木有指定,那所有的方法入参们的异常类型,就是此方法能够处理的所有异常们
	// 3、若最终还是空,那就抛出异常:No exception types mapped to " + method
	private List<Class<? extends Throwable>> detectExceptionMappings(Method method) {
		List<Class<? extends Throwable>> result = new ArrayList<>();
		detectAnnotationExceptionMappings(method, result);
		if (result.isEmpty()) {
			for (Class<?> paramType : method.getParameterTypes()) {
				if (Throwable.class.isAssignableFrom(paramType)) {
					result.add((Class<? extends Throwable>) paramType);
				}
			}
		}
		if (result.isEmpty()) {
			throw new IllegalStateException("No exception types mapped to " + method);
		}
		return result;
	}

	// 对于添加方法一样有一句值得说的:
	// 若不同的Method表示可以处理同一个异常,那是不行的:"Ambiguous @ExceptionHandler method mapped for [" 
	// 注意:此处必须是同一个异常(比如Exception和RuntimeException不属于同一个...)
	private void addExceptionMapping(Class<? extends Throwable> exceptionType, Method method) {
		Method oldMethod = this.mappedMethods.put(exceptionType, method);
		if (oldMethod != null && !oldMethod.equals(method)) {
			throw new IllegalStateException("Ambiguous @ExceptionHandler method mapped for [" + exceptionType + "]: {" + oldMethod + ", " + method + "}");
		}
	}

	// 给指定的异常exception匹配上一个Method方法来处理
	// 若有多个匹配上的:使用ExceptionDepthComparator它来排序。若木有匹配的就返回null
	@Nullable
	public Method resolveMethod(Exception exception) {
		return resolveMethodByThrowable(exception);
	}
	// @since 5.0 递归到了couse异常类型 也会处理
	@Nullable
	public Method resolveMethodByThrowable(Throwable exception) {
		Method method = resolveMethodByExceptionType(exception.getClass());
		if (method == null) {
			Throwable cause = exception.getCause();
			if (cause != null) {
				method = resolveMethodByExceptionType(cause.getClass());
			}
		}
		return method;
	}

	//1、先去exceptionLookupCache找,若匹配上了直接返回
	// 2、再去mappedMethods这个缓存里找。很显然可能匹配上多个,那就用ExceptionDepthComparator排序匹配到一个最为合适的
	// 3、匹配上后放进缓存`exceptionLookupCache`,所以下次进来就不需要再次匹配了,这就是缓存的效果
	// ExceptionDepthComparator的基本理论上:精确匹配优先(按照深度比较)
	@Nullable
	public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
		Method method = this.exceptionLookupCache.get(exceptionType);
		if (method == null) {
			method = getMappedMethod(exceptionType);
			this.exceptionLookupCache.put(exceptionType, method);
		}
		return method;
	}
}

对于本类的功能,可总结如下:

  1. 找到指定Class类(可能是Controller本身,也可能是@ControllerAdvice)里面所有标注有@ExceptionHandler的方法们
  2. 同一个Class内,不能出现同一个(注意理解同一个的含义)异常类型被多个Method处理的情况,否则抛出异常:Ambiguous @ExceptionHandler method mapped for ... 1. 相同异常类型处在不同的Class内的方法上是可以的,比如常见的一个在Controller内,一个在@ControllerAdvice内~
  3. 提供缓存: 1. mappedMethods:每种异常对应的处理方法(直接映射代码上书写的异常-方法映射) 2. exceptionLookupCache:经过按照深度逻辑精确匹配上的Method方法
  4. 既能处理本身的异常,也能够处理getCause()导致的异常
  5. ExceptionDepthComparator的匹配逻辑是按照深度匹配。比如发生的是NullPointerException,但是声明的异常有ThrowableException,这是它会根据异常的最近继承关系找到继承深度最浅的那个异常,即Exception



ExceptionHandlerExceptionResolver(重要)

该子类实现就是用于处理标注有@ExceptionHandler注解的HandlerMethod方法的,是@ExceptionHandler功能的实现部分。

请注意命名上和ExceptionHandlerMethodResolver做区分~

// @since 3.1
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver implements ApplicationContextAware, InitializingBean {

	// 这个熟悉:用于处理方法入参的(比如支持入参里可写HttpServletRequest等等)
	@Nullable
	private List<HandlerMethodArgumentResolver> customArgumentResolvers;
	@Nullable
	private HandlerMethodArgumentResolverComposite argumentResolvers;
	
	// 用于处理方法返回值(ModelAndView、@ResponseBody、@ResponseStatus等)
	@Nullable
	private List<HandlerMethodReturnValueHandler> customReturnValueHandlers;
	@Nullable
	private HandlerMethodReturnValueHandlerComposite returnValueHandlers;

	// 消息处理器和内容协商管理器
	private List<HttpMessageConverter<?>> messageConverters;
	private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();

	// 通知(因为异常是可以做全局效果的)
	private final List<Object> responseBodyAdvice = new ArrayList<>();
	@Nullable
	private ApplicationContext applicationContext;

	// 缓存:异常类型对应的处理器
	// 它缓存着Controller本类,对应的异常处理器(多个@ExceptionHandler)~~~~
	private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap<>(64);
	// 它缓存ControllerAdviceBean对应的异常处理器(@ExceptionHandler)
	private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache = new LinkedHashMap<>();

	// 唯一构造函数:注册上默认的消息转换器
	public ExceptionHandlerExceptionResolver() {
		StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
		...
		this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
	}
	... // 省略所有的get/set方法

	@Override
	public void afterPropertiesSet() {
		// Do this first, it may add ResponseBodyAdvice beans
		// 这一步骤同RequestMappingHandlerAdapter#initControllerAdviceCache
		// 目的是找到项目中所有的`ResponseBodyAdvice`,然后缓存起来。
		// 并且把它里面所有的标注有@ExceptionHandler的方法都解析保存起来
		// exceptionHandlerAdviceCache:每个advice切面对应哪个ExceptionHandlerMethodResolver(含多个@ExceptionHandler处理方法)
		
		//并且,并且若此Advice还实现了接口:ResponseBodyAdvice。那就还可干预到异常处理器的返回值处理上(基于body)
		//可见:若你想干预到异常处理器的返回值body上,可通过ResponseBodyAdvice来实现哟~~~~~~~~~ 
		// 可见ResponseBodyAdvice连异常处理方法也是生效的,但是`RequestBodyAdvice`可就木有啦。
		initExceptionHandlerAdviceCache();

		// 注册默认的参数处理器。支持到了@SessionAttribute、@RequestAttribute
		// ServletRequest/ServletResponse/RedirectAttributes/ModelMethod等等(当然你还可以自定义)
		if (this.argumentResolvers == null) {
			List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
			this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
		}
		// 支持到了:ModelAndView/Model/View/HttpEntity/ModelAttribute/RequestResponseBody
		// ViewName/Map等等这些返回值 当然还可以自定义
		if (this.returnValueHandlers == null) {
			List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
			this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
		}
	}
	...

	// 处理HandlerMethod类型的异常。它的步骤是找到标注有@ExceptionHandler匹配的方法
	// 然后执行此方法来处理所抛出的异常
	@Override
	@Nullable
	protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {

		// 这个方法是精华,是关键。它最终返回的是一个ServletInvocableHandlerMethod可执行的方法处理器
		// 也就是说标注有@ExceptionHandler的方法最终会成为它

		// 1、本类能够找到处理方法,就在本类里找,找到就返回一个ServletInvocableHandlerMethod
		// 2、本类木有,就去ControllerAdviceBean切面里找,匹配上了也是欧克的
		//   显然此处会判断:advice.isApplicableToBeanType(handlerType) 看此advice是否匹配
		// 若两者都木有找到,那就返回null。这里的核心其实是ExceptionHandlerMethodResolver这个类
		ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
		if (exceptionHandlerMethod == null) {
			return null;
		}

		
		// 给该执行器设置一些值,方便它的指定(封装参数和处理返回值)
		if (this.argumentResolvers != null) {
			exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
		}
		if (this.returnValueHandlers != null) {
			exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
		}
	}

	...
	// 执行此方法的调用(比couse也传入进去了)
	exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod);
	... // 下面处理model、ModelAndView、view等等。最终返回一个ModelAndView
	// 这样异常梳理完成。
}

对它的功能,总结如下:

  1. @ExceptionHandler的处理和执行是由本类完成的,同一个Class上的所有@ExceptionHandler方法对应着同一个ExceptionHandlerExceptionResolver,不同Class上的对应着不同的~
  2. 标注有@ExceptionHandler的方法入参上可写:具体异常类型、ServletRequest/ServletResponse/RedirectAttributes/ModelMethod等等 1. 注意:入参写具体异常类型时只能够写一个类型。(若有多种异常,请写公共父类,你再用instanceof来辨别,而不能直接写多个)
  3. 返回值可写:ModelAndView/Model/View/HttpEntity/ModelAttribute/RequestResponseBody/@ResponseStatus等等
  4. @ExceptionHandler只能标注在方法上。既能标注在Controller本类内的方法上(只对本类生效),也可配合@ControllerAdvice一起使用(对全局生效)
  5. 对步骤4的两种情况,执行时的匹配顺序如下:优先匹配本类(本Controller),再匹配全局的。
  6. 有必要再强调一句:@ExceptionHandler方式并不是只能返回JSON串,步骤4也说了,它返回一个ModelAndView也是ok的

异常处理优先级

上篇文章 加上本文介绍了多种处理异常的方案,在实际生成环境中,我们的项目中一般确实也会存在多个HandlerExceptionResolver异常处理器,那么对于抛出的一个异常,它的处理顺序到底是怎样的呢?

理解了DispatcherServlet默认注册的异常处理器们和它们的执行原理后,再去解答这个问题就易如反掌了。这是DispatcherServlet默认注册的异常处理器们:

所以在我们没有自定义HandlerExceptionResolver来干扰这种顺序的情况下(绝大部分情况下我们都不会干扰它),最最最最先执行的便是@ExceptionHandler方式的异常处理器,只有匹配不上才会继续执行其它的处理器。 根据此规律,我从使用层面总结出一个结论,供现在还不想深入理解原理的小伙伴参考和记忆:

  1. @Controller + @ExceptionHandler优先级最高
  2. @ControllerAdvice + @ExceptionHandler次之
  3. HandlerExceptionResolver最后(一般是DefaultHandlerExceptionResolver

全局异常示例

在很多Spring MVC项目中你或许都可以看到一个名字叫GlobalExceptionHandler(名字大同小异)的类,它的作用一般被标注上了@ControllerAdvice/@RestControllerAdvice用于处理全局异常。 但据我了解,大多数项目对此类的设计是相当不完善的,它只做了一个通用处理:处理Exception类型。显然这种宽泛的处理是很不优雅的,理应做细分,那么读了本文后我相信你已有能力去协助完善这一块内容,为你的团队带来改变哈。

此处我给出示例代码,抛砖引玉仅供参考:

@Slf4j
@RestControllerAdvice // 全部返回JSON格式,因为大都是REST项目
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    // 处理所有不可知的异常,作为全局的兜底
    @ExceptionHandler(Exception.class)
    AppResponse handleException(Exception e){
        log.error(e.getMessage(), e);

        AppResponse response = new AppResponse();
        response.setFail("未知错误,操作失败!");
        return response;
    }

    // 处理所有业务异常(一般为手动抛出)
    @ExceptionHandler(BusinessException.class)
    AppResponse handleBusinessException(BusinessException e){
        log.error(e.getMessage(), e);

        AppResponse response = new AppResponse();
        response.setFail(e.getMessage());
        return response;
    }

    // 处理所有接口参数的数据验证异常(此处特殊处理了这个异常)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    AppResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e){
        log.warn(e.getMessage(), e); //此处我不建议使用error异常...

		// 关于校验的错误信息的返回,此处我知识简单处理,具体你可以加强
        AppResponse response = new AppResponse();
        response.setFail(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
        return response;
    }


    // 自己定制化处理HttpRequestMethodNotSupportedException这个异常类型喽
    @Override
    protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        log.warn(ex.getMessage());

        String method = ex.getMethod();
        String[] supportedMethods = ex.getSupportedMethods();
        Map<String, Object> map = new HashMap<>();
        map.put("code", status.value());
        map.put("message", "不支持的请求类型:" + method + ",支持的请求类型:" + Arrays.toString(supportedMethods));

        return super.handleExceptionInternal(ex, map, headers, status, request);
    }
}

可能有人并不清楚为何我要继承ResponseEntityExceptionHandler这个类,下面我就简单介绍一下它。

ResponseEntityExceptionHandler

它是个抽象类,可谓是Spring 3.2后对REST应用异常支持的一个暖心举动。它包装了各种Spring MVC在处理请求时可能抛出的异常的处理,处理结果都是封装成一个ResponseEntity对象。通过ResponseEntity我们可以指定需要响应的状态码headerbody等信息~

因为它是个抽象类,所以我们要使用它只需要定义一个标注有@ControllerAdvice的类继承于它便可(如上示例): 加上全局处理前(被DefaultHandlerExceptionResolver处理的结果):

加上后:

因此个人建议若你是REST应用,可以在全局异常处理类上都设计为继承自此类,做兜底使用。它能处理的异常类型如下(同DefaultHandlerExceptionResolver处理的异常类型):

ResponseEntityExceptionHandler:
	@ExceptionHandler({
			HttpRequestMethodNotSupportedException.class,
			HttpMediaTypeNotSupportedException.class,
			HttpMediaTypeNotAcceptableException.class,
			MissingPathVariableException.class,
			MissingServletRequestParameterException.class,
			ServletRequestBindingException.class,
			ConversionNotSupportedException.class,
			TypeMismatchException.class,
			HttpMessageNotReadableException.class,
			HttpMessageNotWritableException.class,
			MethodArgumentNotValidException.class,
			MissingServletRequestPartException.class,
			BindException.class,
			NoHandlerFoundException.class,
			AsyncRequestTimeoutException.class
		})
	@Nullable
	public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception { ... }

处理异常时又发生了异常怎么办呢?

这个问题你是否曾思考过呢?其实在理解了上文和本文的内容后,此问题的答案也就浮出水面了,强烈建议有兴趣的同学在本地调试出这种case,有助于你的理解~

结论:若处理器内部又抛出异常,一般就会交给tomcat处理把异常栈输出到前端,显示非常不友好的页面。因此:请务必保证你的异常处理程序中不要出现任何异常,保证健壮性。(当然最最最最为兜底的方案就是架构师统一设计一个HandlerExceptionResolver放在末位,用最简单、最不会出bug的代码来处理一切前面不能处理的异常)

如何优雅统一处理Filter异常

因为我们无法通过@ControllerAdvice+@ExceptionHandler的方式去处理Filter过滤器抛出的异常(理由希望读者自己能明白),所以此处我提供较为优雅的处理方式作为参考。

传统Spring MVC

指导思想步骤:

  1. catch住Filter所有异常
  2. Exception放进请求attr属性里
  3. 把该请求forward转发到专门处理错误的Controller里
  4. 该Controller里拿出异常throw出去,从而便可交给全局异常统一处理了

附参考代码: Filter:

@Component("helloFilter")
@WebFilter(urlPatterns = "/*")
public class HelloFilter extends OncePerRequestFilter {

    @Override
    protected void initFilterBean() throws ServletException {
        System.out.println("HelloFilter初始化...");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            System.out.println(1 / 0);
            filterChain.doFilter(request, response);
        } catch (Exception e) { // 捕获所有异常做转发用
            request.setAttribute(ErrorController.EXCEPTION_ATTR, e);
            request.getRequestDispatcher(ErrorController.ERROR_URL).forward(request, response);
        }
    }
}

ErrorController:

@Slf4j
@RestController
public class ErrorController {

    public static final String ERROR_URL = "/do/filter/errors";
    public static final String EXCEPTION_ATTR = ErrorController.class.getName() + ".error";

    /**
     * 把Filter里的异常同意交给全局异常处理
     */
    @GetMapping(value = "/do/filter/errors")
    public void doFilterErrors(HttpServletRequest request) throws Exception {
        throw Exception.class.cast(request.getAttribute(EXCEPTION_ATTR));
    }
}

GlobalExceptionHandler:全局异常处理

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    // 处理所有不可知的异常,作为全局的兜底
    @ExceptionHandler(Exception.class)
    Object handleException(Exception e) {
        log.error(e.getMessage(), e);
        return "hello error";
    }
}

Spring Boot

本文针对性的特别提出了SpringBoot case下的解决方案。因为SpringBoot它会把所有的异常情况都转换为请求/error,所以扩展它还是容易些的:

Filter:没必要自己catch了,交给SpringBoot全局处理即可

@Component("helloFilter")
@WebFilter(urlPatterns = "/*")
public class HelloFilter extends OncePerRequestFilter {

    @Override
    protected void initFilterBean() throws ServletException {
        System.out.println("HelloFilter初始化...");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        System.out.println(1 / 0);
        filterChain.doFilter(request, response);
    }
}
@RestController
public class MyErrorController extends BasicErrorController {

    // 最终使用的是此构造函数,所以魔方着只需要使用它即可
    // return new BasicErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers);
    public MyErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties) {
        super(errorAttributes, serverProperties.getError());
    }

    @RequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Map<String, Object>> errorJson(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity<>(status);
        }
        Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
        body.put("myErrorType", "this is my diy error");
        return new ResponseEntity<>(body, status);
    }
}

说明:若你在SpringBoot采用上面Spring MVC方式处理,优先级是更高的。至于为何:不解释

相关阅读:

web九大组件之—HandlerExceptionResolver异常处理器使用详解【享学Spring MVC】

总结

本文呼吁,在实际生产中,请务必重视对异常的处理,我想表达的包含两层含义:

  1. 什么时候该抛异常,什么情况下它不是异常。(异常会使得JVM停顿,所以异常的使用请不要泛滥)
  2. 对于异常的统一处理,请务必要分而治之。不是所有异常都叫Exception~ 1. 合理的处理异常,这对于微服务架构在服务治理层面具有重要的意义,这也是对一个优秀架构师的考验之一

本文推荐多使用@ExceptionHandler方式去处理异常,因为它不仅书写方便、容易管理,而且还有缓存,效率也稍高一些。 Tips:@ExceptionHandler仅能处理HandlerMethod方式的异常。理论上是还可以有非HandlerMethod的控制处理器的,但实际上真的还有吗?还有吗?有吗?

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券