第5章〓HTTP请求处理编程


 HTTP请求内容

 HTTP请求行

 HTTP请求头

 HTTP请求体

 Jakarta EE请求对象类型

 Jakarta EE请求对象功能和方法

 Jakarta EE请求对象的生命周期

 请求对象编程应用案例
Web应用工作在请求/响应模式下,要访问Web文档,需要使用浏览器通过URL地址对该文档进行HTTP请求。当Web服务器接收并处理该请求后,向请求的客户端发送HTTP响应,客户端接收到HTTP响应后进行显示,用户即可看到请求文档的内容,主要是HTML网页及其他类型文档。

在动态Web应用中,用户需要将信息输入Web系统中,通常使用HTML FORM表单和表单元素,如文本框、单选按钮、复选框、文本域等。将客户端信息提交到Web服务器端,服务器接收客户提交的数据,按具体业务进行处理,完成业务功能,如验证用户是否合法、增加新员工等。

Jakarta EE提供了HTTP请求对象,可以取得客户提交的数据。HTTP请求对象保存客户在发送HTTP请求时传递给Web服务器的所有信息,并提供相应的方法取得不同的提交信息。

确定HTTP请求中包含的数据类型和内容,并使用Jakarta EE规范中的请求对象取得HTTP请求中包含的这些数据是开发动态Web的关键。

5.1HTTP请求内容

当客户端对Web文档进行HTTP请求时,在请求中不但包含请求协议(如HTTP)、请求URL(如localhost:8080/web01/login.jsp),还包含其他客户端提交的数据。因此,在Web应用编程中,开发人员需要了解客户端发送请求中包含的数据和类型。

5.1.1HTTP请求中包含信息

当在浏览器地址中输入http://localhost:8080/web01/admin/login.jsp,对此Web JSP组件进行请求时,Web服务器会收到请求中包含的如下信息: 



GET /dumprequest HTTP/1.0

Host: djce.org.uk

User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 3.5.30729)

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Accept-Language: zh-cn,en-US;q=0.5

Accept-Encoding: gzip,deflate

Accept-Charset: GB2312,utf-8;q=0.7,*;q=0.7

Referer: http://www.google.cn/search?hl=zh-CN&source=hp&q=Http+request&btnG=Google+%E6%90%9C%E7%B4%A  2&aq=f&oq=Via: 1.1 cache3.dlut.edu.cn:3128 (squid/2.6.STABLE18)

X-Forwarded-For: 210.30.108.201

Cache-Control: max-age=259200

Connection: keep-alive





以上请求信息按类别分为如下3部分。

(1) 请求行(Request Query)信息。请求行信息包括请求的协议HTTP、请求的方式、请求的地址URL、URI等。

(2) 请求头(Request Header)信息。请求头信息主要包含请求指示信息,用于通知Web容器请求中信息的类型、请求方式、信息的大小、客户的IP地址等。根据这些信息,Web组件可以采取不同的处理方式,实现对HTTP的请求处理。 

(3) 请求体(Request body)信息。请求体信息中包含客户提交给服务器的数据,如表单提交中的数据、上传的文件等。

下面分别介绍每个组成部分的具体内容和意义。

5.1.2请求行

请求行(Request Query)信息位于请求信息的第一行,包括请求的协议、请求的地址URL、请求的方式等。请求行的案例代码如下: 



GET https://www.baidu.com/content-search.xml HTTP/1.1





其中,GET是请求方法,https://www.baidu com/是URL地址,HTTP/1.1指定了协议版本。

不同的HTTP版本能够使用的请求方法也不同,具体介绍如下。

(1) HTTP 0.9: 只有基本的文本GET功能。

(2) HTTP 1.0: 具有完善的请求/响应模型,并将协议补充完整,定义了GET、POST和HEAD 3种请求方法。

(3) HTTP 1.1: 在HTTP 1.0基础上进行更新,新增了5种请求方法: OPTIONS、PUT、DELETE、TRACE和CONNECT。

(4) HTTP 2.0: 请求/响应首部的定义基本没有改变,只是所有首部键必须全部小写,而且请求行要独立为:method、:scheme、:host、:path等键值对。

不同请求方式的意义如下。

(1) GET: 请求指定的页面信息,并返回实体主体。

(2) POST: 向指定资源提交数据请求处理(如提交表单或者上传文件),数据被包含在请求体中。POST 请求可能会导致新的资源的建立和已有资源的修改。

(3) HEAD: 类似于GET请求,只不过返回的响应中没有具体内容,用于获取报头。

(4) PUT: 这种请求方式下,从客户端向服务器传送的数据取代指定的文档内容。

(5) DELETE: 请求服务器删除指定的页面。

(6) OPTIONS: 允许客户端查看服务器的性能。

(7) TRACE; 回显服务器收到的请求,主要用于测试或诊断。

当Web客户向服务器发出HTTP请求时,请求头首先被发送到服务器端,服务器根据请求行的URL信息定位指定的文档,如果文档不存在,则服务器给客户端发送404错误信息,根据请求的方式,调用文档的指定的处理方法,如Servlet的doGet、doPost、doPut、doDelete等。

5.1.3请求头

当Web客户向服务器发出HTTP请求时,发送请求行信息后,请求头信息被发送到服务器端,告知服务器此请求中包含的指示信息,服务器以便根据这些指示信息采取不同的处理。请求头中主要是客户端的一些基础信息,其中关键信息如下。

(1) accept: 表示当前浏览器可以接受的文件类型。假设这里有 image/webp,表示当前浏览器可以支持 webp 格式的图片,那么当服务器给当前浏览器下发送 webp类型图片时,可以更省流量。

(2) acceptencoding: 表示当前浏览器可以接收的字符编码。如果服务器发送的响应数据不是浏览器可接收的字符编码,就会显示乱码。

(3) acceptlanguage: 表示当前客户端使用的语言,也包含客户所在的国家或地区。Web服务器端应用要实现国际化,可根据此请求头信息给客户端发送对应的语言文本,如给中国大陆客户发送中文简体、给美国客户发送英语。

