从web.xml进入Spring MVC 源码分析(4.x)

1. 从web.xml文件说起

   1.1  web.xml的作用

web.xml是web工程的配置文件,容器加载web工程时,会首先从WEB-INF中查询web.xml,并加载其中的配置信息,可以将web.xml认为是web工程的入口。

初始化Java EE 工程的配置信息一般会涉及以下方面:比如Welcome页面、servlet、servlet-mapping、filter、listener、启动加载级别等等。

     所有这些元素都是可选的。因此,可以省略掉某一元素,但不能把它放于不正确的位置。加粗的代表常用元素

    l icon icon元素指出IDE和GUI工具用来表示Web应用的一个和两个图像文件的位置。
    l display-name display-name元素提供GUI工具可能会用来标记这个特定的Web应用的一个名称。
    l description description元素给出与此有关的说明性文本。
    l context-param context-param元素声明应用范围内的初始化参数。
    l filter 过滤器元素将一个名字与一个实现javax.servlet.Filter接口的类相关联。
    l filter-mapping 一旦命名了一个过滤器,就要利用filter-mapping元素把它与一个或多个servlet或JSP页面相关联。
    l listener servlet API的版本2.3增加了对事件监听程序的支持,事件监听程序在建立、修改和删除会话或servlet环境时得到通知。Listener元素指出事件监听程序类。
    l servlet 在向servlet或JSP页面制定初始化参数或定制URL时,必须首先命名servlet或JSP页面。Servlet元素就是用来完成此项任务的。
    l servlet-mapping 服务器一般为servlet提供一个缺省的URL:http://host/webAppPrefix/servlet/ServletName。但是,常常会更改这个URL,以便servlet可以访问初始化参数或更容易地处理相对URL。在更改缺省URL时,使用servlet-mapping元素。
    l session-config 如果某个会话在一定时间内未被访问,服务器可以抛弃它以节省内存。可通过使用HttpSession的setMaxInactiveInterval方法明确设置单个会话对象的超时值,或者可利用session-config元素制定缺省超时值。
    l mime-mapping 如果Web应用具有想到特殊的文件,希望能保证给他们分配特定的MIME类型,则mime-mapping元素提供这种保证。
    l welcom-file-list welcome-file-list元素指示服务器在收到引用一个目录名而不是文件名的URL时,使用哪个文件。
    l error-page error-page元素使得在返回特定HTTP状态代码时,或者特定类型的异常被抛出时,能够制定将要显示的页面。
    l taglib taglib元素对标记库描述符文件(Tag Libraryu Descriptor file)指定别名。此功能使你能够更改TLD文件的位置,而不用编辑使用这些文件的JSP页面。
    l resource-env-ref resource-env-ref元素声明与资源相关的一个管理对象。
    l resource-ref resource-ref元素声明一个资源工厂使用的外部资源。
    l security-constraint security-constraint元素制定应该保护的URL。它与login-config元素联合使用
    l login-config 用login-config元素来指定服务器应该怎样给试图访问受保护页面的用户授权。它与sercurity-constraint元素联合使用。
    l security-role security-role元素给出安全角色的一个列表,这些角色将出现在servlet元素内的security-role-ref元素的role-name子元素中。分别地声明角色可使高级IDE处理安全信息更为容易。
    l env-entry env-entry元素声明Web应用的环境项。
    l ejb-ref ejb-ref元素声明一个EJB的主目录的引用。
    l ejb-local-ref ejb-local-ref元素声明一个EJB的本地主目录的应用。

下面给出一个常用的配置(已经包含了Spring, Spring MVC,Shiro等配置项):

