第 6 章 贴近servlet

注意

仅仅使用jsp做项目是难以想像的。难以维护,难以调试,难以分层设计,难以统一管理。现在我们要从jsp跨入servlet领域,从而得到更多的功能。因为从这里开始将进入中级部分,只想随便玩玩jsp的朋友可以到此为止了,期望以此为生的朋友请继续。

如果你不满足以下任一条件,请继续阅读,否则请跳过此后的部分,进入下一章:第 7 章 使用filter过滤请求

  1. 了解servlet。

  2. 了解servlet与jsp的关系。

6.1. servlet是什么

sun公司给自己产品定的名称都十分诡异,最早听说有applet,是Application let(小应用程序)的意思。在jsp中写的java代码叫做scriptlet,Script let(小脚本程序)。现在又有servlet,Server let(小服务程序)。

简单来说,servlet就是运行在服务器上的小程序。

让我们通过一个简单示例来了解servlet是如何开发和运作的,把lingo-sample/06-01目录复制到tomcat的webapps目录下,启动服务器并访问http://localhost:8080/06-01/,会看到如下效果。

页面效果非常简单,只是显示“hello”这个单词。为了让servlet实现这个效果,我们需要进行以下步骤。

  1. 写一个名叫HelloServlet.java的类,编译成class放到WEB-INF/classes下。

    记得要建立与包名对应的目录结构,比如package anni;就对应着WEB-INF/classes/anni/,因为需要引用javax.servlet包下的类,编译时要将tomcat的common/lib目录下的servlet-api.jar加入classpath,具体编译命令可参考lingo-sample/06-01/WEB-INF/src/compile.bat。

    package anni;
    
    import java.io.IOException;
    import java.io.PrintWriter;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    public class HelloServlet extends HttpServlet {
    
        public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
    
            PrintWriter out = response.getWriter();
            out.println("hello");
    
        }
    
    }
                    

    HelloServlet中实现的功能很简单,在doGet()方法中先取得response(响应)的输出流,再向里面写入“hello”。

  2. 修改WEB-INF目录下web.xml,添加HelloServlet的配置,让它负责处理路径是http://localhost:8080/06-01/的请求。

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://java.sun.com/xml/ns/j2ee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
        version="2.4">
    
        <servlet>
            <servlet-name>HelloServlet</servlet-name>
            <servlet-class>anni.HelloServlet</servlet-class>
        </servlet>
    
        <servlet-mapping>
            <servlet-name>HelloServlet</servlet-name>
            <url-pattern>/</url-pattern>
        </servlet-mapping>
    
    </web-app>
                    

    只要注意servlet和servlet-mapping两部分就可以了。

    1. servlet标签中定义一个了名字叫做HelloServlet的servlet。

      这个HelloServlet对应的class是anni.HelloServlet,请注意这里的class要写全名。

    2. servlet-mapping标签则是把刚刚定义的HelloServlet映射到“/”这个请求路径上。

      请回忆我们之前讲过的 第 3.4.1 节 “绝对路径与相对路径” 中的部分,这里的“/”就代表当前应用的根路径,既http://localhost:8080/06-01/,凡是有客户对此路径发出请求,就会交由HelloServlet处理。

例子在lingo-sample/06-01目录下,将整个目录放入tomcat的webapps即可使用。对应的源代码放在lingo-sample/06-01/WEB-INF/src目录下,要放到tomcat的webapps目录下,才能使用compile.bat脚本进行编译。

6.2. jsp与servlet的关系

通过上面的例子,我们可以看到servlet与jsp的功能差不多,都是处理请求并返回响应。甚至在操作时使用到的变量都极为相似,request,response,out这些在我们使用jsp的时候也遇到过。不同的时是,jsp中这些变量拿来即用,servlet中还要通过参数来获得。

jsp和servlet如此相似,它们之间难道有什么联系吗?为什么sun创造了两种功能相似的东西呢?

其实jsp就是servlet,你所写的jsp页面最终都会被服务器转换成servlet。为了验证这一说法,我们编写一个效果与HelloServlet完全一样的hello.jsp,它的内容就只有这么点儿。

hello
        