(4) Cookie: 存储和用户相关的信息,每次用户在向服务器发送请求时会带上Cookie。例如,用户在一个网站上登录之后,下次访问时就不用再登录,就是因为登录成功的 token 放在了 Cookie 中; 另外,随着每次请求发送给服务器,服务器就会知道当前用户已登录。

(5) useragent: 表示浏览器的类型和版本信息。当服务器收到浏览器的请求后,通过该请求头知道浏览器的类型和版本,进而知道支持的语言版本(如JavaScript、HTML、CSS等),开发者可以针对此类型浏览器编写对应版本的代码。

请求头中也可以包含客户端自定义的信息,通常前端框架(如Vue、Angular等)都可以在请求头中加入自己定义的name和value值。表51列出了W3C规范中规定的常用HTTP请求头标记和说明。


表51W3C规范中规定的HTTP请求头标记和说明


头标记说明包含的值示例


UserAgent客户端的类型(包含浏览器名称)LIICello/1.0 libwww/2.5
Accept浏览器可接收的MIME类型各种标准的MIME类型
AcceptCharset浏览器支持的字符编码字符编码,如ISO88591
AcceptEncoding浏览器知道如何解码的数据编码类型xcompress; xzipAcceptLanguage
浏览器指定的语言如en:English
Connection是否使用持续连接KeepAlive: 持续连接
ContentLength使用POST方法提交时,传递数据的字节数2352
Cookie保存的Cookie对象userid=9001
Host主机和端口192.168.100.3:8080


在Jakarta EE Web组件的Servlet和JSP中,可以使用请求对象的方法读取这些请求头的信息,进而进行相应的处理。5.2.4小节将讲述请求头信息的取得方法。

5.1.4请求体

每次HTTP请求时,在请求头后面会有一个空行,之后是请求中包含的提交数据,即请求体。请求体通常是表单元素中输入的数据,所有Web应用都需要客户输入数据。登录淘宝、京东的账号和密码信息,增加新产品的信息数据等,这些数据通常包含在请求体中。

不是所有的请求都有请求体,当为GET请求时,则没有请求体,因为请求数据直接附加在请求文档的URL地址中,请求体作为URL的一部分发送到Web服务器。例如,http://localhost:8080/web01/login.do?id=9001&pass=9001,这时请求体为空,因为提交数据直接在URL中,作为请求行的一部分传输到Web服务器,通过解析URL的QueryString部分就可以得到提交的参数数据。这种方式对提交的数据大小有限制,不同浏览器会有所不同,如IE为2083字节。GET请求时,数据会出现在URL中,保密性差,因此在实际项目编程中要尽量避免在URL地址栏中传递请求数据。

POST请求时,请求体数据单独打包为数据块,通过Socket直接发送到Web服务器端,数据不会在地址栏中出现,因此可以提交数据的类型和大小基本没有限制,可以包括二进制文件,可以实现文件上传功能。原则上POST请求对提交的数据没有大小限制,但为了应用需要,一般在编程时会对文件的大小加以限制。

Jakarta EE Web组件(如Serlvet、JSP、Filter、Listener等)规范中都定义了如何取得请求体数据的方法,在5.2节中会详细说明这些方法和编程应用。

5.2Jakarta EE请求对象

为取得客户HTTP请求中包含的信息,Jakarta EE的Web组件规范定义了请求对象接口规范,通过实现该接口的请求对象可以取得请求中包含的所有信息,包括请求行、请求头和请求体。

5.2.1请求对象接口类型与生命周期
1. 请求对象接口类型

Jakarta EE规范中,通用请求对象(不依赖请求协议的情况)要实现接口: 



jakarta.servlet.ServletRequest





而本书重点介绍的是HTTP下工作的请求对象,要实现接口: 



jakarta.servlet.http.HttpServletRequest





这两个接口的所有方法和属性可参阅Eclipse Jakarta EE API文档。

2. 请求对象生命周期

在Java Web组件开发中,不需要开发者自己创建Servlet或JSP使用的请求对象,它们由Web容器自动创建,并传递给Servlet和JSP的服务方法doGet、doPost、doPut、doDelete及doHead等。在这些HTTP请求处理方法中可以直接使用请求对象,调用其方法,取得客户端提交的数据。

(1) 创建请求对象。每次Web服务器接收到HTTP请求时,会自动创建实现HttpServletRequest接口的对象。具体的请求对象实现类由Jakarta EE服务器厂家实现,不同的服务器产品(如Tomcat、GlassFish、WebLogic等)实现请求对象接口的实现类不一定相同,但开发者不需要了解具体的请求对象的实现类型,只需掌握请求对象接口的方法即可。创建请求对象后,Web服务器将请求行、请求头和请求体信息存入请求对象,并自动把请求对象传递给请求的Web组件,如Servlet、JSP等。这些Web组件可以通过请求对象的方法取得这些请求信息,即客户端用户提交的数据。

(2) 销毁请求对象。当Web服务器处理HTTP请求,向客户端发送HTTP响应结束后,会自动销毁请求对象,保存在请求对象中的数据随即丢失。当下次请求时新的请求对象又会创建,重新开始请求对象新的生命周期。

5.2.2请求对象的功能与方法

Jakarta EE提供的HttpServletRequest请求对象用于取得HTTP请求中包含的请求行、请求头和请求体的数据信息。HttpServletRequest接口定义的方法分类如下。

(1) 取得请求行的数据。

(2) 取得请求头信息。

(3) 取得请求体中包含的提交参数数据,包含表单元素或地址栏URL的参数。

(4) 取得服务器端的相关信息,如服务器的IP等。

(5) 取得请求对象存储的属性信息。

请求对象除了可以取得客户端的各种信息外,还提供了作为传递数据的容器的方法,用于在Web组件间传递数据。该功能在Web开发中使用得非常多,在7.2.3小节中会详细讲解请求对象的此项功能和方法。

5.2.3取得请求行方法

请求对象接口HttpServletRequest提供了如下方法,用于取得请求行中包含的数据。

(1) String getProtocol(): 取得使用的请求协议。

(2) String getMethod(): 取得请求的方式,返回字符串类型的GET、POST、PUT、DELETE等。

