关于springboot的异常处理以及源码分析(二)

在上一篇文章关于springboot的异常处理以及源码分析(一)我们通过官方文档起手,看到了springboot对于异常的处理大纲。以及我们分析了一些细节,这里我们将再次回顾官方文档,把其余的内容进行剖析,以及其源码逻辑都会做一个梳理。

一、自定义异常

我们上一篇文章通过自己去配置4xx 5xx的页面来实现了自定义异常页面的跳转。但是我们看到文档中还提供了一些其他的自定义异常处理的逻辑,我们这里就来一一解析。

1、@ControllerAdvice注解+@ExceptionHandler注解

1.1、案例演示

在官方文档中有这么一段描述。
在这里插入图片描述
翻译过来就是您还可以定义一个带注释的类来@ControllerAdvice定制 JSON 文档以返回特定的控制器和/或异常类型。如果YourException在与 相同的包中定义的控制器抛出,则使用 POJOAcmeController的 JSON 表示形式。
换句话就是说,如果我们用@ControllerAdvice注解标注了一个类,并且指定了扫描包的范围(不指定就是全部的),那么这个路径下的所有异常都会抛到这里,并且根据你@ExceptionHandler标注的异常类型进行匹配,能匹配上的,就会来到这里来处理异常,返回异常视图。进而我们就可以自己定义异常视图了。那么我们来测试一下。

  • 1、我们先定义一个我们自己异常,以后我们业务中都抛出这个异常然后统一处理
public class MyException extends RuntimeException {
	// 省略构造
}
  • 2、然后我们再定义一个异常页面,controllerAdviceHtml.html我们待会让他统一跳到我们这个页面。这个页面异常简单,就一个一级标题。放在静态资源目录下面就行。
    在这里插入图片描述
<!DOCTYPE html>
<html lang="en">
	<head>
	    <meta charset="UTF-8">
	    <title>SSE Chat</title>
	</head>
	<body>
		<h1>AcmeControllerAdviceHtml</h1>
	</body>
</html>
  • 3、接着我们就来定义一个全局异常处理器。
// 异常处理我们自己的controller,实际你不配就是整个包
@ControllerAdvice(basePackageClasses = MyController.class)
public class AcmeControllerAdvice extends ResponseEntityExceptionHandler {

	// 我们这里处理的异常就是我们自己定义的异常MyException,实际开发,这里可以配置多个,然后根据异常类型来处理,你自己灵活处理就行
	@ExceptionHandler(MyException.class)
	String handleControllerException(HttpServletRequest request, Throwable ex) {
		// 返回我们的视图名称,如果正常,他就会跳转去这个页面,模型就是视图,直接返回字符串
		return "controllerAdviceHtml";
	}
}
  • 4、最后我们再来定义一个controller,然后接口抛出我们自己的异常
    看看能不能被这个全局异常处理器处理到。
@RestController
@RequestMapping("/test")
public class MyController {
	@GetMapping("testControllerAdvice")
	public String testControllerAdvice() {
		// 测试 ControllerAdvice,抛出我们自己的异常
		throw new MyException("testControllerAdvice error");
	}
}

好了,我们来在页面发起这个get请求。
在这里插入图片描述
于是我们验证得到了这个玩意他是好使的,那么为什么好使呢,我们就再来看一下源码。

1.2、源码分析,ExceptionHandlerExceptionResolver登场

在分析之前,我先来梳理一下我们上一篇文章的源码逻辑。后面的几种异常我们就不梳理了,但是最后我会给出完整的流程图。

# 异常源码梳理
1、org.springframework.web.servlet.DispatcherServlet#doDispatch
首先我们在DispatcherServlet的doDispatch方法中找到了真正执行我们目标方法(也就是你的接口)的地方。
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

2、在目标方法抛出异常,之后他捕获了异常
// 把异常保存在了dispatchException 中
dispatchException = new NestedServletException("Handler dispatch failed", err);

3、然后在processDispatchResult开始处理异常
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

4、最后来到org.springframework.web.servlet.DispatcherServlet#processHandlerException
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
	exMv = resolver.resolveException(request, response, handler, ex);
	if (exMv != null) {
		break;
	}
}
在这里他遍历所有的异常处理器,然后逐个处理该异常,一旦有一个处理器能处理,就直接跳出了。OK
我们先就过到这里。

