SpringMVC作为MVC框架近年来被广泛地使用,其与Mybatis和Spring的组合,也成为许多公司开发web的套装。SpringMVC继承了Spring的优点,对业务代码的非侵入性,配置的便捷和灵活,再加上注解方式的简便与流行,SpringMVC自然成为web开发中MVC框架的首选。

SpringMVC的设计理念,简单来说,就是将Spring的IOC容器与Servlet结合起来,从而在IOC容器中维护Servlet相关对象的生命周期,同时将Spring的上下文注入到Servlet的上下文中。依靠Servlet的事件和监听机制来操作和维护外部请求,以及组装和执行请求对应的响应。

XML配置

SpringMVC想与Servlet相结合,首先得在Servlet容器中进行配置。以Tomcat为例,通常在web.xml文件中配置一个监听器和SpringMVC的核心Servlet。

监听器

  1. <context-param>
  2. <param-name>contextConfigLocation</param-name>
  3. <param-value>classpath:spring.xml</param-value>
  4. </context-param>
  5.  
  6. <listener>
  7. <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  8. </listener>

核心Servlet

  1. <servlet>
  2. <servlet-name>dispatcher</servlet-name>
  3. <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  4. <init-param>
  5. <param-name>contextConfigLocation</param-name>
  6. <param-value>classpath:spring-mvc.xml</param-value>
  7. </init-param>
  8. <load-on-startup></load-on-startup>
  9. </servlet>
  10. <servlet-mapping>
  11. <servlet-name>dispatcher</servlet-name>
  12. <url-pattern>/</url-pattern>
  13. </servlet-mapping>

当我准备研究SpringMVC源码时,我问出了一个早应该问的问题:为什么配置了DispatcherServlet,还需要一个监听器,而且都能加载配置文件?在context-param中的配置文件要不要在DispatcherServlet中的init-param再加上?相信很多刚用SpringMVC的人都闪现过这样的问题。翻阅过源码后,明白了SpringMVC通过这种方式实现了父子上下文容器结构

Tomcat启动时,监听器ContextLoaderListener创建一个XMLWebApplicationContext上下文容器,并加载context-param中的配置文件,完成容器的刷新后将上下文设置到ServletContext。当DispatcherServlet创建时,先进行初始化操作,从ServletContext中查询出监听器中创建的上下文对象,作为父类上下文来创建servlet的上下文容器,并加载Servlet配置中的init-param的配置文件(默认加载/WEB-INF/servletName-servlet.xml,servletName为DispatcherServlet配置的servlet-name),然后完成容器的刷新。子上下文可以访问父上下文中的bean,反之则不行。

父子上下文容器结构如下

通常是将业务操作及数据库相关的bean维护在Listener的父容器中,而在Servlet的子容器中只加载Controller相关的业务实现的bean。从而将业务实现和业务的具体操作分隔在两个上下文容器中,业务实现bean可以调用业务具体操作的bean。

ServletContext启动监听

ServletContextListener监听ServletContext的生命周期。每个web application对应一个ServletContext,用于servlet与servlet容器沟通的中介。它定义两个方法,context初始化和context销毁。

  1. public interface ServletContextListener extends EventListener {
  2.  
  3. public void contextInitialized(ServletContextEvent sce);
  4.  
  5. public void contextDestroyed(ServletContextEvent sce);
  6. }

SpringMVC的ContextLoaderListener实现了此接口,在web application启动时创建一个Spring的ROOT上下文。

根上下文的创建

SpringMVC根上下文是通过ServletContext的监听器进行创建,默认的监听器为ContextLoaderListener。当web应用启动时,会调用监听器的contextInitialized方法。

  1. public void contextInitialized(ServletContextEvent event) {
  2. initWebApplicationContext(event.getServletContext());
  3. }

contextInitialized方法接受参数ServletContext,实际的web上下文的创建交给了子类ContextLoader。

  1. public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
  2. // 判断ServletContext是否已存在SpringMVC根上下文,存在则报错
  3. if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
  4. throw new IllegalStateException(
  5. "Cannot initialize context because there is already a root application context present - " +
  6. "check whether you have multiple ContextLoader* definitions in your web.xml!");
  7. }
  8.  
  9. // Store context in local instance variable, to guarantee that
  10. // it is available on ServletContext shutdown.
  11. if (this.context == null) {
  12. // 创建上下文根容器
  13. this.context = createWebApplicationContext(servletContext);
  14. }
  15. if (this.context instanceof ConfigurableWebApplicationContext) {
  16. ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
  17. if (!cwac.isActive()) {
  18. // The context has not yet been refreshed -> provide services such as
  19. // setting the parent context, setting the application context id, etc
  20. if (cwac.getParent() == null) {
  21. // The context instance was injected without an explicit parent ->
  22. // determine parent for root web application context, if any.
  23. ApplicationContext parent = loadParentContext(servletContext);
  24. cwac.setParent(parent);
  25. }
  26. // 加载并刷新上下文环境,也就是初始化Spring容器
  27. // 绑定ServletContext到Spring根上下文
  28. configureAndRefreshWebApplicationContext(cwac, servletContext);
  29. }
  30. }
  31.  
  32. // 将创建完的根上下文绑定到ServletContext
  33. servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
  34.  
  35. return this.context;
  36. }

我们来看看创建根容器 createWebApplicationContext

  1. protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
  2. Class<?> contextClass = this.determineContextClass(sc);
  3. if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
  4. throw new ApplicationContextException("Custom context class [" + contextClass.getName() + "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
  5. } else {
  6. return (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass);
  7. }
  8. }

