springboot-spring源码系列(十一)-MVC

293 阅读10分钟
   //使用官网的启动方式
   public class demo implements WebApplicationInitializer {
     @Override   
         public void onStartup(javax.servlet.ServletContext servletCxt) throws ServletException {
                //创建一个web容器
                AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
                //手动注册配置类
                ac.register(AppConfig.class);
                /**
                 * 我觉得这个地方进行容器的刷新毫无作用,甚至还会报错
                 * 此时你进行刷新,springMVC的容器并没有往springMVC独有的一些beanDefiniton/beanPostProcessor
                 * 所以如果此时你扫描到的类使用了某些MVC才有的beanPostProcessor就会报错
                 * 而且当Tomcat第一次接收请求会执行DispatcherServlet的init方法,在该方法中又进行了一次refersh()
                 * 所以我觉得此处的refersh()方法毫无用处。
                 */
                ac.refresh();
        /**
         * DispatcherServlet本质上就如其名字所展示的那样是一个Java Servlet。
         * 同时它是Spring MVC中最为核心的一块——前端控制器。它主要用来拦截符合要求的外部请求,
         * 并把请求分发到不同的控制器去处理,根据控制器处理后的结果,生成相应的响应发送到客户端。
         * DispatcherServlet作为统一访问点,主要进行全局的流程控制
         */
        DispatcherServlet servlet = new DispatcherServlet(ac);
        ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
        registration.setLoadOnStartup(1);
        registration.addMapping("/app/*");
    }    
}

上述的代码完全替代了web.xml中的配置,讲一下为什么

 * 为什么启动 tomcat 加载该容器能执行该类的onStartup方法
 * 因为 spi 和 servlet3.0 以后提供的一种规范
 * 如果你把这个项目交给tomcat管理,tomcat会自动去找jar包下的META-INF/services/目录下找一个ServletContainerLnitializer文件
 * 然后加载该文加中的指定的类

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
    @Override
    /**
     * tomcat会执行该方法,并且servlet提供了一个注解 @HandlesTypes(WebApplicationInitializer.class)
     * 这个注解的作用就是在执行这个类的方法时候,动态找到注解上的接口的所有实现类,然后放到set<Class<?>> webAppInitializerClasses 集合中
     *
     */
    public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
            throws ServletException {
        List<WebApplicationInitializer> initializers = new LinkedList<>();
        if (webAppInitializerClasses != null) {
            for (Class<?> waiClass : webAppInitializerClasses) {
                //一些servlet容器为我们提供了无效的类
                //我们需要对这webAppInitializerClasses中的类进行剔除
                if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
                        WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        initializers.add((WebApplicationInitializer)
                        ReflectionUtils.accessibleConstructor(waiClass).newInstance());
                    }
                    catch (Throwable ex) {
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
                    }
                }
            }
        }
        if (initializers.isEmpty()) {
            servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
            return;
        }
        servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
        //对所有的类进行排序
        AnnotationAwareOrderComparator.sort(initializers);
        //按照顺序执行所有类的onStartUp()方法,所以就会加载到我们用来初始化spring容器的onStartup方法
        for (WebApplicationInitializer initializer : initializers) {
            initializer.onStartup(servletContext);
        }
    }
}

至于所有onStartup()方法做了什么,有兴趣自己看

DispathcherServlet中并没有init()方法,该方法存在于他的父类HttpServletBean中

    /**

     * 通过init()方法来对springMVC的容器进行初始化
     */
    public final void init() throws ServletException {
        //获取Web.xml里面的servlet的init-param
        PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
        if (!pvs.isEmpty()) {
            try {
                //为给定类型的所有属性注册给定的自定义属性编辑器,提供了设置和获取属性值,它有对应的BeanWrapperImpl  
                BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
                //可以根据一个资源地址加载文件资源。classpath:这种方式指定SpringMVC框架bean配置文件的来源
                ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
                bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
                initBeanWrapper(bw);
                bw.setPropertyValues(pvs, true);
            } catch (BeansException ex) {
            }
        }
        //调用子类继续对容器进行初始化
        initServletBean();
    }

