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等配置项):
URS
contextConfigLocation
classpath:spring/spring-*.xml
org.springframework.web.context.ContextLoaderListener
org.springframework.web.context.request.RequestContextListener
encodingFilter
org.springframework.web.filter.CharacterEncodingFilter
true
encoding
UTF-8
forceEncoding
true
encodingFilter
/*
shiroFilter
org.springframework.web.filter.DelegatingFilterProxy
true
targetFilterLifecycle
true
shiroFilter
/*
spring
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath:spring/dispatcherServlet.xml
1
true
spring
*.html
java.lang.Throwable
/500.html
500
/500.html
404
/404.html
配置部分需要更详细的理解可参见:web.xml 中的listener、 filter、servlet 加载顺序及其详解
首先可以肯定的是,加载顺序与它们在 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(过滤器)的常见应用可以自己参照写法自定义自己的过滤器。
spring
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath:spring/dispatcherServlet.xml
1
true
spring
*.html
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);
}
public FrameworkServlet() {
}
对于泛型Class> 中?只是一个占位符,可以传入任何类型,只是为了兼容早期的版本中集合取数据时的拆箱操作。下面对类的类型做一个简介
Class
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 requiredProperties)
throws ServletException {
Set missingProps = (requiredProperties != null && !requiredProperties.isEmpty() ?
new HashSet(requiredProperties) : null);
Enumeration 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。
至此,我们的web项目启动完成了。
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.
* 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.
* 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;
}