Servlet理论与实践综述

        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 的由来

 
        我们日常所接触到的应用有很大一部分都是 基于请求/响应架构的,如下图所示。在这种架构中,一般由两个角色组成,即:Server 和 User Agent。特别地, 根据 User Agent 的不同,我们可以将应用分为 B/S模式(User Agent 为浏览器时) 和 C/S模式。但无论哪种模式,Server 与 User Agent 的交互使用的都是同一个请求和应答的标准,即 HTTP 协议。
        一般地,以浏览器为例,User Agent 的作用就是根据用户的请求URL生成相应的 HTTP请求报文发送给服务器,并对服务器的响应进行解析(或渲染),使用户看到一个丰富多彩的页面。但是,如果我们需要在网页上完成一些业务逻辑(比如,登陆验证),或者需要从服务器的数据库中取一些数据作为网页的显示内容,那么除了负责显示的HTML标记之外,必须还要有完成这些业务功能的代码存在,这种网页我们称之为动态网页。
        对于 静态网页而言,服务器上存在的是一个个纯HTML文件。当客户端浏览器发出HTTP请求时,服务器可以根据请求的URL找到对应的HTML文件,并将HTML代码返回给客户端浏览器。但是 对于动态网页,服务器上除了找到需要显示的HTML标记外,还必须执行所需要的业务逻辑,然后将业务逻辑运算后的结果和需要显示的HTML标记一起生成新的HTML代码。最后,将新的带有业务逻辑运算结果的HTML代码返回给客户端。为了实现动态网页的目标,Servlet技术(利用输出流动态生成 HTML 页面)应运而生,它能够以一种可移植的方法来提供动态的、面向用户的内容。
 
 
 

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。 支持的常用属性如下表所示:

属性名是否必需类型描述
nameString指定Servlet的name属性,等价于<servlet-name>标签,默认取值为Servlet类的全限定名
valueString[]该属性等价于urlPatterns属性,这两个属性不能同时使用
initParamWebInitParam[]指定一组Servlet初始化参数,等价于<init-param>标签
loadonStartupint指定Servlet的加载时机和顺序,等价于<load-on-startup>标签
asyncSupportedboolean声明 Servlet 是否支持异步操作模式,等价于<async-supported>标签
descriptionString该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应用程序的其他部分的联系。

 
本文内容主要转自:
 
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值