<?xml version="1.0" encoding="UTF-8"?>
<web-app
        xmlns="http://java.sun.com/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
        version="3.0"
        metadata-complete="true">

    <display-name>URS</display-name>

    <!-- Spring配置文件开始  -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:spring/spring-*.xml
        </param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <!-- Spring配置文件结束 -->

    <!-- 可以使用RequestContextHolder.currentRequestAttributes() 获取到请求的attr -->
    <listener>
        <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
    </listener>

    <!-- 设置servlet编码开始 统一编码filter  -->
    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <async-supported>true</async-supported>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <!-- 设置servlet编码结束 -->

    <!-- shiro 安全过滤器 -->
    <filter>
        <filter-name>shiroFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <async-supported>true</async-supported>
        <init-param>
            <param-name>targetFilterLifecycle</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>

    <!-- shiro的filter-mapping-->
    <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- spring mvc前端控制器配置 -->
    <servlet>
        <servlet-name>spring</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring/dispatcherServlet.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
        <async-supported>true</async-supported>
    </servlet>
    <servlet-mapping>
        <servlet-name>spring</servlet-name>
        <url-pattern>*.html</url-pattern>
    </servlet-mapping>

    <error-page>
        <exception-type>java.lang.Throwable</exception-type>
        <location>/500.html</location>
    </error-page>
    <error-page>
        <error-code>500</error-code>
        <location>/500.html</location>
    </error-page>
    <error-page>
        <error-code>404</error-code>
        <location>/404.html</location>
    </error-page>

</web-app>

配置部分需要更详细的理解可参见:web.xml 中的listener、 filter、servlet 加载顺序及其详解

1.2  web.xml的配置的执行顺序

     首先可以肯定的是,加载顺序与它们在 web.xml 文件中的先后顺序无关。即不会因为 filter 写在 listener 的前面而会先加载 filter。读取顺序是 context-param -> listener -> filter -> servlet。但是listener 和 filter是有顺序的。两者初始化都是顺序执行,listenner在项目启动时执行,filte是当请求资源匹配多个 filter-mapping 时(在请求到达Servlet之前),根据在web.xml中的先后顺序形成一个filter链(filterChain记录了web.xml中定义好的Filter的顺序,如果是filtermapping顺序是a-b-c,那么执行顺序是a-b-c-c-b-a),俗称过滤器链,filter 拦截资源是按照 filter-mapping 配置节出现的顺序来依次调用 doFilter() (此方法是回调函数)方法的。

其中面试常考点:Filter与Inteceptor的区别:

1、拦截器是基于java反射机制的(Spring AOP原理及拦截器),而过滤器是基于函数回调(执行doFilter()方法时传入了httpRequest,httpResponse,filterChain,其中FilterChain的实例记录了执行链顺序的相关信息)的。
2、过滤器依赖与servlet容器,而拦截器不依赖与servlet容器。
3、拦截器只能对Action请求起作用,而过滤器则可以对几乎所有请求起作用。
4、拦截器可以访问Action上下文、值栈里的对象,而过滤器不能。

现在流行使用Spring MVC已经封装好的Filter,可以直接拿来使用,常见的可参见Spring MVC常见Filter的使用

同时可参见Java中常用的Filter过滤器 ,以及Filter(过滤器)的常见应用可以自己参照写法自定义自己的过滤器。

2. 从DispatcherServlet开始进入SpringMVC的世界

1. 简介web.xml 中Spring MVC的配置

     <servlet>
        <servlet-name>spring</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring/dispatcherServlet.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
        <async-supported>true</async-supported>
    </servlet>
    <servlet-mapping>
        <servlet-name>spring</servlet-name>
        <url-pattern>*.html</url-pattern>
    </servlet-mapping>

Servlet容器初始化时会初始化该Servlet类,根据Servlet 3.0规范可知。在单一的容器环境中,比如一个Tomcat,只会存在一个Servlet。分布式系统时可能会存在多个。

load-on-startup

1)它的值必须是一个整数,表示servlet应该被载入的顺序
2)当值为0或者大于0时,表示容器在应用启动时就加载并初始化这个servlet;
3)当值小于0或者没有指定时,则表示容器在该servlet被选择时才会去加载。
4)正数的值越小,该servlet的优先级越高,应用启动时就越先加载。
5)当值相同时,容器就会自己选择顺序来加载。