我们说他是遍历异常处理器的然后挨个处理的,那么实际上我们上一篇文章在debug的时候看到了这些异常处理器如下图,我们看到其实一共是四个处理器,其中第一个是存放一些页面能返回啥的,并不决定是哪个处理器。
在这里插入图片描述
但是我们在上文debug的时候发现,下面三个啥也没干,就直接过去了。最后走了个白页。算了我不卖关子了,直接来说,下面三个处理器就是处理你自定义异常处理的逻辑。我们这里就来debug看一下。我们直接把断点打在这里,然后发起请求。
在这里插入图片描述
然后我们来到这里的时候发现依然是那四个异常处理器,其中上面那个依然啥也没干,下面三个是一组。我们来到下面三个的处理。我们debug进来来到这里org.springframework.web.servlet.handler.HandlerExceptionResolverComposite#resolveException()
在这里插入图片描述
下面就来遍历这三个处理器,看看能不能处理,能就直接返回视图跳出了。
其中第一个处理器就是ExceptionHandlerExceptionResolver,然后就进入了他的resolveException方法,进行异常的处理。

public ModelAndView resolveException(
		HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
	// 这里是判断,这个异常处理器,能不能处理这次请求的异常,这里判断可以处理
	if (shouldApplyTo(request, handler)) {
		prepareResponse(ex, response);
		/**
			来到这里处理这个异常,处理逻辑很长,但是主要逻辑就是通过你这个异常
			去判断哪个处理器可以处理,通过注解标记的异常类型,发现是不是能处理
			如果能处理,底层会通过反射调用我们那个方法
			smoketest.simple.config.AcmeControllerAdvice#handleControllerException
			然后返回一个视图,如图1.2.1
		*/
		ModelAndView result = doResolveException(request, response, handler, ex);
		if (result != null) {
			// Print debug message when warn logger is not enabled.
			if (logger.isDebugEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) {
				logger.debug("Resolved [" + ex + "]" + (result.isEmpty() ? "" : " to " + result));
			}
			// Explicitly configured warn logger in logException method.
			logException(ex, request);
		}
		return result;
	}
	else {
		return null;
	}
}

1.2.1
于是我们这里就返回了这个视图,跳出我们的循环回到org.springframework.web.servlet.DispatcherServlet#processDispatchResult,然后往下开始渲染我们的这个视图

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
			@Nullable Exception exception) throws Exception {

		boolean errorView = false;

		if (exception != null) {
			if (exception instanceof ModelAndViewDefiningException) {
				logger.debug("ModelAndViewDefiningException encountered", exception);
				mv = ((ModelAndViewDefiningException) exception).getModelAndView();
			}
			else {
				Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
				// 这里经过遍历解析器,返回了视图
				mv = processHandlerException(request, response, handler, exception);
				errorView = (mv != null);
			}
		}

		// Did the handler return a view to render?
		if (mv != null && !mv.wasCleared()) {
			// 渲染视图,就是把我们那个页面渲染出来,然后下面就直接返回了,这样就
			// 找到了这个页面,并且渲染出来返回了。后面就tomcat会把这个页面通过流返回客户端
			render(mv, request, response);
		}
		// ...... 省略没用的
	}

2、@ResponseStatus注解,自定义异常返回

2.1、案例演示

  • 定义一个异常
// 通过在异常上标注注解,来定义异常的状态码和返回信息
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "Status Exception")
public class StatusException extends RuntimeException{

	// 省略构造
}

  • 定义接口,抛出这个异常信息
@RestController
@RequestMapping("/test")
public class MyController {
	@GetMapping("testResponseStatus")
	public String testResponseStatus() {
		// 测试 StatusException,抛出我们自己的异常
		throw new StatusException();
	}
}

我们看到返回的异常页面为:
在这里插入图片描述

2.2、源码分析,ResponseStatusExceptionResolver的作用

下面我们进行源码分析,其实经过上面你也知道了,我们有一个组合的异常解析器的集合,其中有一个是ResponseStatusExceptionResolver,其实看名字也知道,这个解析器就是给我们这个注解的异常准备的。
在这里插入图片描述
我们直接进去源码看解析逻辑,我debug都不想打了。

@Override
@Nullable
protected ModelAndView doResolveException(
		HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {

	try {
		// 判断你的异常是不是ResponseStatusException,所以其实我们没必要继承RuntimeException
		// 继承了ResponseStatusException也是可以被处理的。
		if (ex instanceof ResponseStatusException) {
			return resolveResponseStatusException((ResponseStatusException) ex, request, response, handler);
		}
		// 找到打@ResponseStatus注解的类,其实就是我们那个异常类
		ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
		if (status != null) {
			return resolveResponseStatus(status, request, response, handler, ex);
		}
		// 如果存在异常,那就开始解析,所以逻辑落到了解析这里
		if (ex.getCause() instanceof Exception) {
			return doResolveException(request, response, handler, (Exception) ex.getCause());
		}
	}
	// 省略没用的......
	return null;
}

我们接着来看解析逻辑

@Override
@Nullable
protected ModelAndView doResolveException(
		HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {

	try {
		if (ex instanceof ResponseStatusException) {
			return resolveResponseStatusException((ResponseStatusException) ex, request, response, handler);
		}

		ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
		// 如果你注解打了状态码,我们是BAD_REQUEST,他就来这里处理,因为这是非必填的,
		// 所以他要兼容
		// 那我们就看这里
		if (status != null) {
			return resolveResponseStatus(status, request, response, handler, ex);
		}
		// 如果你没打状态码,那就来这里
		if (ex.getCause() instanceof Exception) {
			return doResolveException(request, response, handler, (Exception) ex.getCause());
		}
	}
	// 省略没用的......
	return null;
}

我们再来看一下打了状态码的异常处理逻辑。

protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus, HttpServletRequest request,
		HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception {
	// 取出状态码BAD_REQUEST
	int statusCode = responseStatus.code().value();
	// 取出异常信息Status Exception
	String reason = responseStatus.reason();
	// 来这里进行视图的处理
	return applyStatusAndReason(statusCode, reason, response);
}

org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver#applyStatusAndReason

protected ModelAndView applyStatusAndReason(int statusCode, @Nullable String reason, HttpServletResponse response)
			throws IOException {
		// 因为我们是标注了异常信息的,所以这里不会走,也是一个兜底,其实和你标了一样,只是
		// 你不标,他给你生成了一个
		if (!StringUtils.hasLength(reason)) {
			response.sendError(statusCode);
		}
		else {
			// 取出异常信息
			String resolvedReason = (this.messageSource != null ?
					this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale()) :
					reason);
			/**
				这里是重点,在上面构建好了异常的相关内容之后,code和reason
				这里调用HttpServletResponse 发了一个Error的方法。下面我们单独解释
			*/
			response.sendError(statusCode, resolvedReason);
		}
		return new ModelAndView();
	}

我们看到上面最后构造好了异常相关的信息,然后触发了response.sendError(statusCode, resolvedReason);我们不妨来看看这行代码是在哪里的,
org.apache.catalina.connector.ResponseFacade#sendError(int, java.lang.String)我们看到这个方法是tomcat的,不是mvc的。所以其实这里他会直接发一个异常信息,然后就走到了我们上一篇文章说的org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml这个方法开始渲染视图,注意我们第一篇说的白页在没有异常处理器能处理的时候,会调用到/error其实也是这个逻辑。这是tomcat发送的,也就是说,在调用到这里之后其实mvc已经结束了,他后面的逻辑还会走,会触发spring的一些逻辑,对数据做一些处理,但是因为我们前端请求是和tomcat交互的,此时tomcat在发送了error之后就已经和前端返回了异常页面了通过http请求。
至于doDispatch之后的那些逻辑都已经没用了,后面就被tomcat接管了,tomcat会调用org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml然后返回渲染的视图,tomcat就会和前端交互了,你渲染的有5xx的就走你的,没有就走空白页,这些我们都在第一篇说过了。mvc后面的还会执行,但是不起作用了,因为tomcat已经和前端交互了,你这就是正常的代码运行了没业务能力。而这个error错误tomcat用/error能处理就返回,不能处理,tomcat就会给你返回那个最原始的蓝白色的那个有个猫的异常页面。

至于tomcat和mvc之间的关系,我后面会单独写文章处理。
所以你知道,我们这个注解就是他被ResponseStatusExceptionResolver解析器处理,在最后被tomcat发了/error请求,,然后tomcat会开始一个新的接口请求,其实就是/error,然后再次走org.springframework.web.servlet.DispatcherServlet#doDispatch
到达org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml,然后渲染视图,返回前端。

3、内部异常

这种异常不是我们自己的异常,他是框架的异常,比如你请求了一个不存在的参数,本来人家参数叫a,你请求了一个c。或者是参数类型不对,本来人家是字符串,你请求了个数字。或者本来人家是对象,你请求了数字,这种都是内部异常,他的处理就是在我们的第三个解析器里面。
DefaultHandlerExceptionResolver。

3.1、案例演示

就不演示了,非常简单。

3.2、源码分析,DefaultHandlerExceptionResolver的实力

先来看下他内部处理的这个解析器的注释。你能看到他会处理这些异常,然后都会给你返回去,所以以后你遇到了这种异常,可以来这个解析器直接定位。
在这里插入图片描述
我们来这里看他的异常解析逻辑。写的那是非常直白,就是N个if else判断

protected ModelAndView doResolveException(
		HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
	// 可以看到各种异常的分支都有判断,我们随便挑一个
	try {
		if (ex instanceof HttpRequestMethodNotSupportedException) {
			return handleHttpRequestMethodNotSupported(
					(HttpRequestMethodNotSupportedException) ex, request, response, handler);
		}
		else if (ex instanceof HttpMediaTypeNotSupportedException) {
			return handleHttpMediaTypeNotSupported(
					(HttpMediaTypeNotSupportedException) ex, request, response, handler);
		}
		else if (ex instanceof HttpMediaTypeNotAcceptableException) {
			return handleHttpMediaTypeNotAcceptable(
					(HttpMediaTypeNotAcceptableException) ex, request, response, handler);
		}
		else if (ex instanceof MissingPathVariableException) {
			return handleMissingPathVariable(
					(MissingPathVariableException) ex, request, response, handler);
		}
		else if (ex instanceof MissingServletRequestParameterException) {
			return handleMissingServletRequestParameter(
					(MissingServletRequestParameterException) ex, request, response, handler);
		}
		else if (ex instanceof ServletRequestBindingException) {
			return handleServletRequestBindingException(
					(ServletRequestBindingException) ex, request, response, handler);
		}
		else if (ex instanceof ConversionNotSupportedException) {
			return handleConversionNotSupported(
					(ConversionNotSupportedException) ex, request, response, handler);
		}
		else if (ex instanceof TypeMismatchException) {
			return handleTypeMismatch(
					(TypeMismatchException) ex, request, response, handler);
		}
		else if (ex instanceof HttpMessageNotReadableException) {
			return handleHttpMessageNotReadable(
					(HttpMessageNotReadableException) ex, request, response, handler);
		}
		else if (ex instanceof HttpMessageNotWritableException) {
			return handleHttpMessageNotWritable(
					(HttpMessageNotWritableException) ex, request, response, handler);
		}
		else if (ex instanceof MethodArgumentNotValidException) {
			return handleMethodArgumentNotValidException(
					(MethodArgumentNotValidException) ex, request, response, handler);
		}
		else if (ex instanceof MissingServletRequestPartException) {
			return handleMissingServletRequestPartException(
					(MissingServletRequestPartException) ex, request, response, handler);
		}
		else if (ex instanceof BindException) {
			return handleBindException((BindException) ex, request, response, handler);
		}
		else if (ex instanceof NoHandlerFoundException) {
			return handleNoHandlerFoundException(
					(NoHandlerFoundException) ex, request, response, handler);
		}
		else if (ex instanceof AsyncRequestTimeoutException) {
			return handleAsyncRequestTimeoutException(
					(AsyncRequestTimeoutException) ex, request, response, handler);
		}
	}
	// 省略没用的
}

我们以第一个case为例看他的解析逻辑:handleHttpRequestMethodNotSupported

protected ModelAndView handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex,
		HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {

	String[] supportedMethods = ex.getSupportedMethods();
	if (supportedMethods != null) {
		response.setHeader("Allow", StringUtils.arrayToDelimitedString(supportedMethods, ", "));
	}
	// 还是发送/error走tomcat交给BasicErrorController来处理,后面就都一样了。
	response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, ex.getMessage());
	return new ModelAndView();
}