先是获取Class对象,然后利用反射实例化对象

  1. protected Class<?> determineContextClass(ServletContext servletContext) {
  2. //可以手动在web.xml中配置contextClass参数
  3. String contextClassName = servletContext.getInitParameter("contextClass");
  4. if (contextClassName != null) {
  5. try {
  6. return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
  7. } catch (ClassNotFoundException var4) {
  8. throw new ApplicationContextException("Failed to load custom context class [" + contextClassName + "]", var4);
  9. }
  10. } else {
  11. //在配置文件中有如下配置
  12. //org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext
  13. contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
  14.  
  15. try {
  16. //利用反射加载类
  17. return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
  18. } catch (ClassNotFoundException var5) {
  19. throw new ApplicationContextException("Failed to load default context class [" + contextClassName + "]", var5);
  20. }
  21. }
  22. }

最后再调用 BeanUtils.instantiateClass 实例化对象

  1. public static <T> T instantiateClass(Class<T> clazz) throws BeanInstantiationException {
  2. Assert.notNull(clazz, "Class must not be null");
  3. if (clazz.isInterface()) {
  4. throw new BeanInstantiationException(clazz, "Specified class is an interface");
  5. } else {
  6. try {
  7. //获取构造器并实例化
  8. return instantiateClass(clazz.getDeclaredConstructor());
  9. } catch (NoSuchMethodException var2) {
  10. throw new BeanInstantiationException(clazz, "No default constructor found", var2);
  11. }
  12. }
  13. }

最后我们来看看容器的初始化 configureAndRefreshWebApplicationContext(cwac, servletContext);

  1. protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
  2. String configLocationParam;
  3. if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
  4. configLocationParam = sc.getInitParameter("contextId");
  5. if (configLocationParam != null) {
  6. wac.setId(configLocationParam);
  7. } else {
  8. wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getContextPath()));
  9. }
  10. }
  11.  
  12. wac.setServletContext(sc);
       //从配置文件中获取全局init参数“
    contextConfigLocation”,也就是spring.xml,并设置到父容器中
  1. configLocationParam = sc.getInitParameter("contextConfigLocation");
  2. if (configLocationParam != null) {
  3. wac.setConfigLocation(configLocationParam);
  4. }
  5.  
  6. ConfigurableEnvironment env = wac.getEnvironment();
  7. if (env instanceof ConfigurableWebEnvironment) {
  8. ((ConfigurableWebEnvironment)env).initPropertySources(sc, (ServletConfig)null);
  9. }
  10.  
  11. this.customizeContext(sc, wac);
      //刷新父容器
  12. wac.refresh();
  13. }

其实是调用 ConfigurableWebApplicationContext 的 refresh() 对容器的初始化。

  1. public void refresh() throws BeansException, IllegalStateException {
  2. synchronized (this.startupShutdownMonitor) {
  3. // Prepare this context for refreshing.
  4. //准备刷新的上下文 环境
  5. /*
  6. * 初始化前的准备工作,例如对系统属性或者环境变量进行准备及验证。
  7. * 在某种情况下项目的使用需要读取某些系统变量,而这个变量的设置很可能会影响着系统
  8. * 的正确性,那么ClassPathXmlApplicationContext为我们提供的这个准备函数就显得非常必要,
  9. * 它可以在Spring启动的时候提前对必须的变量进行存在性验证。
  10. */
  11. prepareRefresh();
  12. // Tell the subclass to refresh the internal bean factory.
  13. //初始化BeanFactory,并进行XML文件读取
  14. /*
  15. * ClassPathXMLApplicationContext包含着BeanFactory所提供的一切特征,在这一步骤中将会复用
  16. * BeanFactory中的配置文件读取解析及其他功能,这一步之后,ClassPathXmlApplicationContext
  17. * 实际上就已经包含了BeanFactory所提供的功能,也就是可以进行Bean的提取等基础操作了。
  18. */
  19. ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
  20. // Prepare the bean factory for use in this context.
  21. //对beanFactory进行各种功能填充
  22. /*
  23. * @Qualifier与@Autowired等注解正是在这一步骤中增加的支持
  24. */
  25. prepareBeanFactory(beanFactory);
  26. try {
  27. // Allows post-processing of the bean factory in context subclasses.
  28. //子类覆盖方法做额外处理
  29. /*
  30. * Spring之所以强大,为世人所推崇,除了它功能上为大家提供了便利外,还有一方面是它的
  31. * 完美架构,开放式的架构让使用它的程序员很容易根据业务需要扩展已经存在的功能。这种开放式
  32. * 的设计在Spring中随处可见,例如在本例中就提供了一个空的函数实现postProcessBeanFactory来
  33. * 方便程序猿在业务上做进一步扩展
  34. */
  35. postProcessBeanFactory(beanFactory);
  36. // Invoke factory processors registered as beans in the context.
  37. //激活各种beanFactory处理器
  38. invokeBeanFactoryPostProcessors(beanFactory);
  39. // Register bean processors that intercept bean creation.
  40. //注册拦截Bean创建的Bean处理器,这里只是注册,真正的调用实在getBean时候
  41. registerBeanPostProcessors(beanFactory);
  42. // Initialize message source for this context.
  43. //为上下文初始化Message源,即不同语言的消息体,国际化处理
  44. initMessageSource();
  45. // Initialize event multicaster for this context.
  46. //初始化应用消息广播器,并放入“applicationEventMulticaster”bean中
  47. initApplicationEventMulticaster();
  48. // Initialize other special beans in specific context subclasses.
  49. //留给子类来初始化其它的Bean
  50. onRefresh();
  51. // Check for listener beans and register them.
  52. //在所有注册的bean中查找Listener bean,注册到消息广播器中
  53. registerListeners();
  54. // Instantiate all remaining (non-lazy-init) singletons.
  55. //初始化剩下的单实例(非惰性的)
  56. finishBeanFactoryInitialization(beanFactory);
  57. // Last step: publish corresponding event.
  58. //完成刷新过程,通知生命周期处理器lifecycleProcessor刷新过程,同时发出ContextRefreshEvent通知别人
  59. finishRefresh();
  60. }
  61. catch (BeansException ex) {
  62. destroyBeans();
  63. // Reset 'active' flag.
  64. cancelRefresh(ex);
  65. // Propagate exception to caller.
  66. throw ex;
  67. }
  68. finally {
  69. resetCommonCaches();
  70. }
  71. }
  72. }

