1.拦截器
1.1 实践
1.编写一个拦截器去实现 HandlerInterceptor 接口
/**
* 登录检查
* 1、配置好拦截器要拦截哪些请求
* 2、把这些配置放在容器中
*/
public class LoginInterceptor implements HandlerInterceptor {
/**
* 目标方法执行之前
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//检查逻辑
HttpSession session = request.getSession();
Object user = session.getAttribute("user");
if( user!=null ) {
//放行
return true;
}
//拦截住。未登录,跳转到登录页
request.setAttribute("msg","请先登录");
request.getRequestDispatcher("/").forward(request,response);
return false;
}
/**
* 目标方法执行完成以后
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
/**
* 页面渲染以后
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
2.拦截器注册到容器中(实现 WebMvcConfigurer 的 addInterceptors())并指定拦截规则
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") //拦截的路径,所有请求都被拦截,包括静态资源
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); //放行的路径
}
}
1.2 拦截器源码分析
DispatcherServlet 中 Interceptor 相关源码如下
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
// 首先找到请求对应的HandlerExecutionChain.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
...
//执行所有拦截器的 preHandler 方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
//有任何拦截器 preHandler 方法返回 false,则直接推出
return;
}
// 所有拦截器 preHandler 方法返回 true,执行目标方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
...
//执行所有拦截器的 postHandler 方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
...
//processDispatchResult 中页面渲染完成后,会倒序执行所有拦截器的 afterCompletion 方法
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
//出现异常,则会倒序执行拦截器的 afterCompletion 方法(已经执行过的烂机器)
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
//出现异常,则会倒序执行拦截器的 afterCompletion 方法(已经执行过的烂机器)
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
...
}
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
...
render(mv, request, response);
...
if (mappedHandler != null) {
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
private void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, Exception ex) throws Exception {
if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, ex);
}
throw ex;
}
HandlerExecutionChain 的源码如下
public class HandlerExecutionChain {
...
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for (int i = 0; i < this.interceptorList.size(); i++) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
triggerAfterCompletion(request, response, null);
return false;
}
this.interceptorIndex = i;
}
return true;
}
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
throws Exception {
for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
interceptor.postHandle(request, response, this.handler, mv);
}
}
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
for (int i = this.interceptorIndex; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
try {
interceptor.afterCompletion(request, response, this.handler, ex);
}
catch (Throwable ex2) {
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
}
}
}
...
}
1、根据当前请求,找到 HandlerExecutionChain【处理请求的 handler 以及 handler 的所有拦截器】
2、顺序执行所有拦截器的 preHandler 方法
如果当前拦截器的 preHandler() 的返回值为 true ,则执行下一个拦截器的 preHandler() ;
如果当前拦截器的 preHandler() 的返回值为 false ,则倒叙执行所有以及执行了的拦截器的 afterCompletion() 方法。
3、如果任何一个拦截器 preHandler() 的返回值为 false,直接跳出,不执行目标方法。
4、拦截器 preHandler() 的返回值为 true,执行目标放方法
5、倒序执行所有拦截器的 postHandle 方法。
6、前面的步骤有任何的异常都会直接倒序触发拦截器 afterCompletion() 方法
7、页面渲染完成后,也会倒序触发拦截器 afterCompletion() 方法
2. 文件上传
2.1 实践
前台 HTML
<!Document>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>测试</title>
</head>
<body>
<form action="login" th:action="@{/form/uploadFile}" method="post" enctype="multipart/form-data">
选择文件: <input type="file" name="file" id="file"/> <br>
选择多个文件: <input type="file" name="multiFile" id="multiFile" multiple/> <br>
<input type="submit" value="上传">
</form>
</body>
</html>
后端源码
/**
* 文件上传测试
*/
@Slf4j
@Controller
public class FileTestController {
@GetMapping("/form/file")
public String formFile() {
return "form/file";
}
@PostMapping("/form/uploadFile")
public String uploadFile(@RequestPart("file") MultipartFile file,
@RequestPart("multiFile") MultipartFile[] multiFile) throws IOException {
log.info("file={},multiFile={}", file.getSize(), multiFile.length);
if (!file.isEmpty()) {
String fiename = file.getOriginalFilename();
file.transferTo(new File("F:\\test\\"+fiename));
}
if (multiFile.length > 0){
for (MultipartFile multipartFile : multiFile) {
if (!multipartFile.isEmpty()) {
String fiename = multipartFile.getOriginalFilename();
multipartFile.transferTo(new File("F:\\test\\multi\\"+fiename));
}
}
}
return "main";
}
}
控制文件上传大小限制的 application.yml 配置
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 100MB
2.2 源码解析
文件上传由 MultipartAutoConfiguration 自动配置的其解析器 MultipartResolver。
原理步骤:
- 请求进来使用文件上传解析判断( isMultipart() ),并封装文件上传请求。
multipartResolver.resolveMultipart() 返回 MultipartHttpServletRequest
- 参数解析器解析请求中的文件内容,并封装成 MultiPartFile。
- 将 request 中文件信息封装成 map,MultiValueMap<String, MultipartFile>
首先在 SpringBoot 启动时,已经通过 MultipartAutoConfiguration 自动加载了文件上传所需的组件和配置。
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class })
@ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", matchIfMissing = true)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(MultipartProperties.class) //文件上传相关配置类
public class MultipartAutoConfiguration {
private final MultipartProperties multipartProperties;
public MultipartAutoConfiguration(MultipartProperties multipartProperties) {
this.multipartProperties = multipartProperties;
}
@Bean
@ConditionalOnMissingBean({ MultipartConfigElement.class, CommonsMultipartResolver.class })
public MultipartConfigElement multipartConfigElement() {
return this.multipartProperties.createMultipartConfig();
}
//文化上传解析器
@Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
@ConditionalOnMissingBean(MultipartResolver.class)
public StandardServletMultipartResolver multipartResolver() {
StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
return multipartResolver;
}
}
文件上传解析器源码如下:
public class StandardServletMultipartResolver implements MultipartResolver {
...
public boolean isMultipart(HttpServletRequest request) {
return StringUtils.startsWithIgnoreCase(request.getContentType(), this.strictServletCompliance ? "multipart/form-data" : "multipart/");
}
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
}
public void cleanupMultipart(MultipartHttpServletRequest request) {
if (!(request instanceof AbstractMultipartHttpServletRequest) || ((AbstractMultipartHttpServletRequest)request).isResolved()) {
try {
Iterator var2 = request.getParts().iterator();
while(var2.hasNext()) {
Part part = (Part)var2.next();
if (request.getFile(part.getName()) != null) {
part.delete();
}
}
} catch (Throwable var4) {
LogFactory.getLog(this.getClass()).warn("Failed to perform cleanup of multipart items", var4);
}
}
}
}
当有请求过来时,首先进入 DispatcherServlet 的 doDispatch() 方法。
第一步、调用 processedRequest = checkMultipart(request) 检查请求中是否有文件上传,如果有的话,则封装【即 new StandardMultipartHttpServletRequest】
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
//1.首先判断调用解析器判断请求是否包含文件上传
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
if (DispatcherType.REQUEST.equals(request.getDispatcherType())) {
logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
}
}
else if (hasMultipartException(request)) {
logger.debug("Multipart resolution previously failed for current request - " +
"skipping re-resolution for undisturbed error rendering");
}
else {
try {
//如果是的话,则调用 文件上传解析器 解析请求。即 new StandardMultipartHttpServletRequest
return this.multipartResolver.resolveMultipart(request);
}
catch (MultipartException ex) {
if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
logger.debug("Multipart resolution failed for error dispatch", ex);
// Keep processing error dispatch with regular request handle below
}
else {
throw ex;
}
}
}
}
// If not returned before: return original request.
return request;
}
第二步、参数解析器解析请求中的文件内容,并封装成 MultiPartFile
接着调试进入方法执行函数【位于 RequestMappingHandlerAdapter】invokeHandlerMethod()。与之前的请求参数解析类似,首先是 RequestMappingHandlerAdapter 中有很多个参数解析器,而跟踪得知,文件上传的参数解析器是 RequestPartMethodArgumentResolver 。
接着往下跟踪到参数解析函数 【位于 InvocableHandlerMethod】
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
return this.doInvoke(args);
}
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
MethodParameter[] parameters = this.getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
} else {
Object[] args = new Object[parameters.length];
for(int i = 0; i < parameters.length; ++i) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] == null) {
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
} catch (Exception var10) {
if (logger.isDebugEnabled()) {
String exMsg = var10.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw var10;
}
}
}
return args;
}
}
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
HandlerMethodArgumentResolver resolver = this.getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
} else {
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
}
最终我们可以看到是调用 文件上传解析器【RequestPartMethodArgumentResolver 】的 resolveArgument() 方法对请求参数中的文件参数进行解析,并包装成 MultiPartFile 。
public class RequestPartMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver {
...
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest request, @Nullable WebDataBinderFactory binderFactory) throws Exception {
...
Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
...
}
...
}
第三步、将 request 中文件信息封装成 map,MultiValueMap<String, MultipartFile>
MultipartResolutionDelegate.resolveMultipartArgument() 方法如下
@Nullable
public static Object resolveMultipartArgument(String name, MethodParameter parameter, HttpServletRequest request) throws Exception {
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest)WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);
boolean isMultipart = multipartRequest != null || isMultipartContent(request);
if (MultipartFile.class == parameter.getNestedParameterType()) {//入参类型为 MultipartFile
if (!isMultipart) {
return null;
} else {
if (multipartRequest == null) {
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
return ((MultipartHttpServletRequest)multipartRequest).getFile(name);
}
} else {
List parts;
if (isMultipartFileCollection(parameter)) { //入参类型为MultipartFile集合
if (!isMultipart) {
return null;
} else {
if (multipartRequest == null) {
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
parts = ((MultipartHttpServletRequest)multipartRequest).getFiles(name);
return !parts.isEmpty() ? parts : null;
}
} else if (isMultipartFileArray(parameter)) {//入参类型为MultipartFile数组
if (!isMultipart) {
return null;
} else {
if (multipartRequest == null) {
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
parts = ((MultipartHttpServletRequest)multipartRequest).getFiles(name);
return !parts.isEmpty() ? parts.toArray(new MultipartFile[0]) : null;
}
} else if (Part.class == parameter.getNestedParameterType()) {//入参类型为Part
return !isMultipart ? null : request.getPart(name);
} else if (isPartCollection(parameter)) {//入参类型为Part集合
if (!isMultipart) {
return null;
} else {
parts = resolvePartList(request, name);
return !parts.isEmpty() ? parts : null;
}
} else if (isPartArray(parameter)) {//入参类型为Part数组
if (!isMultipart) {
return null;
} else {
parts = resolvePartList(request, name);
return !parts.isEmpty() ? parts.toArray(new Part[0]) : null;
}
} else {
return UNRESOLVABLE;
}
}
}
MultipartResolutionDelegate.resolveMultipartArgument() 会根据方法的参数类型,调用对应的方法。跟踪进入后发现,在 request 请求进来时,会先将所有的文件相关内容封装到 MultiValueMap 中,如下图所示
3.异常处理
3.1 错误处理
3.1.1 默认规则
- 默认情况下,SpringBoot 提供 /error 处理所有错误的映射。
- 对于机器客户端,它将生产 JSON 响应,其中包含错误信息,HTTP 状态和异常消息的详细信息。对于浏览器客户端,响应一个 “whitelabel” 错误视图,以 HTML 格式呈现相同的数据。
- 要对齐进行自定义,添加 View ,解析为 error
- 要完全替换默认行为,可以实现 ErrorController 并注册该类型的 Bean 定义,或添加 ErrorAttributes 类型的组件以适应现有机制但替换其内容。
- error/下的 4xx、5xx 页面会被自动解析。
浏览器客户端访问不存在页面
POSTMAN 访问不存在页面
error/下的 4xx、5xx 页面会被自动解析
如下,添加 error 文件夹并增加 4xx、5xx.html。
当有以 4 开头的错误码的时候会展示 4xx.html 的内容, 当有以 5 开头的错误码的时候会展示 5xx.html 的内容。
<!--4xx.html-->
<!Document>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>4xx错误</title>
</head>
<body>
<h1 th:text="${status}">4xx</h1>
<div th:text="${error}"></div>
<div th:text="${#message}"></div>
<div th:text="${trace}"></div>
</body>
</html>
<!--5xx.html-->
<!Document>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>5xx错误</title>
</head>
<body>
<h1 th:text="${status}">4xx</h1>
<div th:text="${error}"></div>
<div th:text="${#message}"></div>
<div th:text="${trace}"></div>
</body>
</html>
错误页面分别为
3.2 异常处理自动配置原理
ErrorMvcAutoConfiguration 自动配置类自动初始化了异常处理的组件。
1、容器中有组件 DefaultErrorAttributes 【id 为 errorAttributes】
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
DefaultErrorAttributes 定义页面中可以包含哪些数据:
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
...
//获取错误处理属性处理
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
if (!options.isIncluded(Include.EXCEPTION)) {
errorAttributes.remove("exception");
}
if (!options.isIncluded(Include.STACK_TRACE)) {
errorAttributes.remove("trace");
}
if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
errorAttributes.remove("message");
}
if (!options.isIncluded(Include.BINDING_ERRORS)) {
errorAttributes.remove("errors");
}
return errorAttributes;
}
private Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, webRequest);
addErrorDetails(errorAttributes, webRequest, includeStackTrace);
addPath(errorAttributes, webRequest);
return errorAttributes;
}
//添加 HTTP 状态码
private void addStatus(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) {
Integer status = getAttribute(requestAttributes, RequestDispatcher.ERROR_STATUS_CODE);
if (status == null) {
errorAttributes.put("status", 999);
errorAttributes.put("error", "None");
return;
}
errorAttributes.put("status", status);
try {
errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());
}
catch (Exception ex) {
// Unable to obtain a reason
errorAttributes.put("error", "Http Status " + status);
}
}
//添加 错误详情
private void addErrorDetails(Map<String, Object> errorAttributes, WebRequest webRequest,
boolean includeStackTrace) {
Throwable error = getError(webRequest);
if (error != null) {
while (error instanceof ServletException && error.getCause() != null) {
error = error.getCause();
}
errorAttributes.put("exception", error.getClass().getName());
if (includeStackTrace) {
addStackTrace(errorAttributes, error);
}
}
addErrorMessage(errorAttributes, webRequest, error);
}
//添加 message 属性
private void addErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest, Throwable error) {
BindingResult result = extractBindingResult(error);
if (result == null) {
addExceptionErrorMessage(errorAttributes, webRequest, error);
}
else {
addBindingResultErrorMessage(errorAttributes, result);
}
}
...
//添加 trace 堆栈跟踪信息
private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) {
StringWriter stackTrace = new StringWriter();
error.printStackTrace(new PrintWriter(stackTrace));
stackTrace.flush();
errorAttributes.put("trace", stackTrace.toString());
}
//添加 path
private void addPath(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) {
String path = getAttribute(requestAttributes, RequestDispatcher.ERROR_REQUEST_URI);
if (path != null) {
errorAttributes.put("path", path);
}
}
...
}
2、容器中有组件 BasicErrorController 【id 为 basicErrorController】
由 BasicErrorController 源码可知,若没有指定【server.error.path】则使用默认的 【/error】路径。它自动适配响应页面和 JSON 数据。
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
...
//处理浏览器端响应,返回HTML页面
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
//处理客户端响应,返回JSON数据
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
//处理内容协商不支持的异常,响应JSON数据
@ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
public ResponseEntity<String> mediaTypeNotAcceptable(HttpServletRequest request) {
HttpStatus status = getStatus(request);
return ResponseEntity.status(status).build();
}
...
}
a)当是浏览器访问出现错误时,页面响应 new ModelAndView("error", model) 。
b)容器中有 id 为【error】的 view 组件。
c)容器中还有 视图解析器组件(BeanNameViewResolver),它按照返回的视图名作为组件的 ID 去容器中找 View 对象。
//ErrorMvcAutoConfiguration.class
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {
private final StaticView defaultErrorView = new StaticView();
//定义 id 为 error 的视图组件
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
// If the user adds @EnableWebMvc then the bean name view resolver from
// WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
//定义根据返回的视图名作为组件id去容器中找View对象的视图解析器组件。
@Bean
@ConditionalOnMissingBean
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver;
}
}
如果想要返回页面,就会找 error 视图【StaticView】。(默认是一个白页)。
3、容器中的组件 DefaultErrorViewResolver【id 为 conventionErrorViewResolver】
它是过时的。源码如下:
//ErrorMvcAutoConfiguration.class
@SuppressWarnings("deprecation")
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({ org.springframework.boot.autoconfigure.web.ResourceProperties.class,
WebProperties.class, WebMvcProperties.class })
static class DefaultErrorViewResolverConfiguration {
private final ApplicationContext applicationContext;
private final Resources resources;
DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties,
WebProperties webProperties) {
this.applicationContext = applicationContext;
this.resources = webProperties.getResources().hasBeenCustomized() ? webProperties.getResources()
: resourceProperties;
}
//视图解析器
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resources);
}
}
DefaultErrorViewResolver 实现如下图:
通过源码,我们可以预知,当发生错误时,会以 HTTP 的状态码作为视图页地址(viewName),最终找到真正的页面【error/viewName.html】,比如之前我们定义的【error/】页面下的 4xx.html和 5xx.html 。
3.4 异常处理原理
例如我们先写一个抛出异常的控制层
@GetMapping("/form/file")
public String formFile() {
int a = 10/0;
return "form/file";
}
当浏览器请求 file 页面时,DispatcherServlet 的 doDispatch() 执行目标方法时将会抛出异常。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
...
// 执行目标方法,此时会抛出异常
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
...
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
//保存异常并执行 processDispatchResult() 方法
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
...
}
}
异常被捕获后保存,并调用 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);
}
}
...
}
processDispatchResult() 方法处理流程:调用 processHandlerException() 来处理异常,其方法内部是是遍历异常解析器,并处理异常。异常解析器有如下几个
- 先执行 DefaultErrorAttribute 的 resolveException() 方法,其内部是将异常设置到 request 的 属性域,并返回null
- 再执行 HandlerExceptionResolverComposite,并遍历其下的三个异常解析器。
- 最终,异常处理器都是返回 null,并没有处理异常。会抛出这个异常。
@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
// Success and error responses may use different content types
request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
// Check registered HandlerExceptionResolvers...
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
if (exMv != null) {
...
}
//最终会抛出异常
throw ex;
}
接着 DispatcherServlet 的 doDispatch() 方法捕获异常,并将请求结束。
SpringBoot 内部重新发出一个地址为 【/error】的请求,由上节可知,这个请求会被 BasicErrorController 进行处理
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
//浏览器端会返回 HTML 页面,bin 调用异常视图解析器进行处理
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
// 调用异常视图解析器进行解析
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
}
跟踪可知,系统中只有一个 DefaultErrorViewResolver ,如下图
由上节可知,此异常视图解析器的作用就是把响应状态码作为错误页的地址,最终页面的响应地址为 【error/Xxx.html】,例如 error/500.html 。
总结:
1、先由 DefaultErrorAttributes 来处理异常。把异常信息保存到 request 请求域,并返回null。
2、所有的异常处理器处理完,发现默认没有任何人能处理异常,所以会抛出异常。
3、如果没有人能处理,最终底层会发送 /error 请求。该请求会被底层的 BasicErrorController 处理。
4、BasicErrorController 会调用 异常视图解析器 ErrorViewResolver 来处理异常。
5、最终 ErrorViewResolver 把响应状态码作为错误页的地址响应到前端【error/Xxx.html】。
3.3 定制错误处理逻辑
1、自定义错误页
error/4xx.html error/5xx.html :有精确的错误状态码页面就匹配精确;没有就就找 4xx.html;如果都没有就触发白页。
2、@ControllerAdvice + @ExceptionHandler 异常处理
它的底层是 ExceptionHandlerExceptionResolver 来进行处理的, ExceptionHandlerExceptionResolver 的原理是找到标注 @ExceptionHandler 的方法,并匹配对应的异常进行处理。这是我们经常使用的异常处理方式。
@ControllerAdvice
public class GlobalExceptionHandler {
//返回值为 String,会自动从资源文件查找对应的页面
@ExceptionHandler(value = {ArithmeticException.class})
public String handlerArithmeticException() {
return "login";
}
}
3、@ResponseStatus+自定义异常
如下,需要在自定义异常标注 @ResponseStatus(value= HttpStatus.FORBIDDEN, reason = "自定义异常") ,其中 value 是状态码,reason 为错误信息。
@ResponseStatus(value= HttpStatus.FORBIDDEN, reason = "自定义异常")
public class CustomException extends RuntimeException {
public CustomException() {
super();
}
public CustomException(String msg) {
super(msg);
}
}
底层是通过 ResponseStatusExceptionResolver 进行处理。ResponseStatusExceptionResolver 把 ResponseStatus 注解的信息底层调用 response.sendError(statusCode, resolvedReason) 发送 /error 请求。
response.sendError() 最终是调用 Tomcat 发送 /error 请求。
4、Spring 底层的异常,如参数类型转换异常;
Spring 底层的异常通过 DefaultHandlerExceptionResolver 处理框架底层的异常。最终它也是调用 response.sendError() 发送 /error 请求。
public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionResolver {
...
@Override
@Nullable
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);
}
}
catch (Exception handlerEx) {
if (logger.isWarnEnabled()) {
logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", handlerEx);
}
}
return null;
}
...
protected ModelAndView handleMissingServletRequestParameter(MissingServletRequestParameterException ex,
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
return new ModelAndView();
}
}
5、实现 HandlerExceptionResolver 处理异常
自定义一个异常解析器,它的优先级较高,可以作为全局异常处理。
@Order(value = Ordered.HIGHEST_PRECEDENCE) //优先级,数值越小,优先级越高,默认优先级最低
@Component
public class CustomExceptionResolver implements HandlerExceptionResolver {
@SneakyThrows
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
response.sendError(511,"我喜欢这个异常");
return new ModelAndView();
}
}
6、ErrorViewResolver 实现自定义处理异常
- response.sendError() ,error 请求就会转给 controller
- 你的异常没有任何人处理。 还是会调用 response.sendError()
- 而 /error 会被底层
BasicErrorController
处理,要去的页面地址是 ErrorViewResolver 进行定义的。