访问http://localhost:8080/06-01/hello.jsp,会看到与刚刚一样的效果。

现在打开tomcat的work目录。

在Catalina/localhost/06-01/org/apache/jsp目录下,可以看到两个文件,分别是hello_jsp.class和hello_jsp.java。

打开hello_jsp.java可以看到里边的源代码,我们只挑出其中一部分来看。

out = pageContext.getOut();
_jspx_out = out;

out.write("hello");
        

获得pageContext的输出流并将"hello"写入,于是浏览器上就看到了hello的字样。

是的,这里的hello_jsp.java就是由hello.jsp生成的。服务器在获得请求的时候会先根据jsp页面生成一个java文件,然后使用jdk的编译器将此文件编译,最后运行得到的class文件处理用户的请求返回响应。如果再有请求访问这jsp页面,服务器会先检查jsp文件是否被修改过,如果被修改过,则重新生成java重新编译,如果没有,就直接运行上次得到的class。

为什么第一次访问jsp的时候速度会那么慢?就是因为要经过生成java和编译class的步骤。以后再次访问同一页面就会感觉到速度明显变快,也是因为class文件已经生成的原因。

为什么jsp要经过这些步骤转换成servlet再去执行呢?因为java起初做网站的时候就只有servlet可以使用,为此还专门指定了一套servlet标准,就是我们在代码中看到的javax.servlet包下的类。但是人们马上就发现,使用servlet显示复杂页面太费力气了,使用servlet里的输出方式简直让人写到手抽筋,于是就有了仿效asp和php的jsp出现,开发人员可以在美工做好的页面上直接嵌入代码,然后让服务器将jsp转换成servlet执行。

有的朋友可能迷糊了,既然jsp是为了简化servlet开发,那么为什么我们现在又要去学习servlet?既然servlet那么麻烦为什么不直接使用jsp就好了呢?

这是因为jsp虽然比servlet灵活,却容易出错,你找不到良好的方式来测试jsp中代码,尤其在需要进行复杂的业务逻辑时,这一点儿很可能成为致命伤。所以一般都不允许在jsp里出现业务操作有关的代码,从这点来看,我们上一章中举的例子就严重违反了这一标准,CRUD的操作都写在了jsp这种,一旦出现问题就会让维护人员头大如斗。

servlet是一个java类,需要编译之后才能使用,虽然显示页面的时候会让人头疼,不过在进行业务操作和数据运算方面就比jsp稳健太多了。因此我们就要结合两者的优点,在servlet进行业务操作和请求转发,jsp全面负责页面显示,这也是目前公司企业里常用的开发方式。

6.3. 使用servlet改写联系簿

既然jsp就是servlet,jsp中的那些功能在servlet中也就都可以实现,当然咱们不能再使用jsp指令(directive)和jsp动作(action)了,不过它们也都有替代方法,我们以后慢慢介绍。

现在我们使用servlet改写第五章中联系簿的例子,将CRUD操作都转移到servlet中,让jsp只负责页面显示。

新建一个ContactServlet.java,让它负责处理那些CRUD操作,它会直接引用ContactDao操作数据,现在我们可以把jsp中对ContactDao的引用删除了,所有数据都将由ContactServlet提供,现在jsp只管从request里取出数据显示出来即可。

为了让ContactServlet起作用,在web.xml中添加处理请求的配置。

<servlet>
    <servlet-name>ContactServlet</servlet-name>
    <servlet-class>anni.ContactServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>ContactServlet</servlet-name>
    <url-pattern>/contact.do</url-pattern>
</servlet-mapping>
        

servlet标签指定使用anni.ContactServlet,servlet-mapping将它绑定到/contact.do请求上,当我们看到浏览器上出现http://localhost:8080/06-02/contact.do的时候,就表明ContactServlet在起作用了。

请别06-02目录下去找contact.do这个文件,它是不存在的。与之前提到的forward()情况类似,虽然浏览器指定要contact.do这个资源,但服务器暗地里把这个请求交给ContactServlet处理,你可以把contact.do当作一个地址,实际上你要找的人是ContactServlet。