当SpringMVC上下文创建完成后,以固定的属性名称将其绑定到Servlet上下文上,用以在servlet子上下文创建时从Servlet上下文获取,并设置为其父上下文,从而完成父子上下文的构成。

  1. servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

Servlet的初始化

Servlet的生命周期从第一次访问Servlet开始,Servlet对象被创建并执行初始化操作。而每次请求则由servlet容器交给Servlet的service方法执行,最后在web application停止时调用destroy方法完成销毁前处理。

  1. public interface Servlet {
  2.  
  3. public void init(ServletConfig config) throws ServletException;
  4.  
  5. public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
  6.  
  7. public void destroy();
  8. }

在web.xml的servlet配置选项中有一个load-on-startup,其值为整数,标识此Servlet是否在容器启动时的加载优先级。若值大于0,按从小到大的顺序被依次加载;若为0,则标识最大整数,最后被加载;若值小于0,表示不加载。默认load-on-startup的值为-1。servlet的加载是在加载完所有ServletContextListener后才执行。

先来看下DispatcherServlet的类图

servlet子上下文的创建是在servlet的初始化方法init中。而SpringMVC的核心Servlet-DispatcherServlet的初始化操作则是在其父类HttpServletBean中。

  1. public final void init() throws ServletException {
  2. if (logger.isDebugEnabled()) {
  3. logger.debug("Initializing servlet '" + getServletName() + "'");
  4. }
  5.  
  6. // Set bean properties from init parameters.
  7. //从初始化参数设置bean属性。例如init-param的contextConfigLocation classpath*:spring-mvc.xml
  8. PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
  9. if (!pvs.isEmpty()) {
  10. try {
  11. //将DispatcherServlet转化成Spring里面的Bean,
  12. BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
  13. //加载配置信息
  14. ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
  15. bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
  16. initBeanWrapper(bw);
  17. //通过Spring的Bean赋值方式给DispatcherServlet初始化属性值
  18. bw.setPropertyValues(pvs, true);
  19. }
  20. catch (BeansException ex) {
  21. if (logger.isErrorEnabled()) {
  22. logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
  23. }
  24. throw ex;
  25. }
  26. }
  27.  
  28. // Let subclasses do whatever initialization they like.
  29. //模板方法,子类可以去自定义
  30. initServletBean();
  31.  
  32. if (logger.isDebugEnabled()) {
  33. logger.debug("Servlet '" + getServletName() + "' configured successfully");
  34. }
  35. }

initServletBean的实现在子类FrameworkServlet中

  1. protected final void initServletBean() throws ServletException {
  2.  
  3. // 创建servlet子上下文
  4. this.webApplicationContext = initWebApplicationContext();
  5. // 可扩展方法
  6. initFrameworkServlet();
  7. }

此处的initWebApplicationContext方法同ContextLoader步骤相似

  1. protected WebApplicationContext initWebApplicationContext() {
  2. // 从ServletContext获取SpringMVC根上下文
  3. WebApplicationContext rootContext =
  4. WebApplicationContextUtils.getWebApplicationContext(getServletContext());
  5. WebApplicationContext wac = null;
  6.  
  7. // 如果SpringMVC的servlet子上下文对象不为空,则设置根上下文为其父类上下文,然后刷新
  8. if (this.webApplicationContext != null) {
  9. // A context instance was injected at construction time -> use it
  10. wac = this.webApplicationContext;
  11. if (wac instanceof ConfigurableWebApplicationContext) {
  12. ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
  13. if (!cwac.isActive()) {
  14. // The context has not yet been refreshed -> provide services such as
  15. // setting the parent context, setting the application context id, etc
  16. if (cwac.getParent() == null) {
  17. // The context instance was injected without an explicit parent -> set
  18. // the root application context (if any; may be null) as the parent
  19. cwac.setParent(rootContext);
  20. }
  21. configureAndRefreshWebApplicationContext(cwac);
  22. }
  23. }
  24. }
  25. if (wac == null) {
  26. // 根据init-param配置的属性名称从ServletContext查找SpringMVC的servlet子上下文
  27. wac = findWebApplicationContext();
  28. }
  29. if (wac == null) {
  30. // 若还为空,则创建一个新的上下文对象并刷新
  31. wac = createWebApplicationContext(rootContext);
  32. }
  33.  
  34. // 子类自定义对servlet子上下文后续操作,在DispatcherServlet中实现
  35. if (!this.refreshEventReceived) {
  36. onRefresh(wac);
  37. }
  38.  
  39. // 发布servlet子上下文到ServletContext
  40. if (this.publishContext) {
  41. // Publish the context as a servlet context attribute.
  42. String attrName = getServletContextAttributeName();
  43. getServletContext().setAttribute(attrName, wac);
  44. if (this.logger.isDebugEnabled()) {
  45. this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
  46. "' as ServletContext attribute with name [" + attrName + "]");
  47. }
  48. }
  49.  
  50. return wac;
  51. }

