Servlet 是 Java Web 应用中最核心的组件
Servlet 运行在 Servlet 容器中,能够为各种各样的客户请求提供相应服务。Servlet 能够轻而易举地完成以下任务:
- 动态生成 HTML 文档
- 把请求转发给同一个 Web 应用中的其他 Servlet 组件
- 把请求转发给其他 Web 应用中的 Servlet 组件
- 读取客户端的 Cookie,以及向客户端写入 Cookie
- 访问其他服务器资源(如数据库或基于 Java 的应用程序)
Servlet 之所以有如此高的本领,主要有两个原因:
- Servlet 是用 Java 语言编写出来的类,只要开发人员有着深厚的 Java 编程功底,就可以编写出各类复杂的 Servlet
- Servlet 对象由 Servlet 容器缔造,能驾轻就熟地调用容器中的各种资源
1. Servlet API
Servlet API 的 JavaDoc 文档可以在 Oracle 的官网和 Apache 网站上查看:
http:tomcat.apache.org/tomcat-9.0-…
1.1 Servlet 接口
Servlet API 的核心是 javax.servlet.Servlet 接口,所有的 Servlet 类都必须实现这一接口。
在 Servlet 接口中定义了 5 个方法,其中有 3 个方法都由 Servlet 容器来调用,容器会在 Servlet 生命周期的不同阶段调用特定的方法:
方法 | 相关说明 |
---|---|
init(ServletConfig config) | 该方法负责初始化 Servlet 对象;容器在创建好 Servlet 对象后,就会调用该方法 |
service(ServletRequest req, ServletResponse req) | 负责响应客户端的请求,为客户端提供相应的服务;容器接收到客户端要求访问特定 Servlet 对象的请求时,就会调用该 Servlet 对象的 service() 方法 |
destroy() | 负责释放 Servlet 对象占用的资源;当 Servlet 对象结束生命周期时,容器会调用此方法 |
getServletConfig() | 返回一个包含 Servlet 初始化参数信息的 ServletConfig 对象 |
getServletInfo() | 返回包含 Servlet 的创建者、版本和版权等信息的 String 对象 |
javax.servlet.GenericServlet implements Servlet, and javax.servlet.http.HttpServlet extends GenericServlet.
When users develop their own Servlet
, they can choose to extend the GenericServlet
or the HttpServlet
.
1.2 GenericServlet 抽象类
GenericServlet 抽象类除了实现 Servlet 接口,还实现了 ServletConfig 接口和 Serializable 接口。如下为 GenericServlet 类的部分重要源代码:
public abstract class GenericServlet implements Servlet, ServletConfig,
java.io.Serializable {
private transient ServletConfig config;
public GenericServlet() {}
/**
* 尽管实现了该方法,但是什么也没做
*/
public void destroy() {}
public String getInitParameter(String name) {
return getServletConfig().getInitParameter(name);
}
public Enumeration<String> getInitParameterNames() {
return getServletConfig().getInitParameterNames();
}
public ServletConfig getServletConfig() {
return config;
}
public ServletContext getServletContext() {
return getServletConfig().getServletContext();
}
public String getServletInfo() {
return "";
}
public void init(ServletConfig config) throws ServletException {
// 使当前 Servlet 对象与 Servlet 容器传入的 ServletConfig 对象关联!
this.config = config;
this.init();
}
public void init() throws ServletException {
// 子类可以重新实现该方法
}
/**
* GenericServlet 类中的唯一抽象方法
*/
public abstract void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
public String getServletName() {
return config.getServletName();
}
...
}
复制代码
从 GenericServlet 类源代码可以看出:其实现了 Servlet 接口中的 init(ServletConfig config)
初始化方法,且 GenericServlet 类有一个 ServletConfig 类型的私有实例变量 config。当 Servlet 容器调用 GenericServlet 的 init(ServletConfig config)
方法时,该方法使得私有实例变量 config 引用由容器传入的 ServletConfig 对象,即关联 GenericServlet 对象和 ServletConfig 对象。
GenericServlet 运用了装饰设计模式,为自己附加了 ServletConfig 装饰身份。通过 ServletConfig 接口的实例来实现其接口中的方法。
装饰设计模式可参考博客:juejin.cn/post/684490…
GenericServlet 类还自定义了一个不带参数的 init()
方法,init(ServletConfig config)
会调用此方法。有如下两种方法覆盖父类(GenericServlet)的初始化方法:
(1)覆盖 init()
方法:
public void init(){
// 子类具体的初始化方法
...
}
复制代码
(2)覆盖 init(ServletConfig config)
方法:
public void init(ServletConfig config){
// 必须先调用父类的 init(config), 否则会出现"空指针异常"
super.init(config);
// 子类具体的初始化行为
...
}
复制代码
1.3 HttpServlet 抽象类
HttpServlet 类是 GenericServlet 类的子类。HttpServlet 类为 Servlet 接口提供了与 Http 协议相关的通用实现。在开发 Java Web 应用时,自定义的 Servlet 类一般都扩展 HttpServlet 类。
HTTP 协议把客户端请求分为 GET、POST、PUT 和 DELETE 等多种方法,HttpServlet 类针对每一种请求方式都提供了相应的服务方法,如:doGet()
、doPost()
、doPut()
和 doDelete()
等方法。如下提供了 HttpServlet 类的部分重要源代码:
public abstract class HttpServlet extends GenericServlet {
private static final String METHOD_GET = "GET";
private static final String METHOD_POST = "POST";
...
public HttpServlet() {}
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
} catch (ClassCastException e) {
throw new ServletException("non-HTTP request or response");
}
service(request, response);
}
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 获取客户端请求方式,以调用对应的服务方法如: doGet()、doPost()...
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
// "GET_METHOD"
long lastModified = getLastModified(req);
if (lastModified == -1) {
doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
} catch (IllegalArgumentException iae) {
ifModifiedSince = -1;
}
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
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)) {
// "POST_METHOD"
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 {
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);
}
}
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_get_not_supported");
if (protocol.endsWith("1.1")) {
// 如果采用 HTTP1.1,则返回响应状态代码为 405 的错误
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
} else {
// 如果采用 HTTP1.0,则返回响应状态代码为 400 的错误
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
}
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_post_not_supported");
if (protocol.endsWith("1.1")) {
// 如果采用 HTTP1.1,则返回响应状态代码为 405 的错误
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
} else {
// 如果采用 HTTP1.0,则返回响应状态代码为 400 的错误
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
}
}
protected void doPut(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {...}
...
}
复制代码
对于 HttpServlet 类的具体子类,一般会针对客户端的特定请求方式,来覆盖 HttpServlet 父类中相应的 doXXX()
方法。为了使 doXXX()
方法能被 Servlet 容器访问,应该把访问权限设为 public。
假定 HttpServletSub 类为 HttpServlet 类的子类,如下重新实现 doGet()
和 doPost()
:
public class HttpServletSub extends HttpServlet{
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException{
// 提供具体的实现代码
...
}
public void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException{
// 服务同 doGet()
doGet(req, resp);
}
}
复制代码
1.4 ServletRequest 接口
Servlet 接口的 service(ServletRequest req, ServletResponse res)
方法中有一个 ServletRequest 类型的参数。ServletRequest 类表示来自客户端的请求。当 Servlet 容器接收到客户端要求访问特定 Servlet 的请求时,容器先解析客户端的原始请求数据,把它包装为一个 ServletRequest 对象。当容器调用 Servlet 对象的 service()
方法时,就会把 ServletRequest 对象作为参数传给 service()
方法。
ServletRequest 接口提供了一系列用于读取客户端请求数据的方法:
方法 | 说明 |
---|---|
getContentLength() |
返回请求正文的长度,如果请求正文的长度未知,则返回 -1 |
getContentType() |
获得请求正文的 MIME 类型,未知返回 null |
getInputStream() |
返回用于读取请求正文的输入流 |
getLocalAddr() |
返回服务器端的 IP 地址 |
getLocalName() |
返回服务器端的主机名 |
getLocalPort() |
返回服务器端的 FTP 端口号 |
getParameter(String name) |
根据给定的请求参数名,返回来自客户请求中的匹配的请求参数值 |
getProtocol() |
返回客户端与服务器端通信所用的协议的名称以及版本号 |
getReader() |
返回用于读取字符串形式的请求正文的 BufferedReader 对象 |
getRemoteAddr() |
返回客户端的 IP 地址 |
getRemoteHost() |
返回客户端的主机名 |
getRemotePort() |
返回客户端的 FTP 端口号 |
此外,ServletRequest 接口中还定义了一组用于在请求范围内存取共享数据的方法:
方法 | 说明 |
---|---|
setAttribute(String name, java.lang.Object object) |
在请求范围内保存一个属性,参数 name 表示属性名,参数 object 表示属性值 |
getAttribute(String name) |
根据 name 参数给定的属性名,返回请求范围内的匹配属性值 |
removeAttribute(String name) |
从请求范围内删除一个属性 |
1.5 HttpServletRequest 接口
HttpServletRequest 接口是 ServletRequest 接口的子接口。
HttpServletRequest 接口提供了用于读取 HTTP 请求中相关信息的方法:
方法 | 说明 |
---|---|
getContextPath() |
返回客户端所请求访问的 Web 应用的 URL 入口 例如:访问 URL 为 http://localhost:8080/hello_world/info,那么该方法返回 “/hello_world” |
getCookies() |
返回 HTTP 请求中的所有 Cookie |
getHeader(String name) |
返回 HTTP 请求头部的特定项 |
getHeaderNames() |
返回一个 Enumeration 对象,它包含了 HTTP 请求头部的所有项目名 |
getMethod() |
返回 HTTP 请求方式 |
getRequestURI() |
返回 HTTP 请求的头部的第 1 行中的 URI |
geetQueryString() |
返回 HTTP 请求中的查询字符串,即 URL 中 “?” 后面的内容 例如:访问 URL 为 http://localhost:8080/hello_world/info?name=Octocat,那么该方法返回 “name=Octocat” |
若使用实现了 Servlet 接口的子类中的 service()
来解析 HTTP 请求中的请求参数,而解析原始请求数据的过程非常繁琐。而根据 Oracle 的 Servlet API 来创建 Servlet 时,则无须费力地解析原始 HTTP 请求,该项工作完全由 Servlet 容器代劳了。Servlet 容器把 HTTP 请求包装成 HttpServletRequest 对象,Servlet 只需调用该对象的各种 getXXX()
方法,就能轻轻松松地读取到 HTTP 请求中的各种数据。
1.6 ServletResponse 接口
Servlet 接口的 service(ServletRequest req, ServletResponse res)
方法中有一个 ServletResponse 类型的参数,Servlet 通过 ServletResponse 对象来生成响应结果。当 Servlet 容器接收到客户端要求访问特定 Servlet 的请求时,容器会创建一个 ServletResponse 对象,并把它作为参数传给 Servlet 的 service()
方法。
ServletResponse 接口中定义了一系列与生成响应结果相关的方法:
方法 | 说明 |
---|---|
setCharacterEncoding(String charset) |
设置响应正文的字符编码。默认为 ISO-8859-1 |
setContentLength(int length) |
设置响应正文的长度 |
setContentType(String type) |
设置响应正文的 MIME 类型 |
getCharacterEncoding() |
返回响应正文的字符编码 |
getContentType() |
返回响应正文的 MIME 类型 |
setBufferSize(int size) |
设置用于存放响应正文数据的缓冲区的大小 |
getBufferSize() |
获取用于存放响应正文数据的缓冲区的大小 |
reset() |
清空缓冲区内的正文数据,并且清空响应状态代码以及响应头 |
resetBuffer() |
仅仅清空缓冲区内的正文数据,不清空响应状态代码以及响应头 |
flushBuffer() |
强制性地把缓冲区内的响应正文数据发送到客户端 |
isCommitted() |
返回一个 boolean 类型的值;如果为 true,表示缓冲区内的数据已经提交给客户,即数据已经发送给客户端 |
getOutputStream() |
返回一个 ServletOutputStream 对象,Servlet 用它来输出二进制的正文数据 |
getWriter() |
返回一个 PrintWriter 对象,Servlet 用它来输出字符串形式的正文数据 |
ServletResponse 中响应正文的默认 MIME 类型为 text/plain,即纯文本类型。而 HttpServletResponse 中相应正文的默认 MIME 类型 为 text/html,即 HTML 文档类型。
Servlet 通过 ServletResponse 对象主要产生 HTTP 响应结果的正文部分。
为了提高输出数据的效率,ServletOutputStream 和 PrintWriter 先把数据写到缓冲区内。在以下几种情况下,缓冲区的数据会被提交给客户,即数据被发送到客户端:
- 缓冲区内数据已满时,ServletOutputStream 或 PrintWriter 会自动把缓冲区内的数据发送给客户端,并清空缓冲区
- Servlet 调用 ServletResponse 对象的
flushBuffer()
方法 - Servlet 调用 ServletOutputStream 或 PrintWriter 对象的
flush()
方法或close()
方法
为了确保 ServletOutputStream 或 PrintWriter 输出的所有数据都会被提交给客户,比较安全的做法是在所有数据都输出完毕后,调用 ServletOutputStream 或 PrintWriter 的 close()
方法。
注意:如果要设置相应正文的 MIME 类型和字符编码,必须先调用 ServletResponse 对象的 setContentType()
和 setCharacterEncoding()
方法,然后再调用 ServletResponse 的 getOutputStream()
或 getWriter()
方法,以及提交缓冲区内的正文数据。只有满足这样的操作顺序,所做的设置才能生效。
1.7 HttpServletResponse 接口
HttpServletResponse 接口是 ServletResponse 的子接口。
HttpServletResponse 接口提供了与 HTTP 协议相关的一些方法,Servlet 可通过这些方法来设置 HTTP 响应头或向客户端写 Cookie:
方法 | 说明 |
---|---|
addHeader(String name, String value) |
向 HTTP 响应头中加入一项内容 |
sendError(int sc) |
向客户端发送一个代表特定错误的 HTTP 响应状态代码 |
sendError(int sc, String msg) |
向客户端发送一个代表特定错误的 HTTP 响应状态代码,并且发送具体的错误消息 |
setHeader(String name, String value) |
设置 HTTP 响应头中的一项内容,存在则覆盖 |
setStatus(int sc) |
设置 HTTP 响应的状态代码 |
addCookie(Cookie cookie) |
向 HTTP 响应中加入一个 Cookie |
HttpServletResponse 接口中定义了一些代表 HTTP 响应状态代码的静态常量,例如:
常量 | 说明 |
---|---|
HttpServletResponse.SC_BAD_REQUEST |
400 |
HttpServletResponse.SC_FOUND |
302 |
HttpServletResponse.SC_NOT_FOUND |
404 |
HttpServletResponse.SC_OK |
200 |
HttpServletResponse.FORBIDDEN |
403 |
... |
… |
以下有三种方式可以设置 HTTP 响应正文的 MIME 类型及字符编码,且三种方式是等价的:
// 方式一
response.setContentType("text/html;charsetr=GBK");
// 方式二
response.setContentType("text/html");
response.setCharacterEncoding("GBK");
// 方式三
response.setHeader("Content-type","text/html;charset=GBK");
复制代码
1.8 ServletConfig 接口
Servlet 接口的 init(ServletConfig config)
方法有一个 ServletConfig 类型的参数。当 Servlet 容器初始化一个 Servlet 对象时,会为这个 Servlet 对象创建一个 ServletConfig 对象。ServletConfig 对象中包含了 Servlet 的初始化参数信息,此外,ServletConfig 对象还与当前 Web 应用的 ServletContext 对象关联。
ServletConfig 接口定义了以下方法:
方法 | 说明 |
---|---|
getInitParameter(String name) |
根据给定的初始化参数名,返回匹配的初始化参数值 |
getInitParameterNames() |
返回一个 Enumeration 对象,里面包含了所有的初始化参数名 |
getServletContext() |
返回一个 ServletContext 对象 |
getServletName() |
返回 Servlet 的名字,即 web.xml 中相应 元素的 子元素的值,若没配置,则返回 Servlet 类的名字 |
以下代码为一个 HelloServlet 类设置了两个初始化参数,username
和 url
:
<servlet>
<servlet-name>HelloServlet</servlet-name>
<servlet-class>com.one.servlet.HelloServlet</servlet-class>
<!-- init-param 是初始化参数、可以有多组参数 -->
<init-param>
<param-name>username</param-name>
<param-value>root</param-value>
</init-param>
<init-param>
<param-name>url</param-name>
<param-value>jdbc:mysql://localhost:3306/test</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
复制代码
1.9 ServletContext 接口
ServletContext 是 Servlet 与 Servlet 容器之间进行通信的接口。每个 Web 应用都有唯一的 ServletContext 对象。可以把 ServletContext 对象形象地理解为 Web 应用的总管家,同一个 Web 应用中的所有 Servlet 对象都共享一个总管家,Servlet 对象们可以通过这个总管家来访问容器中的各种资源。
ServletContext 接口提供的方法可分为以下几种类型:
(1)在 Web 应用范围内存取共享数据的方法
方法 | 说明 |
---|---|
setAttribute(String name, java.lang.Object object) |
将 java 对象和属性名绑定 |
getAttribute(String name) |
根据参数给定的属性名,返回一个 Object 类型的对象 |
getAttributeNames() |
返回一个 Enumeration 对象,该对象包含了所有存放在 ServletContext 中的属性名 |
removeAttribute(String name) |
根据参数指定的属性名,从 ServletContext 中删除匹配的属性 |
(2)访问当前 Web 应用的资源
方法 | 说明 |
---|---|
getContextPath() |
返回当前 Web 应用的 URL 入口 |
getInitParameter(String name) |
根据给定的参数名,返回 Web 应用范围内匹配的初始化参数 |
getInitParameterNames() |
返回一个 Enumeration 对象,它包含了 Web 应用范围内所有的初始化参数名 |
getServletContextName() |
返回 Web 应用的名字,即 |
getRequestDispatcher(String path) |
返回一个用于向其他 Web 组件转发请求的 RequestDispatcher 对象 |
(3)访问 Servlet 容器中的其他 Web 应用
方法 | 说明 |
---|---|
getContext(String uripath) |
根据 URI,返回当前 Servlet 容器中其他 Web 应用的 ServletContext 对象 |
(4)访问 Servlet 容器的相关信息
方法 | 说明 |
---|---|
getMajorVersion() |
返回 Servlet 容器支持的 Java Servlet API 的主版本号 |
getMinorVersion() |
返回 Servlet 容器支持的 Java Servlet API 的次版本号 |
getServerInfo() |
返回 Servlet 容器的名字和版本 |
(5)访问服务器端的文件系统资源
方法 | 说明 |
---|---|
getRealPath(String path) |
根据参数指定的虚拟路径,返回文件系统中一个真实的路径 |
getResource(String path) |
返回一个映射到参数指定的路径的 URL |
getResourceAsStream(String path) |
返回一个用于读取参数指定文件的输入流 |
getMimeType(String file) |
返回参数指定的文件的 MIME 类型 |
(6)输出日志
方法 | 说明 |
---|---|
log(String msg) |
向 Servlet 的日志文件中写日志 |
log(String message, java.lang.Throwable throwable) |
向 Servlet 日志文件中写错误日志,以及异常的堆栈信息 |
在 HttpServlet 类或 GenericServlet 类以及子类中都可以直接调用 getServletContext()
方法来得到当前 Web 应用的 ServletContext 对象。
2. Servlet 生命周期
2.1 初始化阶段
Servlet 的初始化阶段包括四个步骤:
- Servlet 容器加载 Servlet 类,把它的 .class 文件中的数据读入到内存。
- Servlet 容器创建 ServletConfig 对象,并使其与当前 Web 应用的 ServletContext 对象关联。
- Servlet 容器创建 Servlet 对象。
- Servlet 容器调用 Servlet 对象的
init(ServletConfig config)
方法。
在下列情况之一,Servlet 会进入初始化阶段:
- Web 应用处于运行时阶段,特定 Servlet 被客户端首次请求访问。多数 Servlet 都会在这种情况下被 Servlet 容器初始化。
- 在 web.xml 文件中为 Servlet 设置 元素,那么当 Servlet 容器启动 Servlet 所属的 Web 应用时,就会初始化这个 Servlet。如果有些 Servlet 专门负责在 Web 应用启动阶段为 Web 应用完成一些初始化操作,则可以让它们在 Web 应用启动时就被初始化。
- 当 Web 应用被重新启动时,所有 Servlet 都会在特定时刻被重新初始化。
2.2 运行时阶段
当 Servlet 容器接收到要求访问特定 Servlet 的客户请求,Servlet 容器创建针对于这个请求的 ServletRequest 对象和 ServletResponse 对象,然后调用相应 Servlet 对象的 service()
方法。service()
方法从 ServletRequest 对象中获得客户请求信息并处理该请求,通过 ServletResponse 对象生成响应结果。
当 Servlet 容器把 Servlet 生成的响应结果发送给了客户,Servlet 容器就会销毁 ServletRequest 对象和 ServletResponse 对象。
2.3 销毁阶段
当 Web 应用被终止时,Servlet 容器会先调用 Web 应用中所有 Servlet 对象的 destroy()
方法,然后再销毁这些 Servlet 对象。在 destroy()
方法的实现中,可以释放 Servlet 所占用的资源(例如关闭文件输入流和输出流,关闭于数据库的连接等)。
此外,容器还会销毁与 Servlet 对象关联的 ServletConfig 对象。
3. ServletContextListener 监听器
在 Servlet API 中有一个 ServletContextListener 接口,它能监听 ServletContext 对象的生命周期,实际上就是监听 Web 应用的生命周期。
当 Servlet 容器启动或终止 Web 应用时,会触发 ServletContextEvent 事件,该事件由 ServletContextListener 来处理。在 ServletContextListener 接口中定义了 ServletContextEvent 事件的两个方法:
方法 | 说明 |
---|---|
contextInitialized(ServletContextEvent sce) |
当 Servlet 容器启动 Web 应用时调用该方法 |
contextDestroyed(ServletContextEvent sce) |
当 Servlet 容器终止 Web 应用时调用该方法 |
如下为 ServletContextListener 的用法:
public class MyServletContextListenerImpl implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
System.out.println("ServletContext 对象被创建了");
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
System.out.println("ServletContext 对象被销毁了");
}
}
复制代码
在 web.xml 中配置:
<!-- 配置监听器 -->
<listener>
<listener-class>com.one.listener.MyServletContextListenerImpl</listener-class>
</listener>
复制代码
以上为监听器的使用范例,而在实际应用中,监听器往往是用于统计网页被客户端访问的次数…
4. 使用 Annotation 标注配置 Servlet
为了简化对 Web 组件的发布过程,可以不必在 web.xml 文件中配置 Web 组件,而是直接在相关的类中使用 Annotation 标注配置。
如:@WebServlet、@WebListener、@WebFilter…
以下示例使用 @WebServlet 来标注:
@WebServlet(name="MyServlet",
urlPatterns={"/myServlet"},
initParams={
@WebInitParam(name="username",value="w"),
@WebInitParam(name="age",value="19")
})
public class MyServlet extends HttpServlet {...}
复制代码
5. 处理 HTTP 请求参数中的中文字符编码
如果浏览器端和服务器端对请求参数采用不同的字符编码进行解析,就会造成乱码!
以下是几种解决中文乱码的有效方法:
(1)对读取到的请求参数进行字符编码转换:
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html; charset=UTF-8");
...
}
复制代码
(2)对于 POST 请求,采用如下方式:
request.setCharacterEncoding("GBK");
复制代码
该段代码只能设置请求正文中的字符编码,而不能设置请求头中的字符编码。而 GET 请求中的请求参数位于请求头中,所以无法用上述方法设置 GET 请求中的请求参数编码。
(3)利用 ServletContet 对象的 getRequestCharacterEncoding()
和 setRequestCharacterEncoding()
方法来分别读取和设置当前 Web 应用中请求正文数据的字符编码。
6. Servlet 下载/上传文件
6.1 下载文件
下载文件是指把服务器端的文件发送到客户端。
以下是下载文件示例的关键代码:
// 1.获取要下载的文件名
String downloadFileName = request.getParameter("filename");
// 2.读取要下载的文件内容(通过 ServletContext 对象可以读取)
ServletContext servletContext = getServletContext();
// 获取要下载的文件类型
String mimeType = servletContext.getMimeType("/file/" + downloadFileName);
// 3.在回传前,通过响应头告诉客户端返回的数据类型
resp.setContentType(mimeType);
// 4.还要告诉客户端收到的数据是用于下载使用(同样使用响应头)
/* Content-Disposition响应头表示收到的数据怎么处理,attachment表示附件(下载使用),filename=表示指定下载的文件名 */
resp.setHeader("Content-Disposition", "attachment; filename=" + downloadFileName);
// 获取读取本地文件的输入流
InputStream resourceAsStream = servletContext.getResourceAsStream("/file/" + downloadFileName);
// 获取响应的输出流
ServletOutputStream outputStream = resp.getOutputStream();
// 5.把下载的文件内容回传给客户端
IOUtils.copy(resourceAsStream, outputStream);
复制代码
6.2 上传文件
上传文件是指把客户端的文件发送到服务器端。
当客户端向服务器上传文件时,客户端发送的 HTTP 请求正文采用 “multipart/form-data” 的数据类型,它表示复杂的包括多个子部分的复合表单。
Apache 开源软件组织提供了与文件上传有关的两个软件包:
- commons-io-xxx.jar
- commons-fileupload-xxx.jar
对于一个正文部分为 “multipart/form-data” 类型的 HTTP 请求,uploadfile 软件包把请求正文包含的复合表单中的每个子部分看作是一个 FileItem 对象。FileItem 对象分为两种类型:
- formField:普通表单域类型,如表单中的文本域以及提交按钮等
- 非 formField:上传文件类型,表单中的文件域就是该类型
如下为上传文件的基本实现:
// 得先判断上传的数据是否为多段数据(只有是多段数据,才是文件上传的)
if (ServletFileUpload.isMultipartContent(req)) {
// 创建 FileItemFactory 工厂实现类
FileItemFactory fileItemFactory = new DiskFileItemFactory();
// 创建用于解析上传数据的工具类 ServletFileUpload 类
ServletFileUpload servletFileUpload = new ServletFileUpload(fileItemFactory);
// 解析上传的数据,得到每一个表单项 FileItem
try {
List<FileItem> list = servletFileUpload.parseRequest(req);
// 遍历判断每一个表单项,是普通类型,还是上传的文件
for (FileItem fileItem : list) {
if (fileItem.isFormField()) {
// 普通表单项
System.out.println("表单项的name属性: " + fileItem.getFieldName());
// 参数 UTF-8 解决乱码问题
System.out.println("表单项的value属性: " + fileItem.getString("UTF-8"));
} else {
// 上传的文件
System.out.println("表单项的 name 属性: " + fileItem.getFieldName());
System.out.println("上传的文件名: " + fileItem.getName());
// 存放到服务器端指定的地址
fileItem.write(new File("D:\\" + fileItem.getName()));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
复制代码
FileItemFactory 是创建 FileItem 对象的工厂。
DiskFileItemFactory 类和 DiskFileItem 类分别实现了 FileItemFactory 接口和 FileItem 接口。
7. 请求转发
请求转发,是服务器的行为,请求由服务器转发给另外一个页面处理,如何转发,何时转发,转发几次,客户端无从得知的。请求转发时,从发送第一次到最后一次请求的过程中,Servlet 容器仅创建一次 Request 和 Response 对象,新的页面继续处理同一个请求。也可以理解为服务器将 Request 对象在页面之间传递。请求转发实际上就是在一个 Servlet 中调用其他的 Servlet。
请求转发的过程发生在服务器内部,只能从当前应用内部查找相应的转发资源,而不能转发到其它应用的资源。解决了一次请求内不同 Servlet 的数据共享问题,但存在重复提交数据的风险~
核心代码:
RequestDispatcher dispatcher = getServletContext.getRequestDispatcher("/");dispatcher.forward(request, response);
复制代码
特点:
- 客户端浏览器发送一次请求,剩下的由服务器进行处理(请求转发不管跳转几个页面,都是一次请求)
- 地址栏不会改变,存在RRL中的数据重复提交的风险,容易造成安全问题
- 参数可以一直传递,参与的 Servlet 共享 Request 和 Response
- 只能跳转到内部资源(项目中), 不能跳转外部资源(项目外)
- 可以访问受保护的资源(WEB-INF)
8. 重定向
重定向,是客户端的行为,每次请求重定向都是由客户端发起的,也就是说重定向一次,就刷新 Request 对象的属性,之前的 Request 对象的属性值就失效了。
重定向可以解决请求转发的一个风险问题:数据重复提交!
作用:
- 保护第一次的请求,避免因为用户的刷新动作频繁触发第一次请求 Servlet 的重复执行(因为如果是 GET 提交的方式,使用请求转发,地址栏的地址不会发生变化且带着相关的数据,刷新页面一次,等于提交一次数据,这就存在脏数据和安全风险的问题,为了解决这一问题,就需要使用重定向技术)
特点:
- 响应重定向通过响应对象实现,跳转几个页面就发送几次请求
- 地址栏会发生改变(最后一次请求的路径)
- 参数不能一直传递, 需要手动传递(因为不是同一次请求)
- 既可以跳转到内部资源,也可以跳转到外部资源
- 不能访问受保护的资源(WEB-INF)
核心代码:
response.sendRedirect("/hello");
复制代码
9. Servlet 其余高级用法
Servlet 还有许许多多高级的用法,主要包括:
- 动态生成图像,并发送给客户端
- 读取客户端的 Cookie,以及向客户端写 Cookie
- 关于 Cookie 的知识,可以参考我写的另一篇文章:待更新
- 访问 Servlet 容器为 Web 应用提供的工作目录
- 进行 Web 组件之间的合作
- 访问 Servlet 容器内的其他 Web 应用
- 处理多个客户端同时访问 Web 应用中的相关资源导致的并发问题
- 对客户请求的异步处理
- 服务器端推送(很多情况下,需要服务器向客户端主动发送一些信息)
- …
正如我开篇所言,Servlet 的上限取决于编程人员的 Java 技术上限,只要其有着深厚的 Java 编程底蕴,就可以编写出各式各样的 Servlet 以实现各种复杂功能…
原创不易? 转载请标明出处?~
若本文对你有所帮助,点赞支持嗷~?
这是我的 GitHub 仓库,欢迎前来围观 ~?