实现方式以及相应的底层源码实现逻辑。
背景
最近需要将一个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<String, String> registerMapping) {
final UrlPathHelper mvcUrlPathHelper = applicationContext.getBean("mvcUrlPathHelper", UrlPathHelper.class);
final ContentNegotiationManager mvcContentNegotiationManager = applicationContext
.getBean("mvcContentNegotiationManager", ContentNegotiationManager.class);
final ServletContext servletContext = applicationContext.getBean(ServletContext.class);
final HandlerMapping resourceHandlerMapping = applicationContext.getBean("resourceHandlerMapping",
HandlerMapping.class);
@SuppressWarnings("unchecked")
final Map<String, Object> handlerMap = (Map<String, Object>) ReflectUtil.getFieldValue(resourceHandlerMapping,
"handlerMap");
final ResourceHandlerRegistry resourceHandlerRegistry = new ResourceHandlerRegistry(applicationContext,
servletContext, mvcContentNegotiationManager, mvcUrlPathHelper);
for (Map.Entry<String, String> 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<String, ?> additionalUrlMap = ReflectUtil
.<SimpleUrlHandlerMapping>invoke(resourceHandlerRegistry, "getHandlerMapping").getUrlMap();
ReflectUtil.<Void>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");
registry.addResourceHandler("/catjs/**").addResourceLocations("classpath:/static/catjs/");
}
以上代码基础上,通过断点大法,我们可以得到如下堆栈:
上述堆栈中,我们挑选WebMvcConfigurationSupport.resourceHandlerMapping() 方法进行进一步解读:
@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");
ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext,
this.servletContext, contentNegotiationManager, urlPathHelper);
addResourceHandlers(registry);
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
|