继续看如何初始化

   /**

     * FrameworkServlet子类对HttpServletBean方法的重写,在任何bean属性设定好之后调用
     * 创建此servlet的WebApplicationContext。
     */
    @Override
    protected final void initServletBean() throws ServletException {
        getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
           //在控制台打印容器初始化已经开始了
            if (this.logger.isInfoEnabled()) {
            this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
        }
         //记录当前的系统时间
        long startTime = System.currentTimeMillis();
        try {
            //初始化SpringMVC容器
            this.webApplicationContext = initWebApplicationContext();
            /**
             *  此方法将在设置任何bean属性之后调用
             *  并且加载WebApplicationContext。默认实现为空;                
             */
            initFrameworkServlet();
        }
        if (this.logger.isInfoEnabled()) {
           //拿到启动所耗用时间
            long elapsedTime = System.currentTimeMillis() - startTime;
            //控制台打印启动所耗用时间
            this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
                    elapsedTime + " ms");
        }
    }

继续追踪

        protected WebApplicationContext initWebApplicationContext() {
        /**
         * 获取根节点上下文,通过ContextLoaderListener加载,服务器启动便加载
         * tomcat的加载顺序是context-param >> listener >> filter >> servlet​。
         */
        WebApplicationContext rootContext =
                WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        WebApplicationContext wac = null;
        //在DispatcherServlet进行创建时,会传递一个ApplicationContext
        if (this.webApplicationContext != null) {
            wac = this.webApplicationContext;
            //判断容器的类型是否是ConfigurableWebApplicationContext(web容器)
            if (wac instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
                    //确定此应用程序上下文是否处于活动状态,即是否已刷新至少一次且尚未关闭
                     if (!cwac.isActive()) {
                    //如果springMVC容器的parent属性为null,则设置parent的值为spring容器,父子容器
                    if (cwac.getParent() == null) { 
                        cwac.setParent(rootContext);
                    }   
                    //初始化springMVC容器,进行refresh()操作,所以这也证实了前面的refresh()操作多余
                    configureAndRefreshWebApplicationContext(cwac);
                }
            }
        }
        if (!this.refreshEventReceived) {
            /**
             * 这是通过springMVC进行初始化的重点!!!!
             * 如果初始化容器成功
             * 就去初始化了SpringMVC的九大组件
             */
            onRefresh(wac);
        }
        if (this.publishContext) {
            //将容器作为servlet容器的属性发布。
            String attrName = getServletContextAttributeName();
            getServletContext().setAttribute(attrName, wac);
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
                        "' as ServletContext attribute with name [" + attrName + "]");
            }
        }
        return wac;
    }

何进行初始化的

WebApplicationInitializer的实现类中把ContextLoaderListener监听器注册给Tomcat的 ServletContext -> contextInitialized()完成了父容器的初始化

ContextLoaderListener继承了ContextLoader,并实现了ServletContextListener接口中的contextInitialized方法。

web容器初始化时调用contextInitialized方法通知listener,继而调用ContextLoader中的initWebApplicationContext方法,初始化spring容器。

在这个init方法中,主要做了两件事:

1)创建容器实例。

2)设置初始化配置,读取配置文件、重启IOC容器(BeanFactory)加载各个资源文件和bean等。然后,将ServletContext中的spring根容器设置为本容器实例。

此时,spring父容器中的bean都已准备就绪。

重点看一下onRefresh()方法,基本上初始化流程差不多,进行一个分析即可