servlet子上下文的创建也有几个关键点

  1. 从ServletContext中获取第一步中创建的SpringMVC根上下文,为下面做准备
  2. 根据init-param中的contextAttribute属性值从ServletContext查找是否存在上下文对象
  3. 以XmlWebApplicationContext作为Class类型创建上下文对象,设置父类上下文,并完成刷新
  4. 执行子类扩展方法onRefresh,在DispatcherServlet内初始化所有web相关组件
  5. 将servlet子上下文对象发布到ServletContext

实例化上下文对象时使用默认的ContextClass,即XmlWebApplicationContext,子类也可以重写此方法来支持其他上下文类型。

  1. protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) {
  2. //this.contextClass = DEFAULT_CONTEXT_CLASS;
  3. //DEFAULT_CONTEXT_CLASS = XmlWebApplicationContext.class;
  4. Class<?> contextClass = this.getContextClass();
  5. if (this.logger.isDebugEnabled()) {
  6. this.logger.debug("Servlet with name '" + this.getServletName() + "' will try to create custom WebApplicationContext context of class '" + contextClass.getName() + "', using parent context [" + parent + "]");
  7. }
  8.  
  9. if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
  10. throw new ApplicationContextException("Fatal initialization error in servlet with name '" + this.getServletName() + "': custom WebApplicationContext class [" + contextClass.getName() + "] is not of type ConfigurableWebApplicationContext");
  11. } else {
  12. //反射创建实例
  13. ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass);
  14. wac.setEnvironment(this.getEnvironment());
  15. //设置父容器
  16. wac.setParent(parent);
        //设置容器启动XML:
    例如init-param的contextConfigLocation classpath*:spring-mvc.xml
  1. wac.setConfigLocation(this.getContextConfigLocation());
  2. //初始化新创建的子容器
  3. this.configureAndRefreshWebApplicationContext(wac);
  4. return wac;
  5. }
  6. }

在上下文的配置刷新方法configureAndRefreshWebApplicationContext中,将ServletContext和ServletConfig都绑定到servlet子上下文对象中。另外设置了默认的namespace,即servletName-servlet

  1. protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
  2. if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
  3. if (this.contextId != null) {
  4. wac.setId(this.contextId);
  5. } else {
  6. wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(this.getServletContext().getContextPath()) + '/' + this.getServletName());
  7. }
  8. }
  9. //将ServletContext和ServletConfig都绑定到servlet子上下文对象中
  10. wac.setServletContext(this.getServletContext());
  11. wac.setServletConfig(this.getServletConfig());
  12. wac.setNamespace(this.getNamespace());
  13. wac.addApplicationListener(new SourceFilteringListener(wac, new FrameworkServlet.ContextRefreshListener(null)));
  14. ConfigurableEnvironment env = wac.getEnvironment();
  15. if (env instanceof ConfigurableWebEnvironment) {
  16. ((ConfigurableWebEnvironment)env).initPropertySources(this.getServletContext(), this.getServletConfig());
  17. }
  18.  
  19. this.postProcessWebApplicationContext(wac);
  20. this.applyInitializers(wac);
  21. //最后初始化子容器,和上面根容器初始化一样
  22. wac.refresh();
  23. }

当所有操作完成后,将servlet子上下文以org.springframework.web.servlet.FrameworkServlet.CONTEXT. + servletName的属性名称注册到ServletContext中,完成和ServletContext的双向绑定。

SpringMVC在DispatcherServlet的初始化过程中,同样会初始化一个WebApplicationContext的实现类,作为自己独有的上下文,这个独有的上下文,会将上面的根上下文作为自己的父上下文,来存放SpringMVC的配置元素,然后同样作为ServletContext的一个属性,被设置到ServletContext中,只不过它的key就稍微有点不同,key和具体的DispatcherServlet注册在web.xml文件中的名字有关,从这一点也决定了,我们可以在web.xml文件中注册多个DispatcherServlet,因为Servlet容器中注册的Servlet名字肯定不一样,设置到Servlet环境中的key也肯定不同。

组件初始化

在servlet子上下文完成创建,调用了模板扩展方法OnRefresh,它在FrameworkServlet中仅仅只是个空方法,但在其子类DispatcherServlet中则至关重要,它是一切组件的起源。

  1. DispatcherServlet.java
  2.  
  3. protected void onRefresh(ApplicationContext context) {
  4. initStrategies(context);
  5. }

初始化所有策略,其实是指各个组件可以通过策略动态地进行配置。

  1. protected void initStrategies(ApplicationContext context) {
  2. // 文件上传解析器
  3. initMultipartResolver(context);
  4. // 本地化解析器
  5. initLocaleResolver(context);
  6. // 主题解析器
  7. initThemeResolver(context);
  8. // 处理器映射器(url和Controller方法的映射)
  9. initHandlerMappings(context);
  10. // 处理器适配器(实际执行Controller方法)
  11. initHandlerAdapters(context);
  12. // 处理器异常解析器
  13. initHandlerExceptionResolvers(context);
  14. // RequestToViewName解析器
  15. initRequestToViewNameTranslator(context);
  16. // 视图解析器(视图的匹配和渲染)
  17. initViewResolvers(context);
  18. // FlashMap管理者
  19. initFlashMapManager(context);
  20. }

