当我们在浏览器发起一个请求时,例如http://xxxx/home 跟http://xxxx/home.html时,后者会直接访问到相应的资源,而前者则会走Controller处理逻辑,最后再经过视图渲染(详情见:SpringMVC视图View解析源码分析)返回视图真正的资源,那么这其中的差别是什么?是哪里的代码来分别处理了这种不同的请求的?资源放置的位置又应该在哪里? 这一系列的问题在接下来的源码分析中将会得到解答。
所有的被SpringMVC管理的资源在请求处理流程上首先都是通过DispatcherServlet,然后在这个servlet中获取Handler,然后再获取handlerAdapter,最后调用到真正的handler或者资源。这一块的详情逻辑可以查看:SpringMVC从配置初始化到HTTP请求全流程解析,这里就不再累述这个过程,我们直接进入代码的关键点DispatcherServlet->getHandler。
@Nullable protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { if (this.handlerMappings != null) { /***普通的Controller请求与静态资源请求处理的不同点就再这个地方被分开 即:根据request匹配到的Handler是不同的。***/ for (HandlerMapping mapping : this.handlerMappings) { HandlerExecutionChain handler = mapping.getHandler(request); if (handler != null) { return handler; } } } return null; }根据Debug代码发现:在handlerMappings列表里面依次有RequestMappingHandlerMapping, BeanNameHandlerMapping,SimpleUrlHandlerMapping。对于第一个我相信大家都不陌生,它就是处理Controller请求的入口,第二个很少会用到可以先不管,而最后一个就是帮助我们处理静态资源的HandlerMapping,因为静态资源是基于URL,所以统一由它来管理并返回对应的Handler. handlerMappings的初始化是在ApplicationContext中获取所有的实现了HandlerMapping的bean,所以我们可以预见到SimpleUrlHandlerMapping会在SpringMVC的某个配置类中被注入到Spring的容器中。 那么按照SpringMVC的配置风格,所有的配置应该会在WebMvcAutoConfiguration中完成,按照这个思路最终整理得到如下的类关系图。
在WebMvcConfigurationSupport中通过resourceHandlerMapping()方法配置并返回了HandlerMapping(这里其实就是SimpleUrlHandlerMapping),而该方法依赖于addResourceHandlers() 注入进来的handler。addResourceHandlers的具体实现是由DelegatingWebMvcConfiguration实现的,而它委托于WebMvcConfigurerComposite来完成用户自定义的resourceHandler配置,用户自定义配置之一就是WebMvcAutoConfigurationAdapter。 所以总结一下静态资源配置最核心的两个方法就是WebMvcConfigurationSupport中的resourceHandlerMapping和WebMvcAutoConfigurationAdapter中的addResourceHandlers。接下来进行代码分析。
重点是ResourceHandlerRegistry -> getHandlerMapping(),接下来我们看下这个接口的实现。
protected AbstractHandlerMapping getHandlerMapping() { //没有任何的注册 if (this.registrations.isEmpty()) { return null; } //用于保存path与handler的映射 Map<String, HttpRequestHandler> urlMap = new LinkedHashMap<>(); //处理每一个注册器 for (ResourceHandlerRegistration registration : this.registrations) { for (String pathPattern : registration.getPathPatterns()) { //获取handler(这里是一个简单的工厂),其中会根据registration里面的locationValues去设置资源位置 ResourceHttpRequestHandler handler = registration.getRequestHandler(); //设置path解析器 if (this.pathHelper != null) { handler.setUrlPathHelper(this.pathHelper); } if (this.contentNegotiationManager != null) { handler.setContentNegotiationManager(this.contentNegotiationManager); } handler.setServletContext(this.servletContext); handler.setApplicationContext(this.applicationContext); //handler的初始化回调 try { handler.afterPropertiesSet(); } catch (Throwable ex) { throw new BeanInitializationException("Failed to init ResourceHttpRequestHandler", ex); } //根据pathPattern将handler缓存 //代码1,后续会回头来看这里。 urlMap.put(pathPattern, handler); } } //生成HandlerMapping并返回。 return new SimpleUrlHandlerMapping(urlMap, this.order); }所以在这里我们可以看到,实际上返回的就是一个SimpleUrlHandlerMapping。 看完了HandlerMapping的生成,那回过头来再看用户自定的resourceHandler的注入是怎么来的。 代码位于:WebMvcAutoConfigurationAdapter.addResourceHandlers
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { //判断开关是否关闭 if (!this.resourceProperties.isAddMappings()) { logger.debug("Default resource handling disabled"); return; } Duration cachePeriod = this.resourceProperties.getCache().getPeriod(); CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl(); //增加位于classpath:/META-INF/resources/webjars/的一个ResourceHandlerRegistration //请求的路径如果是/webjars/开头的,则回匹配到这个目录下。 if (!registry.hasMappingForPattern("/webjars/**")) { customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/") .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)); } //增加一个由mvcProperties和resourceProperties共同定义的ResourceHandlerRegistration String staticPathPattern = this.mvcProperties.getStaticPathPattern(); if (!registry.hasMappingForPattern(staticPathPattern)) { customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern) .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations())) .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)); } }这里我们主要关注的是自定义的ResourceHandlerRegistration,它由mvcProperties.getStaticPathPattern()来指定pathPattern,由this.resourceProperties.getStaticLocations()来指定静态资源的目录位置。其中mvcProperties配置的前缀为:spring.mvc,而staticPathPattern的默认值为:/**,所以当我们需要改变静态资源的pathPattern时可以通过该配置来改变,例如可以为spring.mvc.staticPathPattern="/static/*",再看看resourceProperties,它的前缀是:spring.resources,其中staticLocations的默认值为:
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" };所以springMVC项目中默认的静态资源位置为/resources/, /static/,/public/,当我们需要改变这个默认值时可以通过staticLocations配置来改变,例如:
spring: resources: static-locations: - classpath:/static/ - classpath:/templates/这样就能覆盖默认值,这里要注意的是,多个资源目录时,如果同名文件在多个目录出现,则是按先后顺序匹配。
这里要注意区分几个配置: 1.SpringMvc视图解析查找咨询的前缀和后缀 2.SpringMvc的静态资源urlPattern 3.Spring级别的静态资源目录位置
配置如下:
spring: profiles: include: - persist resources: static-locations: - classpath:/static/ mvc: view: prefix: /templates/ suffix: .html static-path-pattern: /**首先MVC的view下面的prefix,用于经过Controller处理后的结果再经过视图出解析器,例如InternalResourceViewResolver来定位视图资源时,会在请求路径上加入这个前缀,而Spring的静态资源位置,则是用于资源加载器把该目录当作根目录开始加载资源,例如在这种配置下面,当我请求/home,其实最终需要加载的资源路径是/templates/home.html,并且资源需要存在于/static/templates/目录下面。最后static-path-pattern其实是用于将哪些请求映射为对应的handler,在代码1处有体现:urlMap.put(pathPattern, handler); 默认情况,从请求的根路径映射为handler,这里假如我们修改为/xx/**,则只有url中含有/xx/路径的才能被映射,并且是在/xx/之后的路径才能被当作资源路径来处理,则访问静态资源的url就需要变为:/xx/templates/home.html,最后会在static目录下面的templates目录下面找到home.html。