第3章Servlet基础 视频讲解 3.1Servlet和JSP Web服务器接收到HTTP请求,处理完毕后会向客户端返回一个HTTP响应。Web服务器接收客户端的请求有两种: 一种是静态页面请求,客户端请求的页面中没有动态的内容需要处理,这些静态的页面直接作为响应返回。此时只需要能够解析HTTP的Web服务器(如Nginx、Apache、IIS等)即可。第二种是动态请求,客户端所请求的页面,需要在服务器端委托给一些应用程序进行处理,从而形成动态页面,最后作为HTTP响应返回。此时需要服务器不仅能处理HTTP,还需要具备处理这些动态请求的能力,这种服务器称作Web应用服务器。之前示例中使用的Tomcat,就是能够处理Servlet以及JSP动态页面的服务器或者称之为Java Web容器。 Servlet以及JSP页面都是运行在服务器上的程序,并能生成动态的内容返回客户端,那么二者有什么联系和区别呢? 从技术产生的先后顺序看,Servlet技术在前,JSP技术在后。在早期的Web应用系统中,动态请求是由Web服务器转发给CGI(Common Gateway Interface,公共网关接口)程序进行处理的,CGI处理完毕后将结果拼接成HTML格式的文档,并返还给Web服务器,再通过Web服务器将响应返回给用户。CGI程序一般由C、C++、Perl或者其他脚本语言编写,但对于每一个客户端的请求,CGI都开启一个新的进程进行处理,对于服务器而言负担较重,执行效率低。因此Sun公司推出了基于Java的Servlet技术,Servlet本质上来说是一个Java类,可以运行在Tomcat这样的容器中。对于用户的请求,Servlet以线程的形式进行处理,执行效率更高,同时功能更为强大,对于HTML请求数据的提取和处理、会话跟踪、Cookie设置等都有对应的方法。 Servlet虽然在处理请求上非常方便,但是对于响应结果的显示却仍然采用CGI的方法,通过代码打印输出的方式去拼接HTML文档,导致如果想生成较为复杂的页面,代码量将急剧增加,同时也不便于页面整体效果的展示。 因此,Sun公司提出了JSP技术,采用HTML模板+嵌入Java代码以及标签的形式,能够简化响应页面输出的代码量,不过JSP的底层实现仍是基于Servlet。在项目Chapt_01中,已经编写过第一个JSP页面,即index.jsp。当项目运行时,第一次访问index.jsp页面后,该JSP页面编译为对应的Servlet类,如 图31所示。Servlet类存放在Tomcat服务器的work目录下,路径为\apachetomcat9.0.33\work\Catalina\localhost\Chapt_01\org\apache\jsp。可以发现index.jsp已经被转化为一个Java类,同时也生成了对应class字节码文件,若再访问index.jsp页面,则直接读取字节码文件即可。 图31JSP页面编译为对应的Servlet类 JSP页面通过模板的形式方便了页面内容的输出,但如果JSP页面中混杂了过多的Java代码,将处理业务逻辑的部分都放在页面中,同样导致了代码量过大,且不利于开发人员编写和维护。因此由于两种技术各有其长处,JSP技术的出现并没有取代Servlet,二者可以并存合作,在开发中发挥各自的优势。 由于Servlet更偏向于底层的实现,因此本书先讲解Servlet技术的原理,然后再介绍JSP的使用。关于Servlet和JSP在具体开发中的使用场景,在后续章节中还会继续讨论。 3.2Tomcat服务器原理 在学习Servlet之前,先了解作为容器的Tomcat服务器的工作原理。 视频讲解 3.2.1Tomcat体系结构 Tomcat是基于组件的Web应用服务器,在2.1.2小节介绍了Tomcat服务器的目录结构,在安装目录下的conf文件夹中,server.xml文件是整个Tomcat服务器的配置文件。该配置文件给出了整个Tomcat服务器中各组件的设置,每个组件作为XML文件中的标签元素(为了方便讲解,只列出了主要的组件节点),大致结构如下: 下面介绍Tomcat服务器中重要组件的作用以及相互之间的关系。 配置文件中的根节点是Server,代表顶级服务器。该节点包含了port="8005" shutdown="SHUTDOWN"两个属性,表示服务器通过8005端口号监听和关闭Tomcat服务器的请求。Server节点包含若干个Listener、GlobaNamingResources和Service等子节点。 (1) Listener节点。该节点表示服务器运行时状态监听的配置,主要监听服务器是否会内存泄露、线程安全以及日志等信息。 (2) GlobaNamingResources节点。该节点表示全局资源的配置,比如指定Tomcat服务器用户信息,这些信息存放在conf目录下的tomcatusers.xml文件中。 (3) Service节点。该节点表示对外提供的应用服务,至少存在一个默认名称为Catalina的Service节点。Service节点又包含若干个Connector和一个Engine组件。 ① Connector: Tomcat服务器的核心组件。其是负责客户端交互的连接器组件,负责接收用户请求并交给Engine组件处理,以及将处理完毕后的响应返还给客户。可以有多个Connector,并设置该Connector来接收客户请求的端口号(如默认的8080),采用的HTTP版本,以及HTTPS转发端口号等。 ② Engine: Tomcat服务器的核心组件。其是负责处理用户请求的组件,有defaultHost和name两个属性值,其中defaultHost表示默认的虚拟主机名称。该组件下又包含若干Host元素和Realm元素,至少有一个Host元素的name属性和Engine的defaultHost值对应。在Host元素下的Context docBase 元素则定义了一个实际的Web项目。Relam元素则用于安全管理的配置,一般与tomcatuesrs.xml配合使用。 对于Server、Listener、GlobaNamingResources等元素,如果没有特殊需要,一般不需要修改其默认配置,以免影响服务器的正常运行。 视频讲解 3.2.2Tomcat核心组件 Tomcat可以根据需求,通过设置不同的监听端口配置多个Connector,当连接器指定的端口号监听到客户端发送过来的TCP请求后,将分别创建一个request和response对象,然后新建一个线程,将request和response对象传送给Engine组件,并等待处理结果,获得响应后,将响应返还给客户端。 Engine组件可以指定多个虚拟主机Host组件,Host可以配置以下3个属性。 (1) appBase属性。Web项目的部署路径,在2.1.2节中设置在webapps路径下。 (2) autoDeploy属性。项目是否自动部署,取值为ture或者false,ture表示自动。 (3) name属性。虚拟主机名称,取值localhost表示本机,刚好对应Engine元素的defaultHost的取值。 在Host组件下又可以具体指定Context组件,实际上对应着已经在Tomcat服务器下运行的Web项目。每当有新的项目部署到服务器时,都会在Host组件下生成一个新的Context元素进行配置。例如: 说明: docBase属性设置了Chapt_01项目的根目录; path属性表示项目访问的路径,即http://localhost:8080/Chatp_01/xxx。reloadable=true,表示服务器会检测项目文件的变动,Tomcat服务器在运行状态下会监视WEBINF/classes和WEBINF/lib目录下class文件的改动,如果监测到class文件有变动,服务器会自动重新加载Web应用。 Context组件实际上就是运行Servlet的基础容器,当用户访问该Web应用项目时,所有的请求都需要到该Context环境(即该项目)下去寻找对应的Servlet类去处理。 3.3Servlet的编写 3.3.1Servlet的创建 视频讲解 在Eclipse中新建一个名为Chapt_03的动态Web项目,由于Servlet是一个Java类,所以需要在项目的src目录下建立,因此需在src目录下新建一个com.test.servlet的包。在Eclipse中可以通过模板来创建Servlet。Servlet的创建步骤如下所述。 (1) 右击com.test.servlet包,在弹出的菜单中选择New→Other菜单项,在弹出的对话框中,找到Web组件下的Servlet选项,选择新建Servlet类,如图32所示,单击Next按钮。 图32新建Servlet类 (2) Servlet命名,如图33所示。在Create Servlet对话框中的Class name文本框中输入FirstServlet,其他选项按照默认的即可,然后单击Next按钮。 图33Servlet命名 (3) 设置Servlet参数,如图34所示。在弹出的对话框中对Servlet的Initialization parameters以及URL mappings参数进行设置,其中Initialization parameters表示Servlet类的初始参数,URL mappings表示访问该Servlet的映射路径,默认设置为/FirstServlet。如果需要设置初始参数,以及添加或者修改映射路径,就可以单击Initialization parameters列表或者URL mappings列表右边的Add按钮进行配置。此处先按照默认设置即可,单击Next按钮。 图34Servlet 参数设置 (4) 选择Servlet重写方法,如图35所示。在弹出的对话框中选中FirstServlet类需要创建的方法,模板默认会有一个父类构造器,以及继承父类的doGet和doPost方法,也可以选中其他需要继承的父类抽象方法。此处采用默认配置,单击Finish按钮,即完成FirstServlet类的创建。 图35选择Servlet重写方法 此时在项目下已经生成了FirstServlet类,代码如下: import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/FirstServlet") public class FirstServlet extends HttpServlet { private static final long serialVersionUID = 1L; public FirstServlet() { super(); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.getWriter().append("Served at: ").append(request. getContextPath()); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } } 从代码中可以看出,FirstServlet类引入了javax.servlet.http包中的一些类。除了父类HttpServlet类以外,还包括HttpServletRequest以及 HttpServletResponse,分别表示请求和响应,它们的实例化对象request和response分别作为doGet和doPost方法的参数。 此外,FirstServlet类还引入了javax.servlet.annotation.WebServlet类,这个类是用于注解的类,可以看到在FirstServlet类上包含有@WebServlet("/FirstServlet")的一行注解代码。这是因为在新建项目时选择的Dynamic Web Module Version为3.1,因此项目采用的是Servlet 3.1规范,在Servlet 3.0以上版本中,默认是使用注解对Servlet进行配置。@WebServlet("/FirstServlet")这条注解语句,其实对应了在新建FirestServlet时配置的URL mapping。 如果项目选择的是Servlet 2.5及以下版本,就在Servlet新建后,需要在项目的web.xml配置文件的节点下,编写如下代码以完成FirstServlet的配置。 FirstServlet com.test.servlet.FirstServlet FirstServlet /FirstServlet 其中,servlet节点下有servletname和servletclass两个元素,其值分别对应Servlet设置的名称和对应的具体类; servletmapping节点下有servletname和urlpattern两个元素,配置了FirstServlet的访问路径。 通过模板创建Servlet,只需要在界面中设置参数,Eclipse就会自动生成对应的配置信息。无论采用注解还是在web.xml中配置,效果都是等同的。而且相同的Servlet配置只能选取一种方式,重复配置将会报错。当然,也可以采用手动的方式进行编写和修改,此时需要注意对应的配置语法和格式。在本书的演示中,均采用Servlet 3.1规范的注解方式。 视频讲解 3.3.2Servlet的运行 在创建完FirstServlet后,可进行如下操作来运行Servlet。 (1) 右击Chapt_03项目,在弹出的菜单中选择Run As→Run on Server,选择Tomcat 9服务器,单击Finish按钮,此时Chapt_03项目被部署到服务器中。 (2) 打开浏览器,输入网址http://localhost:8080/Chapt_03/FirstServlet,FirstServlet运行效果如图36所示。 图36FirstServlet运行效果 为什么会显示这样一行文本信息呢?实际上在访问FirstServlet时,Tomcat服务器按照以下步骤进行处理。 (1) 该请求中使用端口号为8080,因此会被一直监听8080端口号的Connector组件获取。 (2) Connector组件把请求交给Engine组件处理,并等待回应。 (3) Engine查找Host组件,找到匹配名字为localhost的虚拟主机。 (4) 在localhost主机上,查找Context组件,匹配到名字为Chapt_03的应用。 (5) 根据请求路径/FirstServlet,在Chapt_03下查找URL mapping配置,找到对应的FirstServlet类去处理。 (6) 构造HttpServletRequest对象和HttpServletResponse对象,作为参数传送给FirstServlet的doGet()方法,处理完毕后,将结果封装到HttpServletResponse对象中。 (7) Context将HttpServletResponse响应返回给Host。 (8) Host将响应返回给Engine。 (9) Engine将响应返回给Connector。 (10) Connector将响应结果返回给浏览器客户端。 从以上步骤看,最终页面的显示结果是来自FirstServlet的doGet()方法,在方法体内部只有一条语句: response.getWriter().append("Served at:").append(request.getContextPath()); 说明: response对象的getWriter()方法获取了一个输出流对象,向客户端进行文本的输出,后面的append()方法表示文本的追加输出,第二个append()方法里的参数,由request对象通过getContextPath()方法获取,表示请求的上下文Context对象路径,即/Chapt_03。因此,最终输出为Served at: /Chapt_03。 3.3.3Servlet的运行机制 当Servlet运行后,最终也会编译成字节码文件,存放在Tomcat服务器对应项目目录下,FirstServlet的字节码文件可以在Chapt_03\WEBINF\classes\com\test\servlet\路径下找到。那么当每次运行Servlet时,都会创建一个实例化对象吗?实际上在默认情况下,Servlet是以单例多线程的形式运行的。下面通过例31演示Servlet运行状态。 视频讲解 【例31】Servlet运行状态。 在Chapt_03项目下再新建一个名为SecondServlet的Servlet类,通过注解@WebServlet("/SecondServlet"),设置其映射路径为/SecondServlet,然后在其构造函数和doGet()方法中编写代码如下: public SecondServlet() { System.out.println("SecondServlet对象被实例化"); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("SecondServlet对象doGet被执行"); } 注意,新建Servlet或者JSP需要重启服务器,项目重新部署后才能访问。为模拟不同客户端访问同一个Servlet的场景,首先通过Firefox浏览器访问SecondServlet,然后观察Eclipse的Console输出内容,首次运行SecondServlet后的输出结果如图37所示。 图37首次运行SecondServlet后的输出结果 在不关闭服务器的情况下,使用Google 浏览器,再次访问SecondServlet后的输出结果如图38所示。 图38再次访问SecondServlet后的输出结果 由此可见,只有第一次访问Servlet时,运行了构造函数和doGet()方法,第二次访问只执行了doGet()方法,因此只有第一次运行时创建了对象,再次访问时并没有再次实例化对象。 Servlet是以多线程的方式去处理每一个请求的。即使多个用户访问同一个Servlet对象,服务器也会各自分配一个线程去运行doGet()方法。 在默认情况下,由于Servlet采用单例模式,因此存在线程安全方面的隐患,一般不要在类中直接定义成员变量。当然也可以通过实现SingleThreadModel接口,让每次请求都初始化一个Servlet去处理,此种方式本书暂不讨论。 3.3.4Servlet与生命周期 1. Servlet 在利用模板新建Servlet类时,默认需要继承HttpServlet这个抽象类。HttpServlet又是继承于GenericServlet这个抽象类,而GenericServlet抽象类又实现了Servlet以及ServleConfig两个接口。因此Servlet可以继承或者重写一些父类方法,这些方法将伴随着Servlet的整个生命周期。 (1) init(ServletConfig config): 该方法继承于GenericServlet类,是实现了Servlet接口声明的init()方法。该方法在Servlet类被加载后被调用, ServletConfig接口对象作为参数传递进来,从而可以获取一些初始化参数。如果有特殊初始参数配置方面的需求,可以重写该方法。 (2) service(HttpServletRequest request,HttpServletResponse response): 该方法根据request对象的getMethod()方法获取请求采用的方式,再去调用对应的do×××()方法。 (3) do×××(HttpServletRequest request, HttpServletResponse response): 包括doGet()、doPost()、doPut()、doDelete()、doHead()、doTrace()、doOptions()方法。这些方法处理不同请求方式的HTTP请求。 (4) destroy(): 当Servlet消亡时会调用该方法。当有特殊需求时,如需要清理某些设置及参数时可以重写该方法。 2. Servlet生命周期 Servlet生命周期的过程: 从第一次加载时调用init()方法,接着调用service()方法获取请求采用的方式,然后调用对应的do×××()方法,执行该方法完毕后返回响应的结果。当Servlet要消亡时(如关闭了服务器),则调用destroy()方法。 在实际开发中,一般只需要根据请求方式重写对应的do×××()方法即可。其中使用最多的是doGet()和doPost()方法,分别用于处理GET和POST类型的请求。 3.4Servlet处理请求与响应 3.4.1doGet()与doPost()方法 doGet()和doPost()方法分别处理GET和POST两种发送方式的请求,两种方法的应用场景有所区别。 GET方式一般针对页面及资源的请求。如访问超链接或者通过URL进行参数传值,以及表单默认的提交,均采用GET方式进行请求。 POST方式一般用于向服务器提交数据,例如当表单method属性设置为POST时,则表单采用POST方式进行提交。 GET方式传递的值直接放在请求行中,与网址内容一起进行编码。POST传递的值则放在请求体中。 doGet()和doPost()方法都包含HttpServletRequest和HttpServletResponse类型的参数,通过request对象获取请求参数,进行处理后,再利用response对象返回响应。在编写代码时,只需在doGet()方法体内编写处理请求的代码即可,然后在doPost()方法内调用doGet()方法。 3.4.2rqequest基本信息的获取 request对象提供了下面的方法用于获取请求中的一些重要信息。 (1) String getMethod()方法: 获取请求方式,如GET或者POST。 (2) String getRequestURI()方法: 获取请求的URI(Uniform Resource Identifier, 统一资源标志符)。 (3) String getProtocol()方法: 获取请求采用的协议。 (4) String getServerPort()方法: 获取请求服务器端口号。 (5) String getServerName()方法: 获取请求服务器的名称。 (6) String getContextPath()方法: 获取请求的上下文路径。 (7) String getRemoteAddr()方法: 获取发送请求的客户端IP地址。 以上信息在某些应用场景中需要用到,例如获取上下文路径可以用于绝对地址的拼接,而获取客户端IP地址则可以记录请求日志信息,甚至可以设置黑名单禁用部分IP地址。下面通过例32演示request对象基本信息的获取过程。 视频讲解 【例32】request对象基本信息的获取过程。 新建一个Servlet类,取名为RequestInfoServlet,通过注解@WebServlet("/RequestInfoServlet")设置映射路径为/RequestInfoServlet,然后在doGet()方法中编写以下代码。 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String method="method:"+request.getMethod()+"\r\n"; String protocol="protocol:"+request.getProtocol()+"\r\n"; String servername="servername:"+request.getServerName()+"\r\n"; String port="port:"+request.getServerPort()+"\r\n"; String requestpath="contextpath:"+request.getContextPath()+"\r\n"; String uri="request URI:"+request.getRequestURI()+"\r\n"; String ipaddress="ip address:"+request.getRemoteAddr(); PrintWriter out=response.getWriter() out.append(method).append(protocol).append(servername).append(port). append(uri).append(requestpath).append(ipaddress); out.close(); } RequestInfoServlet的doGet()方法中使用了request的相应方法获取基本信息。访问RequestInfoServlet后,request对象的基本信息如图39所示。 图39request对象的基本信息 Servlet输出时利用response对象使用getWriter()或者getOutputStream()方法以获取输出流,二者互斥不能混用。例子中使用的PrintWriter对象用于向客户端输出字符流,包括以下5个常用的方法。 (1) void print()方法: 输出文本信息后不换行。 (2) void println()方法: 输出文本信息后换行。 (3) void append()方法: 和print()类似,但方法可以直接追加,例32中即采用此方法,因此在相应变量后添加了\r\n进行换行操作。 (4) void flush()方法: 输出缓冲区数据,在客户端输出后清除缓冲区数据。 (5) void close()方法: 关闭输出流。 3.4.3URL传值数据的获取 超链接是网页中常见的元素,单击超链接可以跳转到指定的URL,该地址可以是服务器外部网址,也可以是服务器内部地址。同时超链接后面可以附带参数一同发送给服务器,从而实现页面之间信息的传递。单击超链接是通过GET方式提出请求,下面通过例33演示通过超链接进行URL传递参数以及服务器通过Servlet获取后再输出到客户端页面中的过程。 视频讲解 【例33】通过超链接进行URL传递参数。 (1) 在项目的WebContent下新建一个HTML页面,取名为hyperlink.html,在标签体内部添加一个超链接,代码如下: 通过超链接进行URL传值 该超链接指向URLServlet,超链接指向的URL后面附带两个参数,访问hyperlink.html页面如图310所示。 图310访问hyperlink.html页面 注意,超链接的href地址前没有加反斜杠(/),表示采用的是相对路径,访问的是/Chapt_03/路径下的URLServlet。如果地址前加了反斜杠,就表示使用的是绝对地址,此时必须加项目路径/Chapt_03/,代码如下: 通过超链接进行URL传值 访问路径的写法是初学时容易犯错的地方,尤其是超链接的href属性以及表单提交的action的写法,如果路径有误,则页面会报404错误,此时应排查采用的是相对还是绝对路径。 (2) 新建一个Servlet,取名为URLServlet,通过注解@WebServlet("/URLServlet")设置映射路径为/URLServlet,然后在doGet()方法中编写代码如下: String a=request.getParameter("a"); String b=request.getParameter("b"); PrintWriter out=response.getWriter(); out.println("parameter a:"+a); out.println("parameter b:"+b); 单击hyperlink.html中的超链接,页面跳转到URLServlet,获取超链接和URL传递的参数,如图311所示。 图311获取超链接和URL传递的参数 此时参数附加在URL后面,并出现在地址栏中,说明超链接的确是通过GET方式进行的请求。在URLServlet类的doGet()方法中,使用了request对象的getParameter(String name)方法。该方法的作用就是通过参数名来获取对应的参数值。该方法使用较为频繁,在获取表单提交的数据时也会采用。 3.4.4表单中单值元素数据的获取 表单提交是Web应用中常见的功能,本节介绍Servlet处理表单中单一元素数据的方法。单值元素是指该表单元素提交数据给服务器时,只包含一个变量。表单中的单值元素包括文本框、密码框、单选按钮、下拉框以及多行文本框等。下面通过例34演示Servlet获取表单中单值元素的数据并处理的操作步骤。 视频讲解 【例34】Servlet获取表单中单值元素的数据。 (1) 新建一个HTML页面,取名为single.html,在标签体内部编写一个包含上述元素的表单,代码如下:
用户名:
密   码:
性别(单选):
所在区域:
请输入个人信息:

说明: 表单action属性的值表示表单提交后,所有元素数据由GetSingleServlet处理,注意此时采用的是相对路径的写法; method属性的值表示提交方式,默认为GET方式,本例使用POST方式进行提交。 (2) 新建GetSingleServlet,通过注解@WebServlet("/GetSingleServlet")设置映射路径为/GetSingleServlet,在doGet()方法中的代码如下: String username=request.getParameter("username"); String password=request.getParameter("password"); String gender=request.getParameter("gender"); String country=request.getParameter("country"); String information=request.getParameter("information"); PrintWriter out=response.getWriter(); out.println("username:"+username); out.println("password:"+password); out.println("gender:"+gender); out.println("country:"+country); out.println("information:"+information); 与获取URL传值方式一样,使用request对象的getParameter()方法,以表单中input元素的name属性为参数,获取input元素的value属性值,从而得到表单元素提交的数据。 (3) 打开浏览器访问single.html并填写表单数据,如图312所示。 图312访问single.html并填写表单数据 单击“提交”按钮将表单数据提交给GetSingleServlet处理,获取单值元素并输出到页面,如图313所示。 图313获取单值元素并输出到页面 3.4.5表单中多值元素数据的获取 表单中如果有多个元素的name属性值相同,当表单提交时,这些同名元素将以数组的形式向服务器发送数据,这样的元素称为多值元素。典型的多值元素包括复选框、多选列表框以及其他同名元素组合。下面通过例35演示Servlet获取表单中多值元素的数据并处理的操作步骤。 视频讲解 【例35】Servlet获取表单中多值元素的数据并处理。 (1) 新建一个HTML页面,取名为multiple.html,在标签体内部编写一个包含上述元素的表单,代码如下:
勾选你的兴趣爱好(可多选):
阅读 跳舞 唱歌 运动
选择精通的语言(可多选):

填写你擅长的其他技能:
技能1:
技能2:
技能3:
说明: multiple.html页面中的下拉框