我们进入这个Servlet看他怎么实例化并调用init()方法。首先我们先看看这个UML类图结构

《从web.xml进入Spring MVC 源码分析(4.x)》

首先web容器启动时,会调用DispatcherServlet的无参的构造方法:

	public DispatcherServlet() {
		super();
		setDispatchOptionsRequest(true);
	}

然后super()调用父类FrameworkServlet的无参构造方法

	public FrameworkServlet() {
	}

对于泛型Class<?> 中?只是一个占位符,可以传入任何类型,只是为了兼容早期的版本中集合取数据时的拆箱操作。下面对类的类型做一个简介

Class<String>  clzz;//表示String类型的类;
Class<? extends Map> clzz; //表示所有继承自Map类型的类;
Class<?> clzz; //表示任意类的类型;

实例创建完成后我们去寻找DispatcherServlet的init()方法,终于在FrameworkServlet的父类HttpServletBean中找到了这个方法

	/**
	 * Map config parameters onto bean properties of this servlet, and
	 * invoke subclass initialization.
	 * @throws ServletException if bean properties are invalid (or required
	 * properties are missing), or if subclass initialization fails.
	 */
	@Override
	public final void init() throws ServletException {
		if (logger.isDebugEnabled()) { 
                       logger.debug("Initializing servlet '" + getServletName() + "'");
		} //判断logger的日志级别是不是debug模式,是的话打印,我的打印是Initializing servlet 'spring'
                 //使用{}语法 log.debug("hello, this is {}", name); 以及 log.debug("hello, this is {}", name);
                 //可以降低性能消耗(以前的日志打印是字符串拼接形式,会造成字符串拼接性能问题。
                // Set bean properties from init parameters.
		try {
			PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties); //pvs是一个中存储了我们传入的键值对
			BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
			ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
			bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
			initBeanWrapper(bw);
			bw.setPropertyValues(pvs, true);
		}
		catch (BeansException ex) {
			if (logger.isErrorEnabled()) {
				logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
			}
			throw ex;
		}

		// Let subclasses do whatever initialization they like.
		initServletBean();

		if (logger.isDebugEnabled()) {
			logger.debug("Servlet '" + getServletName() + "' configured successfully");
		}
	}
		/**
		 * Create new ServletConfigPropertyValues.
		 * @param config ServletConfig we'll use to take PropertyValues from
		 * @param requiredProperties set of property names we need, where
		 * we can't accept default values
		 * @throws ServletException if any required properties are missing
		 */
		public ServletConfigPropertyValues(ServletConfig config, Set<String> requiredProperties)
			throws ServletException {

			Set<String> missingProps = (requiredProperties != null && !requiredProperties.isEmpty() ?
					new HashSet<String>(requiredProperties) : null);

			Enumeration<String> paramNames = config.getInitParameterNames();
			while (paramNames.hasMoreElements()) {
				String property = paramNames.nextElement();
				Object value = config.getInitParameter(property);
				addPropertyValue(new PropertyValue(property, value));
				if (missingProps != null) {
					missingProps.remove(property);
				}
			}

			// Fail if we are still missing properties.
			if (!CollectionUtils.isEmpty(missingProps)) {
				throw new ServletException(
					"Initialization from ServletConfig for servlet '" + config.getServletName() +
					"' failed; the following required properties were missing: " +
					StringUtils.collectionToDelimitedString(missingProps, ", "));
			}
		}

此处我们需要对入参进行讲解

ServletContext是servlet与servlet容器之间的直接通信的接口。Servlet容器在启动一个Webapp时,会为它创建一个ServletContext对象,即servlet上下文环境。每个webapp都有唯一的ServletContext对象。同一个webapp的所有servlet对象共享一个ServeltContext,servlet对象可以通过ServletContext来访问容器中的各种资源。