(3) StringBuffer getRequestURL(): 取得请求的URL地址。需要注意的是,其返回类型不是String,而是StringBuffer,需要调用其toString()方法将其转换为String类型再显示。

(4) String getRequestURI(): 取得请求的URI地址。URI地址是Web站点内的地址,从Web应用的起始站点名开始,假如Web的站点起始路径为/jakartaweb05, Web文档JSP目录是/employee,文件名是list.jsp, 则URI地址是/jakartaweb05/employee/list.jsp。而URL地址是包含协议、IP地址和端口的全地址,上面JSP文件的URL地址如下: 



http://localhost:8080/jakartaweb05/employee/list.jsp





从URL地址可以看出,URL包含URI,URI是URL的一部分,即: 



URL=协议://IP:端口/URI   





测试取得请求行的Servlet代码如程序51所示。

程序51RequestQueryGetting.java取得请求行的测试Servlet类代码。



package com.city.oa.servlet;

import jakarta.servlet.ServletConfig;

import jakarta.servlet.ServletException;

import jakarta.servlet.annotation.WebInitParam;

import jakarta.servlet.annotation.WebServlet;

import jakarta.servlet.http.HttpServlet;

import jakarta.servlet.http.HttpServletRequest;

import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

import java.io.PrintWriter;

/**

 * 取得请求行的测试Servlet

 */

@WebServlet(urlPatterns = { "/requestquery/get.do" })

public class RequestQueryGetting extends HttpServlet {

private static final long serialVersionUID = 1L;

/**

* GET请求处理

*/

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

//取得请求协议

String protocol=request.getProtocol();

//取得请求方式

String method=request.getMethod();

//取得请求地址URL

String url=request.getRequestURL().toString();

//取得请求地址URI

String uri=request.getRequestURI();

//发送响应数据		

response.setContentType("text/html");








response.setCharacterEncoding("UTF-8");

PrintWriter out=response.getWriter();

out.println("<h1>取得请求行信息</h1>");

out.println("<hr/>");

out.println("请求协议:"+protocol+"<br/>");

out.println("请求方式:"+method+"<br/>");

out.println("请求URL地址:"+url+"<br/>");

out.println("请求URI地址:"+uri+"");		

out.println("<hr/>");

out.flush();

out.close();

}

/**

* POST请求处理

*/

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

doGet(request, response);

}

}





运行此Servlet代码,结果如图51所示。



图51取得请求行的Servlet代码运行结果


5.2.4取得请求头方法

5.1.3小节介绍了请求中包含的主要请求头信息,HttpServletRequest接口提供了如下方法用于取得不同类型的请求头中的数据。通常请求头中的数据类型主要有String、int、Date等。

(1) String getHeader(String name): 取得指定请求头字符串类型的内容。例如,在Servlet的doGet或doPost方法中取得客户端浏览器类型的代码如下: 



String browser=request.getHeader("User-Agent");





(2) int getIntHeader(String name): 取得整数类型的指定请求头内容。如当HTTP请求中包含请求体数据时,通常会在请求中包含名称为ContentLength的请求头,表示请求体的长度,服务器端可以根据该请求头的值得知请求体数据的字节长度。取得请求体长度的示例代码如下: 



int size=request.getIntHeader("Content-Length");





请求头ContentLength中包含的请求体长度为int类型。该方法在编程文件上传类型应用中特别有用。

(3) long getDateHeader(String name): 此方法取得日期类型的指定请求头的内容。其返回的类型不是Date型,而是long型(表示从1970年1月1日0点开始计时的毫秒数),根据此long值计算出Date类型日期。如下代码为取得IfModifiedSince的请求头的值,表达请求文档的最近修改日期: 



long datetime=request.getDataHeader("If-Modified-Since");





上述代码取得请求文档的最后修改日期的毫秒数。如果想要取得具体的日期,则使用java.util.Date的构造方法传递此long类型的值即可。其示例代码如下: 


Date modifyDate=new Date(datetime);





(4) Enumeration getHeaderNames(): 此方法取得所有请求头的name的列表,以枚举类型返回。可以使用遍历枚举类型的方法取得所有的请求头,包括name和value。

程序52的代码展示了取得并输出所有请求头名称及每个请求头name对应的值的Servlet编程。

程序52RequestHeaderGetting.java取得请求头的Servlet类代码。



package com.city.oa.servlet;

import jakarta.servlet.ServletException;

import jakarta.servlet.annotation.WebServlet;

import jakarta.servlet.http.HttpServlet;

import jakarta.servlet.http.HttpServletRequest;

import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

import java.io.PrintWriter;

import java.util.Enumeration;

/**

 * 取得请求头的Servlet

 */

@WebServlet(urlPatterns = { "/requestheaders/get.do" })

public class RequestHeaderGetting extends HttpServlet {

private static final long serialVersionUID = 1L;

/**

* GET请求处理方法

*/

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

response.setContentType("text/html");

response.setCharacterEncoding("UTF-8");

PrintWriter out=response.getWriter();

out.println("<h1>取得请求头信息</h1>");

out.println("<hr/>");

out.println("请求头User-Agent:"+request.getHeader("User-Agent")+"<br/>");

out.println("请求头Host:"+request.getHeader("Host")+"<br/>");

out.println("客户IP地址:"+request.getRemoteAddr()+"<br/>");

out.println("<hr/>");

out.println("<h1>所有请求头遍历</h1>");

Enumeration<String> headers=request.getHeaderNames();







for(;headers.hasMoreElements();) {

String headerName=headers.nextElement();

out.println(headerName+"="+request.getHeader(headerName)+"<br/>");

}

out.println("<hr/>");

out.flush();

out.close();

}

/**

* POST处理方法

*/

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

doGet(request, response);

}

}





上述代码首先使用request请求对象的方法取得了常用的请求头,然后使用遍历方法遍历所有请求头的名称和值。将上述Servlet代码部署到Tomcat上运行,使用浏览器请求该Servlet取得图52所示的请求头信息。



图52Servlet使用请求对象取得的请求头信息


5.2.5取得请求体方法

