目录实现方式以及相应的底层源码实现逻辑。
背景实现原理剖析参考
背景最近需要将一个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(MapregisterMapping) { // 这些值的由来参见: 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
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)