第 14 章 生命周期

注意

讲一下servlet的生命周期与运行时的线程模型,对了解servlet的运行原理有所帮助,这样才能避免一些有冲突的设计。
如果你不满足以下任一条件,请继续阅读,否则请跳过此后的部分,进入下一章:第 15 章 分页
  1. 了解servlet的生命周期。
  2. 了解servlet运行时的线程模型,及设计程序时需要注意的部分。

14.1. 生命周期

我们之前使用的都是javax.servlet.http.HttpServlet,这个类实现了javax.servlet.Servlet接口,而这个接口中定义的三个方法是所有servlet都必须实现的。

package javax.servlet;
public interface Servlet {void init(ServletConfig config);void service(ServletRequest request, ServletResponse response);void destroy();
}

jsp-ch-14-01-lifecycle-01.png
如图所示,tomcat之类的服务器首先根据web.xml中的定义实例化servlet,然后调用它的init()方法进行初始化,init()方法的ServletConfig参数是服务器传递进servlet的,其中包含web.xml配置的初始化信息和ServletContext对象等共享内容。
初始化后的servlet实例便进入等待请求的状态,当有与servlet-mapping匹配的请求进入时,服务器会调用servlet实例的service方法,传入ServletRequest与ServletResponse两个参数等待servlet处理完毕。
注意一点,对于每个web应用,内存中只存在一个servlet实例,所有请求都是调用这个servlet实例,所以我们说servlet不是线程安全的,所有操作都要限制在service()方法中进行,不要在servlet中定义类变量。(doGet()和doPost()是HttpServlet覆盖service()方法后分支出来的辅助方法,实际上服务器调用的还是service()。)
当web应用卸载时,服务器会调用每个已经初始化的servlet的destroy(),然后销毁这些servlet实例,如果你需要在servlet销毁时释放什么资源的话,可以写在destory()方法中。
那么servlet是在什么时候进行初始化的呢?我们可以通过web.xml中的load-on-startup标签。

<servlet><servlet-name>TestServlet</servlet-name><servlet-class>anni.TestServlet</servlet-class><load-on-startup>1</load-on-startup>
</servlet>

load-on-startup的值是一个整数&#xff0c;当它大于等于零的时候服务器会在web发布的时候初始化servlet。当它小于零或者我们没有设置load-on-startup的时候&#xff0c;服务器会在用户第一次访问servlet的时候才去初始化servlet。
或许你对load-on-startup为什么是一个整数存有疑问&#xff0c;为什么不是true和false呢&#xff1f;这是因为如果我们在web.xml中设置了多个servlet的时候&#xff0c;可以使用load-on-startup来指定servlet的加载顺序&#xff0c;服务器会根据load-on-startup的大小依次对servlet进行初始化。不过即使我们将load-on-startup设置重复也不会出现异常&#xff0c;服务器会自己决定初始化顺序。
回头看看javax.servlet.Filter中也有init()和destroy()方法&#xff0c;它的声明周期与servlet基本一致&#xff0c;服务器使用init()对Filter初始化&#xff0c;销毁Filter的时候调用destroy()方法&#xff0c;只是过滤器就不在有load-on-startup设置了&#xff0c;它总是会在服务器启动的时候进行初始化&#xff0c;然后按照web.xml定义的顺序依次执行。

14.2. 线程模型

我们做一个试验&#xff0c;以此来证明某些编写servlet的方法是绝对错误的。
第一步&#xff0c;我们打开浏览器&#xff0c;浏览14-02的index.jsp页面&#xff0c;输入“叮咚”。
jsp-ch-14-02-thread-01.png
第二步&#xff0c;我们再打开一个14-02/index.jsp页面&#xff0c;输入“lingirl”。
jsp-ch-14-02-thread-02.png
第三步&#xff0c;点击第一个页面的提交按钮&#xff0c;然后在10秒之内点击另一个页面的提交按钮&#xff0c;等两个页面都提交成功后&#xff0c;我们会看到如下页面。
url上有乱码这个就是提交“叮咚”的页面&#xff0c;会惊讶吧&#xff1f;本来这时应该显示“叮咚”的。
jsp-ch-14-02-thread-03.png
这个页面对应提交“lingirl”的页面&#xff0c;它似乎是显示正常的。
jsp-ch-14-02-thread-04.png
到底是哪里出错了&#xff0c;为什么第一个页面提交了数据&#xff0c;却得到第二个页面提交的结果&#xff0c;首先让我们看一下TestServlet的代码。