以上基本将SpringMVC中的主要组件都罗列出来了,其中最重要的当是HandlerMapping,HandlerAdapter和ViewResolver。由于各组件的初始化策略方式相似,我们以HandlerMapping来介绍。

  1. private void initHandlerMappings(ApplicationContext context) {
  2. this.handlerMappings = null;
  3.  
  4. // 是否查找所有HandlerMapping标识
  5. if (this.detectAllHandlerMappings) {
  6. // 从上下文(包含所有父上下文)中查找HandlerMapping类型的Bean,是下文中的RequestMappingHandlerMapping,其中包含URL和Mapping的映射Map
  7. Map<String, HandlerMapping> matchingBeans =
  8. BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
  9. if (!matchingBeans.isEmpty()) {
            //将从容器中查找出来的HandlerMapping加入到DispatcherServlet的handlerMappings属性中
  10. this.handlerMappings = new ArrayList<HandlerMapping>(matchingBeans.values());
  11. // We keep HandlerMappings in sorted order.
  12. AnnotationAwareOrderComparator.sort(this.handlerMappings);
  13. }
  14. }
  15. else {
  16. try {
  17. // 根据指定名称获取HandlerMapping对象
  18. HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
  19. this.handlerMappings = Collections.singletonList(hm);
  20. }
  21. catch (NoSuchBeanDefinitionException ex) {
  22. // Ignore, we'll add a default HandlerMapping later.
  23. }
  24. }
  25.  
  26. // 确保至少有一个HandlerMapping,如果没能找到,注册一个默认的
  27. if (this.handlerMappings == null) {
  28. this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
  29. if (logger.isDebugEnabled()) {
  30. logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default");
  31. }
  32. }
  33. }

策略的逻辑很简单:有一个标识,是否查找所有HandlerMapping(默认为true)。如果为是,则从上下文(包括所有父上下文)中查询类型为HandlerMapping的Bean,并进行排序。如果为否,则从上下文中按指定名称去寻找。如果都没有找到,提供一个默认的实现。这个默认实现从DispatcherServlet同级目录的DispatcherServlet.properties中加载得

  1. org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
  2.  
  3. org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver
  4.  
  5. org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
  6. org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping
  7.  
  8. org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
  9. org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
  10. org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
  11.  
  12. org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\
  13. org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
  14. org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
  15.  
  16. org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
  17.  
  18. org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
  19.  
  20. org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

可以看到SpringMVC为每个组件都提供了默认的实现,通常情况下都能够满足需求。如果你想对某个组件进行定制,可以通过spring的xml文件或者@Configuration类中的@Bean来实现。比如常配置的视图解析器:

xml方式

  1. <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  2. <property name="prefix" value="/WEB-INF/"></property>
  3. <property name="suffix" value=".jsp"></property>
  4. </bean>

由于其他组件的初始化方式完全一致,这里就不赘述了。需要关注的一点是,当匹配到合适的组件时,都会通过Spring的方式实例化组件。而有些组件在实例化时也会对自身运行环境进行初始化。

initHandlerAdapters(context);

URL-Controller方法映射初始化

在使用SpringMVC时,需要在xml文件中添加一行注解

  1. <mvc:annotation-driven />

或者在配置类上增加注解@EnableWebMvc,才能使SpringMVC的功能完全开启。我们以xml的配置为例,看看它都做了什么。根据spring对namespace的解析策略找到MvcNamespaceHandler类,在其init方法中

  1. registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser());

直接看AnnotationDrivenBeanDefinitionParser的parse方法,上下有一百多行,浏览一遍,主要就是注册各种组件和内部需要的解析器和转换器。找到HandlerMapping组件的实现

  1. RootBeanDefinition handlerMappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class);

因而实际用的HandlerMapping实现即为RequestMappingHandlerMapping。其抽象基类AbstractHandlerMethodMapping实现了InitializingBean接口。

  1. public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean

当RequestMappingHandlerMapping对象初始化时,会调用InitializingBean接口的afterPropertiesSet方法。在此方法中完成了Controller方法同请求url的映射表。

  1. public void afterPropertiesSet() {
  2. initHandlerMethods();
  3. }
  4.  
  5. protected void initHandlerMethods() {
  6. if (logger.isDebugEnabled()) {
  7. logger.debug("Looking for request mappings in application context: " + getApplicationContext());
  8. }
  9. // 默认只从当前上下文中查询所有beanName
  10. String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
  11. BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
  12. getApplicationContext().getBeanNamesForType(Object.class));
  13.  
  14. for (String beanName : beanNames) {
  15. if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
  16. Class<?> beanType = null;
  17. try {
  18. beanType = getApplicationContext().getType(beanName);
  19. }
  20. catch (Throwable ex) {
  21. // An unresolvable bean type, probably from a lazy bean - let's ignore it.
  22. if (logger.isDebugEnabled()) {
  23. logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
  24. }
  25. }
  26. // 如果类上有@Controller或@RequestMapping注解,则进行解析
  27. if (beanType != null && isHandler(beanType)) {
  28. detectHandlerMethods(beanName);
  29. }
  30. }
  31. }
  32. handlerMethodsInitialized(getHandlerMethods());
  33. }

isHandler通过反射工具类判断类上是否有 Controller.class 或者 RequestMapping.class 注解

  1. protected boolean isHandler(Class<?> beanType) {
  2. return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class);
  3. }

