本篇文章将结合源代码介绍servlet-mapping
的映射机制。
1 DefaultServlet
Tomcat包中有个类的名字叫DefaultServlet
,顾名思义就是默认的Servlet。那这个类有何作用呢?我们看下它的Javadoc描述:
DefaultServlet的javadoc
简单翻译下就是说DefaultServlet
会默认映射到/
路径下,它的作用是处理所有的静态资源请求。
Web应用启动后默认有两个文件夹下的文件是不能直接被客户端访问的,一个是META-INF
,另一个是WEB-INF
,其余的路径则可以被客户端正常访问。也就是说,DefaultServlet
可以支持客户端通过request指定相对路径,访问除上述两个路径之外的所有其他路径的文件。可以看出来,DefaultServlet
的访问权限其实还是蛮高的……
另外,这个/
大家应该很熟悉。我们在Spring Web MVC中,会将DispatcherServlet
的servlet-mapping
配置成/
。那这两者有什么关系呢?
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
我们在Spring Web MVC配置方式汇总(二)中曾有介绍过DispatcherServlet
、WebApplicationContext
、ServletContext
三者之间的关系,结合上一篇文章中给出的Tomcat核心对象之间的关联,我们现在可以写一段测试代码,来看看Web应用启动后,它里面包含了哪些对象。
private void printServlet() {
WebApplicationContext webApplicationContext = (WebApplicationContext)applicationContext;
ServletContext servletContext = webApplicationContext.getServletContext();
// servletContext.context.context.children --> 拿到所有的servlet对象
ApplicationContext applicationContext = (ApplicationContext)getFieldValue("context", servletContext);
StandardContext standardContext = (StandardContext)getFieldValue("context", applicationContext);
HashMap children = (HashMap)getFieldValue("children", standardContext);
children.forEach((key, value) -> {
StandardWrapper servlet = (StandardWrapper)value;
System.out.println("name: " + key + ", mapping: " + Arrays.toString(servlet.findMappings()) + ", type: " + servlet.getServletClass());
});
}
上面这段代码,会将Tomcat中加载了的所有servlet的名称、类型、servlet-mapping
打印出来。
当我们使用/
对DispatcherServlet
进行配置时,输出的结果如下:
正常配置
可以看出来,DefaultServlet
还是会被初始化,只不过servlet-mapping
变成空的了。这意味着没有任何请求会转给DefaultServlet
对象。
那若使用/*
对DispatcherServlet
进行配置会出现什么情况呢?
使用/*进行配置
此时DefaultServlet
对应的servlet-mapping
是/
,而DispatcherServlet
对应的则是/*
。如果使用/**
对DispatcherServlet
进行配置的话,则DispatcherServlet
的servlet-mapping
会是/**
。
那这些配置是不是都是有效配置,它们具体又有什么区别呢?
2 配置有效性
现在我们需要回头分析下上篇文章中提到的internalMapWrapper
方法了。
这个方法的作用,是根据传入的request对象来匹配到处理该请求的具体的servlet。匹配逻辑为:
-
首先找request的请求地址和servlet的mapping值完全相同的(完全匹配);
-
若未找到,则使用前缀匹配算法,比如请求/foo/bar/hello.do会找到
servlet-mapping
是/foo/bar/*的servlet; -
还未找到,则是用扩展名匹配算法查找,默认支持*.jsp和*.jpx;
-
还未找到,则检查request是不是
/
,若是则确认系统是否有配置欢迎页面,默认查找index.html
、index.htm
和index.jsp
三个; -
还未找到,则确认是否有配置
/
的Servlet,如果有则直接将请求交由它处理;
此处需要说明一点:Tomcat的映射支持使用*
,但不支持使用**
。/*
对于Tomcat来讲是前缀匹配,/**
则是完全匹配。/**
的这种配置,只能处理http://localhost:8080/**这一个请求,但**
在Spring框架中又有着特殊含义,这样的请求基本可以认为是无法被框架正确处理的。因此在Spring框架中,使用/**
来配置servlet-mapping
其实是无效的。
3 应用场景
讨论Tomcat的DefaultServlet
的特性的原因,主要是为了考虑在Spring Web MVC应用中该怎样处理静态资源。有三种处理静态资源的的备选方案:
3.1 使用ResourceHandler处理静态资源
如下是启用ResourceHandler处理静态资源的标准配置方法:
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/public/").setCachePeriod(31556926);
}
}
这种方式是向DispatcherServlet
中注入了一个SimpleUrlHandlerMapping
对象。它利用了DispatcherServlet
类的doService
方法在处理请求时,会根据条件选择合适的HandlerMapping
对象,并调起该对象处理请求的机制。所以在访问静态资源时,SimpleUrlHandlerMapping
最终会被调起,获取到静态资源并返回给客户端。
3.2 使用DefaultServletHttpRequestHandler
就算我们将DispatcherServlet
映射到/
上,Tomcat依然会创建一个DefaultServlet
。可以通过在DispatcherServlet
中注册一个RequestMapping
对象,然后让该对象将request请求转给DefaultServlet
。这样,即使DefaultSevlet
没有注册任何servlet-mapping
,依然会有request被转发给它。
根据这个思路,可以使用第二种处理静态资源的方法。Spring Web MVC框架已经将上述过程完整封装,只需使用configurer.enable()
开启功能即可。
@Configuration
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
4.3 直接使用DefaultServlet
访问静态资源
让DispatcherServlet
映射到/*
,而不是/
,这样DispatcherServlet
和DefaultServlet
便都会生效。因为/
对应的是默认匹配规则,而/*
对应的是前缀匹配,前缀匹配的优先级高于默认匹配,所以也能保证优先使用DispatcherServlet
。
但这有个弊端:
-
请求http://localhost:8080/hello.pdf会被
DispatcherServlet
处理,因为/hello.jsp会匹配到/*【错误】; -
请求http://localhost:8080/static/hello.pdf会被
DefaultServlet
处理,因为/static/hello.jsp不能匹配"/*"
【正确】; -
请求http://localhost:8080/api/hello.do会给到
DefaultServlet
处理,但其不是一个静态资源的访问地址【错误】;
4 总结
结合上一篇文章的分析可得结论:
-
DispatcherServlet
应该直接映射到"/"
上,其他的选项都存在问题; -
静态资源的处理,优先使用ResourceHandler,其他的方式也都存在弊端
也就是说,虽然Tomcat仍然会初始化一个DefaultServlet
,但它基本已经没啥用了,我们就直接忘了它吧。