SpringBoot 教程核心功能-Web 开发(拦截器、文件上传、异常处理)

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 进行定义的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值