一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第11天,点击查看活动详情。
前言
一般有两种方式处理静态资源,一是通过重写WebMvcConfigurationSupport下的addResourceHandlers方法,这是最常用的,就不展示了,二是自己写一个Controller,并返回资源,这种通常用来校验用户有没有权限获取这个资源,可以动态返回资源,但还不是我们本文所说的动态。
我们所说的动态,是在SpringBoot启动后,还可以增加其他路径下的资源,比如一开始你对/home/a路径下做了映射,可直接通过http://localhost/res/1.txt访问其下资源,但后来还要对/home/b下做映射,怎么办?
当然可以编写一个Controller,自己实现逻辑,但也不太优雅。
这种情况也很少见,一般把系统要访问的资源都统一放在一个root路径下,root路径下再去细分,总之,我们映射时候只映射root这个路径即可。
但这种情况也不能说没有,而我就遇到了,所以下面展示一下如何在运行时,动态注册一个地址。
我们先看下代码吧。
private lateinit var applicationContext: ApplicationContext;
@javax.annotation.Resource(name = "dispatcherServlet")
lateinit var dispatcherServlet: DispatcherServlet
private val resourceMapping = mutableMapOf<String, SimpleUrlHandlerMapping>()
override fun setApplicationContext(applicationContext: ApplicationContext) {
this.applicationContext = applicationContext
}
fun register(urlPath: String, localtion: Path) {
var newUrlPath = urlPath
if (!newUrlPath.endsWith("/**")) {
newUrlPath = "$newUrlPath/**"
}
val urlMap: MutableMap<String, HttpRequestHandler> = LinkedHashMap()
val handler: ResourceHttpRequestHandler = getRequestHandler(localtion)
urlMap[newUrlPath] = handler
val simpleUrlHandlerMapping = SimpleUrlHandlerMapping(urlMap, 0)
simpleUrlHandlerMapping.applicationContext = this.applicationContext
simpleUrlHandlerMapping.initApplicationContext()
resourceMapping[newUrlPath] = simpleUrlHandlerMapping
getHandlerMappings().add(0,simpleUrlHandlerMapping)
}
fun unregister(urlPath: String) {
getHandlerMappings().remove(resourceMapping[urlPath] as SimpleUrlHandlerMapping)
}
private fun getHandlerMappings(): MutableList<HandlerMapping> {
val handlerMappingsField = dispatcherServlet::class.java.getDeclaredField("handlerMappings")
handlerMappingsField.isAccessible = true
return handlerMappingsField.get(dispatcherServlet) as MutableList<HandlerMapping>
}
private fun getRequestHandler(jarPath: Path): ResourceHttpRequestHandler {
val handler = ResourceHttpRequestHandler()
val resources: MutableList<Resource> = arrayListOf()
resources.add(UrlResource("jar:file:${jarPath}!/"))
handler.setLocationValues(arrayListOf())
handler.locations = resources
handler.afterPropertiesSet()
return handler
}
要理解这段代码,需要很多知识,我们从一开始说起,也就是DispatcherServlet,他是SpringMVC的核心之一,用来做请求分发,任何一个请求,都首先会进入到这里,其后才会进入我们编写的Controller,重点方法在doDispatch下,会调用下面方法获取一个HandlerExecutionChain。
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
handlerMappings变量是一个集合,保存着所有HandlerMapping,他是用来处理请求的一个对象,但还不完全由他处理,他只是抽象出他能处理哪一类的请求,比如我们编写的Controller,会由RequestMappingHandlerMapping处理,但除了他,还有一个SimpleUrlHandlerMapping用来处理静态资源,当然SpringBoot内部也不止这两个,还有如BeanNameUrlHandlerMapping、RouterFunctionMapping,这两个很少用,也可以编写一个接口,我们以后说。
而上面代码也很简单,就是遍历这个集合,从这个集合中找出能处理这个请求的HandlerMapping,我们在以前文章说过SpringMVC的思想,用一句话来说明就是,谁能处理这个请求,我就交给他,而调用getHandler()方法,传递一个请求过去,可以理解为询问他你能不能处理这个请求,如果他能处理,就返回一个执行链,如果不能处理,则返回null。
而静态资源处理首先要做的就是添加一个SimpleUrlHandlerMapping对象到这个集合中,这个对象中包含pathPatterns,也就是匹配路径,还有具体资源的路径,这个资源路径不仅仅是本地文件,还可以是任意Resource,而SpringBoot内部有大量Resource的实现,比如UrlResource,使用他我们甚至可以调用其他系统上的资源,再次为SpringBoot的设计叹为观止。
但是让我们失望的是,SpringBoot并没有提供任何方法支持我们这样做,其内部的handlerMappings变量没有对外开放,虽然有一个getHandlerMappings()返回了一个新集合,但是在新集合上做添加不会影响原来的(另外这个集合是UnmodifiableList,也不支持修改),所以我们也不能使用getHandlerMappings()添加。
所以我们也只能用反射了。
实现过程第一步就是获取DispatcherServlet实例,好在SpringBoot已经把他放入到了容器中。
@javax.annotation.Resource(name = "dispatcherServlet")
lateinit var dispatcherServlet: DispatcherServlet
第二步就是获取这个变量。
private fun getHandlerMappings(): MutableList<HandlerMapping> {
val handlerMappingsField = dispatcherServlet::class.java.getDeclaredField("handlerMappings")
handlerMappingsField.isAccessible = true
return handlerMappingsField.get(dispatcherServlet) as MutableList<HandlerMapping>
}
第三步就是构造一个新的SimpleUrlHandlerMapping,添加到容器中,而构建他必须传入applicationContext,并且必须调用其initApplicationContext方法,另外构造的ResourceHttpRequestHandler用来保存某个资源地址,这里我们拿jar文件做示例。
fun register(urlPath: String, localtion: Path) {
var newUrlPath = urlPath
if (!newUrlPath.endsWith("/**")) {
newUrlPath = "$newUrlPath/**"
}
val urlMap: MutableMap<String, HttpRequestHandler> = LinkedHashMap()
val handler: ResourceHttpRequestHandler = getRequestHandler(localtion)
urlMap[newUrlPath] = handler
val simpleUrlHandlerMapping = SimpleUrlHandlerMapping(urlMap, 0)
simpleUrlHandlerMapping.applicationContext = this.applicationContext
simpleUrlHandlerMapping.initApplicationContext()
resourceMapping[newUrlPath] = simpleUrlHandlerMapping
getHandlerMappings().add(0,simpleUrlHandlerMapping)
}
这样便可以在运行时候动态注册,但是最重要的还有下面这个配置。
spring.mvc.servlet.load-on-startup=1
我们知道Tomcat会根据这个值初始化Servlet,默认情况下,只有在第一个请求到来时,才进行初始化,当值为0或者大于0时,代表容器启动时就初始化这个Servlet,正数的值越小,初始化的优先级越高。
而我们要想动态注册,必须在DispatcherServlet执行完init方法后才可以注册,否则内部变量handlerMappings为null。