前言
这篇文章的出发点是为了整理Servlet相关知识点,以免在相关概念混淆或分不清的时候到处查阅资料。
一、什么是Servlet
-
Servlet
是JavaWeb
的三大组件(Servlet
、Filter
、Listener
)中最重要的组件,它属于动态资源。 - Servlet的作用是处理请求,服务器会把接收到的请求交给Servlet来处理,在Servlet中通常需要:接收请求数据;处理请求;完成响应。
- 每个
Servlet
都是唯一的,不同的Servlet
处理不同的请求,用下图说明:
说明:假如两个浏览器同时发送a请求,并不会创建两个相同的Servlet,这两个请求使用同一Servlet实例:Servlet是线程不安全的。
二、Servlet生命周期
Servlet的生命周期要从Servlet接口来认识,Servlet接口一共5个方法,其中前三个是生命周期方法:
(1) void init(ServletConfig)
(2) void service(ServletRequest,ServletResponse)
(3) void destory()
(4) ServletConfig getServletConfig()
(5) String getServletInfo()
出生:服务器创建Servlet
- 当Servlet第一次被请求时,或服务器启动时,服务器会创建Servlet实例。
- 默认情况下,服务器会在某个Servlet第一次收到请求时创建它。也可以在web.xml中对Servlet进行配置,使服务器启动时就创建Servlet。
(1)在<servlet>元素中配置<load-on-startup>元素可以让服务器在启动时就创建该Servlet
(2)假如多个<servlet>元素都欧配置了<load-on-startup>元素,那么就会按照数字大小顺序来启动,数字越小,启动越早!
(3)<load-on-startup>元素的值必须是大于等于0的整数。
<servlet>
<servlet-name>hello2</servlet-name>
<servlet-class>cn.itcast.servlet.Hello2Servlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
服务器默认是在servlet第一次被请求时创建Servlet实例,如果希望服务器启动时就创建Servlet实现需要在web.xml中配置。
- 服务器只为一个类型的Servlet创建一个实例对象,所以Servlet是单例的。
长大:服务器初始化Servlet
- 当服务器创建Servlet实例后会马上调用Servlet的init(ServletConfig)方法,完成对Servlet的初始化;
- init(ServletConfig)只会被调用一次
- 服务器会在调用init()方法时传递ServletConfig参数
工作:服务器使用Servlet处理请求
- 当Servlet被请求时,服务器会调用Servlet的
service(ServletRequest,ServletResponse)
方法; - 该方法每处理一次请求,就会被调用一次,所以它可能会被调用N次;
- 因为Servlet是单例的,所以可能在同一时刻一个Servlet对象会被多个请求同时访问,所以这可能出现线程安全问题;
- Servlet不是线程安全的,这有助与提高效率,但不能让Servlet具有状态,以免多个线程争抢数据;
死亡:服务器销毁Servlet
- 服务器通常不会销毁Servlet,通常只有在服务器关闭时才会销毁Servlet;
- 服务器会在销毁Servlet之前调用Servlet的destory()方法
- 可以在destory()方法中给出释放Servlet占有的资源,但通常Servlet是没什么可要释放的,所以该方法一般都是空的
三、实现Servlet的方式
1、实现Servlet有下面三种方式
- 实现javax.servlet.Servlet接口;
继承javax.servlet.GenericServlet类;
继承javax.servlet.http.HttpServlet类;(一般采用这种方式)
三者关系为:
GenericServlet
是Servlet
接口的实现类;HttpServlet
类是GenericServlet
的子类,它提供了对HTTP请求的特殊支持。
public abstract class GenericServlet implements Servlet, ServletConfig,
java.io.Serializable {......}
public abstract class HttpServlet extends GenericServlet {......}
2、Servlet接口
public interface Servlet {
public void init(ServletConfig config) throws ServletException;
public ServletConfig getServletConfig();
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
public String getServletInfo();
public void destroy();
-
ServletConfig
ServletConfig
对象是由服务器创建的,然后传递给Servlet
的init()
方法,对应web.xml
文件中的<servlet>
元素。该对象的常用方法有如下几个:
String getServletName()
获取Servlet在web.xml文件中的配置名称,即<servlet-name>指定的名称;
ServletContext getServletContext()
用来获取ServletContext对象
String getInitParameter(String name)
用来获取在web.xml中配置的初始化参数,通过参数名来获取参数值;
Enumeration getInitParameterNames()
用来获取在web.xml中配置的所有初始化参数名称;
例如web.xml
有如下servlet
元素:
<servlet>
<servlet-name>Hello</servlet-name>
<servlet-class>com.example.servlet.HelloServlet</servlet-class>
<init-param>
<param-name>paramName1</param-name>
<param-value>paramValue1</param-value>
</init-param>
<init-param>
<param-name>paramName2</param-name>
<param-value>paramValue2</param-value>
</init-param>
</servlet>
那么,可以通过以下方法来获取相应的信息:
public class Hello implements Servlet {
public void init(ServletConfig config) throws ServletException {
config.getServletName();//将得到Hello
config.getInitParameter("paramName1");//将得到paramValue1
config.getInitParameter("paramName2");//将得到paramValue2
...
}
...
}
-
ServletRequest
表示请求对象,它封装了所有与请求相关的数据,它是由服务器创建的; -
ServletResponse
表示响应对象,在service()方法中完成对客户端的响应需要使用这个对象;
3、GenericServlet
-
GenericServlet
是Servlet
接口的实现类,可以通过继承GenericServlet
来编写自己的Servlet
。下面是GenericServlet类的源代码:
public abstract class GenericServlet implements Servlet, ServletConfig,
java.io.Serializable {
private static final long serialVersionUID = 1L;
private transient ServletConfig config;
public GenericServlet() {}
@Override
public void destroy() {}
@Override
public String getInitParameter(String name) {
return getServletConfig().getInitParameter(name);
}
@Override
public Enumeration<String> getInitParameterNames() {
return getServletConfig().getInitParameterNames();
}
@Override
public ServletConfig getServletConfig() {
return config;
}
@Override
public ServletContext getServletContext() {
return getServletConfig().getServletContext();
}
@Override
public String getServletInfo() {
return "";
}
@Override
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
public void init() throws ServletException {}
public void log(String msg) {
getServletContext().log(getServletName() + ": " + msg);
}
public void log(String message, Throwable t) {
getServletContext().log(getServletName() + ": " + message, t);
}
@Override
public abstract void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
@Override
public String getServletName() {
return config.getServletName();
}
}
从代码中可以看出:
(1)
GenericServlet
定义了一个ServletConfig config
实例变量,并在init(ServletConfig)
方法中把参数ServletConfig
赋给了实例变量,然后在该类的很多方法中使用了实例变量config
。子类不能覆盖init(ServletConfig config)
,而应该去覆盖GenericServlet
提供的init()
方法,它是没有参数的init()方法。
(2)GenericServlet
实现了ServletConfig
接口,所以可以直接调用getInitParameter()
、getServletContext()
等ServletConfig
的方法。
4、HttpServlet
-
HttpServlet
类是GenericServlet
的子类,它提供了对HTTP
请求的特殊支持,所以通常我们都会通过继承HttpServlet
来完成自定义的Servlet
。 -
HttpServlet
覆盖了service()
方法
HttpServlet
把ServletRequest
和ServletResponse
强转成了HttpServletRequest
和HttpServletResponse
,HttpServlet
的service(HttpServletRequest,HttpServletResponse)
方法会去判断当前请求是GET
还是POST
(或其他),如果是GET
请求,那么会去调用本类的doGet()
方法,如果是POST
请求会去调用doPost()
方法,也就是说,我们在子类中去覆盖doGet()或doPost()方法即可。 -
HttpServletRequest
HttpServletRequest
是ServletRequest
的子类
public interface HttpServletRequest extends ServletRequest {......}
HttpServletRequest
的方法:
String getParameter(String paramName)
获取指定请求参数的值;
String getMethod()
:获取请求方法,例如GET或POST;
String getHeader(String name)
:获取指定请求头的值;
void setCharacterEncoding(String encoding)
:设置请求体的编码。
因为`GET`请求没有请求体,所以这个方法只只对POST请求有效。
当调用`request.setCharacterEncoding(“utf-8”)`之后,
再通过`getParameter()`方法获取参数值时,
那么参数值都已经通过了转码,即转换成了UTF-8编码。
所以,这个方法必须在调用getParameter()方法之前调用!
-
HttpServletResponse
HttpServletResponse
是ServletResponse
的子类。
public interface HttpServletResponse extends ServletResponse{......}
HttpServletResponse
方法:
PrintWriter getWriter()
获取字符响应流,使用该流可以向客户端输出响应信息。
例如response.getWriter().print(“<h1>Hello JavaWeb!</h1>”);
ServletOutputStream getOutputStream()
获取字节响应流,当需要向客户端响应字节数据时,需要使用这个流;
void setCharacterEncoding(String encoding)
用来设置字符响应流的编码,
例如在调用setCharacterEncoding(“utf-8”);之后,
再response.getWriter()获取字符响应流对象,这时的响应流的编码为utf-8,
使用response.getWriter()输出的中文都会转换成utf-8编码后发送给客户端;
void setHeader(String name, String value)
向客户端添加响应头信息,
例如setHeader(“Refresh”, “3;url=http://www.example.cn”),
表示3秒后自动刷新到http://www.example.cn;
void setContentType(String contentType)
该方法是setHeader(“content-type”, “xxx”)的简便方法,
即用来添加名为content-type响应头的方法。
content-type响应头用来设置响应数据的`MIME`类型;
void sendError(int code, String errorMsg)
向客户端发送状态码,以及错误消息。
例如给客户端发送404:response(404, “您要查找的资源不存在!”)。
四、Servlet与线程安全
因为一个类型的
Servlet
只有一个实例对象,那么就有可能会出现一个Servlet同时处理多个请求的情况,那么Servlet是否为线程安全的呢?答案是:“不是线程安全的”。这说明Servlet的工作效率很高,但也存在线程安全问题:当两个或多个线程同时访问同一个Servlet时,可能会发生多个线程同时访问同一资源的情况,数据可能会变得不一致。
所以我们不应该在Servlet中随意创建成员变量,因为可能会存在一个线程对这个成员变量进行写操作,另一个线程对这个成员变量进行读操作。
所以:
- 不要在Servlet中创建成员!创建局部变量即可!
- 可以创建无状态成员!
- 可以创建有状态的成员,但状态必须为只读的!
Servlet体系结构是建立在Java多线程机制之上的,它的生命周期是由Web容器负责的。当客户端第一次请求某个Servlet 时,Servlet容器将会根据web.xml配置文件实例化这个Servlet类。当有新的客户端请求该Servlet时,一般不会再实例化该 Servlet类,也就是有多个线程在使用这个实例。Servlet容器会自动使用线程池等技术来支持系统的运行。
有助理解的一个例子:
public class Concurrent extends HttpServlet {
PrintWriter output;
public void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username;
response.setContentType("text/html; charset=gb2312");
username = request.getParameter("username");
output = response.getWriter();
try {
Thread.sleep(5000); // 为了突出并发问题,在这设置一个延时
} catch (Exception e) {
}
output.println("用户名:" + username + "<BR>");
}
}
该Servlet中定义了一个实例变量output(servlet实例的成员变量),在service方法将其赋值为用户的输出。当一个用户访问该Servlet时,程序会正常的运行;但当多个用户并发访问时,就可能会出现其它用户的信息显示在另外一些用户的浏览器上的问题。
解决方法:
- 使用Javax.servlet.SingleThreadModel(Servlet2.4中已经废弃该接口),此时Servlet容器将保证Servlet实例以单线程方式运行,也就是说,同一时刻,只会有一个线程执行Servlet的service()方法。但是,如果一个Servlet实现了 SingleThreadModel接口,Servlet引擎将为每个新的请求创建一个单独的Servlet实例,这将引起大量的系统开销。 SingleThreadModel在Servlet2.4中已不再提倡使用;
将前面的Concurrent Test类的类头定义更改为:
Public class Concurrent Test extends HttpServlet implements SingleThreadModel {
……
}
- 去除实例变量,使用局部变量。这是保证Servlet线程安全的最佳选择。
public class Concurrent extends HttpServlet {
public void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
PrintWriter output;
String username;
response.setContentType("text/html; charset=gb2312");
username = request.getParameter("username");
output = response.getWriter();
try {
Thread.sleep(5000); // 为了突出并发问题,在这设置一个延时
} catch (Exception e) {
}
output.println("用户名:" + username + "<BR>");
}
}
- 使用同步代码块:synchronized{…};但是在程序中使用同步来保护要使用的共享的数据,也会使系统的性能大大下降。这是因为被同步的代码块在同一时刻只能有一个线程执行它,使得其同时处理客户请求的吞吐量降低,而且很多客户处于阻塞状态。另外为保证主存内容和线程的工作内存中的数据的一致性,要频繁地刷新缓存,这也会大大地影响系统的性能。在实际的开发中也应避免或最小化 Servlet 中的同步代码;
public void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username;
response.setContentType("text/html; charset=gb2312");
username = request.getParameter("username");
synchronized(this){
output = response.getWriter();
try {
Thread.sleep(5000); // 为了突出并发问题,在这设置一个延时
} catch (Exception e) {
}
output.println("用户名:" + username + "<BR>");
}
}
不同变量的线程安全问题
在Servlet和JSP中,变量可以归为下面的几类:类变量:request,response,session,config,application,以及JSP页面内置的page, pageContext。其中除了application外,其它都是线程安全的。
实例变量:实例变量是实例所有的,在堆中分配。在Servlet和JSP容器中,一般仅实例化一个Servlet和JSP实例,启动多个该实例的线程来处理请求。而实例变量是该实例所有的线程所共享,所以,实例变量不是线程安全的。
局部变量:局部变量在堆栈中分配,因为每一个线程有自己的执行堆栈,所以,局部变量是线程安全的。
五、<url-pattern>
-
<url-pattern>
是<servlet-mapping>
的子元素,用来指定Servlet
的访问路径,即URL
。 - 它必须以
/
开头! - 可以在<servlet-mapping>中给出多个<url-pattern>,例如:
<servlet-mapping>
<servlet-name>AServlet</servlet-name>
<url-pattern>/AServlet</url-pattern>
<url-pattern>/BServlet</url-pattern>
</servlet-mapping>
说明一个Servlet
绑定了两个URL
,无论访问/AServlet
还是/BServlet
,访问的都是AServlet。
- 在<url-pattern>中使用通配符
(1)所谓通配符就是星号
*
,使用通配符可以命名一个Servlet
绑定一组URL。
(2)通配符要么为前缀,要么为后缀,不能出现在URL
中间位置,也不能只有通配符。
(3)通配符是一种模糊匹配URL
的方式,如果存在更具体的<url-pattern>
,那么访问路径会去匹配具体的<url-pattern>
。
六、ServletContext
6.1 ServletContext概述
- 服务器会为每个应用创建唯一一个ServletContext对象;(应用内唯一)
- ServletContext对象的创建是在服务器启动时完成的;(出生)
- ServletContext对象的销毁是在服务器关闭时完成的。(死亡)
- ServletContext对象的作用是在整个Web应用的动态资源之间共享数据。(作用)
6.2 获取ServletContext
- 在Servlet中获取ServletContext对象
在
void init(ServletConfig config)
中:ServletContext context = config.getServletContext();
,ServletConfig
类的getServletContext()
方法可以用来获取ServletContext
对象;
- 在GenericeServlet或HttpServlet中获取ServletContext对象
直接使用
this.getServletContext()
来获取;
6.3 ServletContext对象的作用
-
ServletContext
是JavaWeb
四大域对象(PageContext
、ServletRequest
、HttpSession
、ServletContext
)之一。 - 所有域对象都有存取数据的功能,因为域对象内部有一个Map,用来存储数据。
-
ServletContext
对象用来操作数据的方法
void setAttribute(String name, Object value)
用来存储一个对象,也可以称之为存储一个域属性,
例如:servletContext.setAttribute(“xxx”, “XXX”),在ServletContext中保存了一个域属性,域属性名称为xxx,域属性的值为XXX。
请注意,如果多次调用该方法,并且使用相同的name,那么会覆盖上一次的值,这一特性与Map相同;
Object getAttribute(String name)
用来获取ServletContext中的数据,当前在获取之前需要先去存储才行,
例如:String value = (String)servletContext.getAttribute(“xxx”);,获取名为xxx的域属性;
void removeAttribute(String name)
用来移除ServletContext中的域属性,如果参数name指定的域属性不存在,那么本方法什么都不做;
Enumeration getAttributeNames()
获取所有域属性的名称;
-
ServletContext
获取应用初始化参数
(1)Servlet也可以获取初始化参数,但它是局部的参数;也就是说,一个Servlet只能获取自己的初始化参数,不能获取别人的,即初始化参数只为一个Servlet准备!
(2)可以配置公共的初始化参数,为所有Servlet而用!这需要使用ServletContext才能使用!
(3)还可以使用ServletContext来获取在web.xml文件中配置的应用初始化参数!注意,应用初始化参数与Servlet初始化参数不同。
<web-app ...>
...
<context-param>
<param-name>paramName1</param-name>
<param-value>paramValue1</param-value>
</context-param>
<context-param>
<param-name>paramName2</param-name>
<param-value>paramValue2</param-value>
</context-param>
</web-app>
-
ServletContext
获取资源(略)