在Web开发中,用户通过表单输入将客户端数据提交到服务器端,这些数据被Web服务器自动保存到请求对象中,Web组件Servlet和JSP可以通过请求对象取得提交的数据。HttpServletRequest请求对象提供了如下方法,用于取得客户提交的数据。

(1) String getParameter(String name): 取得指定名称的请求体数据。此方法用于单个数据的参数,所有取得的参数值都是String类型,开发者需要根据业务需求,将其转换为对应的数据类型,如int、double、Date等。

参数name为FORM表单元素的name属性或URL参数名称,如: 



产品名称:<input type="text" name="productName" />

productSearch.do?productName=Acer





如下代码可取得以上参数名为productName的数据: 



String productName=request.getParameter("productName");





(2) String[] getParameterValues(String name): 取得指定参数名称的数据数组,用于多值参数的情况,如复选框、复选列表等。如下示例代码中展示复选框形式的爱好选择数据: 



爱好:<input type="checkbox" name="behave" value="旅游" />旅游

<input type="checkbox" name="behave" value="读书" />读书

<input type="checkbox" name="behave" value="体育" />体育





取得上面复选框选中爱好数据的示例代码如下: 



String[] behaves=request.getParameterValues("behave");

for(int i=0;i<behaves.length;i++){

out.println(behaves[i]);

}





注意: 此数组不需要事先确定大小,由Web容器自动根据参数名对应值的个数确定数组的容量大小。

(3)  Enumeration getParameterNames(): 取得所有请求参数的名称,返回遍历器类型。使用遍历器的方法可以取得所有请求的参数名。如下示例代码为遍历所有请求参数名: 



for (Enumeration enum=request. getParameterNames(); enum.hasMoreElements();) 

{String paramName = (String)enum.nextElement();

System.out.println("Name = " + paramName);

}





(4) Map getParameterMap(): 取得所有请求参数名和值,包装在一个Map对象中,可以使用该对象同时取得所有参数名和参数值。如下示例代码取得所有请求参数名和参数值: 



Map params=request.getParameterMap();

Set names=params.keySet();

for(Object o:names) {

String paramName=(String)o;

out.print(paramName+"="+params.get(paramName)+"<br/>");

}





(5)  ServletInputStream getInputStream() throws IOException: 取得客户提交数据的输入流。当使用getParamerer方法后,就无法使用getInputStream方法,反之亦然,二者只能使用其一。当用户使用POST方式提交包含文件上传的数据时,请求数据以二进制编码方式提交到服务器,此时Servlet无法使用之前的getParameter方法取得请求数据,只能取得纯文本数据。要取得包含文本和二进制编码的请求数据,只能使用getInputStream方法以二进制方式取得请求数据,再对此数据进行解析,从而分离出文本数据和上传的文件。如果开发者自己编程解析将非常复杂,因此经常使用成熟的框架技术来处理这种有文件上传的请求。目前市场上已经存在多种第三方框架来实现上传文件处理,如Apache的Common upload组件、JSP Smartupload等。如下代码示例为有文件上传的表单HTML: 



<form action="addEmp.do" method="POST" enctype="multipart/form-data"/>

姓名:<input type="text" name="name" /><br/>

照片:<input type="file" name="photo" /><br/>

<input type="submit" value="提交"/>

</form>





使用第三方框架技术,如Common Upload,可以非常方便地取得表单中包含的姓名和照片文件,具体可参阅相应的框架文档资料。

(6) Part getPart(String name): 从Servlet 3.0开始,请求对象提供了取得上传文件的方法getPart。在此之前要取得上传文件,必须使用getInputStream方法取得所有请求数据的输入流,开发者自己解析此输入流取得上传的文件,或者使用第三方框架。现在直接使用getPart方法就可以取得表单中的上传文件,非常方便。其中getPart方法的参数是提交数据项的name,返回Part类型的数据,其接口类型为jakarta.servlet.http.Part,该接口的具体实现类由使用的服务器(如Tomcat)实现。

Part接口提供了如下方法用于处理取得的上传文件。

① InputStream getInputStream(): 取得其输入流对象,进而可以通过流的编程取得上传文件。

② long getSize(): 取得上传文件的大小(字节数)。

③ String getContentType(): 取得上传文件的类型,为MIME类型,将在第6章中详细介绍。

④ String getSubmittedFileName(): 取得文件的名称。

⑤ void write(String fileName): 将上传的文件写入指定的文件中。当需要保存上传文件到指定目录时,此方法特别有用。

⑥ void delete(): 将此上传文件删除。当服务器接收到上传文件时,会在服务器指定的临时目录中保存此文件。使用此方法可以立即删除此文件。

5.2.6请求对象取得常用请求头数据的便捷方法

对于取得请求行和请求头数据,HttpServletRequest请求对象除提供通用的方法以外,还提供了专门的便捷方法来取得请求行和请求头信息,如客户端信息、请求方式、客户端IP地址等。下面是请求对象提供的取得专门信息的便捷方法。

(1) String getRemoteHost(): 取得请求客户的主机名。其与通用方法getHeader("Host")等价。

(2) String getRemoteAddr(): 取得请求客户端的IP地址。其没有专门的等价通用方法,需要先取得Host请求头,再解析客户的IP地址。

(3) int getRemotePort(): 取得请求客户的端口号。其没有等价的通用方法,也需要先取得Host请求头,再解析客户的端口。

(4)  String getProtocol(): 直接取得请求行中的协议。

(5)  String getContentType(): 取得请求体的内容类型,以MIME表达。其与通用的取得请求头方法getHeader("ContentType")等价。


(6)  int getContentLength(): 取得请求体的长度(字节数),当处理有文件上传请求时特别有用。其与通用的取得请求头的方法getIntHeader("ContentLength")等价。

(7)  String getMethod(): 取得请求的方式,返回GET、POST、PUT、DELETE等信息。

5.2.7取得服务器端信息

通过HttpServletRequest请求对象还可以取得服务器的信息,如服务器名称、接收端口等。如下方法用于服务器端信息的取得。

(1) String getServerName(): 取得服务器的HOST,一般为IP地址。

