第二篇 为“Servlet核心技术”
第3章 Servl et基础
前两章学习了Web的基础知识、HTTP协议、HTML等知识,见过Servlet和JSP的例子,也学习了一些工具的配置,已经有足够的基础进行下一步的学习了。本章将介绍Servlet的基础知识,
本章内容包括
★ Servlet的基本结构及其常用方法介绍。
★ 了解跟Servlet相关的对象。
★ 进一步学习在Web容器中配置Servlet。
通过本章的学习,读者将明白Servlet的具体含义,知道Servlet的结构及其各个方法的作用。除了第2章介绍的Servlet配置外,还将学习更多在Web容器中配置Servlet的知识。
进入第03章
3.1 Servlet介绍
在前面的章节中,已经了解过什么是Servlet,以及它跟Web容器是如何配合工作的。本节将对这些内容作一个回顾并总结。
3.1.1 什么是Servlet
在第1章的介绍中可以知道,Servlet的意思就是“服务器端程序”,也就是说它是运行在Web容器内的Java程序,在Web程序的调度下工作,用于处理用户请求并生成动态资源。
从语言结构上来看,一个Servlet就是一个Java类,但要求继承自特定的Servlet基类,如图3-1所示。
图3-1 Servlet的继承结构
从图3-1中可以看出最上层是一个抽象类javax.servlet.GenericServlet。虽然就目前而言,Servlet只用于Web服务,但它的目标并不限于此,也希望在FTP、Email等其他服务中使用Servlet技术,因此该类的定义规定了所有使用Servlet技术的服务都必须遵循的接口,不管是Web服务、FTP服务(如果以后使用的话),都必须遵循这里定义的接口。
中间一层是javax.servlet.http.HttpServlet,它实现了GenericServlet中定义的接口,同时也有自己的扩展,因为对于Web服务来说,它使用的是HTTP协议,有HTTP协议的特别之处。HttpServlet的实现一般由Web容器提供,在第2章介绍如何编译Servlet时,就可以看到如何将Tomcat提供的包加入到CLASSPATH中让编译器使用。
前面两层介绍的是Servlet的底层结构,但不必对它过分深究,只需了解即可,这里关心的是上图中最下面一层——自己编写的Servlet。由前面介绍可以知道,Servlet实际只是普通的Java类,但对于使用HTTP协议的Servlet,自己编写的类必须继承HttpServlet,这样Web容器才会承认它。根据需要将重载其中某些方法;除此之外,Servlet并无任何特殊之处。
提示
在本书中,提到的Servlet是专指继承自HttpServlet的子类。因为就目前而言,Servlet都是用于Web服务的,都派生自HttpServlet。很多文献、资料也是这样不加区分地使用这些词语的。
3.1.2 Web容器如何找到Servlet
现在已经揭开Servlet的神秘面纱,知道它只是个符合一定要求的Java类。接下来需要知道,Web容器是如何使用Servlet的。直接回答这个问题较麻烦,先将它分成几个小问题,如下所示。
1 Servlet应该放在哪些地方,使得Web容器可以找到并使用它?
2 浏览器对服务器内部组织一无所知,发送过来的只是URL,服务器如何判断是否需要Servlet来处理?如果需要又如何确定由哪个Servlet来处理?
3 我们知道可以在Web容器中同时部署多个Web程序,但是浏览器不知道这一点,它只知道发送URL。那么Web容器又应该如何确定这个请求该由哪个Web程序负责处理?
在实际中,Web容器是按从(3)到(1)的顺序解决上述这些问题的,其过程如图3-2所示。
图3-2 Web容器根据URL查找Servlet的过程
当接收到来自浏览器的请求时,Web容器首先确定该由哪个Web应用程序来处理该请求。如图3-2所示,这是根据URL中第一个目录的名称,通过查找Web容器的配置文件来决定的,图中第一个目录的名称是“webappname”。
需要注意,这一步的配置与Web容器相关,没有统一标准,在开发时应参考所用Web容器的文档。如Tomcat可以通过对Tomcat安装目录“/conf/”下的文件进行修改来实现。所幸的是很多Web容器都提供了一种简单的默认方法,如Tomcat,如果第一个目录名为“webappname”,则它使用Tomcat安装目录“/webapps/webappname”这个目录下的Web程序来进行处理。目前使用默认方法即可。
提示
这里还有不少细节问题。如在Tomcat中,如果找不到对应的Web程序时它会如何处理?这个问题的回答是,Tomcat将使用默认的程序——也就是放在webapps/ROOT/下面的这个程序来处理。如URL为http://localhost:8080/webappname/others/,但webapps目录下却没有webappname这个Web程序,这时Tomcat将使用默认的Web程序。注意,这时Web程序名为空字符串,而将“/webappname/others/ ”这部分提供给Web程序进行下一步的处理。
Web容器确定处理的Web程序后,还需要根据URL中剩余的部分进一步确定如何处理。其中一个依据就是web程序配置文件——也就是Web程序目录下的/WEB-INF/web.xml文件,这在图3-2中可以看到。对Servlet来说,一般需要在web.xml中设定URL跟Servlet的对应关系,这样当接收到符合该定义的URL时,Web容器将调用相应的Servlet进行处理。
提示
如果在web.xml中没有对如何对应当前的URL进行定义,Web容器将使用默认的处理方式进行。如URL为http://localhost:8080/webappname/dire1/try.jsp,其中“/dire1/try.jsp ”没有在web.xml中指定对应规则,则Web容器默认使用 webappname 这个程序跟目录下的 dire1/try.jsp 这个文件匹配,然后再根据文件的后缀名进行处理。如假设后缀名是 htm、html、jpg等,Web容器自动设置合适的HTTP响应头,并将文件内容返回;如果后缀是 jsp 的,则Web容器执行该JSP——JSP的内容将在第三篇学习。这当然是对支持JSP的容器来说的,如果是只支持ASP的Web容器又有不同的默认规则,它可能只将JSP文件作为普通的文件返回。
在实际开发中,对Servlet一般通过配置web.xml跟URL对应,而其他则一般使用默认方式来进行。
关于Servlet跟URL的对应,第2章介绍了Tomcat的一种简单的配置方法,读者可以重温一下第2章的内容;在本章的第3.4节将继续介绍标准的方法,它是Servlet的标准,对所有Web容器都适用。
如果Web容器确定当前的URL应该由Servlet处理,根据Web程序的配置,它会调用相应的Servlet。由前面内容已经知道,Servlet只是个Java类,因此在配置时,Servlet需要指定它完整的类名——即包含包名的完整名称,如mystudy.MyServlet。Web容器调用这个类的工作跟普通的Java程序一样——如果还没加载,Web容器将在类路径(CLASSPATH)中寻找相应的class文件并加载,之后再调用它。
Web容器在启动时自动将Web程序根目录/WEB-INF/classes/ 作为该Web程序的类路径之一。常见的做法,也是好的习惯,是将所有的Servlet都放在classes目录下,如图3-2所示这样Web容器就可以使用这里的Servlet。当然,将Servlet放到其他地方甚至Web程序目录之外也是可以的,只要所在的位置包含在类路径中即可,但这样带来的混乱将是难以想象的。
3.1.3 Servlet生命周期
现在读者应该已经明白Servlet是什么,也明白了Web容器如何根据请求寻找Servlet进行处理了。但Servlet在Web容器内是怎么样运转的,或者说,Servlet的生命周期是怎样的?本节将探讨这个问题。
如图3-3所示,Servlet首先由Web容器加载,然后才能运行。一般情况下,Web容器在第一次遇到需要由该Servlet处理的请求时,才对它进行加载;但也可以通过web.xml的配置在Web容器启动并装载Web程序时自动加载该Servlet,这些将在第3.4节中的配置中介绍。
图3-3 Servlet的生命周期
Servlet生命周期主要集中在“等待”和“处理请求”这两步。当遇到需要该Servlet处理的请求时,Web容器将自动调用Servlet的特定方法进行处理——这将在下一节介绍。
最后,当Web容器认为不需要该Servlet时,就对它进行卸载。那么该什么时候卸载呢?当Web容器关闭时,一切的Servlet都会被卸载;或者该Servlet已经很长时间没有处理请求了(该时间一般可通
过Web容器相关的配置来指定),为了节省资源,Web容器也会将它卸载。
注意
卸载并不表示该Servlet从此就不能处理请求。如某Servlet因长时间没处理请求,被卸载了;但之后终于来了需要它处理的请求,这时Web容器又会重新加载它。
3.2 Servlet的基本结构
现在已经知道了Servlet在Web容器中运作的生命周期。如果想进一步了解如何控制生命周期各部分的行为,编写出满足自己需要的Servlet,就需要了解Servlet的结构,本节讲述的就是这部分内容。
Web应用使用的是HttpServlet。简单来说,Web容器是通过在合适的时候调用HttpServlet类特定的方法来实现它们之间的合作关系,这些方法构成了Servlet的基本结构。Servlet派生自HttpServlet,根据需要重载特定方法并编写自己的代码,就可以控制Servlet的行为,以实现自己的需求。主要的方法如图3-4所示。
图3-4 Servlet的结构
图中列出的方法由Web容器在合适的时候调用,以下各节将更详细解释这些方法。
3.2.1 init方法
init方法由Web容器在加载时Servlet调用,可以在这里进行初始化工作,以方便后面的处理。如某Servlet频繁地处理请求,但在处理时需要使用数据库或其他资源,如果觉得在每次处理时才分配这些资源效率太低,则可以重载该方法,在加载时首先获取数据库连接和其他资源并保存起来,之后在处理请求时就可以直接使用这些资源了。
注意
有人可能认为,对每个独立的请求,Web容器都构建一个新的Servlet对象来专门处理该请求,因此得出init方法会频繁地被调用的结论。这是不对的,Web容器只会构建唯一一个Servlet对象,但所有的处理都由它进行,因此init方法在Servlet的生存期间“会且仅会”被调用一次。
在图3-4中可以看到,init方法有两个原型,分别如下:
1 void init(ServletConfig config)
2 void init()
这两个方法有什么区别呢?如图3-5所示。
图3-5 加载时Web容器调用init方法
当接收到需要Servlet进行处理的请求时,如果Web容器得知该Servlet还没加载(或者已经卸载了),则Web容器会先加载该Servlet,然后才能将请求交给它处理。
Servlet加载后,Web容器会调用方法(1),让Servlet进行初始化工作。这里有两点需要注意。
首先是传入的参数,这是个ServletConfig对象。通过Web程序描述文件web.xml配置S ervlet时,可以给它设定一些“名:值”形式的参数。Web容器在调用方法(1)时,会将这些参数信息封装在ServletConfig对象中,这样Servlet在初始化或后续处理时就可以参考这些参数信息了。关于ServletConfig对象的使用以及在web.xml中配置参数的方法,本章后面几节会有更详细的说明。
其次,HttpServlet所实现的public void init(ServletConfig config) 中做了两项工作。
★ 保存ServletConfig对象。
在图3-4中可以看到,ServletConfig对象只在方法(1)被调用时传入,其他方法没有。如果希望其他方法也可以访问这个对象,则必须将它保存起来。方法(1)在HttpServlet中的默认实现做了这一步,在其他方法中,可以通过调用public ServletConfig getServletConfig() 这个方法来获得,如:
★ 调用不带参数的方法init()。
方法(1)的默认实现会调用方法(2),也就是不带参数的init()。上面刚讲到,默认的方法(1)会保存ServletConfig,这里希望使用这个功能,因此不应该重载方法(1);但如果确实有一些初始化工作需要在init方法中完成,该如何做?这时可以重载方法(2)。默认的方法(2)是不做任何工作的,因此开发中一般重载方法(2)而保留方法(1),代码如下:
3.2.2 service方法
当需要Servlet处理请求时,Web容器会调用它的service方法,该方法的原型是:
void service(HttpServletRequest req, HttpServletResponse resp)
运作的示意图如图3-6所示。
图3-6 Web容器调用service方法处理请求
从图3-6可以看到,当Web容器需要Servlet处理的请求时,它会调用service(…)方法。
Web容器会传给service方法两个参数,分别是HttpServletRequest、HttpServletResponse的对象。这两个对象是由Web容器创建,且是独立于请求的,也就是说,对每个请求Web容器都会创建两个新对象——这跟使用同一个Servlet对象处理所有的请求是不同的。
HttpServletRequest对象封装了本次请求的相关信息,程序调用它的方法就可以方便地获得这些信息。例如,该对象封装了HTTP请求,只需要调用方法就可以获得HTTP消息头的内容,也调用方法通过名字获取“名=值”形式的参数值——不管是跟在URL的或放在消息体的都可以,而无需自己写代码解释。
HttpServletResponse则封装了响应内容。例如,程序只需调用它的方法指明返回哪些消息头,调用方法返回资源内容即可,该对象自动将这些数据组织成HTTP响应的格式返回。
关于HttpServletRequest、HttpServletResponse的使用,后面几节会简单介绍,更深入的内容则在后续章节讨论。
看到这里,读者可能会想:“那么,只需重载service方法,添加代码就能定制自己的Servlet了。”是的,这种想法是正确的。在下一节还会继续深入讨论这个问题。
3.2.3 doGet、doPost、doXxx方法
从名字,读者可能会猜想它们应该跟HTTP的请求方法有联系。是的,它们跟service方法一样,都是为处理请求而定义的。分出这几个方法的目的是方便Servlet可以对不同的HTTP请求方法做不同的处理。
这些方法的原型都如下:
(void doXXX(HttpServletRequest req, HttpServletResponse resp)
XXX是符号,HttpServlet的定义中,每种HTTP请求都有对应的方法,如最常用的doGet,doPost;此外还有doPut,doDelete,doTrace等。可以看出,除了方法名它们跟service方法的原型是一样的。而这些方法的调用时机如图3-7所示。
图3-7 doXXX方法的调用时机
从图3-7中可以看出,doXXX(…)和service(…)的关系类似于init()和init(…)。Web容器通过调用service方法来通知该Servlet处理请求,而默认的实现(也就是HttpServlet中的实现)是根据HTTP的请求方法来调用对应的doXXX。当请求方法为GET时,service调用doGet;当请求方法为POST时,调用doPOST。同样如果请求方法为PUT,DELETE等,则分别调用doPut,doDelete。不过在实际中,大部分的应用都只用GET和POST,很少使用其他方法。
现在继续讨论上一节最后提出的问题。现在知道,可以重载service方法来处理请求,也可以重载doXXX方法而保留默认的service来处理请求,应该如何选择?
根据上面的讨论知道,Web容器是直接调用service方法的,如果重载了它,则对于跟它相关的所有请求,不管请求方法是什么都会做相同的处理;但如果保留service的默认机制,则会根据请求方法调用对应的doXXX,显得较灵活。
提示
事实上,由于各种各样的原因,处理不同的HTTP请求时总会遇到不少细节问题。如请求时以“名=值”形式附带的参数,处理它们时经常会遇到编码问题。因为它们可以附带在请求消息头的URL后,也可以用POST方法在消息体中发送。
因此,为了扩展的方便,建议一般采用后一种处理方式——保留默认的service方法,重载需要的doXXX。即使确定对所有请求方法其处理方式都一样,但为了将来不可预测的扩展问题,也尽量不要覆盖service,而采用类似下面的方式。public class MyServlet extends HttpServlet public void doGet(HttpServletRequest req, HttpServletResponse resp) ……{……}public void doPost(HttpServletRequest req, HttpServletResponse resp) ……{doGet(req, resp);}}此处是处理请求的代码使用和doGet相同的处理方式
3.2.4 destroy方法
现在来谈destroy方法,它和init方法是相对的。Web容器加载Servlet后会调用init方法,进行数据、资源的初始化等准备工作;而Web容器卸载Servlet时,会调用destroy方法,提供了让Servlet进行资源释放、数据保存的机会。它的原型如下:
void destroy()
如图3-8所示列出了卸载Servlet的过程。
图3-8 卸载Servlet的过程
关于该过程,需要注意以下几点。
★ 什么时候需要卸载Servlet?当Web容器关闭时,所有的Servlet自然会卸载,但很多Web容器都会监测Servlet的使用情况,如果某个Servlet长时间没处理请求(也就是一直处于生命周期中的“等待”状态),Web容器也会卸载它。
★ Web容器在卸载前,必须让Servlet不再处理新的请求。要注意的是,这并非意味着需要该Servlet处理的请求将得不到响应。必须明白,Web容器内每个Servlet都只有一个实例对象在处理请求。对每个请求,Web容器会启动一个新的线程,该线程执行同一个Servlet实例的代码,使用同一个Servlet的成员数据,而不是对每个请求都新建一个对象——读者可能记得在介绍init方法时已提过这一点了。因此所谓的“不再处理新的请求”,是指当前的Servlet实例不再处理请求;但Web容器可以在完成卸载工作后,重新创建新的实例并让它处理请求。当然,新创建的Servlet实例有自己独立的生命周期,跟前面的实例没有太大的联系。
★ Web容器还会尽量让Servlet完成正在处理的请求,但它只会等待一段时间,超时了处理还未完成则会强行终止。之后Web容器调用Servlet的destroy方法进行清理工作。
3.2.5 Servlet结构综合
前面已经学习了Servlet结构,知道了主要的方法及其调用机制,本节将给出一个例子来将这些内容综合起来。由于目前还未学习Servlet相关的对象和数据库操作等知识,因此例子中这方面的操作都用伪代码来代替源代码,但通过这个例子可以从整体上了解Servlet编写的结构,而后续学习的则是对该结构下各部分内容的细化。
这个例子中的Servlet除了处理请求外,还带有统计访问次数的功能,设计如下:
★ 访问次数存放在数据库的特定表中,加载时Servlet从数据库中获取该数值作为初始值并保存在成员变量中,之后处理每个请求时都将该变量加一。在卸载时将该数值保存到数据库。
★ 所用的表名及字段名是固定的,而所用的数据库、登录数据库的名字、密码等内容则在web. xml中通过参数形式配置,可以通过所传入的ServletConfig对象获得这些信息。
该伪代码如图3-9所示。
图3-9 Servlet示例
这个例子虽然简单,但它综合了本节所学的内容。要注意实际中还需要考虑更多的问题,如下:
★ Servlet一般是同时处理多个请求的,而Web容器为每个请求专门建立一个线程来处理,因此上面的代码可能出现多个线程同时增加访问计数的值——记住,只有一个Servlet对象在处理请求,从而只有一个计数值供所有线程共同使用。因此还需要考虑多线程对数据修改的同步问题。
★ 实际中还会遇到不可预测的问题导致Servlet不能正常卸载。如系统突然死机,这时Servlet将计数值保存到数据库的操作将不能正确进行。为此,可能要修改设计,如取消将计数值保存在成员变量的做法,而直接修改数据库中的数据,但如果每次都操作数据库太影响效率,这时又要考虑如何平衡了。
当然这些问题的全面考虑超出了Servlet能控制的范围了。跟许多知识类似,学习Servlet并不太难,但设计严格的程序却并不简单。
3.3 Servlet相关的主要对象
上一节介绍的Servlet主要方法中,有的需要Web容器传入参数。如init方法中传入的ServletConfig对象,service、doXXX方法中传入的HttpServletRequest、HttpServletResponse对象,本节简单介绍这几个对象,它们的作用如图3-10所示。
图3-10 Servlet相关的主要对象
ServletConfig是定义在javax.servlet中的一个接口(interface),包含了Servlet相关的信息,由Web容器在调用init时传入。
ServletConfig定义的方法及作用如图3-11所示。
图3-11 ServletConfig的方法
HttpServletRequest是定义在javax.servlet.http中的接口,它是获取请求信息的核心对象,定义了很多方法,也提供了很强大的功能。但这里只简单介绍几个方法,如图3-12所示。后续章节还有关于这些方法的详细说明,有需要时也可以查阅Servlet的API文档。
图3-12 HttpServletRequest的几个方法
最后来看HttpServletResponse,它是定义在javax.servlet.http中的一个接口。它封装了HTTP返回响应信息的操作,程序只需调用适当的方法设置消息头或传入要返回的内容即可,Web容器会将这些数据按HTTP的格式组织好再返回到浏览器。
该对象是进行响应操作的核心,也定义了很多方便而且功能强大的方法。现在只介绍其中几个,如图3-13所示。后续章节会对它定义的方法作更详细的说明。
图3-13 HttpServletResponse的几个方法
使用HttpServletResponse的方法,最需要注意的是顺序问题,因为HTTP格式要求的顺序是先消息头后消息体,而消息头的响应状态码又必须在第一行,因此调用时最好按先后顺序。读者可能会认为Web容器是收集完所有响应信息和内容后,再一起组织成HTTP格式返回,但实际上一般不是这样的,因为如果对多个同时的请求都这样处理,内存占用量是很可观的;限制缓冲大小虽然在一定程序上缓解这个问题,但假如发送了大量资源内容的数据(数据放在消息体中,数量超过缓冲大小,使得已有数据被返回到浏览器),再返回来设置消息头,这时消息头就会被忽略——这是Servlet规范明确定义的。
一般返回HTML页面的Servlet可以用如下的代码模板。
3.4 Servlet的基本配置
在第2章学习了如何在Tomcat中配置Servlet的一种方法,如果忘记了可以回顾一下。那种方法很方便,只需设置一次即对所有Servlet生效,如果没特殊原因,一般用它来进行调试学习。
但该方法带来的问题也是明显的,可以总结如下:
★ Servlet是跟Web容器相关的,虽然Tomcat可以这样用,但其他Web容器未必就可以这样,这是最大的问题。
★ 它一旦设置就对所有Servlet都生效,但有时只希望部分Servlet能直接被浏览器访问,用这种方式则不够灵活。这里的“直接访问”是指通过URL来直接得到Servlet处理,实际上Servlet标准还规定,可以在处理过程中跳转到其他的Servlet,这些在后续章节中会学习。
事实上,Servlet规范专门为Servlet的配置定义了一种标准方法,本节将进行介绍。它是在Web应用程序描述文件(也就是WEB-INF/web.xml)进行配置的,在任何支持Servlet的Web容器中都能使用。
由于要修改web.xml,因此在正式讲解前,先简单介绍一下web.xml的结构要求。如图3-14所示,它列出了web.xml文件的大致结构。
图3-14 web.xml的标签顺序
web.xml是按照一定要求编写的XML文件。实际应用中很少会从头写起,一般是复制一份模板进行修改,在接下来的相关章节中,读者可以使用Tomcat安装目录下的/webapps/ROOT/WEB-INF/web.xml文件作为模板。
关于web.xml各标签元素的说明,后续章节会逐步介绍。现在需要注意以下几点。
★ 如图3-14所示,<!DOCTYPE…>标签中有两个数字表示web.xml文件的版本。因为随着Servlet及JSP技术的发展,可能需要越来越多的标签以提供更多的配置项,也可能为了方便而改变格式。Web容器会根据指定的版本号来检查内容是否符合要求,如果使用了新版本的写法,但却提供旧的版本号,Web容器将不能正确识别。图3-14列出的是版本2.3的大致结构。
★ web.xml的所有配置项都包含在<web-app>…</web-app>这对标签下。
★ 所有标签都是可选的,也就是说可以根据自己的需要而提供部分配置的标签。各标签都有自己的格式要求,如可以配置哪些属性、可以包含哪些子标签等。图3-14中只是列出了示意图,并没有详细列出各标签的属性、可包含子标签等内容。
★ 在2.3或之前的版本中,标签必须严格按照图3-14列出的顺序排列。例如,在web.xml中提供了<description>和<servlet>这两个标签,则<servlet>…</servlet>必须跟在<description>…</description>后面,否则Web容器检查格式时会报错。读者在web.xml增加标签时,可参考该图的顺序。
本节中介绍的Servlet配置跟<servlet>和<servlet-mapping>这两个标签有关。<servlet>标签定义了Servlet的“名字”与“类名”的对应关系,而<servlet-mapping>则提供了URL跟“名字”的对应关系。也就是说,URL和Servlet是通过“名字”这个“中间人”来联系的,其示意关系如图3-15所示。
图3-15 URL和Servlet class通过“名字”联系
3.4.1 Servlet的名字及路径配置
这一节介绍<servlet>标签如何提供“名字”并将它跟实际的class联系。在web.xml中可以通过如下方式实现。
<servlet-name>定义了Servlet名字。其他和Servlet相关的标签都是通过这个“名字”来关联的,这样在改变Servlet的类名时也不影响这些标签。Servlet的init方法中会传入一个ServletConfig对象,调用它的getServletName()方法,返回的就是这个名字。
<servlet-class>则指定该名字关联的完整类名——包含package、class的完整名称。这和Java源文件中“import”指定完整类名的方式是一样的,Web容器(实际上是运行Web容器的Java虚拟机)会在类路径(CLASSPATH)中寻找它的class文件,Web程序下的WEB-INF/classes目录正是路径之一,如果忘记了可回顾第3.1.2节——Web容器如何找到Servlet。
3.4.2 初始化参数
Servlet被初始化时,Web容器调用init方法并传入一个ServletConfig对象作为参数,通过调用该对象的getInitParameter(String name)方法,可以获取为该Servlet设置的参数。这些参数在web.xml中的设置方法示例如下:
如例中所示,参数是在<servlet>标签内,用<init-param>标签进行定义。<init-param>必须跟在<servlet-class>后。每个<init-param>标签定义一个参数,用<param-name>定义名字,用<param-value>定义参数值;多个参数则需要多个<init-param>标签。
示例定义了两个参数,如用“名=值”的方式写出分别是“dbUserName=admin”、“dbPassword=secret”。在该Servlet中则可以获取该参数,如下:
3.4.3 启动装入优先级
Servlet一般是第一次遇到请求时加载并初始化的。但如果初始化要做的工作很多,如Servlet需要使用大量在运行过程中不变的常数,而这些常数零散地存储在多个数据库中,在初始化时就需要从多个数据库分别获取这些数据。如果这个过程耗费的时间较多,如十几秒,这样第一个请求该Servlet的用户就要等待相当长的时间才能得到响应,这就显得不够友好了。
所以希望这样的Servlet能在第一次请求前就已经进行了初始化。Servlet标准考虑到这点需求并定义了相应地规则,只要在<servlet>标签里提供了子标签<load-on-startup>,Web程序启动时就会同时加载并初始化这个Servlet。配置的例子如下:
<load-on-startup>标签中还可以包含一个数字,表示启动时初始化的顺序。数字越小则越早进行初始化,而没有指定的则总是在最后,如下:
现在写几个Servlet来验证以上讨论的内容。该Servlet很简单,只是在初始化时往标准输出流输出一个字符串。在Tomcat中,标准输出流的数据显示在console窗口中,可以直接看到结果。而其他的Web容器可能会重定向到特定的文件,如果读者使用的不是Tomcat,应注意参考文档看看数据被送到哪里去。
这里定义的三个类分别是FirstServlet、SecondServlet、LastServlet,代码及web.xml的配置如图3-16所示。
图3-16 测试启动时Servlet初始化顺序的代码和配置
编译Java文件,将class文件放在Web程序目录/WEB-INF/classes/com/cxpub/ 下;按图中配置文件修改web.xml,注意标签是有顺序要求的,读者可参考图3-14所示列出的顺序进行修改。
启动(或重启)Tomcat,窗口会显示启动时输出的信息,如图3-17所示。
图3-17 启动时Servlet初始化的输出信息
如图3-17所示,信息显示的先后为“1st… 2nd… last…”,读者对照图3-16中的配置,可以发现<load-on-startup>中数字小的确实先加载,而没指定数字的则排到最后。图3-17中最下面是Tomcat显示启动完成需要的时间,说明这些Servlet确实是在加载Web程序时(Tomcat启动时自动加载所有Web程序)进行初始化的。
3.4.4 URL到Servlet的映射
到现在为止,相信读者已经基本掌握了Web容器和Servlet的协作关系了,知道Web容器在什么时候调用Servlet的什么方法,知道怎样通过配置使Servlet初始化时获得参数……但还缺少最关键的一步,就是怎样将URL跟处理请求的Servlet对应起来。本节专门讨论这个问题,解决后,从浏览器发送URL、Web容器查找Servlet、Servlet处理并返回响应这一系列的步骤,读者都可以有个完整的认识了。
URL到Servlet的映射,是指定义一个“URL模式”和指定一个Servlet,使得当浏览器请求的URL符合这个模式时,Web容器会调用指定的Servlet来处理请求。这个映射关系在web.xml中用<servlet-mapping>标签来定义的,运作过程如图3-18所示。
图3-18 URL和Servlet映射的运作示例
如图3-18所示,<servlet-mapping>必须在<servlet>标签后给出。<servlet>里面按顺序包含<servlet-name>和<url-pattern>这两个标签。
<servlet-name>指定所关联的Servlet的名字,而该名字必须在之前提供的<servlet>标签中出现。
<url-pattern>这个标签给出需要该Servlet处理时,URL必须匹配的模式。这里需要注意如下内容。
★ URL模式匹配的是相对于Web程序目录的URL,而不是浏览器发送过来的整个URL。如图3-18所示,Web容器会“抽取”第一个目录名用于决定处理该请求的Web程序,剩余的部分(第一个目录名后,但不包括参数)才由被选中的Web程序进行URL匹配。图中,剩余部分是 “/servlet”,刚好和<servlet-mapping>指定的模式完全匹配。要注意的是,匹配过程对大小写敏感。
★ 本例中<url-pattern>给出的是完整的URL,但实际上还有很多灵活的写法。Servlet标准定义了如下的匹配方式。
1 “/目录/*”方式,以斜杠(/)开头,斜杠加通配符(/*)结尾。在这种模式中,以“/目录”作为开头的URL的都可以匹配,注意地址不需要包含通配符前的斜杠。如对于 /servlet/*,/servlet、 /servlet/Example都能匹配。
2 “*.ext”方式,以通配符加点号(*.)开头,后缀名结尾,表示以.ext为结尾的地址都可以匹配。例如对于 *.jsp,则 /a.jsp、/dict/dict1/b.jsp都可以匹配。
3 “/”方式,仅包含一个斜杠(/),表示默认的匹配方式。当找不到其他匹配项时,Web容器会提供自己的默认处理方式,一般情况下不定义这种类型的URL模式。
4 其他方式,用于完全匹配。只有当URL和模式完全一样时才匹配。如对于/mydict/,/mydict/sub、/mydict都不匹配,因为它们不完全相同。
★ 如果web.xml提供了多个<servlet-mapping>,则URL使用最接近的匹配。Servlet规范定义了匹配的顺序,一旦匹配成功则停止,其匹配顺序如下:
1 按完全匹配的方式比较。
2 按<servlet-mapping>的定义顺序查找前缀最长的匹配。例如定义了两个模式为 /dict/longer/*、 /dict/*,而URL为 /dict/longer/class.ext,则匹配前一个。
3 按后缀查找匹配。例如在(2)中如果没有匹配,则看是否有定义 *.ext这样的模式,有的话就和它匹配。
4 当以上方式都无法匹配时,将使用默认的处理方式。Web容器一般将根据Web程序根目录/对应URL的文件 来决定如何处理。如请求的URL为http://localhost:8080/chpt3/nowhere/none.html时,“/nowhere/none.html”找不到匹配项,则Web容器尝试查找“Web程序根目录/nowhere/none.html”这个文件并作后续处理。但如果定义了默认的匹配项——仅有斜杠的方式,就不会使用Web容器的默认方式,因此使用时要注意。
尝试
第2章中介绍过Tomcat有一种方便访问Servlet的方法,请读者尝试理解这种的配置的大概过程,它是如何通过 /servlet/class-full-name 这种方式就可以访问所有Servlet的?提示:它是由Tomcat自带的Servlet来处理并调用Web程序里面的Servlet。Tomcat安装目录下的conf/web. xml文件的内容会和所有Web程序的web.xml合并,而 common/classes/、common/lib/下的所有.jar 文件也加入到所有Web程序的类路径中。
3.5 开发部署一个简单的Servlet
通过前面几节的学习,现在读者应该已经掌握了Servlet的基本知识,简单总结如下:
★ Web应用的Servlet是个继承javax.servlet.http.HttpServlet的Java类,需放在Web程序的类路径中,一般放在Web程序根目录/WEB-INF/classes/ 下。
★ Web容器会在初始化、通知处理请求、卸载时调用Servlet的init,service,destroy方法,而这些方法默认的实现又会调用其他特定的方法。
★ 可以通过web.xml配置给Servlet指定“Servlet名”、设置参数,这些信息可通过初始化时Web容器传入的ServletConfig对象获得。
★ Servlet是通过HttpServletRequest对象获取请求信息,通过HttpServletResponse对象返回响应信息。这由Web容器在调用service方法时传入,而service调用其他方法时也传入这些对象。
★ 可在web.xml文件中配置URL和Servlet的映射关系,浏览器请求发送的URL符合指定的模式则由对应的Servlet处理。
本节通过一个简单的servlet实例来综合这些知识,简单设计如下:
★ 完整类名是com.cxpub.chpt3.ExamServlet;
★ 在web.xml文件对这个Servlet配置,名为“my example servlet”,同时提供两个初始参数,其“名:值”对方式是“param1:1st”、“param2:2nd”;
★ 可以处理GET、POST方式的请求,处理时会获取浏览器发送的参数,参数名为“input”;
★ Servlet返回一个HTML文档,文档中显示它配置的Servlet名、两个初始参数,还有浏览器发送的参数input。
Servlet的代码如图3-19所示。
图3-19 ExamServlet的代码
编译这个文件,可以直接使用命令行方式调用javac编译,但要先将Tomcat的Servlet包加到CLASSPATH中,调用方法如下:
将编译好的ExamServlet.class文件放到“程序目录/WEB-INF/classes/com/cxpub/chpt3/”下(注意源文件中的package com.cxpub.chpt3,放置的目录要跟它一致)。
打开WEB-INF/web.xml文件,在合适的位置加入如图3-20所示的配置代码。可以在Tomcat自带的Web程序中复制一份过来修改,只需按图3-14要求的顺序将这些内容插入到适当的位置即可。
图3-20 web.xml的配置片段
在配置中将URL模式设置为“/ExamServlet”,假如Web程序放在mystudy目录下,则可通过http://localhost:8080/mystudy/ExamServlet来访问该Servlet。
要做的工作就这么多,现在来看结果。假如Web程序放在为mystudy目录下,则应在浏览器中输入如下URL。
图3-21 测试例子的显示结果
现在在附加参数中输入HTML的特殊字符,如“input=<b>good</b>”,这时的情况如图3-22所示。
图3-22 输入带HTML保留字符参数的结果
可以看到,“<b>good</b>”这个参数没有正确显示。看一下浏览器得到的HTML文档就知道原因了,如图3-23所示。
图3-23 图3-22显示页面的源文件
从图3-23可以看到,虽然Servlet获取和输出参数都没问题,但输出内容包含的HTML标签被浏览器解释了。为了保证正确性,应该在输出数据前,先将HTML的保留字符用其他表达方式替换,如下:
< -> < > -> > & -> & " -> " 空格->  ;
事实上,不少设计较差的网站经常因为这种情况出现问题,比如一些论坛,如果用户的主题内容包含像“<”这类字符,而网站输出时没有转换,则显示的内容就可能会混乱。
注意
在参数中使用中文字符,会出现如图3-24所示的乱码。这次确实是Servlet获取参数过程出现问题了,和文字编码有关。下一章将会详细讨论这个问题。
图3-24 中文参数引起乱码
上面测试结果时,都是用和web.xml定义的模式匹配的URL来访问的。但在Tomcat中可以通过设置,使用“/Servlet/完整类名”来访问servlet的方式,如果用这种方法来访问本例中的Servlet,在Tomcat 5.5里测试结果如图3-25所示。
图3-25 使用Tomcat的特殊方式访问Servlet
可以看到,web.xml中的配置完全没有反映出来。从中可以明白,Tomcat这种特殊方法虽然方便,但却并不能代替所有的Servlet应用,毕竟它只是与Web容器相关的功能,并不是J2EE的标准。一般情况下,只能用它来方便进行常用功能的调试,而对特殊功能,或者在实际Web应用中,应该按照J2EE的标准方法,在web.xml中配置Servlet信息和定义URL模式,而浏览器则使用匹配模式的URL来访问Servlet。当然,如果使用的是Tomcat,部署实际Web应用时还要注意禁止使用特殊方式访问Servlet,避免让Servlet留给外界一个“后门”。
提示
在Tomcat下用特殊方式访问Servlet时,Web容器实际另外创建了一个Servlet实例,而不是使用之前已经存在的对象,因此它使用了另外的ServletConfig对象,从而输出的信息和在web.xml中配置的不同。这种做法违背了“Web容器只有一个Servlet实例处理请求的原则,毕竟它不是J2EE的标准,因此实际部署时应严格禁止这个配置。
本书后续与Servlet有关的内容,如无特殊说明均可使用用Tomcat提供的这种方便形式。在特殊情况下会有专门说明,所以不必担心后面学习时要一直循规蹈矩地做这些麻烦的web.xml文件配置。
3.6 小结
本章学习了Servlet的结构和标准配置,现在简单总结一下。
Servlet的init方法在Servlet加载时被调用,该方法有public void init(ServletConfig config)和public void init()两种形式,实际中一般只覆盖不带参数的形式,在其中进行初始化工作,如分配资源、获取后续处理需要的数据等。
Web容器是调用Servlet的Service方法来进行处理的,默认的做法是根据请求方法调用相应的doXXX方法。一般不覆盖service方法,而在doGet,doPost,doXXX中定制自己的功能。
destroy方法在卸载时被调用,应该在这里做好一切善后工作,如清除和释放init方法中占有的资源,保存需要的数据等。
Servlet的配置以及与URL的映射,是在Web程序描述文件web.xml中通过<servlet>和<servlet-mapping>这两个标签实现的,这两个标签通过“Servlet名”联系起来。
<servlet>中可以按顺序包含定义名字的<servlet-name>,指定完整类名的<servlet-class>,提供初始参数的<init-param>和设定启动时自动加载的<load-on-startup>这4个标签。其中<init-param>可以出现多次,每个标签定义一个参数,它顺序包含<param-name>、<param-value>这两个标签,分别定义参数名和参数值。
<servlet-mapping>中顺序包含<servlet-name>、<url-pattern>两个标签。<servlet-name>指定关联的Servlet名字,它必须在之前的<servlet>中定义;<url-pattern>指定了相对Web程序起始路径的URL模式,任何符合该模式的URL都将由指定的Servlet进行处理。