protected void initStrategies(ApplicationContext context) {
		/**
		 * SpringMVC的九大组件
		 */
		/**
		 * 用于处理上传请求
		 * 将普通的Request包装成MultipartHttpServletRequest
		 * 可以通过getFile() 直接获得文件,如果是多个文件上传,还可以通过调用getFileMap
		 * 得到Map<FileName,File> 这样的结构。
		 */
		initMultipartResolver(context);
		/**
		 * 在上面我们有看到ViewResolver 的resolveViewName()方法,需要两个参数。那么第
		 * 二个参数Locale是从哪来的呢,这就是LocaleResolver 要做的事了。和国际化相关,可以理解为设置编码
		 */
		initLocaleResolver(context);
		/**
		 * 从名字便可看出,这个类是用来解析主题的。主题,就是样式,图片以及它们所形成的
		 * 显示效果的集合
		 */
		initThemeResolver(context);
		/**
		 *  HandlerMapping是用来查找Handler的,也就是处理器,具体的表现形式可以是类也
		 *  可以是方法。比如,标注了@RequestMapping 的每个 method 都可以看成是一个
		 *  Handler,由 Handler 来负责实际的请求处理。 HandlerMapping 在请求到达之后,
		 *  它的作用便是找到请求相应的处理器Handler和Interceptors。
		 *  这一步完事后  handlerMappings 中最少存放了2个对象
		 */
		initHandlerMappings(context);
		/**
		 *  从名字上看,这是一个适配器。因为SpringMVC中Handler可以是任意形式的,只要
		 *  能够处理请求便行, 但是把请求交给 Servlet 的时候,就需要根据传递过去的具体类型(方法、类)
		 *  找到对应的 HandlerAdapter然后执行handler的方法
		 */
		initHandlerAdapters(context);
		/**
		 * 从这个组件的名字上看,这个就是用来处理Handler过程中产生的异常情况的组件。 具
		 * 体来说,此组件的作用是根据异常设置ModelAndView, 之后再交给 render()方法进行
		 * 渲 染 , 而 render() 便 将 ModelAndView 渲 染 成 页 面 。 不 过 有 一 点 ,
		 * HandlerExceptionResolver 只是用于解析对请求做处理阶段产生的异常,而渲染阶段的
		 * 异常则不归他管了,这也是Spring MVC 组件设计的一大原则分工明确互不干涉。 
		 */
		initHandlerExceptionResolvers(context);
		/**
		 *  这个组件的作用,在于从 Request 中获取 viewName. 因为 ViewResolver 是根据
		 *  ViewName 查找 View, 但有的 Handler 处理完成之后,没有设置 View 也没有设置
		 *  ViewName,便要通过这个组件来从Request中查找viewName,就是处理完成之后,跳转到发送request请求的页面
		 */
		initRequestToViewNameTranslator(context);
		/**
		 * 从方法的定义就可以看出,Controller层返回的String类型的视图名viewName,( return "xxxx" )
		 * 最终会在这里被解析成为View。View 是用来渲染页面的,也就是说,它会将程序返回
		 * 的参数和数据填入模板中,最终生成 html 文件 直接显示给用户
		 * ViewResolver会找到渲染所用的模板(使用什么模板来渲染?)和所用的技术(如JSP)填入参数
		 */
		initViewResolvers(context);
		/**
		 * 用于重定向Redirect时的参数数据传递,比如,在处理用户订单提交时,为
		 * 了避免重复提交,可以处理完post请求后redirect到一个get请求,这个get请求可以
		 * 用来显示订单详情之类的信息。这样做虽然可以规避用户刷新重新提交表单的问题,但
		 * 是在这个页面上要显示订单的信息,那这些数据从哪里去获取呢,因为redirect重定向
		 * 是没有传递参数这一功能的,如果不想把参数写进url(其实也不推荐这么做,url有长度
		 * 限制不说,把参数都直接暴露,感觉也不安全), 那么就可以通过flashMap来传递。只
		 * 需 要 在 redirect 之 前 , 将 要 传 递 的 数 据 写 入 request ( 可 以 通 过
		 * ServletRequestAttributes.getRequest() 获 得 ) 的 属 性
		 * OUTPUT_FLASH_MAP_ATTRIBUTE 中,这样在 redirect 之后的 handler 中 Spring 就
		 * 会自动将其设置到Model中,在显示订单信息的页面上,就可以直接从Model 中取得
		 * 数据了。而FlashMapManager就是用来管理FlashMap的。
		 */
		initFlashMapManager(context);
	}

