Java Web技术已经成为当今主流的互联网 Web 应用技术之一,而 Servlet 是Java Web 技术的核心基础。本文首先从请求/响应架构应用的大背景谈起 Servlet 的由来,明确 Servlet 的产生动机,并揭示了 Servlet 的本质以及其在标准MVC模式中所扮演的角色。紧接着,给出了 Servlet族的继承结构,并对族内的接口和抽象类作了进一步的介绍,并给出开发一个Servlet程序的常用做法。在此基础上,我们图文并茂地介绍了 Servlet 的生命周期与执行流程,清晰展现 Servlet 的原理。特别地,由于Servlet容器默认采用单实例多线程的方式来处理请求,我们进一步介绍了Servlet 与并发的联系,并就Servlet容器如何同时来处理多个请求和如何开发线程安全的Servlet两个问题进行讨论和总结。最后,结合Java Web 应用的结构演变历程,给出了MVC架构的基本组成要素、内在联系和作用分工,并给出了 Servlet 在其中所扮演的角色。
一. Servlet 概述
1、从请求/响应架构谈 Servlet 的由来
2、Servlet 的本质与角色
Web 技术成为当今主流的互联网 Web 应用技术之一,而 Servlet 是 Java Web 技术的核心基础。JSP也只是为了弥补使用 Servlet 作为表现层的不足而提出的。JSP规范通过实现普通静态HTML和动态部分的混合编码,使得逻辑内容与外观相分离,大大简化了表示层的实现。但是,JSP并没有增加任何本质上不能用Servlet实现的功能,只是在JSP中编写静态HTML更加方便。事实上,JSP的本质仍然是Servlet,并且站在表现层的角度上来看,JSP 是 Servlet 的一种就简化。
Servlet是J2EE标准的一部分,是一种运行在Web服务器端的小型Java程序,更具体的说,Servlet是按照Servlet规范编写的一个Java类,用于交互式地浏览和修改数据,生成动态Web内容。要注意的是,由于Servlet是服务器端小程序,所以Servlet必须部署在Servlet容器中才能使用,例如Tomcat,Jetty等。
在标准的MVC模式中,Servlet仅作为控制器使用,而控制器角色的作用是:负责接收客户端的请求,它既不直接对客户端输出响应,也不处理用户请求,只是调用业务逻辑组件(JavaBean)来处理用户请求。一旦业务逻辑组件处理结束后,控制器会根据处理结果,调用不同的表现层页面向浏览器呈现处理结果。
二. Servlet的生命周期与执行流程
1、Servlet的生命周期
当 Servlet 在容器中运行时,其实例的创建及销毁等都不是由程序员所决定的,而是由Web容器进行控制的。一个Servlet对象的创建有两个时机:用户请求之时或应用启动之时。
(1) 客户端第一次请求某个Servlet时,容器创建该Servlet实例,这也是大部分Servlet创建实例的时机;
(2) Web应用启动时立即创建Servlet实例,即 load-on-startup Servlet。
但每个Servlet都遵循一个生命周期,即:Servlet 实例化–>Servlet 初始化—>服务—>销毁。具体流程如下图所示:
1、创建Servlet实例;
2、Web容器调用Servlet的init()方法,对Servlet进行初始化。特别地,在Servlet的生命周期中,仅执行一次init()方法;
3、Servlet 被初始化后,将一直存在于Web容器中,用于响应客户端请求。默认的请求处理、响应方式是调用与HTTP请求的方法相应的do方法;
4、Web容器决定销毁Servlet时,先调用Servlet的destroy() 的方法回收资源,通常在关闭Web应用之时销毁 Servlet。和 init()方法类似,destroy()方法在Servlet的生命周期中也仅执行一次。当Servlet对象退出生命周期时,负责释放占用的资源。一个Servlet在运行service()方法时可能会产生其他的线程,因此需要确保在调用destroy()方法时,这些线程已经终止或完成。
2、Servlet的执行流程
Servlet的执行流程:
(1) User Agent 向 Servlet容器(Tomcat)发出Http请求;
(2) Servle容器接收 User Agent 发来的请求;
(3) Servle容器根据web.xml文件中Servlet相关配置信息,将请求转发到相应的Servlet;
(4) Servlet容器创建一个 HttpServlet对象,用于处理请求;
(5) Servlet容器创建一个 HttpServletRequest对象,将请求信息封装到这个对象中;
(6) Servlet容器创建一个HttpServletResponse对象;
(7) Servlet容器调用HttpServlet对象的service方法,并把HttpServltRequest对象与HttpServltResponse对象作为参数传给 HttpServlet 对象;
(8) HttpServlet调用HttpServletRequest对象的有关方法,获取Http请求信息;
(9) HttpServlet调用JavaBean对象(业务逻辑组件)处理Http请求;
(10) HttpServlet调用HttpServletResponse对象的有关方法,生成响应数据;
(11) Servlet容器将HttpServlet的响应结果传给 User Agent。
3、load-on-servlet
load-on-servlet 指的是应用启动时就创建的Servlet,这些Servlet通常是用于后台服务的Servlet或者需要拦截很多请求的Servlet。也就是说,这些Servlet常常作为应用的基础Servlet使用,提供重要的后台服务。例如:
@WebServlet(loadOnStartup=1)
public class TimerServlet extends HttpServlet {
public void init(ServletConfig config)throws ServletException {
super.init(config);
Timer t = new Timer(1000,new ActionListener() { // 匿名内部类
public void actionPerformed(ActionEvent e) {
System.out.println(new Date());
}
});
t.start();
}
}
我们看到,这个 load-on-servlet没有提供 service()方法,这表明它不能响应用户请求,所以无需为它配置URL映射。由于它不能接收用户请求,所以只能在应用启动时实例化。特别需要注意的是,loadOnStartup属性用于标记容器是否在启动的时候加载这个servlet。当值为0或者大于0时,表示容器在应用启动时就加载这个servlet;当是一个负数时或者没有指定时,则指示容器在该servlet被选择时才加载。其中,正数值越小,启动该servlet的优先级越高。
三. Servlet的配置
为了让Servlet能够响应用户请求,必须将Servlet配置到Web应用中。从 J2EE 6(即 Servlet 3.0)开始,配置Servlet有两种方式,即 @WebServlet注解配置和传统的 web.xml 配置。
我们看下面这个简单的示例:
public class TestServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
//获取请求参数
String param1 = request.getParameter("name");
String param2 = request.getParameter("gentle");
//获取Servlet参数并放到request中
String age = this.getServletConfig().getInitParameter("age");
request.setAttribute("age",age);
// 此处进行业务逻辑处理
//根据处理结果转发到相应的表现层进行显示
request.getRequestDispatcher("/showInfo.jsp").forward(request, response);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doGet(req, resp);
}
}
web.xml配置文件片段:
<context-param>
<param-name>campus</param-name>
<param-value>NEU</param-value>
</context-param>
<servlet>
<servlet-name>TestServlet</servlet-name>
<servlet-class>com.edu.tju.rico.servlet.TestServlet</servlet-class>
<init-param>
<param-name>age</param-name>
<param-value>24</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>TestServlet</servlet-name>
<url-pattern>/servlet/test</url-pattern>
</servlet-mapping>
显示逻辑JSP视图:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<html>
<head>
<title>showInfo</title>
</head>
<body>
请求参数: Name: <%= request.getParameter("name")%><br>
Gentle: <%= request.getParameter("gentle")%><br>
<br>
----------------我是分割线--------------------<br>
<br>
Web应用初始化参数: <%= application.getInitParameter("campus")%><br>
<br>
----------------我是分割线--------------------<br>
<br>
TestServlet 初始化参数: ${requestScope.age}<br>
<br>
</body>
</html>
开发一个Servlet程序时,如果其是基于HTTP协议的,那么我们一般继承 HttpServlet 抽象类并重写 doGet() 和 doPost() 方法,或者直接重写 service() 方法去处理Http请求。
使用@WebServlet注解进行配置Servlet,使用 web.xml 配置的方法与该种方式只是在形式不同,作用方式是一样的,此不赘述。一旦我们使用 @WebServlet 配置了Servlet,那我们就不用在 web.xml 进行再次配置了,并且不能在web.xml中将 metadata-complete 属性设置为true。 支持的常用属性如下表所示:
属性名 | 是否必需 | 类型 | 描述 |
name | 否 | String | 指定Servlet的name属性,等价于<servlet-name>标签,默认取值为Servlet类的全限定名 |
value | 否 | String[] | 该属性等价于urlPatterns属性,这两个属性不能同时使用 |
initParam | 否 | WebInitParam[] | 指定一组Servlet初始化参数,等价于<init-param>标签 |
loadonStartup | 否 | int | 指定Servlet的加载时机和顺序,等价于<load-on-startup>标签 |
asyncSupported | 否 | boolean | 声明 Servlet 是否支持异步操作模式,等价于<async-supported>标签 |
description | 否 | String | 该Servlet的描述信息,等价于<description>标签 |
将上述示例使用@WebServlet注解配置,如下:
@WebServlet(name = "Test",
urlPatterns = { "/servlet/test" },
initParams = {@WebInitParam(name = "age", value = "24")})
public class TestServlet extends HttpServlet {
//...
}
四. Servlet3.0的新特性:异步处理
1、Servlet 不支持异步处理会带来哪些痛点?
上面已经提到Servlet容器处理请求的方式。对于每个到达Web容器的请求,Web容器会为其分配一条执行线程来专门负责该请求,直到回应完成前,该执行线程都不会被释放回Web容器的线程池。 我们知道,执行线程会耗用系统资源,若某些请求需要长时间处理(例如长时间运算、等待某个资源),就会长时间占用执行线程,若这类的请求很多,许多执行线程都被长时间占用,对于整个系统而言就会是个性能负担,甚至造成应用的性能瓶颈。
特别地,基本上一些需长时间处理的请求,通常客户端也较不在乎请求后要有立即的回应,若可以,让这类请求先释放容器分配给该请求的执行线程,让容器可以有机会将执行线程资源分配给其它的请求,这样可以减轻系统负担。这样,原先释放了容器所分配执行线程的请求,其回应将被延后,直到处理完成(例如长时间运算完成、所需资源已获得)再行对客户端的回应。
2、如何使用 Servlet 3.0 去支持对耗时事务的异步处理
在 Servlet 3.0 之前的规范中,如果Servlet作为控制器调用了一个耗时的业务方法,那么 Servlet 必须等到业务方法完全返回之后才能生成响应,这使得 Servlet 对业务方法的调用是一种阻塞式调用,因此效率比较低。Servlet 3.0 规范引入了异步处理来解决问题,异步处理允许Servlet重新发起一个线程去调用耗时的业务方法,这样就可以避免等待。Servlet 3.0 的异步处理是通过 AsyncContext 类来处理的,Servlet 可以通过 ServletRequest 的如下两个方法开启异步调用、创建 AsyncContext 对象。在这里,AsyncContext 对象代表异步处理的上下文。
AsyncContext startAsync()
AsyncContext startAsync(ServletRequest,ServletRequest)
这两个方法都会返回 AsyncContext 对象,前者会直接利用原有的请求与响应对象来创建AsyncContext对象,后者则允许你传入自行创建的请求、响应对象。在调用了startAsync()方法取得AsyncContext对象之后,这次的响应就会被延后,并释放容器所分配的执行线程。
我们可以通过AsyncContext的getRequest()、 getResponse()方法取得请求、响应对象,此次对客户端的响应将暂缓至调用AsyncContext的complete()方法或dispatch()方法为止,前者表示响应完成,后者表示将调用指定URL对应的内容进行响应。特别需要注意的是,dispatch()前后仍是同一个请求,并且被异步请求dispatch的目标页面必须指定:session=”false”。如果我们要支持 Servlet 的异步处理,我们的 Servlet 就必须能够支持非同步处理。也就是说,如果我们使用@WebServlet来标注的话,则必须将其asyncSupported属性设为true,如下所示:
@WebServlet(urlPatterns = "/some.do", asyncSupported = true )
public class AsyncServlet extends HttpServlet {
//...
}
特别需要注意的是,如果Servlet将支持非同步处理,并且其前端有过滤器,那么过滤器也必须表明其支持非同步处理,如果使用@WebFilter注解的方式,同样是需要设定其asyncSupported属性为true,如下所示:
@WebFilter(urlPatterns = "/some.do", asyncSupported = true )
public class AsyncFilter implements Filter{
//...
}
进行异步处理的Servlet类:
@WebServlet(urlPatterns = "/async",asyncSupported=true )
public class AsyncServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
request.setAttribute("param1", "我在异步处理前被设置...");
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out.println("<title>异步调用示例</title>");
out.println("进入Servlet的时间:" + new java.util.Date() + ".<br/>");
//创建AsyncContext对象,开始异步调用
final AsyncContext async = request.startAsync(); // 在局部(匿名)内部类直接使用,必须设为 final
// 设置异步调用的请求时长
async.setTimeout(10 * 1000);
// 启动线程去处理耗时任务
async.start(new Runnable() { // 匿名内部类
@Override
public void run() {
try {
Thread.sleep(5000);
HttpServletRequest req = (HttpServletRequest) async
.getRequest();
req.setAttribute("param2", "我在耗时任务处理线程中被设置...");
async.dispatch("/async.jsp");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
out.println("结束Servlet的时间:" + new java.util.Date() + ".<br/>");
out.flush();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doGet(req, resp);
}
}
表现层JSP:
<%-- 被异步请求dispatch的目标页面必须指定:session="false" --%>
<%@ page contentType="text/html; charset=utf-8" language="java" session="false"%>
<div style="background-color:#ffffdd;height:80px;">
param1:${param1}<br />
param2:${param2}<br />
<%
out.println("业务调用结束的时间:" + new java.util.Date());
%>
</div>
结果展示:
五. Servlet与并发
1、Servlet容器如何同时处理多个请求
Servlet 采用多线程来处理多个请求同时访问。更具体地,Servlet 依赖于一个线程池来服务请求,所谓线程池实际上是一系列的工作者线程集合,该集合包含的是一组等待执行任务的线程。此外,Servlet 使用一个调度线程来管理这些工作者线程。
当Servlet容器收到一个Servlet请求时,调度线程会从线程池中选出一个工作者线程,并将请求传递给该工作者线程,然后由该线程来执行Servlet的service()方法。当这个线程正在执行的时候,如果容器收到另外一个请求,调度线程将同样从线程池中选出另一个工作者线程来服务新的请求,特别需要注意的是,容器并不关心这个请求是否访问的是同一个Servlet。当容器同时收到对同一个Servlet的多个请求时,那么这个Servlet的service()方法将在多线程中并发执行。这样,当两个或多个线程同时访问同一个Servlet时,可能会发生多个线程同时访问同一资源的情况,数据可能会变得不一致。所以在用Servlet构建的Web应用时如果不注意线程安全的问题,会使所写的Servlet程序有难以发现的错误。
Servlet容器默认采用单实例多线程的方式来处理请求,这样减少产生Servlet实例的开销,提升了对请求的响应时间,对于Tomcat容器,我们可以在其server.xml中通过元素设置线程池中的线程数目。
我们都知道servlet是多线程的,同时一个servlet实现类只会有一个实例对象,也就是说它是Singleton的,所以多个线程是可能会访问同一个servlet实例对象的。每个线程都会为实例对象开辟单独的引用,那么servlet会是线程安全的吗?要判断是否是线程安全,我们需要知道线程安全问题是由什么引起的。线程安全问题都是由全局变量及静态变量引起的。
2、如何开发线程安全的Servlet
Servlet容器采用多线程来处理请求,提高性能的同时也造成了线程安全问题。要开发线程安全的Servlet应该从这几个方面进行:
(1)变量的线程安全: 多线程并不共享局部变量,所以我们要尽可能的在Servlet中使用局部变量;
(2)代码块的线程安全: 可以使用Synchronized、Lock和原子操作(java.util.concurrent.atomic)来保证多线程对共享变量的协同访问;但是要注意的是,要尽可能得缩小同步代码的范围,尽量不要在service方法和响应方法上直接使用同步,这会严重影响性能;
(3)属性的线程安全: ServletContext,HttpSession,ServletRequest对象中属性是线程安全的;
(4) 使用线程安全容器: 使用java.util.concurrent包下的线程安全容器代替ArrayList、HashMap等非线程安全容器;
六.ServletConfig对象与ServletContext对象
1、ServletConfig对象
在Servlet的配置文件中,可以使用一个或多个<init-param>标签为servlet配置一些初始化参数(配置在某个servlet标签或者整个web-app下)。当servlet配置了初始化参数后,web容器在创建servlet实例对象时,会自动将这些初始化参数封装到ServletConfig对象中,并在调用servlet的init方法时,将ServletConfig对象传递给servlet。进而,程序员通过ServletConfig对象就可以得到当前servlet的初始化参数信息。
首先,需要创建私有变量:private ServletConfig config=null;
其次,要重写init方法,传入config,令this.config=config;从而获得ServletConfig对象
最后,就可以获得<init-parm>中的配置信息了
//获取初始化参数
String value1 = this.config.getInitParameter("x1");
//获得配置文档中<init-param>标签下name对应的value
String vlaue2 = this.config.getInitParameter("x2");
//获取所有的初始化参数(用Enumeration接收)
Enumeration e = this.config.getInitParameterNames();
while(e.hasMoreElements()) {
String name = (String)e.nextElement();
String value = this.config.getInitParameter(name);
System.out.println(name + " = " + value);
}
在开发中ServletConfig的作用有如下三个:
1)获得字符集编码
String charset=this.config.getInitParameter("charset");
2)获得数据库连接信息
String url=this.config.getInitParameter("url");
String username=this.config.getInitParameter("username");
String password=this.config.getInitParameter("password");
3)获得配置文件
String configFile=this.config.getInitParameter("config");
ServletConfig是一个Servlet的配置信息对象,在Servlet初始化时,Servlet容器用它将sevlet配置信息传递给Servlet.
2、ServletContext对象
WEB容器在启动时,它会为每个WEB应用程序都创建一个对应的ServletContext对象,它代表当前web应用。ServletContext对象应用在以下方面:
1)多个web组件之间使用它实现数据共享
ServletConfig对象中维护了ServletContext对象的引用,开发人员在编写servlet时,可以通过ServletConfig.getServletContext()方法获得ServletContext对象。由于一个WEB应用中的所有Servlet共享同一个ServletContext对象,因此Servlet对象之间可以通过ServletContext对象来实现通讯。ServletContext对象通常也被称之为context域对象。
在serlvet中,可以使用如下语句来设置数据共享:
ServletContext context=this.getServletContext();//servletContext域对象
context.setAttribute("data","共享数据");//向域中存了一个data属性
在另一个servlet中,可以使用如下语句来获取域中的data属性:
ServletContext context = this.getServletContext();
String value = (String)context.getAttribute("data");//获取域中的data属性
System.out.println(value);
2)通过servletContext对象获取到整个web应用的配置信息,代码如下:
String url = this.getServletContext().getInitParameter("url");
String username = this.getServletContext().getInitParameter("username");
String password = this.getServletContext().getInitParameter("password");
3)通过servletContext对象实现servlet转发
由于servlet中的java数据不易设置样式,所以serlvet可以将java数据转发到JSP页面中进行处理:
this.getServletContext().setAttribute("data","serlvet数据转发");
RequestDispatcher rd = this.getServletContext().getRequestDispatcher("/viewdata.jsp");
rd.forward(request,response);
每个servlet都可以获得一个ServletConfig对象,也可以从web应用程序获得唯一的那个ServletContext对象。在分布式环境中,可以从每个JVM中获取一个ServletContext对象。ServletContext是用于Servlet交互的场景参数的集合。Servlet的运行模式是一种典型的“握手型”交互运行模式,即两个参与交互的模块,在交互时会准备一个交互场景,这个交互场景将一直存在,直到交互完成。这个交互场景的初始化由交互对象指定的参数进行定制,而所谓“指定的参数”通常是一个配置类。因此,在这里,交互场景由ServletContext来描述,ServletRequest和ServletResponse就是参与交互的具体对象。一个servlet可以通过ServletContext建立起与Servlet容器、web应用程序的其他部分的联系。