在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内部被处理的。