Jsp/Servlet容器初始化一个Servlet类型的对象时,会为这个Servlet对象创建一个ServletConfig对象。在ServletConfig对象中包含了Servlet的初始化参数信息。此外,ServletConfig对象还与ServletContext对象关联。Jsp/Servlet容器在调用Servlet对象的init(ServletConfig config)方法时,会把ServletConfig类型的对象当做参数传递给servlet对象。Init(ServletConfig config)方法会使得当前servlet对象与ServletConfig类型的对象建立关联关系。

参见Servlet、ServletContext与ServletConfig的详解及区别

此处config传递了web.xml中的参数信息

《从web.xml进入Spring MVC 源码分析(4.x)》

可以看到此Servlet配置的相关信息确实已经传入进来了。

BeanWrapper 是spring 底层核心的JavaBean包装接口, 默认实现类BeanWrapperImpl.所有bean的属性设置都是通过它来实现。可以反射获取一个实例然后设置bean(即实例对象)的属性值。参见Spring BeanWrapper分析

这个类这是主要是作用是将Servlet初始化参数设置到DispatcherServlet上,主要是web.xml中定义的属性值

下面进入initServletBean()是一个空的构造方法,此时是选择FrameworkServlet类重写的方法,

该方法最核心的操作就是调用initWebApplicationContext()执行上下文Bean初始化。

我们下面分析该方法

	/**
	 * Overridden method of {@link HttpServletBean}, invoked after any bean properties
	 * have been set. Creates this servlet's 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");
		}//打印FrameworkServlet 'spring': initialization started
		long startTime = System.currentTimeMillis();

		try {
			this.webApplicationContext = initWebApplicationContext();
			initFrameworkServlet();
		}
		catch (ServletException ex) {
			this.logger.error("Context initialization failed", ex);
			throw ex;
		}
		catch (RuntimeException ex) {
			this.logger.error("Context initialization failed", ex);
			throw ex;
		}

		if (this.logger.isInfoEnabled()) {
			long elapsedTime = System.currentTimeMillis() - startTime;
			this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
					elapsedTime + " ms");
		}
	}

FrameworkServlet.initWebApplicationContext方法首先获取自己的双亲上下文(也就是ContextLoaderListener初始化成功的WebApplicationContext);然后创建或者获取当前Servelet的WebApplicationContext。

无论是自己创建还是获取现有的WebApplicationContext,最终都会让Servlet级别的WebApplicationContext执行configureAndRefreshWebApplicationContext()方法进行上下文容器初始化。最终输出 Servlet ‘spring’ configured successfully。

至此,我们的web项目启动完成了。

2.从发出一个HTTP请求到收到响应发生了什么?

1.首先,Servlet容器获取到请求后把请求分发到Servlet进行处理,然后调用该Servlet的service()方法进行处理

2.Dispatcher中并没有重写service()方法,根据类图,追踪到上一级是FrameworkServlet,此service()方法以及重写了Servlet自带的service()方法。我们进入该方法详细了解,先上源码

	/**
	 * Override the parent class implementation in order to intercept PATCH requests.
	 */
	@Override
	protected void service(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
		if (HttpMethod.PATCH == httpMethod || httpMethod == null) {
			processRequest(request, response);
		}
		else {
			super.service(request, response);
		}
	}

HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());

此处是获得请求的header属性中请求方法,header一般都如

GET / HTTP/1.1
Host: www.baidu.com
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.6)
Gecko/20050225 Firefox/1.0.1
Connection: Keep-Alive

一般情况下,我们常用的有GET,PUT,DELETE,POST四个。

此处Spring MVC重写了service方法主要是为了PATCH方法,关于这些方法之间的用法与区别,参见RESTful, 说说 http 的 patch method

不是PATCH方法的话直接调用HttpServlet的service()方法。

