SpringBoot动态注册静态资源映射分析

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

前言

一般有两种方式处理静态资源,一是通过重写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。

- END -


原文始发于微信公众号(十四个字节):SpringBoot动态注册静态资源映射分析