目录实现方式以及相应的底层源码实现逻辑。
背景实现原理剖析参考
背景最近需要将一个SpringMVC WAR应用切换到SpringBoot架构上来,在完成相关代码迁移之后,发现原项目存在一个需求:”应用可以在运行时动态更新静态文件的映射“。
其实这功能本身是由Tomcat实现的,并非SpringMVC架构体系自带,所以实现思路大概有如下几种:
- 理解Tomcat相关实现逻辑,在springboot自带的embed-tomcat中进行配置。将springboot默认的jar部署形式调整为war部署形式。理解springboot是如何进行静态文件映射的,进而实现动态调用。
经过测试验证和综合考量,笔者最终选择了第三种实现方式。
- 对于方法一,虽然笔者早期确实阅读过Tomcat相关源码,但奈何年代比较久远,捡起来成本比较大。对于方法二,笔者在自己的测试项目是可以成功的,但在实际中始终存在些许问题,真要去一个个解决,ROI属实有点低了。
直接上代码。
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
@Api
@RestController
@RequestMapping("dynamicStatic")
public class StaticResourceDynamicRegistryController {
@Autowired
private ApplicationContext applicationContext;
@ApiOperation(value = "registry")
@PostMapping(value = "registry", produces = MediaType.APPLICATION_JSON_VALUE)
public String registry(@ApiParam(defaultValue="/fulizhe") @RequestParam String resourceHandler, //
@ApiParam(defaultValue="E:/data/") @RequestParam String resourceLocations) {
registerHandlersForAdditionalStatisResource(Collections.singletonMap(resourceHandler, resourceLocations));
return "SUCCESS";
}
private void registerHandlersForAdditionalStatisResource(Map registerMapping) {
// 这些值的由来参见:
final UrlPathHelper mvcUrlPathHelper = applicationContext.getBean("mvcUrlPathHelper", UrlPathHelper.class);
final ContentNegotiationManager mvcContentNegotiationManager = applicationContext
.getBean("mvcContentNegotiationManager", ContentNegotiationManager.class);
final ServletContext servletContext = applicationContext.getBean(ServletContext.class);
// SimpleUrlHandlerMapping.java
final HandlerMapping resourceHandlerMapping = applicationContext.getBean("resourceHandlerMapping",
HandlerMapping.class);
// 这里存放的是springmvc已经建立好的映射处理
@SuppressWarnings("unchecked")
final Map handlerMap = (Map) ReflectUtil.getFieldValue(resourceHandlerMapping,
"handlerMap");
final ResourceHandlerRegistry resourceHandlerRegistry = new ResourceHandlerRegistry(applicationContext,
servletContext, mvcContentNegotiationManager, mvcUrlPathHelper);
for (Map.Entry entry : registerMapping.entrySet()) {
String urlPath = entry.getKey();
String resourceLocations = entry.getValue();
final String urlPathDealed = StrUtil.appendIfMissing(urlPath, "/**");
final String resourceLocationsDealed = StrUtil.appendIfMissing(resourceLocations, "/");
// 先移除之前自定义注册过的...
handlerMap.remove(urlPathDealed);
// 重新注册
resourceHandlerRegistry.addResourceHandler(urlPathDealed)
.addResourceLocations("file:" + resourceLocationsDealed);
}
final Map additionalUrlMap = ReflectUtil
.invoke(resourceHandlerRegistry, "getHandlerMapping").getUrlMap();
ReflectUtil.invoke(resourceHandlerMapping, "registerHandlers", additionalUrlMap);
}
}
最终效果如下:
上面的实现只能算是开胃小菜,重头戏还得是源码部分。
首先让我们看看常规场景下SpringBoot是如何实现静态文件映射的:
@Configuration
public class XxxxStaticResourceConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/cat.html").addResourceLocations("classpath:/static/cat.html");
// 目录映射, 注意 ResourceLocations 配置部分一定要带上 /
registry.addResourceHandler("/catjs/**").addResourceLocations("classpath:/static/catjs/");
}
以上代码基础上,通过断点大法,我们可以得到如下堆栈:
上述堆栈中,我们挑选WebMvcConfigurationSupport.resourceHandlerMapping()方法进行进一步解读:
// WebMvcConfigurationSupport.java // 其子类WebMvcAutoConfiguration.EnableWebMvcConfiguration.java 会进行覆写, 加入springboot默认静态配置 // 上一小节<实现>正是参考了本方法参数 // HandlerMapping接口在SpringMVC整体架构上处于核心位置, 其抽象的是为当前请求找出对应的处理类. 相关介绍详见笔者之前的博客. @Bean @Nullable public HandlerMapping resourceHandlerMapping( @Qualifier("mvcUrlPathHelper") UrlPathHelper urlPathHelper, @Qualifier("mvcPathMatcher") PathMatcher pathMatcher, @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager, @Qualifier("mvcConversionService") FormattingConversionService conversionService, @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) { Assert.state(this.applicationContext != null, "No ApplicationContext set"); Assert.state(this.servletContext != null, "No ServletContext set"); // 这个使用new实例化出来的registry, 正是我们在实现 WebMvcConfigurer.addResourceHandlers(ResourceHandlerRegistry registry)的方法参数 ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext, this.servletContext, contentNegotiationManager, urlPathHelper); // 回调容器中所有的 WebMvcConfigurer.addResourceHandlers(ResourceHandlerRegistry registry) 实现 // 正是在这个回调里, springboot默认的 /** 对应 [classpath:/meta-INF/resources/, classpath:/resources/, classpath:/static/, classpath:/public/] 正是通过 WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter类, 这个WebMvcConfigurer实现类来完成的. 具体参见: WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter.addResourceHandlers() 方法 addResourceHandlers(registry); // 将上述自定义配置转换为spring内部类型 // 实际返回类型为 SimpleUrlHandlerMapping, 这个类覆写了基类的initApplicationContext() 方法, 以将收集到的自定义配置 urlMap 注册到自身的 handlerMap 字段中(其实是在基类中定义的); 之后在进行静态文件响应时候, 该 SimpleUrlHandlerMapping 就根据注册的映射关系, 读取并向前端返回静态资源了.. AbstractHandlerMapping handlerMapping = registry.getHandlerMapping(); if (handlerMapping == null) { return null; } // 装填其它实例字段信息 handlerMapping.setPathMatcher(pathMatcher); handlerMapping.setUrlPathHelper(urlPathHelper); handlerMapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider)); handlerMapping.setCorsConfigurations(getCorsConfigurations()); return handlerMapping; }
关于该HandlerMapping实例Bean 如何装载进SpringMVC核心类DispatcherServlet中的,可以参见SpringMVC源码研究之DispatcherServlet初始化 。
最后让我们看一下上面注册的HandlerMapping实例在运行时的状态。嗯,倒数第二,前面的都不匹配才轮得到它…(其中第一个是swagger,第二,三个属于actuate)
- SpringMVC源码研究之DispatcherServlet处理请求
spring-boot-war-tomcat-deploy