源代码如下:

    protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
    {
        String method = req.getMethod();

        if (method.equals(METHOD_GET)) {
            long lastModified = getLastModified(req);
            if (lastModified == -1) {
                // servlet doesn't support if-modified-since, no reason
                // to go through further expensive logic
                doGet(req, resp);
            } else {
                long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                if (ifModifiedSince < lastModified) {
                    // If the servlet mod time is later, call doGet()
                    // Round down to the nearest second for a proper compare
                    // A ifModifiedSince of -1 will always be less
                    maybeSetLastModified(resp, lastModified);
                    doGet(req, resp);
                } else {
                    resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                }
            }

        } else if (method.equals(METHOD_HEAD)) {
            long lastModified = getLastModified(req);
            maybeSetLastModified(resp, lastModified);
            doHead(req, resp);

        } else if (method.equals(METHOD_POST)) {
            doPost(req, resp);
            
        } else if (method.equals(METHOD_PUT)) {
            doPut(req, resp);
            
        } else if (method.equals(METHOD_DELETE)) {
            doDelete(req, resp);
            
        } else if (method.equals(METHOD_OPTIONS)) {
            doOptions(req,resp);
            
        } else if (method.equals(METHOD_TRACE)) {
            doTrace(req,resp);
            
        } else {
            //
            // Note that this means NO servlet supports whatever
            // method was requested, anywhere on this server.
            //

            String errMsg = lStrings.getString("http.method_not_implemented");
            Object[] errArgs = new Object[1];
            errArgs[0] = method;
            errMsg = MessageFormat.format(errMsg, errArgs);
            
            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
        }
    }

很明显,此处是

String method = req.getMethod();

这个方法获取到请求头里面的请求方法类型后,与已经定义好的常量比较。但是,此处并不会直接调用,在DiapatcherServlet的父类FrameworkServlet中重写了所有的doGet,doPost等等方法,这些方法源代码如下,此处有个疑问,为什么断点没有进入该方法而是直接调用了子类的doGet方法?

	/**
	 * Delegate GET requests to processRequest/doService.
	 * <p>Will also be invoked by HttpServlet's default implementation of {@code doHead},
	 * with a {@code NoBodyResponse} that just captures the content length.
	 * @see #doService
	 * @see #doHead
	 */
	@Override
	protected final void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		processRequest(request, response);
	}

注意到doGet,doPost,doPut,doDelete,以及刚进入的PATCH方法,现在都由一个方法执行,另外两个用得少,不详细说明了

processRequest(request, response);

现在是时候进入这个方法查看源码了(该方法继承自FrameworkServlet)

	/**
	 * Process this request, publishing an event regardless of the outcome.
	 * <p>The actual event handling is performed by the abstract
	 * {@link #doService} template method.
	 */
	protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		long startTime = System.currentTimeMillis(); 
		Throwable failureCause = null;

              //获取之前的位置信息,最后finally时恢复之前配置
		LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
		LocaleContext localeContext = buildLocaleContext(request);

		RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
		ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
		asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());

		initContextHolders(request, localeContext, requestAttributes);

		try {
			doService(request, response);
		}
		catch (ServletException ex) {
			failureCause = ex;
			throw ex;
		}
		catch (IOException ex) {
			failureCause = ex;
			throw ex;
		}
		catch (Throwable ex) {
			failureCause = ex;
			throw new NestedServletException("Request processing failed", ex);
		}

		finally {
			resetContextHolders(request, previousLocaleContext, previousAttributes);
			if (requestAttributes != null) {
				requestAttributes.requestCompleted();
			}

			if (logger.isDebugEnabled()) {
				if (failureCause != null) {
					this.logger.debug("Could not complete request", failureCause);
				}
				else {
					if (asyncManager.isConcurrentHandlingStarted()) {
						logger.debug("Leaving response open for concurrent processing");
					}
					else {
						this.logger.debug("Successfully completed request");
					}
				}
			}

			publishRequestHandledEvent(request, response, startTime, failureCause);
		}
	}

我们先分析这一句:

LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();

进入该方法,可以看到

	public static LocaleContext getLocaleContext() {
		LocaleContext localeContext = localeContextHolder.get();
		if (localeContext == null) {
			localeContext = inheritableLocaleContextHolder.get();
		}
		return localeContext;
	}

    原文作者:Spring MVC
    原文地址: https://blog.csdn.net/canfengli/article/details/78132692
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