(2) int getServerPort(): 取得服务器接收端口。实际编程中很少使用此方法,因为服务器的地址和端口是开发人员已知的,而客户端的地址和端口是未知的。因此,取得客户端的IP是必要的,而且经常使用。例如,所有的聊天类、BBS公告板、贴吧等应用都需要取得客户的IP地址,以便追踪客户的上网地址。

如下代码演示了取得服务器的IP地址和端口号: 



out.println("服务器名称:"+request.getServerName()+"<br/>");

out.println("服务器端口:"+request.getServerPort()+"<br/>");





上述代码将显示图53所示的内容。



图53取得服务器名称和端口的显示信息


5.3取得客户端HTML表单提交数据案例

本案例使用HttpServeltRequest请求对象取得客户端表单提交的业务数据编程。在实际项目开发中,经常需要向服务器提交数据,如用户注册、产品增加等类似应用都非常普遍。

5.3.1业务描述

在线购物网站中要求有客户注册功能,只有已经注册且登录的用户才能进行购物结算和发送订单。本案例即实现用户注册和处理功能,并且在Servlet中直接完成数据库的处理。这样做的目的是演示Servlet能完成的功能和编程,企业实际应用开发中并不会使用Servlet直接进行数据库的操作,而是使用MVC模式,通过持久化DAO层进行数据库的操作。

本案例使用JSP页面完成用户注册界面的编程,用户注册的处理使用Servlet完成。其中,用户注册页面如图54所示。

用户在注册页面(/customer/register.jsp)输入注册信息,提交给注册处理Servlet(CustomerRegisterServlet),该Servlet将取得的注册信息写入数据库表中。Servlet处理成功后,跳转到注册处理成功显示页面(/customer/registersuccess.jsp),显示注册成功消息,如图55所示。



图54用户注册页面




图55客户注册成功显示页面



5.3.2案例编程

本案例使用一个JSP注册页面和一个处理Servlet,使用STS和Tomcat 10.1.17进行开发和部署。

1. 创建Maven Web项目: oaweb2024

创建步骤参见第3章的Eclipse创建Maven项目的流程,创建的项目目录结构如图56所示。



图56案例Maven Web项目的目录结构


2. 客户注册页面的JSP编程

客户注册页面采用JSP实现,显示一个简单的客户注册表单。客户输入表单中对应的注册信息,单击“提交”按钮,会请求注册处理Servlet。该JSP页面的代码如程序53所示。

程序53register.jsp客户注册页面的JSP代码。



<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

<!DOCTYPE html>

<html>







<head>

<meta charset="UTF-8">

<title>网上商城系统</title>

</head>

<body>

<h1>客户注册</h1>

<form method="post" action="add.do" >

登录账号:<input type="text" name="id" /><br/>

登录密码:<input type="password" name="password"/><br/>

确认密码:<input type="password" name="repassword"/><br/>

用户名称:<input type="text" name="name"/><br/>

<input type="submit" value="提交"  />

</form>

</body>

</html>





使用表单和表单元素提交数据推荐使用POST方式,即设置<form>标记的属性method="post"。

3. 客户注册处理Servlet编程

客户注册处理Servlet取得注册信息,并将注册信息写入数据库中。如果出现异常,将自动重定向到用户注册页面。注册处理Sevlet代码如程序54所示。

程序54CustomerRegisterController.java客户注册处理Servlet代码。



package com.city.oa.controller;

import java.io.IOException;

import java.io.PrintWriter;

import java.sql.*;

import jakarta.servlet.ServletConfig;

import jakarta.servlet.ServletException;

import jakarta.servlet.http.HttpServlet;

import jakarta.servlet.http.HttpServletRequest;

import jakarta.servlet.http.HttpServletResponse;

//用户注册处理Servlet

@WebServlet(

urlPatterns = { "/customer/register.do" }, 

initParams = { 

@WebInitParam(name = "driver", value = "com.mysql.cj.jdbc.Driver"), 

@WebInitParam(name = "url", value = "jdbc:mysql://localhost:3319/cityoa"), 

@WebInitParam(name = "user", value = "root"),

@WebInitParam(name = "password", value = "root1234")

})

public class CustomerRegisterController extends HttpServlet 

{

//定义数据库连接对象

private Connection cn=null;

//数据库驱动器

private String driverName=null;

//数据库地址URL

private String url=null;

//初始化方法,取得数据库连接对象

public void init(ServletConfig config) throws ServletException 

{








super.init(config);

driverName=config.getInitParameter("driverName");

url=config.getInitParameter("url");

try	{

Class.forName(driverName);

cn=DriverManager.getConnection(url);

} catch(Exception e)	{

System.out.println("取得数据库连接错误:"+e.getMessage());

}

}

//处理GET请求方法

public void doGet(HttpServletRequest request, HttpServletResponse response)

throws ServletException, IOException 

{

//取得用户注册表单提交的数据

String userid=request.getParameter("userid");

String password=request.getParameter("password");

String repassword=request.getParameter("repassword");

String name=request.getParameter("name");

//判断登录账号为空,自动跳转到注册页面

if(userid==null||userid.trim().length()==0) {

response.sendRedirect("register.jsp");

}

//如果登录密码为空,则自动跳转到注册页面

if(password==null||password.trim().length()==0)	{

response.sendRedirect("register.jsp");

}

//如果确认登录密码为空,则自动跳转到注册页面

if(repassword==null||repassword.trim().length()==0) 	{

response.sendRedirect("register.jsp");

}

//如果密码和确认密码不符,则自动跳转到注册页面

if(!password.equals(repassword)) 	{

response.sendRedirect("register.jsp");

}

//将姓名进行汉字乱码处理

if(name!=null&&name.trim().length()>0)	{

name=new String(name.getBytes("ISO-8859-1")); 	}

//增加新用户处理

String sql="insert into USERINFO (USERID,PASSWORD,NAME) values (?,?,?)";

try	{

PreparedStatement ps=cn.prepareStatement(sql);

ps.setString(1, userid);

ps.setString(2, password);

ps.setString(3, name);

ps.executeUpdate();

ps.close();

//处理结束后,跳转到注册成功提示页面

response.sendRedirect("registersuccess.jsp");

} catch(Exception e)	{

System.out.println("错误:"+e.getMessage());

response.sendRedirect("register.jsp");

}

}







//处理POST请求方法

public void doPost(HttpServletRequest request, HttpServletResponse response)

throws ServletException, IOException 	{

doGet(request,response);

}

//销毁方法

public void destroy() 	{

super.destroy(); 

try	{

cn.close();

} catch(Exception e) {

System.out.println("关闭数据库错误:"+e.getMessage());

}

}

}