对于类上有@Controller或@RequestMapping注解,都进行了detect。

  1. protected void detectHandlerMethods(final Object handler) {
  2. Class<?> handlerType = (handler instanceof String ?
  3. getApplicationContext().getType((String) handler) : handler.getClass());
  4. final Class<?> userType = ClassUtils.getUserClass(handlerType);
  5.  
  6. // 方法内省器,用于发现@RequestMapping注解的方法
  7. Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
  8. new MethodIntrospector.MetadataLookup<T>() {
  9. @Override
  10. public T inspect(Method method) {
  11. try {
  12. return getMappingForMethod(method, userType);
  13. }
  14. catch (Throwable ex) {
  15. throw new IllegalStateException("Invalid mapping on handler class [" +
  16. userType.getName() + "]: " + method, ex);
  17. }
  18. }
  19. });
  20.  
  21. if (logger.isDebugEnabled()) {
  22. logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods);
  23. }
  24.  
  25. // 遍历所有有效方法,封装方法为可执行的Method,注册到URL-Controller方法映射表
  26. for (Map.Entry<Method, T> entry : methods.entrySet()) {
  27. Method invocableMethod = AopUtils.selectInvocableMethod(entry.getKey(), userType);
  28. T mapping = entry.getValue();
  29. registerHandlerMethod(handler, invocableMethod, mapping);
  30. }
  31. }

方法内省器中的内部类调用的getMappingForMethod方法为抽象方法,实现在RequestMappingHandlerMapping中。

  1. protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
  2. RequestMappingInfo info = createRequestMappingInfo(method);
  3. if (info != null) {
  4. RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
  5. if (typeInfo != null) {
  6. info = typeInfo.combine(info);
  7. }
  8. }
  9. return info;
  10. }
  11.  
  12. private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
  13. RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
  14. RequestCondition<?> condition = (element instanceof Class ?
  15. getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
  16. return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
  17. }

解析方法上的@RequestMapping注解返回RequestMappingInfo,其实就是请求映射信息

  1. protected RequestMappingInfo createRequestMappingInfo(RequestMapping requestMapping, RequestCondition<?> customCondition) {
  2. return RequestMappingInfo.paths(this.resolveEmbeddedValuesInPatterns(requestMapping.path())).methods(requestMapping.method()).params(requestMapping.params()).headers(requestMapping.headers()).consumes(requestMapping.consumes()).produces(requestMapping.produces()).mappingName(requestMapping.name()).customCondition(customCondition).options(this.config).build();
  3. }

而在上面的detect方法最后,注册URL-Controller方法映射表由registerHandlerMethod方法完成。

  1. protected void registerHandlerMethod(Object handler, Method method, T mapping) {
  2. this.mappingRegistry.register(mapping, handler, method);
  3. }

mappingRegistry是AbstractHandlerMethodMapping的核心构成,主要作用就是维护HandlerMethod的映射关系,以及提供映射的查询方法。其register方法的实现如下:

  1. public void register(T mapping, Object handler, Method method) {
  2. // 加锁,保证一致性
  3. this.readWriteLock.writeLock().lock();
  4. try {
  5. HandlerMethod handlerMethod = createHandlerMethod(handler, method);
  6. assertUniqueMethodMapping(handlerMethod, mapping);
  7.  
  8. if (logger.isInfoEnabled()) {
  9. logger.info("Mapped \"" + mapping + "\" onto " + handlerMethod);
  10. }
  11. // 添加mapping->HandlerMethod的映射
  12. this.mappingLookup.put(mapping, handlerMethod);
  13.  
  14. List<String> directUrls = getDirectUrls(mapping);
  15. for (String url : directUrls) {
  16. // 添加url->mapping的映射
  17. this.urlLookup.add(url, mapping);
  18. }
  19.  
  20. String name = null;
  21. if (getNamingStrategy() != null) {
  22. name = getNamingStrategy().getName(handlerMethod, mapping);
  23. addMappingName(name, handlerMethod);
  24. }
  25.  
  26. CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
  27. if (corsConfig != null) {
  28. this.corsLookup.put(handlerMethod, corsConfig);
  29. }
  30.  
  31. // 添加mapping->MappingRegistration的映射
  32. this.registry.put(mapping, new MappingRegistration<T>(mapping, handlerMethod, directUrls, name));
  33. }
  34. finally {
  35. this.readWriteLock.writeLock().unlock();
  36. }
  37. }

注册过程增加了三个个映射关系,一个是mapping->HandlerMethod的映射,一个是url->mapping的映射,一个是mapping->MappingRegistration的映射。通过前两个映射,可以将请求定位到确定的Controller的方法上,最后一个映射保留所有Mapping注册信息,用于unregister。而方法加锁则是保证所有映射的一致性。

至此,请求URL和Controller方法之间的关系就初始化完成了。上面 initHandlerMappings 就能从容器中拿到所有的HandlerMapping。

对应的关系是:SpringMvc子容器中有RequestMappingHandlerMapping对象,这个对象中有一个mappingRegistry内部类,这个内部类中有URL和Mapping的映射的Map,还有MappingInfo和方法的映射的Map

  1. class MappingRegistry {
  2. private final Map<T, AbstractHandlerMethodMapping.MappingRegistration<T>> registry = new HashMap();
  3. private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap();
  4. private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap();
  5. private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap();
  6. private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap();
  7. private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  8.  
  9. MappingRegistry() {
  10. }
  11.  
  12. public Map<T, HandlerMethod> getMappings() {
  13. return this.mappingLookup;
  14. }
  15.  
  16. public List<T> getMappingsByUrl(String urlPath) {
  17. return (List)this.urlLookup.get(urlPath);
  18. }
  19. }

参数解析器和返回值解析器的初始化