不管怎样,只要是对contact.do发起的请求,最后都是由ContactServlet处理。我们依然继承HttpServlet,不过这次实现两个方法doGet()和doPost()分别处理http的GET和POST方式的请求。使用GET方式的请求会交由doGet()方法处理,使用POST方法的请求会交给doPost()处理,这些都是由HttpServlet控制的,我们可以直接使用。

/**
 * 处理get请求.
 */
public void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {

    this.process(request, response);
}

/**
 * 处理post请求
 */
public void doPost(HttpServletRequest request,HttpServletResponse response)
    throws ServletException, IOException {

    this.process(request, response);
}
        

为了方便,我们把GET和POST请求都交给process()方法处理,在process()中根据不同的请求进行不同的操作。

/**
 * 处理请求.
 */
public void process(HttpServletRequest request,HttpServletResponse response)
    throws ServletException, IOException {

    request.setCharacterEncoding("gb2312");

    String method = request.getParameter("method");
    if (method == null) {
        method = "list";
    }

    try {
        // 分发请求
        if ("list".equals(method)) {
            this.list(request, response);
        } else if ("save".equals(method)) {
            this.save(request, response);
        } else if ("edit".equals(method)) {
            this.edit(request, response);
        } else if ("update".equals(method)) {
            this.update(request, response);
        } else if ("remove".equals(method)) {
            this.remove(request, response);
        }
    } catch(Exception ex) {
        System.err.println(ex);
    }
}
        

为了解决中文编码问题,记得先要给request设置gb2312编码格式,第二步就属于我们自己的设计方案了,为了分辨不同的操作,我们为每个请求都附加一个method参数,默认下是method等于list,显示所有联系信息的列表。

一大堆的if else虽然比较丑,但流程非常直观,根据method的值执行对应的方法。

method=list的情况。

默认的索引页面index.jsp中,将list.jsp改成contact.do?method=list,把请求转发到contact.do顺便再带上操作参数。ContactServlet里的list()方法如下。

/**
 * 显示联系信息列表.
 */
public void list(HttpServletRequest request,HttpServletResponse response)
    throws Exception {

    List list = contactDao.getAll();
    request.setAttribute("list", list);

    request.getRequestDispatcher("/list.jsp").forward(request, response);
}
        

调用contactDao的getAll()方法获得联系信息列表,然后把list放到request里,因为pageContext是与jsp页面对应的,servlet里要把变量放到request作用域里,保证使用forward转发请求之后在jsp里也可以使用这个变量。

进行操作之后即刻使用forward跳转到list.jsp,记得这里要用forward才能保证request中的变量不会消失。这样依赖list.jsp中改成从request中获得我们需要的数据。

List list = (List) request.getAttribute("list");
        

其他的操作都与这个类似,比如把save.jsp改成contact.do?method=save,把edit.jsp?id=1改成contact.do?method=edit&id=1,把update.jsp?id=1改成contact.do?method=update&id=1,把remove.jsp?id=1改成contact.do?method=remove&id=1。

经过一轮改造,原来的save.jsp, update.jsp, remove.jsp的内容都归入了ContactServlet。06-02目录下只剩下index.jsp, list.jsp, create.jsp, edit.jsp四个页面,看页面中的链接全部指向了contact.do,这下是不是觉得清爽多了?

ContactServlet作为统一的请求转发器发挥着强大的作用,基本所有的请求都是由它接收并中转的,正因为有它的存在,我们才得以把进行数据库操作的ContactDao与表现层的jsp隔离开,让处理业务的代码更加集中。

这种分层方式通常被称为MVC,Model View Controller三层结构。请求由Controller(控制器)开始,分发给对应业务代码,Model(模型)代表的数据模型承担业务操作,最后将得到的结果送到View(视图)层渲染显示。

这里ContactServlet对应的就是Controller(控制器),用来做请求的分发。Model(模型)就值得ContactDao和数据库了,它提供我们需要的各种数据信息。几个jsp构成了View(视图)这一层,用来显示结果数据。

完整的例子在lingo-sample/06-02下,对应的源代码在WEB-INF/src下,将06-02复制到tomcat的webapps下就可以执行compile.bat进行编译。