声明:本文基于Servlet3.1规范翻译整理,如有谬误,烦请指正。
JSR 340,原文请查阅 https://www.jcp.org/en/jsr/summary?id=Servlet+3.1
本文中的示例代码 可以 从https://gitee.com/Ouroboros_K/springboot2-demo.git项目中的servlet-service模块中获得
1 Servlet 概括
- Servlet 是基于Java技术的Web组件
- Servlet 用于生成动态内容。
- 动态: 数据动态
- Servlet 是一组Java规范,以接口的形式对外提供,各个Servlet提供商按照规范提供实现
- Servlet 是在Servlet容器中的,例如 Tomcat, Apache等 Web服务器都扩展了Servlet组件,因此这些Web 服务器也可以称为Servlet容器或Servlet引擎
- Servlet与平台无关(基于Java)
1.1 Servlet容器概括
- Servlet容器主要提供基于请求/响应发送模式的网络服务,处理基于MIME的请求并格式化基于MINE的响应。
- MIME: http协议中规定的数据类型,不同的MIME类型使用不同的方式解析并处理
- Servlet容器都支持基于HTTP协议的请求/响应模式(HTTP/1.0 和 HTTP/1.1版本)。
- Servlet容器一般是Web Server的一部分,浏览器发送请求到Web服务器,Web服务器将请求交由Servlet组件处理,Servlet在处理完请求后将格式化后的响应返回给Web服务器,Web服务器再将响应返回给浏览器
1.2 Servlet与Java版本
这里使用Tomcat官网提供的图表展示。
注意Servlet3.1 需要Java 7 及以上版本 和 Tomcat 8 及以上版本支持
2 Servlet 接口
Servlet接口位于javax.servlet 包下,是Java Servlet API的核心抽象。其定义了 3 个生命周期方法 和 2 个普通方法 共5个方法。
- init(servletConfig)
- 生命周期-初始化方法
- 该方法接收一个ServletConfig参数,支持Servlet在初始化时根据参数进行配置
- ServltConfig中主要内容为ServletContext和initParamters
- service(ServletRequest,ServletResponse)
- 生命周期-运行时方法
- 核心方法,该方法用于处理ServletRequest并将处理结果写入到ServletResponse中
- destory()
- 生命周期-销毁方法
- getServletConfig():ServletConfig
- 获取初始化时的配置信息
getServletInfo:String- 获取Servlet的信息,该方法在GenericServlet中被实现,返回空字符串。该方法目前似乎没有有效价值
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package javax.servlet;
import java.io.IOException;
public interface Servlet {
void init(ServletConfig var1) throws ServletException;
ServletConfig getServletConfig();
void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
String getServletInfo();
void destroy();
}
2.1 Servlet的当前基础实现
Servlet源生实现当前主要有GenericServlet
抽象类 和 HttpServlet
抽象类,通常情况下,开发者只需要继承HttpServlet
类去实现自己的Servlet即可。
2.1.1 GenericServlet
GenericServlet抽象类实现了Servlet接口,主要完成了以下任务:
- 将
init()
中的ServletConfig
赋给一个成员变量,可以由getServletConfig
获得; - 为 Servlet 所有方法提供默认实现;
- 可以直接调用 ServletConfig 中的方法;
- 新提供了获取初始化参数的延展方法和日志记录方法
基础结构如下
public abstract class GenericServlet implements Servlet, ServletConfig, Serializable {
private static final long serialVersionUID = -8592279577370996712L;
private static final String LSTRING_FILE = "javax.servlet.LocalStrings";
private static ResourceBundle lStrings = ResourceBundle.getBundle("javax.servlet.LocalStrings");
private transient ServletConfig config;
public GenericServlet() {
}
public void destroy() {}
//新提供获取键值对初始化参数的方法
public String getInitParameter(String name) {...}
//新提供获取所有初始化参数方法
public Enumeration<String> getInitParameterNames() {...}
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
//新提供两个日志记录方法,日志格式为servletName + msg
public void log(String msg) {...}
public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
}
继承GenericServlet
需要实现service
方法。
2.1.2 HttpServlet
HttpServlet抽象类进一步封装了GenericServlet
,在其基础上扩展了Http的内容。主要完成了以下内容:
- 提供了
service
方法的默认实现,其中将请求和响应进一步由ServletRequest
和ServletResponse
转为HttpServletRequest
和HttpServletResponse
- 提供了基于
HttpServletRequest
和HttpServletResponse
的service
方法,其中根据不同的请求方式调用不同的方法 - 提供了基于Http请求动词的
doGet
,doPost
,doDelete
等7种方法- doGet 处理 HTTP GET 请求
- doPost 处理 HTTP POST 请求
- doPut 处理 HTTP PUT 请求
- doDelete 处理 HTTP DELETE 请求
- doHead 处理 HTTP HEAD 请求
- 返回由doGet方法产生的header信息,也就是类似get方法,但不做任何处理,将直接返回一个NobodyResponse
- doOptions 处理 HTTP OPTIONS 请求
- 返回当前Servlet支持的HTTP方法
- doTrace 处理 HTTP TRACE 请求
- 返回请求中的所有头部信息
在HTTP1.0环境下进行开发时,只需要重写doGet和doPost方法即可,当需要支持HTTP1.1版本时,还需要重写doPut和doDelete方法。
3 Servlet生命周期
Servlet具有严格定义的生命周期,其生命周期交由Servlet容器管理,开发人员无需关心其生命周期的管理,只需要定义好其生命周期的主要实现即可。总体上,Servlet生命周期分为初始化、处理请求和销毁3个阶段。也可以细分为 加载、初始化、处理请求、销毁四个阶段。
3.1 加载和初始化
Servlet容器负责加载和实例化Servlet,当Servlet容器启动时,根据 load-on-start元素规则 加载并初始化Servlet。将首先扫描所有Servlet的非抽象实现类的类名,并通过反射实例化类,此为加载过程,实例化完成之后调用init()
方法完成初始化过程。
3.1.1 init()
init()
方法用于Servlet的初始化,开发人员通常需要实现该方法并完成初始化工作。注意此时Servlet类已完成实例化。如下是一个Servlet的Demo实现
/**
* <p>
* Demo Servlet
* </p>
*
* @author Kern
*/
@Log4j2
public class DemoServlet extends HttpServlet {
public DemoServlet() {
log.info("DemoServlet is Instantiated");
}
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
log.info("Servlet is initializing");
//获取配置
ServletConfig servletConfig = getServletConfig();
//获取初始化参数
String demoParameter = servletConfig.getInitParameter("demoParameter");
log.info("Servlet Name: {}", getServletName());
log.info("Servlet type: {}", getClass().getName());
log.info("初始化参数demoParameter: {}", demoParameter);
//TODO OTHER THINGS
}
}
3.1.2 初始化配置
Servlet在初始化时主要有如下三个配置
- 加载方式和优先级顺序 load-on-start
- 初始化参数配置 init-param
- 路径寻址配置 servlet-mapping
3.1.2.1 load-on-start
load_on_start 元素为一个整数值, 定义了Servlet的加载和初始化优先顺序。
load_on_start 规则:
- 该元素值为正整数时,将在Servlet容器启动时随之加载和初始化,数值越小优先级越高
- 该元素值为空或者负数时,将在Servlet被使用时才进行加载和初始化。
配置方式:
- Servlet3.0 之前的版本Servlet配置在web.xml文件中,配置方式如下:
<web-app>
<!--配置Servlet -->
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
</web-app>
- Servlet 3.0 之后,随着
ServletContainerInitializer
接口的出现,Servlet支持在启动时动态加载Servlet,因而不仅同样支持web.xml中的配置方式,也支持Java编码的配置方法。如下例所示:
public class KernServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
ServletRegistration.Dynamic dynamic = servletContext.addServlet("dispatcher", new DemoServlet());
dynamic.setLoadOnStartup(1);
//TODO OTHER THINGS
}
}
3.1.2.2 init-aram 初始化参数
Servlet在初始化时支持以键值对的方式传递初始化参数,初始化参数需要配合特定的Servlet实现,是一个灵活的配置方式。例如在Spring MVC的前端总控制器 DispatcherServlet
的初始化中,需要在初始化参数中指定Spring 上下文配置文件的路径地址。
配置方式,xml配置和java编码配置的原因同上文所述,是由版本支持导致的不同方式。
xml配置方式:
<web-app>
<!--配置Servlet -->
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<!--初始化参数中配置上下文文件地址 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:app-context.xml</param-value>
</init-param>
</servlet>
</web-app>
Java编码方式:
public class KernServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
ServletRegistration.Dynamic dynamic = servletContext.addServlet("dispatcher", new DemoServlet());
dynamic.setLoadOnStartup(1);
dynamic.setInitParameter("contextConfigLocation", "classpath:app-context.xml");
//TODO OTHER THINGS
}
}
3.1.2.3 servlet-mapping 资源映射地址
Servlet在初始化时,需要指定其寻址方式以便于Servlet容器决定调用哪个Servlet以处理请求。
同样展示两种配置方式
xml配置方式:
<web-app>
<!--配置Servlet -->
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<!--初始化参数中配置上下文文件地址 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:app-context.xml</param-value>
</init-param>
</servlet>
<!--配置Servlet资源映射 -->
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
Java编码方式
public class KernServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
ServletRegistration.Dynamic dynamic = servletContext.addServlet("dispatcher", new DemoServlet());
dynamic.setLoadOnStartup(1);
dynamic.setInitParameter("contextConfigLocation", "classpath:app-context.xml");
dynamic.addMapping("/");
//TODO OTHER THINGS
}
}
如3.1所示的配置完成后,及具备了一个Servlet初始化的所有必要条件,此外,开发人员可以在init()
方法中根据传入的ServletConfig
参数对象获取初始化参数并完成Servlet的初始化实现。
3.2 处理请求
关于多线程和异步处理的内容,不多做讨论,除非你真正需要自己编写一个用于生产的Servlet,才可能涉及到该部分内容。本文仅作Servlet3.1的功能理解和部分组件配置了解用,因此不多做讨论,如需了解以上内容,请查阅文首提供的文档。
- Servlet 完成初始化后,Servlet 容器就可以使用它处理客户端请求了。
- Servlet 处理请求对应的
service()
方法,接收request
和response
两个请求参数。在 HTTP 请求的场景下,容器提供的请求和响应对象具体类型分别是HttpServletRequest
和
HttpServletResponse
。
- 客户端请求由ServletRequest 类型的request 对象表示。
- 客户端响应由ServletResponse 类型的 response 对象表示。
- sevice方法不是线程安全的,开发人员需要针对性地完成请求处理的线程安全处理。
3.3 销毁
当Servlet容器确定一个Servlet需要进行销毁时,将调用destory
方法,同时需要确保该Servlet正在执行的所有service
方法线程完成或超过限制时间。一旦调用destory
方法,Servlet实例将交由JVM的垃圾回收器回收,因此一个Servlet在Web服务运行期间将只能被初始化和销毁一次。目前不支持在运行时动态注册Servlet。
4 Servlet 主要对象
4.1 ServletContext
ServletContext(Servlet 上下文)接口定义了 servlet 运行在的 Web 应用的视图。容器供应商负责提供 Servlet容器的 ServletContext
接口的实现。Servlet 可以使用 ServletContext 对象记录事件,获取 URL 引用的资源,存取当前上下文的其他 Servlet 可以访问的属性。
简而言之,ServletContext是当前应用中Servlet的视图抽象,其持有注册的Servlet、Listener、Filter以及其他上下文内容,包括初始化参数,运行时属性等,Servlet容器和Servlet通过其完成转发通信等工作。例如tomcat的ServletContext实现,如下所示
- 注:仅展示部分代码
public class ApplicationContext implements ServletContext {
protected Map<String, Object> attributes = new ConcurrentHashMap();
private final Map<String, String> readOnlyAttributes = new ConcurrentHashMap();
private final StandardContext context;
private static final List<String> emptyString;
private static final List<Servlet> emptyServlet;
private final Map<String, String> parameters = new ConcurrentHashMap();
}
public class StandardContext extends ContainerBase implements Context, NotificationEmitter {
private String[] applicationListeners = new String[0];
private List<Object> applicationEventListenersList = new CopyOnWriteArrayList();
private Map<ServletContainerInitializer, Set<Class<?>>> initializers = new LinkedHashMap();
private ApplicationParameter[] applicationParameters = new ApplicationParameter[0];
private Map<String, ApplicationFilterConfig> filterConfigs = new HashMap();
private Map<String, FilterDef> filterDefs = new HashMap();
private final Map<String, String> parameters = new ConcurrentHashMap();
private Map<String, String> servletMappings = new HashMap();
private Set<Servlet> createdServlets;
}
Servlet 可以通过getServletConfig().getServletContext()
方法获取上下文。
public void test(Servlet servlet) {
ServletContext context = servlet.getServletConfig().getServletContext();
}
在一个Web应用中,所有Servlet拥有共同的ServletContext 上下文环境。ServletContext支持以Java编码的形式添加Servlet、Listener(包括上下文监听器、事件监听等所有监听器)、Filter过滤器。如下所示
/**
* <p>
* Servlet容器初始化类
* </p>
* @see javax.servlet.ServletContainerInitializer
* @author Kern
*/
@Log4j2
@HandlesTypes(DemoWebAppInitializer.class)
public class DemoServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
ServletRegistration.Dynamic dynamic = servletContext.addServlet("demo", DemoServlet.class);
int loadOnStartup = 1;
String primaryMapping = "/";
dynamic.setLoadOnStartup(loadOnStartup);
dynamic.setInitParameter("demoParameter", "I'm init parameter");
dynamic.addMapping(primaryMapping);
log.info("Demo Servlet config complete! loadOnStartup:{}, url-pattern{}", loadOnStartup, primaryMapping);
log.info("=== addListeners ===");
servletContext.addListener(ContextListener.class);
servletContext.addListener(RequestLoggingListener.class);
log.info("=== addFilters ===");
servletContext.addFilter("wrapperRequestFilter", WrapperRequestFilter.class)
.addMappingForServletNames(null, false, "ROOT");
servletContext.addFilter("unifiedEncodingUTF8Filter", UnifiedEncodingUTF8Filter.class)
.addMappingForUrlPatterns(null, false, "/*");
}
}
以上代码等效于
<web-app>
<!--配置过滤器 -->
<filter>
<filter-name>wrapperRequestFilter</filter-name>
<filter-class>
cn.kerninventor.springboot2.servlet.service.filter.WrapperRequestFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>wrapperRequestFilter</filter-name>
<!--Servlet映射 -->
<servlet-name>ROOT</servlet-name>
</filter-mapping>
<filter>
<filter-name>unifiedEncodingUTF8Filter</filter-name>
<filter-class>
cn.kerninventor.springboot2.servlet.service.filter.UnifiedEncodingUTF8Filter
</filter-class>
</filter>
<filter-mapping>
<filter-name>unifiedEncodingUTF8Filter</filter-name>
<!--路径映射 -->
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--配置事件监听器 -->
<listener>
<listener-class>cn.kerninventor.springboot2.servlet.service.listener.RequestLoggingListener</listener-class>
</listener>
<listener>
<listener-class>cn.kerninventor.springboot2.servlet.service.listener.ContextListener</listener-class>
</listener>
<!--配置Servlet -->
<servlet>
<servlet-name>ROOT</servlet-name>
<servlet-class>cn.kerninventor.springboot2.servlet.service.servlet.DemoRootHttpServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<!--配置Servlet资源映射 -->
<servlet-mapping>
<servlet-name>ROOT</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
4.2 Request
请求对象封装了客户端请求的所有信息。在 HTTP 协议中,这些信息是从客户端发送到服务器请求的 HTTP
头部和消息体。主要包括ServletRequest
接口和HttpServletRequest
接口,HttpServletRequest
扩展了ServletRequest
接口以提供Http Servlet的请求信息
请求的主要信息如下:
- Http协议参数 Parameter
- 以键值对的形式存储,键与值是一对多的关系,例如调用
getParameterMap()
方法,将返回一个Map<String, String[]>
类型的Map集合 。
- 以键值对的形式存储,键与值是一对多的关系,例如调用
- 文件上传 Part
- content-type 为
multipart/form-data
时,将以文件形式处理,通过Part关键字相关方法获取,返回一个Part对象,该对象提供getInputStream()
方法来访问文件输入流
- content-type 为
- 属性 Attribut
- 属性是与请求相关联的对象。用于在Servlet容器和Servlet之间传递信息,也可以在Servlet之间传递信息
- 头部 Header
- Http请求的头部信息
- 请求路径 Path
-
Context Path:Servlet相对于Web服务器中的相对路径
-
Servlet Path: Servlet的映射地址
-
PathInfo:请求路径的一部分
-
RequestURI: 等于 contextPath + servletPath + pathInfo
- 官方提供的示例
-
其他路径方法
getRealPath
:获取该请求在服务器文件系统中的真实寻址路径,得到的地址字符串对应本地文件系统的一个文件getPathTranslated
:用于推断请求的pathInfo的实际请求路径
-
- Cookies信息
- cookies将以键值对的形式存储
- SSL属性
- 如果请求以HTTPS等安全协议发送过,将在请求信息中包含该部分内容。开发人员可以通过
isSecure()
方法查阅该请求是否经过安全协议
- 如果请求以HTTPS等安全协议发送过,将在请求信息中包含该部分内容。开发人员可以通过
- 国际化 Locale
- 客户可以选择希望Web服务器用什么语言来响应。该信息在请求的头部属性 Accept-Language中设置
getLocale
getLocales
- 客户可以选择希望Web服务器用什么语言来响应。该信息在请求的头部属性 Accept-Language中设置
- 请求数据编码 Charset
- 编码格式通过
getCharacterEncoding
方法获取,如果客户端没有在Content-Type中存储编码格式的话,将返回null,此时可以在读取请求信息前使用s``etCharacterEncoding
方法统一编码格式
- 编码格式通过
以下是我的一个Demo测试返回的一些request对象的方法,供参考
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// super.doPost(req, resp);
log.info("{} 接收到POST请求", getServletName());
log.info("=== remote information ===");
log.info("remoteAddr : {}", req.getRemoteAddr());
log.info("remoteHost : {}", req.getRemoteHost());
log.info("remotePort : {}", req.getRemotePort());
log.info("remoteUser : {}", req.getRemoteUser());
log.info("=== path information ===");
log.info("requestMethod : {}", req.getMethod());
log.info("contentType : {}", req.getContentType());
log.info("contextPath : {}", req.getContextPath());
log.info("servletPath : {}", req.getServletPath());
log.info("pathInfo : {}", req.getPathInfo());
log.info("requestURI : {}", req.getRequestURI());
log.info("pathTranslated: {}", req.getPathTranslated());
log.info("realPath : {}", getServletContext().getRealPath("/"));
log.info("=== cookies information ===");
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
log.info("cookie-{} : {}", cookie.getName(), cookie.getValue());
}
}
log.info("=== locale information ===");
Enumeration<Locale> localeEnumeration = req.getLocales();
if (localeEnumeration != null) {
while (localeEnumeration.hasMoreElements()) {
log.info("locale : {}", localeEnumeration.nextElement().toString());
}
}
log.info("=== parameters information ===");
Map<String, String[]> parameters = req.getParameterMap();
if (parameters != null) {
for (Map.Entry<String, String[]> stringEntry : parameters.entrySet()) {
log.info("parameter: key={}, value={}", stringEntry.getKey(), ToStringUtils.toString(stringEntry.getValue(), ","));
}
}
log.info("=== body information ===");
BufferedReader reader = req.getReader();
StringBuilder bodyStringBuilder = new StringBuilder();
String lineStr = "";
while ((lineStr = reader.readLine()) != null) {
bodyStringBuilder.append(lineStr);
}
log.info(bodyStringBuilder);
try {
Person person = new JsonMapper().readValue(bodyStringBuilder.toString(), Person.class);
log.info(person.toString());
} catch (JsonProcessingException e) {
e.printStackTrace();
}
resp.setCharacterEncoding("UTF-8");
ServletOutputStream outputStream = resp.getOutputStream();
outputStream.print("Hello Servlet");
outputStream.flush();
}
psotman测试请求
结果如下
4.3 Response
响应(response)对象封装了从服务器返回到客户端的所有信息。在 HTTP 协议中,从服务器传输到客户端
的信息通过 HTTP 头信息或响应的消息体。主要包括ServletResponse
接口和HttpServletResponse
接口,同样,HttpServletResponse
扩展了ServletResponse
接口以提供Http Servlet的请求信息。
response主要包含以下内容
- 缓存 buffer
- 支持Servlet设置和修改响应的缓冲区域
- 头部 header
- 包含响应的头部信息,同时Servlet允许添加和修改头部信息
- 国际化 Locale
- 存放在响应的头部属性
Content-Language
中,同时也支持配置统一的默认国际化属性。此外还提供Locale的查询和设置方法
- 存放在响应的头部属性
- 请求数据编码 Charset
- 存放在响应的头部信息
Content-Type
中,例如application/json; charset=utf-8
,提供setCharacterEncoding
和getCharacterEncoding
。需要注意的是,请求和响应的编码格式一般需要保持一致。
- 存放在响应的头部信息
- 其他简便方法
sendRedirect
,重定向到另外一个网络地址sendError
,直接返回错误信息
4.4 Session
Servlet规范定义了一个 HttpSession
接口用于弥补HTTP协议的无状态特性。
开发人员可以使用 HttpServletRequest#getSession();
方法直接获取,将返回一个HttpSession
对象。
使用Session池与Response对象的Cookie值相结合,可以实现用户会话的保持,以及较为复杂的单点登录等。当然相关的替代方案也比较多。这里不多讨论
5 Servlet主要自定义组件
Servlet针对其整个生命周期,甚至包括Servlet容器及上下文的整个生命周期,请求/响应对象的生命周期都提供了一些基础的自定义组件,主要包括过滤器和监听器,有针对性的在生命周期的各个阶段提供额外功能。
- Listener 针对容器的启动和运行期间的事件进行监听,在
EventListener
接口的基础上,Servlet 针对生命周期的不同元素及其不同阶段扩展了多种实现。 - Filter 在Servlet的路径映射规则基础上添加过滤器,过滤器主要用于请求和响应的包装,可以映射到指定请求路径或指定Servlet(Servlet本身具有路径映射)上。
5.1 各组件的加载和执行顺序
首先以Tomcat容器实现描述一下Servlet容器的主要组件加载顺序
Servlet Container 加载顺序
- 加载上下文参数
- 实例化Listener
- 实例化Filter并调用Filter#init(config:FilterConfig)初始化Filter
- 实例化Servlet并调用Servlet#init(config:ServletConfig)初始化Servlet
- 懒加载(load-on-start== null || load-on-start < 0)的Servlet将在第一次使用时被实例化并初始化
截取部分代码,通过源代码的注释可以很清楚看到加载顺序
@Override
protected synchronized void startInternal() throws LifecycleException {
try {
// Set up the context init params
mergeParameters();
// Call ServletContainerInitializers
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
initializers.entrySet()) {
try {
entry.getKey().onStartup(entry.getValue(),getServletContext());
} catch (ServletException e) {
log.error(sm.getString("standardContext.sciFail"), e);
ok = false;
break;
}
}
// Configure and call application event listeners
if (ok) {
if (!listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}
}
// Configure and call application filters
if (ok) {
if (!filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
}
// Load and initialize all "load on startup" servlets
if (ok) {
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}
// Start ContainerBackgroundProcessor thread
super.threadStart();
} finally {
// Unbinding thread
unbindThread(oldCCL);
}
}
其次是一次请求各个组件的调用顺序
- 请求相关的事件监听器
- 符合映射规则的过滤器
- 符合映射规则的Servlet
5.2 Filter
过滤器技术基于代码重用,在Servlet规范中,Filter被定义为javax.servlet.Filter
接口,其核心功能是包装请求和响应,此外也可以在Filter中修改和调整资源的请求,对请求进行拦截,数据加密,日志记录等。
**
应用场景
- 验证过滤器
- 日志记录和审计过滤器
- 图像转换过滤器
- 数据压缩过滤器
- 加密过滤器
- 词法(Tokenizing)过滤器
- 触发资源访问事件过滤器
- 转换 XML 内容的 XSL/T 过滤器
- MIME-类型链过滤器
- 缓存过滤器
5.2.1 Filter接口
/**
* @since Servlet 2.3
*/
public interface Filter {
default public void init(FilterConfig filterConfig) throws ServletException {}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain)
throws IOException, ServletException;
default public void destroy() {}
}
Filter接口主要包含三个生命周期方法,初始化,过滤处理和销毁。
三个方法的主要职责如下
- init 进行初始化,将传入一个FilterConfig参数,该参数主要包括其初始化参数、映射路径或映射Servlet等内容
- doFilter 过滤请求,过滤器的核心功能,主要功能在其中实现。
- 需要注意的是,根据Servlet规范,一般情况下,doFilter方法的执行结尾应当调用
chain.doFilter(request,response)
将请求传递给下一个过滤器。
- 需要注意的是,根据Servlet规范,一般情况下,doFilter方法的执行结尾应当调用
- destory 销毁方法,应当在销毁时释放必要的资源
5.2.2 Filter配置示例
Servlet3.0后除了常规的web.xml配置外,还支持Java方式配置。主要的配置方式本文4.1 ServletContext 章节已有展示。下面简要提炼
主要配置内容
- FilterName: Filter的唯一标识符
- FilterClass:Filter实现类的类名,需要实现
javax.servlet.Filter
接口 - mapping:映射规则,可以映射到一个路径中,也可以映射到Servlet(实际上也是间接映射到路径)
- dispatcherTypes:调度者类型,该例说明比较复杂,建议查阅Servlet规范文档,此处仅作简要说明。该值为枚举值,查阅
javax.servlet.DispatcherType
- REQUEST 请求从客户端发起时,调用该Filter,这是默认的值(dispatcherTypes为空时默认为REQUEST)
- FORWARD 请求经由
request.getRequestDispatcher().forward()
转发时,调用该Filter- 关于forward方法本身存在规约,即调用者对响应做出的任何操作都将被忽略,既完全由被调用者处理响应,调用者本身只是相当于转发的中介,如果被调用者发现响应已经提交,需要抛出异常。
- INCLUDE 请求经由
request.getRequestDispatcher().include()
转发时,调用该Filter- 关于include方法本身存在规约,即调用者与被调用者存在包含关系,被调用者可以在调用者的资源基础上进行添加。当然也有诸多限制(例如不能处理响应头部信息)。具体查阅Servlet规约文档第9.3节
- ASYNC 请求经由 request.startAsync().dispatch() 异步转发时,调用该Filter
- ERROR 当调用者的调用发生异常时,将调用该Filter
- init-param:初始化参数,该参数将封装 FilterConfig 在 init 方法中作为参数获得
注意,Filter映射规则需要符合两个条件,即 请求路径 + DispatcherTypes
Filter实现类
/**
* <p>
* UnifiedEncodingUTF8Filter
* 统一请求与响应的编码格式
* </p>
* @see javax.servlet.Filter
* @author Kern
*/
@Log4j2
public class UnifiedEncodingUTF8Filter implements javax.servlet.Filter {
public UnifiedEncodingUTF8Filter() {
log.info(this.getClass().getName() + " constructed");
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info(this.getClass().getName() + " initializing");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("UnifiedEncodingUTF8Filter handle");
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
5.2.2.1 Java编码方式示例
java编码格式通过 SerlvletContext#addFilter()
方法完成配置
ServletContainerInitializer实现(仅展示部分代码)
/**
* <p>
* Servlet容器初始化类
* </p>
* @see javax.servlet.ServletContainerInitializer
* @author Kern
*/
@Log4j2
@HandlesTypes(DemoWebAppInitializer.class)
public class DemoServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
//注册Servlet
ServletRegistration.Dynamic dynamic = servletContext.addServlet("ROOT", DemoRootHttpServlet.class);
int loadOnStartup = 1;
String primaryMapping = "/";
dynamic.setLoadOnStartup(loadOnStartup);
dynamic.setInitParameter("demoParameter", "I'm init parameter");
dynamic.addMapping(primaryMapping);
log.info("Demo Servlet config complete! loadOnStartup:{}, url-pattern{}", loadOnStartup, primaryMapping);
//注册Filter
log.info("=== addFilters ===");
servletContext.addFilter("unifiedEncodingUTF8Filter", UnifiedEncodingUTF8Filter.class)
.addMappingForUrlPatterns(null, false, "/*");
}
}
5.2.2.2 XML配置方式示例
xml配置示例等效于Java配置实例
<web-app>
<!--配置Filter -->
<filter>
<filter-name>unifiedEncodingUTF8Filter</filter-name>
<filter-class>
cn.kerninventor.springboot2.servlet.service.filter.UnifiedEncodingUTF8Filter
</filter-class>
</filter>
<!--配置Filter映射 -->
<filter-mapping>
<filter-name>unifiedEncodingUTF8Filter</filter-name>
<url-pattern>/*</url-pattern>
<!--该配置等效于上面的url-pattern -->
<!-- <servlet-name>ROOT</servlet-name> -->
</filter-mapping>
<!--配置Servlet -->
<servlet>
<servlet-name>ROOT</servlet-name>
<servlet-class>cn.kerninventor.springboot2.servlet.service.servlet.DemoRootHttpServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<!--配置Servlet资源映射 -->
<servlet-mapping>
<servlet-name>ROOT</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
5.2.3 路径映射规则,Filter与Servlet的绑定
Filter的路径映射可以映射到 一个指定路径中, 也可以直接映射到一个Servlet,
5.2.4 FilterChain的构建\执行原理简析
以Tomcat Servlet容器实现为例。容器根据Filter配置和定义加载并实例化完Filter之后,所有Filter将被存放在一个FilterMap数组中,存放在标准容器上下文环境中。当发生一次请求时,调用org.apache.catalina.core.ApplicationFilterFactory#createFilterChain()
方法。组装过滤器链。
public class ApplicationFilterFactory {
//截取部分代码
public static ApplicationFilterChain createFilterChain(ServletRequest request,
Wrapper wrapper, Servlet servlet) {
// Add filters that match on servlet name second
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersServlet(filterMap, servletName))
continue;
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}
// Return the completed filter chain
return filterChain;
}
}
public class ApplicationFilterChain implements FilterChain {
void addFilter(ApplicationFilterConfig filterConfig) {
// Prevent the same filter being added multiple times
for(ApplicationFilterConfig filter:filters)
if(filter==filterConfig)
return;
if (n == filters.length) {
//这里时New的FilteConfig
ApplicationFilterConfig[] newFilters =
new ApplicationFilterConfig[n + INCREMENT];
System.arraycopy(filters, 0, newFilters, 0, n);
filters = newFilters;
}
filters[n++] = filterConfig;
}
}
组装完过滤链后,将执行过滤链, 通过移动 pos
下标,结合规约中定义的 Filter调用chain.doFilter(req, resp)
完成链的顺序调用,将链执行完毕后,将调用目标Servlet处理请求。
public class ApplicationFilterChain implements FilterChain {
@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
if( Globals.IS_SECURITY_ENABLED ) {
//加密请求下的操作,最终也是调用internalDoFilter
} else {
internalDoFilter(request,response);
}
}
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {
// Call the next filter if there is one
if (pos < n) {
//pos++ ,移动指针
ApplicationFilterConfig filterConfig = filters[pos++];
try {
//这里实际上也是利用反射实例化一个新的Filter实例
//this.filter = (Filter) context.getInstanceManager().newInstance(filterClass);
Filter filter = filterConfig.getFilter();
//忽略大部分代码,最终执行
filter.doFilter(request, response, this);
}
// We fell off the end of the chain -- call the servlet instance
// 此处忽略大部分代码
servlet.service(request, response);
}
}
可以简单总结流程如下图
整个过程中,无论是Filter的配置和Filter实例都是新创建的对象,对于多线程高并发环境也提供了支持。
5.2.5 Filter和DispatcherType
由于分派机制与Filter有较多的关联,因而在本章内我整理了部分Dispatcher机制,以图表的形式展示不同分派类型的执行流程。
5.2.5.1 RequestDispatcher接口
RequestDispatcher接口是Servlet封装的分派接口,主要API有forward转发和include包含两个,转发和包含其本意上都是描述调用者与被调用者之间响应资源的关系。
forward调用时,调用者仅作为请求的转发者,被调用者将通过 response.flushBuffer()
清除调用者对响应的所有修改。
include调用时,调用者作为响应处理的主体,负责响应头部的设置和内容的修改,被调用者在前者的基础上,可以获得前者所有添加在request中的信息,同时在调用者的基础上添加响应内容,此外被调用者还有一个重要的职责是提交响应。
Servlet借由RequestDispatcher接口分派规约,结合3.0版本提出的异步上下文AsyncContext以及错误处理机制,整合了REQUEST\FORWARD\INCLUDE\ASYNC\ERROR 五种分派机制。下面对五种分派机制进行图解。
如下图解中没有显示路径映射规则,需要注意Filter映射规则 = 路径映射 + DispatcherType映射
ASYNC 异步暂时不做讨论
5.2.5.2 Request 请求
由客户端发起的请求其分派类型为REQUEST,是默认的请求分派类型。在此过程中,配置DispatcherTypes 中含有 REQUEST 的Filter将拦截请求并调用过滤链。
5.2.5.3 Forward 分派
服务端从请求中获得分派对象并调用 forward 方法指定到对应路径的 Servlet进行分派处理
req.getRequestDispatcher("/").forward(req,resp)
在此过程中,配置DispatcherTypes 中含有 FORWARD 的Filter将拦截请求并调用过滤链。
5.2.5.4 Include 包含分派
服务端从请求中获得分派对象并调用 include 方法指定到对应路径的 Servlet进行分派处理
req.getRequestDispatcher("/").include(req,resp)
在此过程中,配置DispatcherTypes 中含有 INCLUDE 的Filter将拦截请求并调用过滤链。
5.2.5.5 Error 错误处理
服务端在整个运行阶段,如果发生异常,将根据规约封装异常信息到request中,同时调用错误处理机制,Servlet容器厂商提供默认的错误处理机制,同时支持自定义的错误处理(映射为文件或Servlet路径)。
在进入错误处理机制前,配置DispatcherTypes 中含有 ERROR 的Filter将拦截请求并调用过滤链。
自定义错误处理配置
<web-app>
<!--配置Filter -->
<filter>
<filter-name>errorFilter</filter-name>
<filter-class>
cn.kerninventor.springboot2.servlet.service.filter.ErrorFilter
</filter-class>
</filter>
<!--配置Filter映射 -->
<filter-mapping>
<filter-name>errorFilter</filter-name>
<servlet-name>ERROR-HANDLER</servlet-name>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
<!--配置Servlet -->
<servlet>
<servlet-name>ERROR-HANDLER</servlet-name>
<servlet-class>cn.kerninventor.springboot2.servlet.service.servlet.DemoErrorHandlerServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<!--配置Servlet映射 -->
<servlet-mapping>
<servlet-name>ERROR-HANDLER</servlet-name>
<url-pattern>/error/</url-pattern>
</servlet-mapping>
<!--根据响应编码配置错误处理,指定到页面 -->
<error-page>
<error-code>404</error-code>
<location>/WEB-INF/404.jsp</location>
</error-page>
<!--根据异常类型配置错误处理,指定到路径 -->
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/error/</location>
</error-page>
</web-app>
处理流程
5.3 Listener
应用事件监听器是实现一个或多个 Servlet 事件监听器接口的类。它们是在部署 Web 应用时,实例化并注册到 Web 容器中。它们由开发人员在 WAR 包中提供。
Servlet 事件监听器支持在 ServletContext、HttpSession 和 ServletRequest 状态改变时进行事件通知。Servlet上下文监听器是用来管理应用的资源或 JVM 级别持有的状态。
应用场景
- 监听Servlet启动
- 请求监听
- 日志记录
- 会话监听
5.3.1 Listener接口
基于Java的事件监听接口
package java.util;
/**
* A tagging interface that all event listener interfaces must extend.
* @since JDK1.1
*/
public interface EventListener {
}
根据事件类型的不同,Servlet定义如下接口
事件主体 | 事件类型 | 描述 | 监听器接口 |
---|---|---|---|
ServletContext | 生命周期 | Servlet 上下文刚刚创建,并可用于服务它的第一个请求,或者Servlet 上下文即将关闭 | javax.servlet. ServletContextListener |
更改属性 | 在 Servlet 上下文的属性已添加,删除、或替换。 | javax.servlet. | |
ServletContextAttributeListener | |||
HttpSession | 生命周期 | 会话已创建、销毁或超时。 | javax.servlet.http. |
HttpSessionListener | |||
更改属性 | 已经在 HttpSession 上添加、移除、或替换属性。 | javax.servlet.http.HttpSessionAttributeListener | |
改变ID | HttpSession 的 ID 将被改变 | javax.servlet.httpHttpSessionIdListener | |
会话迁移 | HttpSession 已被激活或钝化。 | javax.servlet.http. | |
HttpSessionActivationListener | |||
对象绑定 | 对象已经从 HttpSession 绑定或解除绑定 | javax.servlet.http. | |
HttpSessionBindingListener | |||
ServletRequest | 生命周期 | 一个 servlet 请求已经开始由 Web 组件处理。 | javax.servlet. |
ServletRequestListener | |||
更改属性 | 已经在 ServletRequest 上添加、移除、或替换属 | ||
性。 | javax.servlet. | ||
ServletRequestAttributeListener | |||
异步事件 | 超时、连接终止或完成异步处理 | javax.servlet.AsyncListener |
5.3.2 Listener配置示例
本例中以上下文监听器为例,实现简单记录一下上下文的启动和销毁
/**
* <p>
* DemoServletContextListener
* </p>
*
* @author Kern
*/
@Log4j2
public class ContextListener implements ServletContextListener {
public ContextListener() {
log.info(this.getClass().getName() + " constructed");
}
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("ServletContext Initialized");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("ServletContext destroyed!");
}
}
5.3.2.1 Java编程配置
/**
* <p>
* Servlet容器初始化类
* </p>
* @see javax.servlet.ServletContainerInitializer
* @author Kern
*/
@Log4j2
@HandlesTypes(DemoWebAppInitializer.class)
public class DemoServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
log.info("=== addListeners ===");
servletContext.addListener(ContextListener.class);
}
}
5.3.2.2 XML配置示例
<web-app>
<listener>
<listener-class>cn.kerninventor.springboot2.servlet.service.listener.ContextListener</listener-class>
</listener>
</web-app>
6 Servlet 3.0+的主要新特性
Servlet3.0 主要在支持Java编程配置和注解编程方面做了非常大的努力,支持在Web应用启动时动态加载Servlet上下文。可惜不能再运行时进行组件的运行时加载和卸载。
下面简述一下两大特性
6.1 ServletContainerInitializer
接口如下
/**
* @see javax.servlet.annotation.HandlesTypes
*
* @since Servlet 3.0
*/
public interface ServletContainerInitializer {
public void onStartup(Set<Class<?>> c, ServletContext ctx)
throws ServletException;
}
6.1.1 加载规则
容器在启动时,将扫描 /META-INF/services 目录中 javax.servlet.ServletContainerInitializer 文件
- 文件名为接口名
- 文件内容为接口实现
ServletContainerInitializer 实现类必须有一个无参的构造器,
经由反射实例化后,将执行 onStratup
方法完成初始化
主要参数如下
- Set<Class<?>> 在实现类中标注
@HandlesTypes
指定class类型。 容器将扫码所有指定class类型的类,并作为参数传入。可以简单理解 ServletContainerInitializer 的实现作为主要加载类,使用@HandlesTypes
标注其他辅助加载类。可以参考Springboot的相关实现 - ServletContext 上下文参数
Spring 的 实现,实际上SpringMVC自3.2版本开始就使用此机制加载DispatcherServlet
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<>();
if (webAppInitializerClasses != null) {
for (Class<?> waiClass : webAppInitializerClasses) {
// Be defensive: Some servlet containers provide us with invalid classes,
// no matter what @HandlesTypes says...
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);
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}
/**
* Base class for {@link org.springframework.web.WebApplicationInitializer}
* @since 3.2
*/
public abstract class AbstractDispatcherServletInitializer extends AbstractContextLoaderInitializer {
//实现省略
}
**
以下是我测试的Demo实现,主要完成了Servlet注册 Filter注册 Listener注册等工作
/**
* <p>
* Servlet容器初始化类
* </p>
* @see javax.servlet.ServletContainerInitializer
* @author Kern
*/
@Log4j2
@HandlesTypes(DemoWebAppInitializer.class)
public class DemoServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
ServletRegistration.Dynamic dynamic = servletContext.addServlet("ROOT", DemoRootHttpServlet.class);
int loadOnStartup = 1;
String primaryMapping = "/";
dynamic.setLoadOnStartup(loadOnStartup);
dynamic.setInitParameter("demoParameter", "I'm init parameter");
dynamic.addMapping(primaryMapping);
log.info("Demo Servlet config complete! loadOnStartup:{}, url-pattern{}", loadOnStartup, primaryMapping);
ServletRegistration.Dynamic lazyLoadingDynamic = servletContext.addServlet("LAZY", DemoLazyLoadingRootHttpServlet.class);
lazyLoadingDynamic.setLoadOnStartup(-1);
lazyLoadingDynamic.setInitParameter("demoParameter", "I'm init parameter");
lazyLoadingDynamic.addMapping("/lazy/");
log.info("DemoLazyLoading Servlet config complete!");
if (set != null) {
Iterator<Class<?>> webAppInitializerIterator = set.iterator();
List<DemoWebAppInitializer> initializers = new ArrayList<>(set.size());
while (webAppInitializerIterator.hasNext()) {
Class<?> webAppInitializerClass = webAppInitializerIterator.next();
if (webAppInitializerClass != null &&
!webAppInitializerClass.isInterface() &&
DemoWebAppInitializer.class.isAssignableFrom(webAppInitializerClass)) {
try {
DemoWebAppInitializer demoWebAppInitializer = (DemoWebAppInitializer) webAppInitializerClass.getConstructor().newInstance();
initializers.add(demoWebAppInitializer);
} catch (Throwable e) {
throw new RuntimeException("WebAppInitializer failed to initialize" + webAppInitializerClass.getName());
}
}
}
if (initializers.size() > 0) {
log.info("Initialize DemoWebAppInitializer!");
DemoWebContext demoWebContext = new DemoWebContext();
initializers.forEach(e -> e.onStartup(demoWebContext));
}
}
log.info("=== addListeners ===");
servletContext.addListener(ContextListener.class);
servletContext.addListener(RequestLoggingListener.class);
log.info("=== addFilters ===");
servletContext.addFilter("wrapperRequestFilter", WrapperRequestFilter.class)
.addMappingForServletNames(null, false, "ROOT");
servletContext.addFilter("unifiedEncodingUTF8Filter", UnifiedEncodingUTF8Filter.class)
.addMappingForUrlPatterns(null, false, "/*");
// servletContext.addFilter("errorFilter", ErrorFilter.class)
// .addMappingForServletNames(EnumSet.of(DispatcherType.ERROR), false, "ERROR-HANDLER");
//TODO OTHER THINGS
}
}
6.2 注解编程
除了ServletContainerInitializer 接口之外,Servlet 也提供了非常多的注解以供快速注册Servlet、Listener和Filter等。用户可以不适用任何xml配置和Java配置编程,仅使用注解完成大部分配置工作。注解编程通过 metadata-complete属性开启或关闭。
6.2.1 注解说明
注解 | 功能 |
---|---|
@WebServlet | 注册Servlet |
@WebFilter | 注册Filter |
@WebListener | 注册Listener |
6.2.2 metadata-complete属性
类型:Boolean
释义:
- true
- 不扫描应用的类的注解
- 不读取jar文件中的
web-fragment.xml
- false 默认值
- 扫描应用的类的注解
- 读取jar文件中的
web-fragment.xml
配置方式
<web-app metadata-complete="true">
</web-app>
6.2.3 可插拔式配置文件 web-fragment.xml
包括上述的注解和 web-fragment.xml 文件,都可以通过 metadata-complete配置实现可插拔。
与web.xml内容差异不大。以 标签包围
仅对特化标签做说明
- name 配置名称
- ording 配置加载顺序,也可以在web.xml文件中通过 absolute-ording 标签强制排序,web.xml 必然是第一个加载的
web-fragment.xml 示例
<web-fragment>
<name>MyFragment1</name>
<ordering><after><name>MyFragment2</name></after></ordering>
...
</web-fragment>
web.xml 排序示例
<web-app>
<absolute-ordering>
<name>MyFragment3</name>
<name>MyFragment2</name>
</absolute-ordering>
...
</web-app>