您的位置:首页 > 编程语言 > Java开发

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

2017-09-29 12:32 405 查看

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>
e063
</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容器启动时,会调用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中的参数信息



可以看到此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;
}


                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: