【SpringMVC(十七)】靜態資源

在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內部被處理的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章