在注册处理Servlet中取得注册页面提交的用户信息,若这些注册信息不为空,则将其插入用户表中。此Servlet使用注解类配置方式,不需要在web.xml文件中编写配置代码。

4. 注册成功显示页面编程

当客户注册处理Servlet完成客户注册的功能后,自动跳转到注册成功显示页面,提醒客户注册成功。其JSP页面代码如程序55所示。

程序55/customer/registersuccess.jsp客户注册成功显示页面。



<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<title>网上商城系统</title>

</head>

<body>

<h2>客户注册成功</h2>

</body>

</html>





启动项目,部署到Tomcat服务器,使用浏览器请求客户注册页面,填写注册信息,单击“提交”按钮,完成注册处理后,即显示注册成功页面。

5.4取得客户端信息并验证案例

有的Web应用需要限制客户的访问,只允许部分IP地址的客户访问指定的页面或控制组件; 有时封杀某些IP的客户访问,因为这些IP已经被记录在黑名单中。本案例使用请求对象取得客户的IP地址,检查IP是否在被封杀之列,从而决定此客户是否可以继续访问Web应用。

5.4.1业务描述

编写Servlet,取得客户端的IP地址,并将此IP与数据库表中保存的封杀IP进行比较。如果此IP在封杀之列,则跳转到错误信息显示页面,阻止客户进一步的访问; 如果IP不在封杀之列,则允许跳转到主页。

5.4.2案例编程

根据案例的功能要求,设计如下数据库表和Web组件。

1. 设计IP封杀数据表

IP封杀数据表保存被封杀的IP列表,表结构设计如表52所示。本案例使用MySQL数据库,在数据库cityoa下创建此IP封杀记录表。


表52IP封杀数据表(表名称LimitIP)结构设计


字段名类型约束说明


IPNOInt主键编号
IPVarchar(50)非空IP地址


2. 监测IP地址是否被封杀的Servlet编程

此Servlet首先取得客户的IP地址,再连接数据库,判断IP是否在封杀列表中。如果IP在封杀之列,则自动跳转到错误信息显示页面; 否则自动跳转到系统的主页面。监测客户IP是否被封杀的Servlet代码如程序56所示。

程序56IPCheckController.java IP检查Servlet类代码。



package com.city.oa.controller;

import java.io.IOException;

import java.io.PrintWriter;

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import jakarta.servlet.ServletConfig;

import jakarta.servlet.ServletException;

import jakarta.servlet.http.HttpServlet;

import jakarta.servlet.http.HttpServletRequest;

import jakarta.servlet.http.HttpServletResponse;

//客户IP检查Servlet

@WebServlet(

urlPatterns = { "/client/ipcheck.do" }, 

initParams = { 

@WebInitParam(name = "driver", value = "com.mysql.cj.jdbc.Driver"), 

@WebInitParam(name = "url", value = "jdbc:mysql://localhost:3319/cityoa"), 

@WebInitParam(name = "user", value = "root"),

@WebInitParam(name = "password", value = "root1234")

})

public class ClientIPCheckAction extends HttpServlet 

{

//定义数据库连接对象

private Connection cn=null;

//数据库驱动器

private String driverName=null;

//数据库地址URL








private String url=null;

public void init(ServletConfig config) throws ServletException 

{

super.init(config);

driverName=config.getInitParameter("driverName");

url=config.getInitParameter("url");

try	{

Class.forName(driverName);

cn=DriverManager.getConnection(url);

} catch(Exception e)	{

System.out.println("取得数据库连接错误:"+e.getMessage());

}

}

//GET请求

public void doGet(HttpServletRequest request, HttpServletResponse response)

throws ServletException, IOException 

{

boolean isLocked=false;

String ip=request.getRemoteAddr();

String sql="select * from LimitIP where IP=?";

try	{

PreparedStatement ps=cn.prepareStatement(sql);

ps.setString(1, ip);

ResultSet rs=ps.executeQuery();

if(rs.next())

{

isLocked=true;   //如果IP在数据表中,则表示被封杀

}

rs.close();

ps.close();

if(isLocked)

{

//如果IP被封杀,自动跳转到封杀信息页面

response.sendRedirect("ipLock.jsp");

}

else

{

//如果IP允许访问,则可以跳转到主页面

response.sendRedirect("main.jsp");

}

} catch(Exception e) {

System.out.println("检查IP是否封杀错误:"+e.getMessage());

response.sendRedirect("errorInfo.jsp");

}

}

//POST请求处理

public void doPost(HttpServletRequest request, HttpServletResponse response)

throws ServletException, IOException 

{

doGet(request,response);

}

//销毁方法

public void destroy() 

{







super.destroy(); 

try	{

cn.close();

} catch(Exception e)	{

System.out.println("关闭数据库错误:"+e.getMessage());

}

}

}





3. 错误信息显示页面编程

当客户IP在被封杀之列时,此页面将被显示。该页面JSP代码如程序57所示。

程序57errorInfo.jsp 客户IP被封杀信息显示页面。



<%@ page language="java" contentType="text/html; charset=UTF-8"

pageEncoding="UTF-8"%>

<!DOCTYPE html>

<html>

<head>

<title>网上商城</title>

</head>

<body>

<h1>错误信息</h1>

<hr/>

对不起,您无法访问网上商城。<br/><br/>

因为您的IP已经被封杀!

<hr/>

</body>

</html>





此错误页面只显示简单的错误信息,关键用于配合Servlet进行演示。实际应用项目中,错误信息页面应设计得与应用页面总体布局相符。

4. 系统主页面编程

当IP通过检查之后,跳转到系统的主页面。该页面通常用于功能导航,其代码如程序58所示。

程序58main.jsp 系统主页面代码。