package anni;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class TestServlet extends HttpServlet {private String username;public void doGet(HttpServletRequest request,HttpServletResponse response)throws ServletException, IOException {this.username &#61; request.getParameter("username");try {Thread.sleep(10000);} catch(InterruptedException ex) {}response.getWriter().write(this.username);}
}

doGet()方法中从request中获得username参数&#xff0c;然后赋给this.username&#xff0c;这是一个类变量。然后暂停10秒&#xff0c;这10秒我们假设正在进行一些很费时间的计算&#xff0c;这样我们就有十秒钟去点两个页面的提交按钮了。最后将this.username写入response。
你也许在想&#xff1a;“这没有问题啊&#xff0c;第一个页面提交了数据&#xff0c;等待10秒返回&#xff0c;第二个页面再提交数据&#xff0c;等待10秒返回&#xff0c;两者并不冲突啊。”可实际上在多线程模型中不会有这种队列让请求一个一个执行&#xff0c;所有请求都是蜂拥而至。
在这个例子里&#xff0c;第一个请求过来将“叮咚”赋值给this.username后进行等待&#xff0c;10秒之内我们的第二个请求又调用了doGet()方法&#xff0c;并把this.username修改为“lingirl”&#xff0c;等到10秒后第一个请求结束等待后&#xff0c;获得的this.username已经是“lingirl”了。
this.username这种写法在servlet中是绝对禁用的&#xff0c;如果有什么信息需要保存&#xff0c;可以考虑放到session或ServletContext中。

14.3. 在jsp中定义类变量

写在<%%>之间的代码&#xff0c;在转换成servlet之后都会service()方法内运行&#xff0c;所以我们不必担心出现上边this.username的问题。
但是我们可以用<%!%>&#xff08;注意多出来的感叹号&#xff09;定义类变量或类方法&#xff0c;把上一个罪大恶极的servlet改造成jsp的话&#xff0c;就像这样。

<%&#64; page contentType&#61;"text/html; charset&#61;gb2312"%>
<%!String username;
%>
<%this.username &#61; request.getParameter("username");try {Thread.sleep(10000);} catch(InterruptedException ex) {}out.write(this.username);
%>

注意

使用14-03下的例子可以测试jsp出错的效果&#xff0c;记得要在10秒之内点击两次。
<%!%>似乎是一个巨大的陷阱&#xff0c;如果我们使用它定义类变量就一定会出现多线程错误。
不过凡事都有正反两面&#xff0c;当我们需要在jsp中定义一个通用方法时&#xff0c;就需要借助<%!%>的力量了&#xff0c;假设我们需要一个方法&#xff0c;根据用户的性别显示不同的html内容&#xff0c;如果sex &#61; 0就输出红色的“男”&#xff0c;如果sex &#61; 1就输出绿色的“女”。为实现这个功能&#xff0c;我们可以定义一个sexRenderer()方法。
14-04/index.jsp页面显示效果如下&#xff1a;
jsp-ch-14-03-jsp-01.png
index.jsp中的代码分两部分。
第一部分定义sexRenderer()方法和

<%!public String sexRenderer(int sex) {if (sex &#61;&#61; 0) {return "";} else if (sex &#61;&#61; 1) {return "";} else {return "";}}
%>

第二部分循环显示保存了性别信息的数组&#xff0c;显示的时候将会调用sexRenderer()方法。

<%int[] people &#61; {0, 1, 1, 0};for (int i &#61; 0; i <this.people.length; i&#43;&#43;) {
%><%&#61;this.sexRenderer(this.people[i])%>
<%}
%>

好的&#xff0c;现在我们知道可以在<%!%>中定义方法和变量了。但是同时也要了解的是<%!%>已经脱离了service()方法&#xff0c;这就导致不能在它里边使用request&#xff0c;response这些默认变量了&#xff0c;如果想要调用request只能写成void doSomething(HttpServletRequest request)的形式了&#xff0c;稍微注意一下即可。

14.4. jsp九大默认对象

分别是request, response, out, pageContext, session, application, page, config, exception。
让我们看看它们与servlet中变量的对应关系。
首先要明确的是&#xff0c;这九个变量都只在<%%>中有效&#xff0c;<%!%>中是无法调用这九个对象的。实际上<%%>最后会成为service()方法中的代码&#xff0c;我们这里就看看如何在service()方法中获得这些对象吧。
  1. request

    public void service(ServletRequest req, ServletResponse res) {HttpServletRequest request &#61; (HttpServletRequest) req;
    }

    jsp中的request就是service()中传入的req参数&#xff0c;因为service中定义的是ServletRequest类型&#xff0c;我们还需要转换成HttpServletRequest类型。
  2. response

    public void service(ServletRequest req, ServletResponse res) {HttpServletResponse response &#61; (HttpServletResponse) res;
    }

    与上例相同&#xff0c;response也是service()中传入的res参数。
  3. out

    Writer out &#61; response.getWriter();

    out对应着从response中取出的writer对象&#xff0c;负责向响应中输出数据。不过jsp和servlet中的out还是有一点区别&#xff0c;虽然它们都实现了java.io.Writer接口&#xff0c;但servlet中实际类型是java.io.PrintWriter&#xff0c;而jsp中实际类型是javax.servlet.jsp.JspWriter。
  4. pageContext
    这是jsp独有的&#xff0c;servlet里没有page的概念。
  5. session

    HttpSession session &#61; request.getSession();

    直接从request中获得会话。
  6. application

    ServletConext application &#61; getServletConfig().getServletContext();

    可以通过servletConfig获得ServletContext&#xff0c;这是整个web应用共享的一个对象。
  7. page

    Object page &#61; this;

    page就代表当前jsp对象&#xff0c;也可以直接使用this引用。
  8. config

    ServletConfig config &#61; getServletConfig();

    这是在servlet初始化时由服务器传入的对象&#xff0c;可以通过它获得web.xml中定义的初始化参数。
  9. exception
    想在jsp中使用这个对象需要满足一些条件了。
    首先我们要在14-05/index.jsp中故意抛出一个异常。

    <%&#64; page contentType&#61;"text/html; charset&#61;gb2312" errorPage&#61;"error.jsp"%>
    <%String str &#61; null;str.length();
    %>

    str值是null&#xff0c;直接在null上调用length()方法会引发NullPointerException&#xff0c;然后我们可以看到页面第一行使用jsp指令&#xff08;directive&#xff09;设置了errorPage&#61;"error.jsp"&#xff0c;这样在出现异常的时候就会自动forward到error.jsp中。现在看看error.jsp中有些什么。

    <%&#64; page contentType&#61;"text/html; charset&#61;gb2312" isErrorPage&#61;"true"%>
    HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
    http-equiv&#61;"Content-Type" content&#61;"text/html; charset&#61;gb2312" /><title>index</title></head><body><%&#61;exception%></body>
    </html>

    最主要的是在jsp指令&#xff08;directive&#xff09;中设置isErrorPage&#61;"true"&#xff0c;这样我们就可以在jsp中使用exception对象了&#xff0c;实际上这个异常是从request中取出来的。
到此为止&#xff0c;jsp九大默认对象已经讲解完毕&#xff0c;其中常用的还是四个作用域对应的对象&#xff0c;其他的了解即可。