最近在項(xiàng)目中碰到了如題的問(wèn)題,在spring-boot項(xiàng)目中,同一次http請(qǐng)求,HandlerInterceptor攔截器執(zhí)行了兩次,與此同時(shí)這個(gè)問(wèn)題還有個(gè)特點(diǎn),它并沒(méi)有干擾具體的業(yè)務(wù)功能,就是controller正常返回,沒(méi)有任何錯(cuò)誤。
什么情況下HandlerInterceptor會(huì)執(zhí)行兩遍?
并不是所有的controller都是這樣的,經(jīng)過(guò)測(cè)試目前發(fā)現(xiàn)有以下兩種的controller會(huì)出現(xiàn)這樣的情況(前提是你沒(méi)有重寫過(guò)它的RequestMappingHandlerAdapter和ViewNameMethodReturnValueHandler)。
@Controller
public class GreetingController {
/**
* 第一種情況:方法返回值類型為 void 類型,并且不存在RequestMapping對(duì)應(yīng)的視圖
*
* @param name
* @throws IOException
*/
@RequestMapping("/greet1")
public void greet1(String name) throws IOException {
System.out.println("name = " + name);
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletResponse response = ((ServletRequestAttributes) requestAttributes).getResponse();
PrintWriter writer = response.getWriter();
writer.write(name);
writer.flush();
writer.close();
}
/**
* 第二種情況:方法返回值類型為 String 類型,并且不存在返回內(nèi)容對(duì)應(yīng)的視圖
*
* @param name
* @return
*/
@RequestMapping("/greet2")
public String greet2(String name) {
System.out.println("name = " + name);
return name;
}
}
對(duì)于以上兩種情況,你所有的HandlerInterceptor都至少會(huì)執(zhí)行兩遍,甚至三遍。這里先給出怎么解決這個(gè)問(wèn)題的方案,后面再分析問(wèn)題原因。
如何解決HandlerInterceptor攔截器執(zhí)行多次問(wèn)題?
重寫RequestMappingHandlerAdapter和ViewNameMethodReturnValueHandler這兩個(gè)類,并將其注入容器中
public class CustomizedHandlerAdapter extends RequestMappingHandlerAdapter {
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
setReturnValueHandlers(getReturnValueHandlers().stream().filter(
h -> h.getClass() != ViewNameMethodReturnValueHandler.class
).collect(Collectors.toList()));
}
}
public class HandlerVoidMethod extends ViewNameMethodReturnValueHandler {
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
/*
* 這里只處理了void返回值的方法,對(duì)于String返回值的方法則沒(méi)有處理,原因是系統(tǒng)中可能還會(huì)用到springmvc的視圖功能(例如jsp)
* 如果說(shuō)是前后分離的項(xiàng)目,springmvc層只提供純接口的話,那么可以將下面代碼全部刪除,
* 只寫上一行 mavContainer.setRequestHandled(true); 即可
*/
if (void.class == returnType.getParameterType()) {
mavContainer.setRequestHandled(true);//這行代碼是重點(diǎn),它的作用是告訴其他組件本次請(qǐng)求已經(jīng)被程序內(nèi)部處理完畢,可以直接放行了
} else {
super.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
}
}
@Configuration
public class WebConfig {
@Bean
public HandlerVoidMethod handlerVoidMethod() {
return new HandlerVoidMethod();
}
@Bean
public CustomizedHandlerAdapter handlerAdapter(HandlerVoidMethod handlerVoidMethod) {
CustomizedHandlerAdapter chl = new CustomizedHandlerAdapter();
chl.setCustomReturnValueHandlers(Arrays.asList(handlerVoidMethod));
return chl;
}
}
通過(guò)以上代碼,則能解決mvc攔截器執(zhí)行多次的問(wèn)題。
springmvc攔截器為什么會(huì)執(zhí)行多次?
簡(jiǎn)單來(lái)講就是controller中的void方法會(huì)導(dǎo)致springmvc使用你的請(qǐng)求url作為視圖名稱,然后它在渲染視圖之前會(huì)檢查你的視圖名稱,發(fā)現(xiàn)這視圖會(huì)導(dǎo)致循環(huán)請(qǐng)求,就拋出一個(gè)ServletException,tomcat截取到這個(gè)異常后就轉(zhuǎn)發(fā)到/error頁(yè)面,就在這個(gè)轉(zhuǎn)發(fā)的過(guò)程中導(dǎo)致了springmvc重新開(kāi)始DispatcherServlet的整個(gè)流程,所以攔截器自然就執(zhí)行了多次。
HandlerInterceptor相關(guān)知識(shí)
對(duì)于攔截器HandlerInterceptor的機(jī)制需要有個(gè)大致的了解,這里簡(jiǎn)單講下springmvc中的攔截器是怎么執(zhí)行的,我們都知道springmvc中處理請(qǐng)求都是從DispatcherServlet的doDispatch方法開(kāi)始的,
// DispatcherServlet類 doDispatch方法
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
......省略部分前面的代碼,直接從 1035行這里開(kāi)始
// HandlerExecutionChain mappedHandler = getHandler(processedRequest); 在1016行有設(shè)置mappedHandler的值,HandlerExecutionChain是一個(gè)攔截器調(diào)用鏈,它是鏈?zhǔn)綀?zhí)行的,這里是鏈?zhǔn)綀?zhí)行所有的攔截器里面的 preHandle 方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 真正代用我們自己寫的業(yè)務(wù)代碼的入口
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
// 這里是鏈?zhǔn)綀?zhí)行所有的攔截器里面的 postHandle 方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
......省略后面的代碼
}
多個(gè)攔截器它的執(zhí)行順序是和棧的入棧出棧順序有點(diǎn)類似,我們把preHandle方法比作入棧,postHandle方法比作出棧,所以就是preHandle先執(zhí)行的postHandle反而后執(zhí)行。
例如我們有三個(gè)攔截器,分別為 A,B,C。它們的執(zhí)行順序如下圖:

攔截器執(zhí)行的相關(guān)源碼:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
HandlerInterceptor[] interceptors = getInterceptors();//獲取所有的攔截器
if (!ObjectUtils.isEmpty(interceptors)) {
for (int i = 0; i < interceptors.length; i++) {// 這里是從第一個(gè)開(kāi)始
HandlerInterceptor interceptor = interceptors[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 {
HandlerInterceptor[] interceptors = getInterceptors();//獲取所有的攔截器
if (!ObjectUtils.isEmpty(interceptors)) {
for (int i = interceptors.length - 1; i >= 0; i--) {// 這里是從最后一個(gè)開(kāi)始
HandlerInterceptor interceptor = interceptors[i];
interceptor.postHandle(request, response, this.handler, mv);
}
}
}
上面簡(jiǎn)要的講了下攔截器從哪里開(kāi)始以及它的執(zhí)行順序相關(guān)的東西,經(jīng)過(guò)了攔截器的前置攔截之后,springmvc通過(guò)反射執(zhí)行了我們的具體業(yè)務(wù)方法,那在執(zhí)行具體的業(yè)務(wù)方法時(shí)有兩個(gè)很重要的問(wèn)題,一是如何處理我們業(yè)務(wù)方法的參數(shù)(千奇百怪的);二是如何處理我們業(yè)務(wù)方法的返回值(也是多種多樣的)。在springmvc中通過(guò)HandlerMethodArgumentResolver來(lái)處理方法參數(shù),通過(guò)HandlerMethodReturnValueHandler來(lái)處理方法返回值。在本文中,方法的入?yún)⑴c本文所討論的問(wèn)題關(guān)系不大,因此這里就不展開(kāi)敘述HandlerMethodArgumentResolver相關(guān)的東西了。重點(diǎn)說(shuō)下與問(wèn)題相關(guān)的HandlerMethodReturnValueHandler類。
HandlerMethodReturnValueHandler
對(duì)于controller方法的返回值的處理,springmvc框架中內(nèi)置了20多種默認(rèn)的返回值處理器,在RequestMappingHandlerAdapter#getDefaultReturnValueHandlers方法中可以看到它設(shè)置的一些默認(rèn)的HandlerMethodReturnValueHandler
private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>();
// Single-purpose return value types
// 返回值類型是ModelAndView或其子類
handlers.add(new ModelAndViewMethodReturnValueHandler());
// 返回值類型是Model或其子類
handlers.add(new ModelMethodProcessor());
// 返回值類型是View或其子類
handlers.add(new ViewMethodReturnValueHandler());
// ResponseBody注解
handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters(), this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager));
handlers.add(new StreamingResponseBodyReturnValueHandler());
// 用來(lái)處理返回值類型是HttpEntity的方法
handlers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.contentNegotiationManager, this.requestResponseBodyAdvice));
handlers.add(new HttpHeadersReturnValueHandler());
handlers.add(new CallableMethodReturnValueHandler());
handlers.add(new DeferredResultMethodReturnValueHandler());
handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));
// Annotation-based return value types
// 返回值有@ModelAttribute注解
handlers.add(new ModelAttributeMethodProcessor(false));
handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.contentNegotiationManager, this.requestResponseBodyAdvice));
// Multi-purpose return value types
// 返回值是void或String, 將返回的字符串作為view視圖的名字
handlers.add(new ViewNameMethodReturnValueHandler());
// 返回值類型是Map
handlers.add(new MapMethodProcessor());
// Custom return value types,自定義返回值處理
if (getCustomReturnValueHandlers() != null) {
handlers.addAll(getCustomReturnValueHandlers());
}
// Catch-all
if (!CollectionUtils.isEmpty(getModelAndViewResolvers())) {
handlers.add(new ModelAndViewResolverMethodReturnValueHandler(getModelAndViewResolvers()));
}
else {
handlers.add(new ModelAttributeMethodProcessor(true));
}
return handlers;
}
在本文的問(wèn)題中,controller的返回值為void和String兩種都有,剛好對(duì)應(yīng)ViewNameMethodReturnValueHandler這個(gè)處理器
public class ViewNameMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> paramType = returnType.getParameterType();
// 返回值類型匹配,void和String
return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType));
}
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
// 返回值處理這里,只處理了String類型的返回值,將返回值的結(jié)果作為視圖名稱設(shè)置到ModelAndViewContainer對(duì)象中
// 對(duì)于void類型的返回值,這里并沒(méi)有處理,
if (returnValue instanceof CharSequence) {
String viewName = returnValue.toString();
mavContainer.setViewName(viewName);
if (isRedirectViewName(viewName)) {
mavContainer.setRedirectModelScenario(true);
}
}
else if (returnValue != null) {
// should not happen
throw new UnsupportedOperationException("Unexpected return type: " +
returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
}
}
}
這里有個(gè)小細(xì)節(jié),handleReturnValue方法中returnValue參數(shù)前面是加了一個(gè)@Nullable注解的,意味這這個(gè)參數(shù)的值可能是null,當(dāng)你的controller為void方法時(shí),returnValue就會(huì)為null,那handleReturnValue這個(gè)方法就不會(huì)對(duì)mavContainer這個(gè)對(duì)象做任何處理。
ModelAndView對(duì)象的流轉(zhuǎn)過(guò)程
mavContainer這個(gè)參數(shù)是從RequestMappingHandlerAdapter中的invokeHandlerMethod方法中創(chuàng)建并一路傳進(jìn)來(lái)的,整個(gè)調(diào)用鏈如下圖:

最終在DispatcherServlet的doDispatch方法中得到上圖中最后返回的ModelAndView對(duì)象
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
......省略部分前面的代碼
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
......省略部代碼
// 直接看這里,設(shè)置默認(rèn)視圖
applyDefaultViewName(processedRequest, mv);
......省略部分前面的代碼
}
private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception {
// 這里判斷是否設(shè)置了視圖,通過(guò)前面的分析,我們可以知道,由于我們的controller是void類型的,所以是沒(méi)有設(shè)置視圖的
if (mv != null && !mv.hasView()) {
// 獲取默認(rèn)視圖名稱
String defaultViewName = getDefaultViewName(request);
if (defaultViewName != null) {
// 設(shè)置默認(rèn)視圖名稱
mv.setViewName(defaultViewName);
}
}
}
獲取默認(rèn)視圖名稱的方法:getDefaultViewName,由于這個(gè)方法里面調(diào)用棧比較深,這里直接給出它調(diào)用的最里面的那個(gè)方法:
org.springframework.web.util.UrlPathHelper#getPathWithinApplication
public String getPathWithinApplication(HttpServletRequest request) {
String contextPath = getContextPath(request);
String requestUri = getRequestUri(request);
String path = getRemainingPath(requestUri, contextPath, true);
if (path != null) {
// Normal case: URI contains context path.
return (StringUtils.hasText(path) ? path : "/");
}
else {
return requestUri;
}
}
最后這個(gè)方法其實(shí)就是獲取了controller的requestMapping,然后返回出去。再結(jié)合前面的getDefaultViewName方法可知,這個(gè)默認(rèn)視圖名稱就是requestMapping的值。在渲染視圖之前springmvc還做了個(gè)判斷,就是看你的視圖名稱是不是本次請(qǐng)的uri中的一部分或者和uri一樣,如果是的話,就會(huì)拋出一個(gè)異常,說(shuō)你是一個(gè)循環(huán)視圖路徑

tomcat對(duì)錯(cuò)誤頁(yè)面的處理
在tomcat的StandardHostValve類中,它獲取到了上面springmvc拋出的ServletException異常,它寫了個(gè)很明了的注釋,尋找一個(gè)應(yīng)用級(jí)別的錯(cuò)誤頁(yè)面,如果存在的話則渲染它(就是重定向到錯(cuò)誤頁(yè)面)

通過(guò)debug直到執(zhí)行到下面的代碼,通過(guò)下圖中第二行后面的debug信息也可以看到錯(cuò)誤頁(yè)面的路徑為/error

通過(guò)RequestDispatcher這個(gè)類名能猜到應(yīng)該是請(qǐng)求分發(fā)器,看看它是如何創(chuàng)建RequestDispatcher對(duì)象的
public RequestDispatcher getRequestDispatcher(final String path) {
......省略部代碼
try {
......省略部代碼
// Construct a RequestDispatcher to process this request
// 創(chuàng)建一個(gè)新的請(qǐng)求調(diào)度器來(lái)處理該請(qǐng)求
return new ApplicationDispatcher(wrapper, uri, wrapperPath, pathInfo,
queryString, mapping, null);
} finally {
mappingData.recycle();
}
}

這里可以很明顯的看到開(kāi)始了請(qǐng)求轉(zhuǎn)發(fā),這對(duì)于springmvc來(lái)講就已經(jīng)開(kāi)始了一個(gè)新的請(qǐng)求,它會(huì)再次進(jìn)入到DispatcherServlet的doDispatch方法中,整個(gè)springmvc的流程會(huì)再重新走一遍,所以攔截器自然也會(huì)再執(zhí)行一次,只不過(guò)這次在攔截器中看到的url已經(jīng)變成/error了,而不是之前的requestMapping里面的值。