<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<!DOCTYPE html>

<html>

<head>

<title>网上商城</title>

</head>

<body>

<h1>网上商城</h1>

<hr/>

欢迎您访问网上商城。<br/>

<a href="product/main.jsp">产品检索</a>

<a href="purchase/main.jsp"购物车</a>

<a href="order/main.jsp">订单管理</a>

<hr/>

</body>

</html>





目前的主页只是一个简单页面,并没有实质功能,仅用于演示Servlet和请求对象的功能,以及简单的功能导航。

5.4.3案例部署和测试

将开发完毕的Web项目部署到Tomcat服务器上,如果客户端IP地址与数据表的IP地址相同,则IP封杀检查Servlet将自动跳转到IP封杀信息页面,如图57所示。



图57IP封杀信息页面


修改数据表中的IP地址,使之与客户端的IP不同,再次请求此Servlet,则自动跳转到网上商城的主页main.jsp。

通过此案例,读者可以知道如何通过请求对象取得客户端的信息,并使用这些客户端信息进行特定的业务处理。

5.5文件上传请求处理案例

任何项目都需要提交上传文件给服务器,如增加员工时上传员工的照片、网上商城增加产品时上传产品图片、新闻网站增加新闻时上传新闻图片等,所有这些都涉及文件的上传处理。有时需要将上传文件写入数据库表,有时则需要将图片保存到服务器端的指定目录中。

本节将详细介绍使用Servlet的请求对象如何取得表单中上传的图片文件,以及在取得上传文件后,如何将其写入数据库或保存到服务器的文件系统的指定目录中。

5.5.1业务描述

在编写员工增加的JSP页面时,需要上传员工的照片。表单中包含文件域表单元素,文件域专门用于上传文件。输入员工信息后,单击“提交”按钮,请求员工增加处理的Servlet,实现增加员工的处理功能。

Servlet使用请求对象的方法,取得员工增加页面提交的数据,包括上传的图片文件,并将员工数据增加到员工表oa_employee中。员工表包含一个保存图片的二进制字段,其类型是longblob,此类型专门用于保存文件的原始格式,最大能存储2GB的文件。员工表oa_employee的字段结构如图58所示。



图58员工表oa_employee的字段结构


5.5.2案例编程

本案例包含一个员工增加的JSP页面,以及员工增加处理的Servlet。其中,员工增加JSP页面负责增加界面的显示,接收用户输入的新员工数据; Servlet负责取得员工增加JSP页面提交的数据,连接数据库,执行增加SQL语句,将新员工数据写入员工表oa_employee,同时将取得的员工照片保存到d:/temp目录下。

1. 员工增加JSP页面编程

员工增加JSP页面主要负责增加表单的显示,使用HTML的表单和表单元素即可。为简化案例的编程,该JSP页面并没有使用使其美观的框架(如Bootstrap等)对其进行美化,而实际项目一定会使用特定的UI框架对操作界面进行美观处理。员工增加JSP页面的代码如程序59所示。

程序59/employee/add.jsp 员工增加JSP页面。



<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<title>Insert title here</title>

</head>

<body>

<h1>员工增加</h1>

<form method="post" action="add.do" enctype="multipart/form-data" >

账号:<input type="text" name="id" /><br/>

密码:<input type="password" name="password"/><br/>

姓名:<input type="text" name="name"/><br/>

年龄:<input type="text" name="age"/><br/>

照片:<input type="file" name="photo"/><br/>

<input type="submit" value="提交"  />

</form>

</body>

</html>





需要注意的是,由于该JSP页面需要文件上传功能,因此对表单<form>的属性有特殊要求。首先,必须使用POST请求,GET请求无法提交上传文件数据,因此设置method="post"。其次,由于表单中有文件域元素,请求时需要传输文件,因此必须使用混合表单数据模式,不能使用默认的纯文本模式数据传输,因此要增加属性enctype="multipart/formdata",其中multipart/formdata表示请求体的数据是文本和二进制混合的数据。如果不指定enctype属性,则其默认值是xwwwformurlencoded格式,即HTML文本格式,只能传输纯文本数据给服务器。

2. 员工增加处理Servlet编程

为处理有文件上传的Servlet,必须进行特殊的配置,即启用Servlet引擎的文件上传功能。在Servlet 3.0之前无法直接处理文件上传,只能由开发者自己编程,或者使用第三方文件上传框架(如Apache Common Upload、JSP Smart Upload等)取得和处理上传的文件。从Servlet 3.0开始,Web组件内置了文件上传处理机制,增加了取得上传文件的类型jakarta.servlet.http.Part; 请求对象增加了getPart方法,用于取得Part类型的上传文件。5.2.5小节已经介绍了Part的常用方法,在此不再赘述。

在Servlet配置中,除了在类级别上使用常规的@WebServlet注解类对Servlet的请求地址和初始参数进行配置外,还需要使用注解类@MultipartConfig对Servlet类进行配置,该注解类用于启用Servlet 3.0内置的文件上传处理机制。如果不使用@MultipartConfig,则Servlet不能处理文件上传,只能处理提交的文本数据。员工增加处理的Servlet实现代码如程序510所示。

程序510EmployeeAddController.java员工增加处理Servlet。



package com.city.oa.controller;

import jakarta.servlet.ServletConfig;

import jakarta.servlet.ServletException;

import jakarta.servlet.annotation.MultipartConfig;

import jakarta.servlet.annotation.WebInitParam;

import jakarta.servlet.annotation.WebServlet;

import jakarta.servlet.http.HttpServlet;

import jakarta.servlet.http.HttpServletRequest;

import jakarta.servlet.http.HttpServletResponse;

import jakarta.servlet.http.Part;

import java.io.IOException;

import java.io.PrintWriter;

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.PreparedStatement;

/**

 * 员工增加处理Servlet

 */

@WebServlet(

urlPatterns = { "/employee/add.do" }, 

initParams = { 

@WebInitParam(name = "driver", value = "com.mysql.cj.jdbc.Driver"), 

@WebInitParam(name = "url", value = "jdbc:mysql://localhost:3319/cityoa"), 

@WebInitParam(name = "user", value = "root"),

@WebInitParam(name = "password", value = "root1234")







})

