SpringMVC文档、源码阅读——DispatcherServlet特殊Bean加载
什么是特殊Bean
DispatcherServlet
作为一个Servlet,它要一方面要接受用户的请求,一方面又要利用各种组件来处理这个请求。举个例子,当它接收到请求,它会交给Controller来处理,Controller返回一个字符串,它又调用ViewResolver来将这个字符串解析成视图。
所以无疑,DispatcherServlet
想要工作,就要有这些组件的支持。在Spring中,你需要通过向WebApplicationContext中注册这些组件为Bean,而这些Bean就被称作特殊Bean。
如下是DispatcherServlet
所要检查的特殊Bean表格:
Bean类型 | 解释 |
---|---|
HandlerMapping |
将一个请求映射到一个Handler(处理器)和一系列对请求做前置后置处理的Interceptor上 |
HandlerAdapter |
帮助DispatcherServlet调用一个Handler而无需知道Handler实际如何被调用 |
HandlerExceptionResolver |
解析异常的策略,可能将异常映射到一个Handler、一个HTML的错误页面或其它地方 |
ViewResolver |
视图解析器,解析一个Handler返回的基于字符串的逻辑视图名到一个实际的视图上 |
LocaleResolver |
解析客户端正在使用的地区以及可能的时区,以便提供国际化的视图 |
ThemeResolver |
解析你的Web应用的主题 |
MultipartResolver |
在一些multipart解析库的帮助下解析multipart请求 |
FlashMapManager |
存储和获取输入输出的FlashMap,可以用于将属性从一个请求传递到另一个请求 |
这表格中的大部分东西我们都见过,少部分我没有使用过。
DispatcherServlet
的源码中也定义了一些静态常量,指定了这些特殊Bean应该有的Bean名。
public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver";
public static final String LOCALE_RESOLVER_BEAN_NAME = "localeResolver";
public static final String THEME_RESOLVER_BEAN_NAME = "themeResolver";
public static final String HANDLER_MAPPING_BEAN_NAME = "handlerMapping";
public static final String HANDLER_ADAPTER_BEAN_NAME = "handlerAdapter";
public static final String HANDLER_EXCEPTION_RESOLVER_BEAN_NAME = "handlerExceptionResolver";
public static final String REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME = "viewNameTranslator";
public static final String VIEW_RESOLVER_BEAN_NAME = "viewResolver";
public static final String FLASH_MAP_MANAGER_BEAN_NAME = "flashMapManager";
特殊Bean的加载
onRefresh
DispatcherServlet
的onRefresh
方法会被父类在WebApplicationContext初始化并刷新完毕后回调,此时,所有Bean都已经注册到context中。
在onRefresh
方法中,它调用了initStrategies
来初始化某种策略:
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
initStrategies
中就是加载特殊Bean的一些调用
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
我们会对其中的HandlerMapping
、HandlerAdapter
和ViewResolver
的加载进行分析。
HandlerMapping
HandlerMapping是用于将一个请求映射到一个Handler和一系列Interceptor上的组件。Handler是用于处理一个请求的组件,所以,在我们常见的SpringMVC开发方式中,Handler就是Controller中的方法。
下面是initHandlerMappings
方法的代码:
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
// 是否检测所有HandlerMapping
if (this.detectAllHandlerMappings) {
// 检测ApplicationContext中所有HandlerMapping,包括祖先context
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
// 将返回的所有HandlerMapping设置给本地变量`handlerMappings`
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// 保证HandlerMappings的顺序
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
// 否则,只通过约定的HandlerMapping BeanName来获取
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}
// 如果没找到任何handlerMapping,那么注册一个默认的以确保我们有至少一个HandlerMapping
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
}
}
- 如果类中的
detectAllHandlerMappings
开关打开,代表允许检查context中所有HandlerMapping类型的Bean并注册 - 否则只通过约定的名字来到context中获取HandlerMapping
- 如果一个都没找到,使用
getDefaultStrategies
来获取默认HandlerMapping
下面是我们提供的唯一ServletConfig配置类:
@Configuration
@ComponentScan(basePackages = "top.yudoge.controller")
public class AppConfig { }
所以这种情况下,必然是通过默认策略获取默认的handlerMappings
,我们来看看获取默认策略这个功能是如何实现的:
getDefaultStrategies
下面是这个方法的代码,看起来注释的内容很难理解并且代码也很难读懂。
/**
* 为给定的策略接口创建一个默认策略对象的列表
*
* @params context 当前WebApplicationContext
* @params strategyInterface 策略接口对象
*/
protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
String key = strategyInterface.getName();
String value = defaultStrategies.getProperty(key);
if (value != null) {
String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
List<T> strategies = new ArrayList<>(classNames.length);
for (String className : classNames) {
try {
Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
Object strategy = createDefaultStrategy(context, clazz);
strategies.add((T) strategy);
}
catch (ClassNotFoundException ex) {
throw new BeanInitializationException(
"Could not find DispatcherServlet's default strategy class [" + className +
"] for interface [" + key + "]", ex);
}
catch (LinkageError err) {
throw new BeanInitializationException(
"Unresolvable class definition for DispatcherServlet's default strategy class [" +
className + "] for interface [" + key + "]", err);
}
}
return strategies;
}
else {
return new LinkedList<>();
}
}
实际上很简单,该方法就是获取一个指定类型的对象的List以在用户没有显式指定该类型对象时作为默认的对象List,比如刚刚的handlerMappings
。那为啥这里要反复强调Strategies
这个单词呢?设计模式没学好吧!
对于DispatcherServlet
来说,它调度一些组件来完成请求的处理,返回相应的视图,但是它负责的只有调度,请求如何被处理,视图如何被解析渲染,这些都不是它的任务,它不关心这些。取而代之的是,它调用这些组件的接口来完成功能,具体如何完成的是这些组件接口的实现来定义的。这些组件接口(比如HandlerMapping)就是策略接口(Strategie Interfaces),这些接口的实现类就是具体的策略(Concrete Strategie),所以,这不就是策略设计模式的一个较为庞大的应用嘛。
举个例子,
DispatcherServlet
只需要知道HandlerMapping
是一个可以将请求映射到一个Handler和一批Interceptor上的策略接口即可,它并不需要知道具体是如何映射的,具体的映射规则由实际的策略来实现。比如RequestMappingHandlerMapping
将请求映射到一个标注有@RequestMapping
的方法上。
好了,该方法的第一行代码获取了策略接口的全限定名,然后试图以全限定名为key调用defaultStrategies.getProperty
来获得一个值,然后它把这个值分割成了一批具体的策略类名,加载并实例化这些策略类,添加到策略列表中。
所以defaultStrategies
中保存有每个策略接口的多个默认实现类。它是这样被初始化的:
private static final Properties defaultStrategies;
static {
try {
ClassPathResource resource = new ClassPathResource("DispatcherServlet.properties", DispatcherServlet.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
catch (IOException ex) {}
}
它的初始化就是读取类路径下的名为DispatcherServlet.properties
的配置文件,然后加载成Properties
对象而已。我们看看SpringMVC的包底下有没有这个文件:
果然在这里有这个文件,它的内容如下:
# 省略除了HandlerMapping策略接口以外的配置项
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
org.springframework.web.servlet.function.support.RouterFunctionMapping
所以,默认情况下有三个HandlerMapping被加载:
BeanNameUrlHandlerMapping
:从URL到BeanName的映射,使用Bean作为HandlerRequestMappingHandlerMapping
:从URL到带有@RequestMapping
方法的映射,使用方法作为HandlerRouterFunctionMapping
:不知道干啥的
打个断点验证一下:
自己配置HandlerMapping
@Configuration
@ComponentScan(basePackages = "top.yudoge.controller")
public class AppConfig {
class MyHandler {
public String handle() {
return "helloPage";
}
}
@Bean
public HandlerMapping handlerMapping() {
return new HandlerMapping() {
@Override
public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
HandlerExecutionChain executionChain = new HandlerExecutionChain(new MyHandler());
return executionChain;
}
};
}
}
可以看到这次走到了这个分支中,并且只有一个this.handlerMappings
中只有一个HandlerMapping,就是我们在AppConfig中定义的内部类MyHandler:
这个代码目前显然还没什么意义,因为我们还没有对应的HandlerAdapter以及ViewResovler,现在通过浏览器访问,你会发现产生如下报错:
上面说的需要一个能够支持AppConfig$MyHandler
这个处理器的HandlerAdapter。
顺便提一嘴,HandlerMapping的
getHandler
方法需要根据传入的HttpServletRequest
来判断自己能否处理这个请求,如果能,就返回对应的HandlerExecutionChain
(包含一个用于处理请求的Handler和若干Interceptor),否则返回null。我们的代码没有判断直接返回了一个HandlerExecutionChain,这代表它能处理所有请求。
对HandlerMapping
、HandlerAdapter
、Handler
比较陌生的可以看这篇文章
HandlerAdapter
万事开头难,有了上面的分析打基础,后面的分析都会变得简单,就比如initHandlerAdapters
方法的分析:
private void initHandlerAdapters(ApplicationContext context) {
this.handlerAdapters = null;
if (this.detectAllHandlerAdapters) {
Map<String, HandlerAdapter> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerAdapters = new ArrayList<>(matchingBeans.values());
AnnotationAwareOrderComparator.sort(this.handlerAdapters);
}
}
else {
try {
HandlerAdapter ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class);
this.handlerAdapters = Collections.singletonList(ha);
}
catch (NoSuchBeanDefinitionException ex) {
}
}
if (this.handlerAdapters == null) {
this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerAdapters declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
?区别???
没有区别,我们直接来查看DispatcherServlet.properties
文件,看看默认情况下有哪些HandlerAdapter为我们服务
org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\
org.springframework.web.servlet.function.support.HandlerFunctionAdapter
HttpRequestHandlerAdapter
:用于调用HttpRequestHandler
SimpleControllerHandlerAdapter
:用于调用实现了org.springframework.web.servlet.mvc.Controller
接口的类,这种情况下Controller
实现类就是Handler。(它对标的应该是BeanNameHandlerMapping
)RequestMappingHandlerAdapter
:用于调用@RequestMapping
标注的方法,它和RequestMappingHandlerMapping
一起工作HandlerFunctionAdapter
:用于调用HandlerFunction
这种Handler(它对标的应该是RouterFunctionMapping
)
现在,我们为AppConfig定义一个HandlerAdapter:
@Bean
public HandlerAdapter handlerAdapter() {
return new HandlerAdapter() {
@Override
public boolean supports(Object handler) {
return handler instanceof MyHandler;
}
@Override
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String viewName = ((MyHandler) handler).handle();
return new ModelAndView(viewName);
}
@Override
public long getLastModified(HttpServletRequest request, Object handler) {
return -1;
}
};
}
在它的supports
方法中,我们判断了Handler是否是MyHandler
的实例,只有当它是MyHandler的实例时才返回支持。
在handle
方法中,我们调用了MyHandler.handle
方法,并把这个方法返回的字符串(helloPage
)当作视图名,构建一个ModelAndView
并返回。
在getLastModified
方法中,返回了-1
代表不支持此功能。
现在运行,还是报错:
这里的错误看起来是一个循环引用,我们先往下深入。
ViewResovler
initViewResovlers
的代码也一样,我就不看了,只看DispatcherServlet.properties
中定义了哪些默认视图解析器吧
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
只定义了一个InternalResourceViewResolver
,这个视图解析器使用内部资源来进行视图解析,它会将视图名加上前缀和后缀,然后以内部资源表示处理后的视图名。说白了就是将请求转发到这个加上前缀后缀的视图名上。如果以后有机会会阅读ViewResovler
的源码,不过我感觉我的时间快不够了哈哈哈哈。
所以,MyHandler
这个逼拦截一切请求,它必然也会拦截视图解析器的转发,所以,这个转发又被转到MyHandler
中进行处理,而如果任由MyHandler处理的化,视图解析器会再次转发,这样就陷入了循环,所以上面报了循环解析异常。
利用所学,解决上面的问题
造成上面的问题的根本原因就是——MyHandler
把视图解析器的转发到helloPage
的请求给拦截了。我们现在打算最终让MyHandler中的返回值helloPage
被/helloPage.jsp
这个jsp文件服务。
想把字符串helloPage
变成内部资源URL/helloPage.jsp
,需要提供一个内部资源视图解析器,并提供前缀后缀:
@Bean
public ViewResolver viewResolver() {
return new InternalResourceViewResolver("/", ".jsp");
}
其次,我们想让MyHandler
不拦截jsp文件的请求,我们可以这样写:
@Bean
public HandlerMapping handlerMapping() {
return new HandlerMapping() {
@Override
public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (request.getServletPath().endsWith(".jsp")) {
return null;
}
HandlerExecutionChain executionChain = new HandlerExecutionChain(new MyHandler());
return executionChain;
}
};
}
但实际上,我们并用不到写这个判断,因为本来所有.jsp
结尾的url也不会被MyHandler
拦截,这个我们稍后分析下原因。
反正现在,页面显示出来了:
为什么我们的DispatcherServlet不拦截jsp结尾的文件
首先,我们所定义的DispatcherServlet
的匹配规则如下:
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
也就是匹配/
这个路径,官方对这种路径是这样描述的:
A string containing only the ’/’ character indicates the "default" servlet of the application.
一个仅仅包含字符“/”的字符串,代表着应用程序的默认Servlet。
还有描述匹配规则优先级中的下面这一段:
If neither of the previous three rules result in a servlet match, the container will attempt to serve content appropriate for the resource requested. If a "default" servlet is defined for the application, it will be used. Many containers provide an implicit default servlet for serving content.
如果上面的三个规则都没有导致一个servlet被匹配,容器将尝试提供适合所请求资源的内容。如果一个“默认”servlet在应用程序中被定义,那么它将被使用。很多容器提供一个隐式的默认servlet来提供内容。
所以,只有当没有Servlet能够提供请求的响应时,我们的DispatcherServlet
才会被使用。这代表着肯定有某个Servlet匹配了对jsp文件的访问。
我们不妨在jsp页面上输出一下当前系统中所有的Servlet、它们匹配的路径以及它们的类名:
<h1>HelloPage</h1>
<ul>
<%
Map<String, ServletRegistration> registrationMap = (Map<String, ServletRegistration>) application.getServletRegistrations();
for (ServletRegistration sr : registrationMap.values()) {
%>
<li><%=sr.getName()%> , <%=sr.getMappings()%>, <%=sr.getClassName()%></li>
<%
}
%>
</ul>
可以看到,有一个默认的,啥也不匹配的servlet,有一个匹配*.jspx
和*.jsp
的servlet,最后一个就是我们的DispatcherServlet
,它匹配/
,是官方定义中的默认servlet,它的优先级不如上面的jsp
。
由org.apache.catalina/org.apache.jsper
这个两个包,可以推断出前两个Servlet来自tomcat内部,由tomcat注册。