在使用SpringMVC时,对Controller中方法的参数和返回值的处理都非常的方便。我们知道,常用类型的参数不需要任何额外配置,SpringMVC即可完美转换,而返回值既可以是ModelAndView, 也可以是String,或者是HashMap,或者是ResponseEntity,多种方式简单配置,完美兼容。它是怎么做到的呢,通过一系列的转换器来完成的。而这些转换器也是需要初始化到运行环境中的, 谁的运行环境, HandlerAdapter的。

同样SpringMVC提供了一个默认的强大实现,RequestMappingHandlerAdapter,同样在<mvc:annotation-driven />中定义。它也实现了InitializingBean接口。

  1. public void afterPropertiesSet() {
  2. // Do this first, it may add ResponseBody advice beans
  3. // 初始化Controller通用切面
  4. initControllerAdviceCache();
  5.  
  6. // 初始化参数解析器
  7. if (this.argumentResolvers == null) {
  8. List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
  9. this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
  10. }
  11. // 初始化InitBinder解析器
  12. if (this.initBinderArgumentResolvers == null) {
  13. List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
  14. this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
  15. }
  16. // 初始化返回值处理器
  17. if (this.returnValueHandlers == null) {
  18. List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
  19. this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
  20. }
  21. }

每个组件都通过内置默认的实现,我们主要来看参数解析器和返回值处理器两个。

参数解析器

  1. private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
  2. List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();
  3.  
  4. // Annotation-based argument resolution
  5. resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
  6. resolvers.add(new RequestParamMapMethodArgumentResolver());
  7. resolvers.add(new PathVariableMethodArgumentResolver());
  8. resolvers.add(new PathVariableMapMethodArgumentResolver());
  9. resolvers.add(new MatrixVariableMethodArgumentResolver());
  10. resolvers.add(new MatrixVariableMapMethodArgumentResolver());
  11. resolvers.add(new ServletModelAttributeMethodProcessor(false));
  12. resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
  13. resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
  14. resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
  15. resolvers.add(new RequestHeaderMapMethodArgumentResolver());
  16. resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
  17. resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
  18. resolvers.add(new SessionAttributeMethodArgumentResolver());
  19. resolvers.add(new RequestAttributeMethodArgumentResolver());
  20.  
  21. // Type-based argument resolution
  22. resolvers.add(new ServletRequestMethodArgumentResolver());
  23. resolvers.add(new ServletResponseMethodArgumentResolver());
  24. resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
  25. resolvers.add(new RedirectAttributesMethodArgumentResolver());
  26. resolvers.add(new ModelMethodProcessor());
  27. resolvers.add(new MapMethodProcessor());
  28. resolvers.add(new ErrorsMethodArgumentResolver());
  29. resolvers.add(new SessionStatusMethodArgumentResolver());
  30. resolvers.add(new UriComponentsBuilderMethodArgumentResolver());
  31.  
  32. // Custom arguments
  33. if (getCustomArgumentResolvers() != null) {
  34. resolvers.addAll(getCustomArgumentResolvers());
  35. }
  36.  
  37. // Catch-all
  38. resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
  39. resolvers.add(new ServletModelAttributeMethodProcessor(true));
  40.  
  41. return resolvers;
  42. }

大家根据解析器名称大概可以推测出其作用,比如@RequestParam解析器,@PathVariable解析器,及@RequestBody和@ResponseBody解析器等等。SpringMVC强大的参数解析能力其实来源于丰富的内置解析器。

另一个返回值处理器的初始化

  1. private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
  2. List<HandlerMethodReturnValueHandler> handlers = new ArrayList<HandlerMethodReturnValueHandler>();
  3.  
  4. // Single-purpose return value types
  5. handlers.add(new ModelAndViewMethodReturnValueHandler());
  6. handlers.add(new ModelMethodProcessor());
  7. handlers.add(new ViewMethodReturnValueHandler());
  8. handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters()));
  9. handlers.add(new StreamingResponseBodyReturnValueHandler());
  10. handlers.add(new HttpEntityMethodProcessor(getMessageConverters(),
  11. this.contentNegotiationManager, this.requestResponseBodyAdvice));
  12. handlers.add(new HttpHeadersReturnValueHandler());
  13. handlers.add(new CallableMethodReturnValueHandler());
  14. handlers.add(new DeferredResultMethodReturnValueHandler());
  15. handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));
  16.  
  17. // Annotation-based return value types
  18. handlers.add(new ModelAttributeMethodProcessor(false));
  19. handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),
  20. this.contentNegotiationManager, this.requestResponseBodyAdvice));
  21.  
  22. // Multi-purpose return value types
  23. handlers.add(new ViewNameMethodReturnValueHandler());
  24. handlers.add(new MapMethodProcessor());
  25.  
  26. // Custom return value types
  27. if (getCustomReturnValueHandlers() != null) {
  28. handlers.addAll(getCustomReturnValueHandlers());
  29. }
  30.  
  31. // Catch-all
  32. if (!CollectionUtils.isEmpty(getModelAndViewResolvers())) {
  33. handlers.add(new ModelAndViewResolverMethodReturnValueHandler(getModelAndViewResolvers()));
  34. }
  35. else {
  36. handlers.add(new ModelAttributeMethodProcessor(true));
  37. }
  38.  
  39. return handlers;
  40. }

同样内置了对多种返回类型,返回方式的处理器, 如处理@ResponseBody 的处理器 RequestResponseBodyMethodProcessor,才支撑起丰富便捷的使用。