4、自定义异常处理器

4.1、案例演示

你去看看上面那几个异常处理器,他们都有一个特点就是实现了HandlerExceptionResolver接口,其实我们也可以自己写一个,把他放到容器里面就完了。他也能被读到那个集合里面,遍历处理就行。

  • 定义解析器
// 放入容器
@Component
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
	@Override
	public ModelAndView resolveException(HttpServletRequest request,
										 HttpServletResponse response,
										 Object handler,
										 Exception ex) {
        try {
			/**
			 * 这里直接发tomcat,和前端交互,其实后面的就都没用了
			 */
            response.sendError(540,"levi define error message");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
		// 返回个空,这里也没啥用,后面都是没业务能力的了
        return new ModelAndView();
	}
}
  • 定义一个接口:
@RequestMapping("/test")
public class MyController {

@GetMapping("testControllerAdvice")
public String testControllerAdvice() {
	throw new MyException("testControllerAdvice error");
}

在这里插入图片描述
醉了,没有我们的540异常码,还是404,所以这里有问题。下面我们来看源码。

4.2、源码分析

其实我们也能猜到,我们的自己的那个处理器加进去之后排在后面,前面的处理器一旦符合逻辑能处理,就直接返回视图了。轮不到我们的。
代码位于org.springframework.web.servlet.DispatcherServlet#processHandlerException
在这里插入图片描述
所以我们要改变一下顺序,我们说异常处理器,你怎么弄他也还是一个spring的bean,我们通过spring的能力改变一下他的加载顺序不就行了吗。其实不就是@order注解吗。

// 优先级最高,数字越小,优先级越高
@Order(Integer.MIN_VALUE)
// 放入容器
@Component
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
	@Override
	public ModelAndView resolveException(HttpServletRequest request,
										 HttpServletResponse response,
										 Object handler,
										 Exception ex) {
        try {
			/**
			 * 这里直接发tomcat,和前端交互,其实后面的就都没用了
			 */
           response.sendError(540,"levi define error message");

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
		// 返回个空,这里也没啥用,后面都是没业务能力的了
        return new ModelAndView();
	}
}

再次来测试。
在这里插入图片描述
我们发现我们的解析器排第一了,而且因为我们的解析器没有什么判定逻辑,根本不判断是不是能解析这类异常,所以我们这个啥异常都能进来处理。这其实不对哈,实际开发可能你会覆盖别人的,所以最好是做一下判断,因为源码就是不能解析就跳过,走下一个判断。因为我这里就是演示,所以不区分了。
在这里插入图片描述
我们看到已经是我们的540码了,这就没问题了。

5、定义真正的底层 ErrorViewResolver

我们两篇文章下来看到几个点我在这里说一下。
1、当没有一个解析器能处理的时候,就会调用tomcat的response.sendError()方法。
2、当我们在异常解析器中被处理之后,会封装异常信息,然后调用response.sendError()方法。

在response.sendError()调用之后tomcat会发起一个请求,就是/error,然后被BasicErrorController接受请求,比如你是html页面发起的请求就会被转发到,org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml来处理,里面会拿到你封装的异常信息,然后通过ErrorViewResolver的接口实现类DefaultErrorViewResolver,来解析这个异常,看你的异常码是什么比如你是500,那就给你读取静态资源下面的error目录下面的5xx或者500.html(优先读取500.html,要是没有就读取系列码对应的,也就是5xx.html),这样就能转发过去了。要是你error下面都没有,那就给你默认那个白页,他自己内部拼的。
所以ErrorViewResolver 他是决定你到底转去哪个视图的。如果你有什么毛病,不想再error下面读取,那就可以自定义,然后改变这个路径,不过一般好像没人这么干。这里同样存在优先级,你可以指定优先级去覆盖。

// 优先级最高,数字越小,优先级越高
@Order(Integer.MIN_VALUE)
// 放入容器
@Component
public class MyErrorViewResolver implements ErrorViewResolver {
	@Override
	public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) throws FileNotFoundException {
		return null;// 实现你可以抄一下DefaultErrorViewResolver
	}
}

二、总结

在这里插入图片描述

  • 17
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值