技巧 - SpringBoot项目下实现动态化静态文件路径注册

技巧 - SpringBoot项目下实现动态化静态文件路径注册,第1张

技巧 - SpringBoot项目下实现动态化静态文件路径注册

实现方式以及相应的底层源码实现逻辑。

目录

背景实现原理剖析参考

背景

最近需要将一个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

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5715843.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-18
下一篇 2022-12-18

发表评论

登录后才能评论

评论列表(0条)

保存