RequestMappingHandlerMapping,RequestMappingHandlerAdapter会被注册到SpringMvc的容器中,此对象中有所有的处理器,也就是List或者Map,最后在DispatcherServlet的init方法中的initHandlerAdapters方法中会找到这个RequestMappingHandlerAdapter并放到DispatcherServlet的handlerAdapters中

  1. private void initHandlerAdapters(ApplicationContext context) {
  2. this.handlerAdapters = null;
  3. if (this.detectAllHandlerAdapters) {
  4. Map<String, HandlerAdapter> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
  5. if (!matchingBeans.isEmpty()) {
  6. this.handlerAdapters = new ArrayList(matchingBeans.values());
  7. AnnotationAwareOrderComparator.sort(this.handlerAdapters);
  8. }
  9. } else {
  10. try {
  11. HandlerAdapter ha = (HandlerAdapter)context.getBean("handlerAdapter", HandlerAdapter.class);
  12. this.handlerAdapters = Collections.singletonList(ha);
  13. } catch (NoSuchBeanDefinitionException var3) {
  14. ;
  15. }
  16. }
  17.  
  18. if (this.handlerAdapters == null) {
  19. this.handlerAdapters = this.getDefaultStrategies(context, HandlerAdapter.class);
  20. if (this.logger.isDebugEnabled()) {
  21. this.logger.debug("No HandlerAdapters found in servlet '" + this.getServletName() + "': using default");
  22. }
  23. }
  24.  
  25. }

下一章我们去探究一个请求如何在SpringMVC各组件中进行流转。

Spring MVC源码(一) ----- 启动过程与组件初始化的更多相关文章

  1. Spring Boot源码分析-启动过程

    Spring Boot作为目前最流行的Java开发框架,秉承"约定优于配置"原则,大大简化了Spring MVC繁琐的XML文件配置,基本实现零配置启动项目. 本文基于Spring ...

  2. 精尽Spring MVC源码分析 - 一个请求的旅行过程

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  3. Spring MVC源码——Root WebApplicationContext

    目录 Spring MVC源码--Root WebApplicationContext 上下文层次结构 Root WebApplicationContext 初始化和销毁 ContextLoaderL ...

  4. 精尽Spring MVC源码分析 - 寻找遗失的 web.xml

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  5. 精尽Spring MVC源码分析 - WebApplicationContext 容器的初始化

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  6. Spring mvc源码分析系列--Servlet的前世今生

    Spring mvc源码分析系列--Servlet的前世今生 概述 上一篇文章Spring mvc源码分析系列--前言挖了坑,但是由于最近需求繁忙,一直没有时间填坑.今天暂且来填一个小坑,这篇文章我们 ...

  7. 精尽Spring MVC源码分析 - 调式环境搭建

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  8. 精尽Spring MVC源码分析 - 文章导读

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  9. 精尽Spring MVC源码分析 - MultipartResolver 组件

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

随机推荐

  1. 从PRISM开始学WPF(七)MVVM(三)事件聚合器EventAggregator?

    原文:从PRISM开始学WPF(七)MVVM(三)事件聚合器EventAggregator? 从PRISM开始学WPF(一)WPF? 从PRISM开始学WPF(二)Prism? 从PRISM开始学WP ...

  2. UWP中String 转为Path Data

    定义方法: Geometry PathMarkupToGeometry(string pathMarkup) { string xaml = "<Path " + " ...

  3. 海康SDK编程指南

    转至心澄欲遣 目前使用的海康SDK包括IPC_SDK(硬件设备),Plat_SDK(平台),其中两套SDK都需单独调用海康播放库PlayCtrl.dll来解码视频流,返回视频信息和角度信息.本文仅对视 ...

  4. QT 那些事

    一直做的是windows开发,有2个小项目想做到手机.看来看去,选择了QT.本文介绍自己的学习的一些心得,希望对你有帮助.1.选择QT2.安卓的一些事3.QML调用C++4.资源集成(比较有用,网上资 ...

  5. ML:机器学习中常用的Octave语句

    coursera上吴恩达的机器学习课程使用Octave/Matlab实现算法,有必要知道Octave简单的语句.最重要的:在遇到不会的语句,使用'''help '''或者'''doc '''查看官方文 ...

  6. WPF使用AForge实现Webcam预览(二)

    本文主要介绍如何让摄像头预览界面的宽高比始终在16:9. 首先我们需要修改一下上一篇随笔实现的UI界面,让Grid变成一个3*3的九宫格,预览界面位于正中间.Xaml示例代码如下: <Windo ...

  7. Qt云服务/云计算平台QTC(Qt Cloud Services)入门(0)

    在这个“大数据”的时代,传统的跨平台C++库Qt已经将魔爪丧心病狂的伸向了“云计算”.在2012年的Qt开发者大会上,Qt发布了BaaS(Backend as a Service)服务——Engini ...

  8. CentOS7下group和group-、passwd与passwd-之间的区别

    今天查看/etc/group时,意外发现另外一个/etc/group-,查看了一下2个文件,发现大部分内容是一致的.这就有点搞不懂了,后面这个group是有啥用呢呢?diff对比了一下,相比group ...

  9. 测试 Components 与 Controls 的区别(嵌套在Panel上的Edit,依然是Form1的Component)

    本例效果图: 代码文件: unit Unit1; interface uses   Windows, Messages, SysUtils, Variants, Classes, Graphics, ...

  10. 原生Js汉语拼音首字母匹配城市名/自动提示列表

    根据城市的汉语名称首字母把城市排序,基本思路: 1.处理数据,按照需要的格式分别添加{HOT:{hot:[],ABCDEFG:{a:[1,2,3],b:[1,2,3]},HIGHLMN:{},OPQR ...