SpringMVC文档、源码阅读——DispatcherServlet特殊Bean加载
2022/7/23 14:25:19
本文主要是介绍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注册。
这篇关于SpringMVC文档、源码阅读——DispatcherServlet特殊Bean加载的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-05-15鸿蒙生态设备数量超8亿台
- 2024-05-13TiDB + ES:转转业财系统亿级数据存储优化实践
- 2024-05-09“2024鸿蒙零基础快速实战-仿抖音App开发(ArkTS版)”实战课程已上线
- 2024-05-09聊聊如何通过arthas-tunnel-server来远程管理所有需要arthas监控的应用
- 2024-05-09log4j2这么配就对了
- 2024-05-09nginx修改Content-Type
- 2024-05-09Redis多数据源,看这篇就够了
- 2024-05-09Google Chrome驱动程序 124.0.6367.62(正式版本)去哪下载?
- 2024-05-09有没有大佬知道这种数据应该怎么抓取呀?
- 2024-05-09这种运行结果里的10.100000001,怎么能最快改成10.1?