说一个重点,我们在最开始的时候new DispatcherServlet(),而这个类有一个静态代码块,所以在创建的时候一定会执行

      static {
		try {
			/**
			 * DispatcherServlet类加载的时候就会执行该静态方法
			 *  创建一个文件读取器,通过读取路径下的 DispatcherServlet.properties文件
			 *  把 key = value 封装到 defaultStrategies的properties对象中
			 *  保存了8组信息,在init()方法的时候会用到
			 */
			ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
			defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
		} catch (IOException ex) {
			throw new IllegalStateException("Could not load '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage());
		}
      }

重点分析一下initHandlerMappings(context)方法

     /**
	 * 建立当前ApplicationContext中的所有controller和url的对应关系
	 */
	private void initHandlerMappings(ApplicationContext context) {
		// 初始化记录 HandlerMapping 对象的属性变量为null
		this.handlerMappings = null;
		// 根据属性detectAllHandlerMappings决定是检测所有的 HandlerMapping 对象,还是使用指定名称的 HandlerMapping 对象
		if (this.detectAllHandlerMappings) {
			// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
			/**
			 * 从容器及其祖先容器查找所有类型为 HandlerMapping 的 HandlerMapping 对象,
			 * spring提供的扩展点,通过实现 requestHandlerMapping类 在注册进spring ioc容器中,
			 * 就可以实现自定义请求类型
			 *
			 * springboot就是通过 import配置类,配置类中 @bean 把扩展的各种handlerMapping、Adapters.....等,
			 * 添加进spring ioc容器,再通过该方法那出来,放到web容器中,子容器
			 */
			Map<String, HandlerMapping> matchingBeans =
					BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
			//如果其不为空
			if (!matchingBeans.isEmpty()) {
				//记录到 handlerMappings
				this.handlerMappings = new ArrayList<>(matchingBeans.values());
				// We keep HandlerMappings in sorted order.
				//根据order属性排序
				AnnotationAwareOrderComparator.sort(this.handlerMappings);
			}
		} else {
			try {
				/**
				 *  获取名称为  handlerMapping 的 HandlerMapping bean 并记录到 handlerMappings
				 */
				HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
				this.handlerMappings = Collections.singletonList(hm);
			} catch (NoSuchBeanDefinitionException ex) {
			}
		}
		/**
		 *  除非你手动扩展handlerMapping,不然从容器是获取不到的
		 *  然后是从DefaultStrategies集合中获取,熟悉不,就是静态代码块中注册那些类
		 */
		if (this.handlerMappings == null) {
			this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
			if (logger.isDebugEnabled()) {
				logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default");
			}
		}
	}

springMVC的创建流程大致重点差不多都分析了,下篇文章将会分析service()方法,也就是对请求的处理,最后再说一下我对父子容器的理解

        spring父子容器
 * 	    当一个项目中引入Spring和SpringMVC这两个框架,其实就是2个容器,Spring是父容器,SpringMVC是其子容器,
 * 	    子容器可以访问父容器对象,而父容器不可以访问子容器对象
 * 	    我们可以理解为springMVC容器中拥有spring容器的引用,对外暴露的是springMVC的容器
 * 	    而且这两个容器是两个独立的容器互不干涉,拥有着自己独立的属性
 * 	    也就是说我们平常用的所有注解扫描及实例化完成的bean,全都存放在MVC的容器中。所以不会出现问题
 * 	    但是如果把所有的实例化bean放到spring容器中就会报错,因为MVC容器中的单例池为空找不到controller的bean,更找不到映射
 * 
 *
 * 在扩展一下springboot是如何启动项目的
 * springboot的启动方式有两种 1.jar 2.war
 *         jar包方式的启动  Tomcat t = new Tomcat(80);
 * 			   ...把spring容器存放到tomcat中.....
 * 			   t.start();
 * 			   也就是说内嵌了一个tomcat,执行了自己的tomcat来启动,
 *
 * 	   war包方式启动  启动类实现一个 springApplicationInitializer接口 这个接口又实现了 WebApplicationInitializer 接口
 * 			 也是通过SPI规范来放到外部的Tomcat