@MultipartConfig

public class EmployeeAddController extends HttpServlet {

private static final long serialVersionUID = 1L;

private Connection cn=null;

/**

* 初始化方法,取得配置的初始化参数

*/

public void init(ServletConfig config) throws ServletException {

String drvier=config.getInitParameter("driver");

String url=config.getInitParameter("url");

String user=config.getInitParameter("user");

String password=config.getInitParameter("password");		

try {

Class.forName(drvier);

cn=DriverManager.getConnection(url,user,password);

}

catch(Exception e) {

e.printStackTrace();

}

}

/**

* Servlet销毁方法,关闭数据库连接

*/

public void destroy() {

try {

cn.close();

}catch(Exception e) {

e.printStackTrace();

}

}

//处理GET请求的方法,取得客户端提交的员工数据,包括上传的员工照片

//将数据增加到数据库表中,并保存员工照片到d:/temp目录

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {



//取得员工增加表单提交的数据

String id=request.getParameter("id");

String password=request.getParameter("password");

String name=request.getParameter("name");

String sage=request.getParameter("age");

//取得员工照片

Part photo=request.getPart("photo");		

response.setContentType("text/html");

response.setCharacterEncoding("UTF-8");

PrintWriter out=response.getWriter();

out.println("<h1>员工增加处理Servlet</h1>");

String sql="insert into oa_employee (EMPID, EMPPASSWORD, EMPNAME, AGE, PHOTO, PHOTOCONTENTTYPE)  values (?,?,?,?,?,?)";

try {

int age=Integer.parseInt(sage); //转换年龄类型为int

//将上传的图片保存到d:/temp目录下

if(photo!=null&&photo.getSize()>0) {

PreparedStatement ps=cn.prepareStatement(sql);







ps.setString(1, id);

ps.setString(2,password);

ps.setString(3,name);

ps.setInt(4, age);

//取得上传文件的输入流,写入SQL语句

ps.setBinaryStream(5, photo.getInputStream(),photo.getInputStream().available());

ps.setString(6, photo.getContentType());

ps.executeUpdate(); //执行SQL语句

ps.close();

//上传文件保存到指定目录

photo.write("d:/temp/"+photo.getSubmittedFileName());

}

}

catch(Exception e) {

out.println("增加员工异常:"+e.getLocalizedMessage());

}

out.flush();

out.close();		

}

// POST请求处理,直接调用GET方法处理,让doGet方法处理请求数据

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

doGet(request, response);

}

}





Servlet代码在类级别上使用了@MultipartConfig,这是需要特别注意的。之前的Servlet都没有此注解类,今后在开发有文件上传功能的Servlet时一定要增加此注解类。

在处理文件上传的代码中,使用Part的getInputStream方法取得上传文件的字节输入流,将其set到数据库表的PHOTO字段。JDBC在执行insert语句时,自动读取字节流中的数据,写入该字段中。

Servlet处理代码最后使用“photo.write("d:/temp/"+photo.getSubmittedFileName())”将上传的图片写入指定目录中。上传文件通常保存到数据库或服务器的目录中,具体选择哪种方式要根据项目的实际需求决定。如果项目需要快速读取上传的文件,则推荐保存到目录中,因为从数据库中读取文件速度太慢; 如果安全性要求较高,则对上传文件的访问有权限限制,推荐将其保存到数据库。

5.5.3案例部署和测试

将编写完成的案例项目部署到Tomcat服务器,使用浏览器请求增加员工的JSP页面,


图59员工增加页面显示


如图59所示。

输入新员工对应的数据,尤其是要选择员工的照片文件。单击“提交”按钮,请求到增加处理Servlet。Servlet取得请求数据后,完成处理,在数据库员工表oa_employee中增加一条新记录。增加的员工记录数据如图510所示。



图510增加的员工记录数据


如果图片上传成功,则PHOTO字段显示“(Binary/Image)”,表明文件已经存储到数据库中; 如果没有文件上传,则显示“(NULL)”。单击PHOTO的“(Binary/Image)”,MySQL的客户端工具SQLYog弹出显示存储的图片对话框,如图511所示。



图511表中存储的图片


Servlet处理完成后,直接在Servlet中显示处理信息,如图512所示。

此Servlet在保存上传图片到数据库中的同时,也将文件保存到D:/temp目录中。使用Windows的资源管理器,可以看到此目录下有上传的文件,如图513所示。



图512员工增加处理后的显示信息




图513将上传图片保存到D:/temp目录中



通过Servlet的编程可见,新版Servlet由于内置了文件上传机制,使得处理文件上传的编程非常简单,不再需要引入并使用第三方文件上传框架。

简答题

1. 简述请求对象的生命周期。

2. 描述请求对象的主要方法。

实验题

1. 创建Web项目: 项目名: erpweb; 项目使用规范: Jakarta EE 6.0。

2. 创建增加客户表单页面/customer/add.jsp,显示增加客户表单: 

编号: 文本框

登录密码: 密码框

公司名称: 文本框

是否上市: 是 否 单选按钮。

购买产品: 复选框(至少有4个产品名称)

公司人数: 文本框

年销售额: 文本框

提交按钮

提交后请求Servelt进行处理。

3. 编写客户增加处理Servlet。

(1)包名: com.city.erp.servlet。(2)类名: CustomerAddAction。(3)映射地址为: /customer/add.do。(4)功能: 取得表单提交的数据,根据需要进行相应的数据类型转换,在创建的Customer表中增加一个新客户。处理成功后显示“处理完毕”和返回超链接,否则显示异常信息。

4. 创建数据库和客户表

使用本机的MySQL,创建数据库。

数据库名称: cityerp。

用户: cityerp。

密码: cityerp。

表: Customer,其结构如表53所示。


表53Customer表字段结构


字段名类型说明


CompanyIDVarchar(20)公司ID
PasswordVarchar(20)密码
CompanyNameVarchar(50)公司名称
staffnumInt公司人数
IncomeDecimal(18,2)年销售额
CompanyTypeChar(4)是否上市
ProductsVarchar(200)购买产品列表,使用空格分开