在SpringMVC中,如果對靜態資源不做特殊處理,請求的path找不到會返回404.
原因是,我們在web.xml中會這樣配置dispatcherServlet的url-pattern,至於爲什麼是/,參見https://blog.csdn.net/u010900754/article/details/79998379:
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
那麼,像/xxx/a.jpg這樣的請求也會被dispatcherServlet處理,但是由於這是靜態資源,沒有對應的controller,所以會報404。
解決辦法有兩個:
1.在servlet的配置文件中加入這個配置<mvc:default-servlet-handler/>
對於mvc前綴的配置項,springMVC中有如下parser來解析:
public class MvcNamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser());
registerBeanDefinitionParser("default-servlet-handler", new DefaultServletHandlerBeanDefinitionParser());
registerBeanDefinitionParser("interceptors", new InterceptorsBeanDefinitionParser());
registerBeanDefinitionParser("resources", new ResourcesBeanDefinitionParser());
registerBeanDefinitionParser("view-controller", new ViewControllerBeanDefinitionParser());
registerBeanDefinitionParser("redirect-view-controller", new ViewControllerBeanDefinitionParser());
registerBeanDefinitionParser("status-controller", new ViewControllerBeanDefinitionParser());
registerBeanDefinitionParser("view-resolvers", new ViewResolversBeanDefinitionParser());
registerBeanDefinitionParser("tiles-configurer", new TilesConfigurerBeanDefinitionParser());
registerBeanDefinitionParser("freemarker-configurer", new FreeMarkerConfigurerBeanDefinitionParser());
registerBeanDefinitionParser("velocity-configurer", new VelocityConfigurerBeanDefinitionParser());
registerBeanDefinitionParser("groovy-configurer", new GroovyMarkupConfigurerBeanDefinitionParser());
}
}
最終會由DefaultServletHandlerBeanDefinitionParser來解析處理:
String defaultServletName = element.getAttribute("default-servlet-name");
RootBeanDefinition defaultServletHandlerDef = new RootBeanDefinition(DefaultServletHttpRequestHandler.class);
defaultServletHandlerDef.setSource(source);
defaultServletHandlerDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
if (StringUtils.hasText(defaultServletName)) {
defaultServletHandlerDef.getPropertyValues().add("defaultServletName", defaultServletName);
}
String defaultServletHandlerName = parserContext.getReaderContext().generateBeanName(defaultServletHandlerDef);
parserContext.getRegistry().registerBeanDefinition(defaultServletHandlerName, defaultServletHandlerDef);
parserContext.registerComponent(new BeanComponentDefinition(defaultServletHandlerDef, defaultServletHandlerName));
這段代碼會註冊一個DefaultServletHttpRequestHandler類型的bean,該bean本身是一個HttpRequestHandler接口的實例,是一種控制器類型。接着我們看下這個控制器是如何處理請求的:
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
RequestDispatcher rd = this.servletContext.getNamedDispatcher(this.defaultServletName);
if (rd == null) {
throw new IllegalStateException("A RequestDispatcher could not be located for the default servlet '" +
this.defaultServletName +"'");
}
rd.forward(request, response);
}
可以看到,這裏構造了一個RequestDispatcher實例,藉助了Servlet的能力,將當前請求通過forward方式轉發給了defaultServlet來處理。defaultServlet是tomcat中一個特殊的servlet,專門用於處理靜態資源的返回,其內部實現就是根據request的path取到對應的靜態資源並返回,其源碼這裏就不展開了。
再回到DefaultServletHandlerBeanDefinitionParser中:
Map<String, String> urlMap = new ManagedMap<String, String>();
urlMap.put("/**", defaultServletHandlerName);
RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class);
handlerMappingDef.setSource(source);
handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
handlerMappingDef.getPropertyValues().add("urlMap", urlMap);
String handlerMappingBeanName = parserContext.getReaderContext().generateBeanName(handlerMappingDef);
parserContext.getRegistry().registerBeanDefinition(handlerMappingBeanName, handlerMappingDef);
parserContext.registerComponent(new BeanComponentDefinition(handlerMappingDef, handlerMappingBeanName));
接着又構造了一個SimpleUrlHandlerMapping的bean,該bean是HandlerMapping的實例,是一種控制器的匹配器。其路徑是/**,也就是任意路徑。該mapping的bean會在dispatcherServlet中被調用,匹配到之前的DefaultServletHttpRequestHandler來處理當前請求。
2.在servlet的配置文件中加入這個配置<mvc:resources mapping="/static/**/" location="/static/"></mvc:resources>
這裏的mapping代表請求路徑,location代表靜態資源目錄。
該標籤也是mvc前綴,從之前的配置類中可以看到,是由ResourcesBeanDefinitionParser類來解析的。
說先會調用registerResourceHandler方法來生成特定的Handler:
String locationAttr = element.getAttribute("location");
if (!StringUtils.hasText(locationAttr)) {
parserContext.getReaderContext().error("The 'location' attribute is required.", parserContext.extractSource(element));
return null;
}
ManagedList<String> locations = new ManagedList<String>();
locations.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(locationAttr)));
RootBeanDefinition resourceHandlerDef = new RootBeanDefinition(ResourceHttpRequestHandler.class);
resourceHandlerDef.setSource(source);
resourceHandlerDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
resourceHandlerDef.getPropertyValues().add("locations", locations);
這裏將xml標籤中的location屬性取出,然後構造了一個ResourceHttpRequestHandler類型的控制器,該控制器也是實現自HttpRequestHandler接口的,並將location屬性賦值。
Map<String, String> urlMap = new ManagedMap<String, String>();
String resourceRequestPath = element.getAttribute("mapping");
if (!StringUtils.hasText(resourceRequestPath)) {
parserContext.getReaderContext().error("The 'mapping' attribute is required.", parserContext.extractSource(element));
return null;
}
urlMap.put(resourceRequestPath, resourceHandlerName);
RuntimeBeanReference pathMatcherRef = MvcNamespaceUtils.registerPathMatcher(null, parserContext, source);
RuntimeBeanReference pathHelperRef = MvcNamespaceUtils.registerUrlPathHelper(null, parserContext, source);
RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class);
handlerMappingDef.setSource(source);
handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
handlerMappingDef.getPropertyValues().add("urlMap", urlMap);
接着,仍是構建了一個SinleUrlHandlerMapping類型的匹配器,並且將xml標籤的mapping屬性賦值到urlMap中,也就是request的path能被mapping屬性匹配到,那麼就會被之前的控制器處理。
再看下ResourceHttpRequestHandler這個控制器是如何處理的?
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
checkAndPrepare(request, response, true);
// check whether a matching resource exists
Resource resource = getResource(request);
if (resource == null) {
logger.trace("No matching resource found - returning 404");
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// check the resource's media type
MediaType mediaType = getMediaType(resource);
if (mediaType != null) {
if (logger.isTraceEnabled()) {
logger.trace("Determined media type '" + mediaType + "' for " + resource);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No media type found for " + resource + " - not sending a content-type header");
}
}
// header phase
if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) {
logger.trace("Resource not modified - returning 304");
return;
}
setHeaders(response, resource, mediaType);
// content phase
if (METHOD_HEAD.equals(request.getMethod())) {
logger.trace("HEAD request - skipping content");
return;
}
writeContent(response, resource);
}
其核心方法如上,首先調用getResource方法取到靜態資源,如果不存在,會在response中設置error,一旦error被設置,該請求就會被tomcat處理。否則存在,就將資源寫入到body中。
可以看到這兩種方法都是註冊了一個特殊的handler,當controller的handler都沒有匹配到就會走到特殊的handler。
這兩種方式的區別:
default-servlet-handler是forward到tomcat的特殊servlet處理的;
而resource是在springmvc內部被處理的。