第3章 请求与响应 C/S架构的程序,其数据请求和数据处理都在客户端完成,只在数据存取时访问服务器后台,设计过程相对简单; B/S架构的程序,其数据请求在客户端的浏览器实现,该请求要发送到后台Web服务器,数据处理与响应由后台服务器完成,最终的处理结果再发送回客户端浏览器。因此,B/S架构的Web程序设计存在两大技术难点,一个是如何把客户端的请求数据发送到服务器,另一个是服务器将请求所需数据处理完成后如何把结果返回给客户端浏览器。 视频讲解 3.1请求响应模型 客户端的JSP页面一般是将页面数据和请求信息放置在
(3) 新建cn.pju.requestapp.svlt包,在该包下新建名为RequestParamsServlet的Servlet类,代码如下。 package cn.pju.requestapp.svlt; 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("/RequestParamsServlet") public class RequestParamsServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); String[] researcharea = request.getParameterValues("researcharea"); System.out.println("用户名: " + username); System.out.println("密码: " + password); System.out.println("研究领域: "); for (String ra : researcharea) { System.out.println(ra); } } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } } (4) 将项目配置到Tomcat容器上,启动服务器,在浏览器中输入访问地址http://localhost:8080/0303RequestApp/regist.jsp,打开如图35所示的用户注册界面。 图35用户注册界面 (5) 在账号栏和密码栏中分别输入admin和sysman,全选三个研究方向复选框,单击“提交”按钮,观察后台Console窗口数据显示,如图36所示。 图36运行结果 2. 解决请求参数的中文乱码问题 如果在如图35所示的用户注册界面输入中文字符的账号(比如“张俊”),再次单击“提交”按钮发送至后台的Servlet程序处理,则解析结果为乱码,如图37所示。 图37中文参数值解析出现乱码 在此分析一下,客户端浏览器向后台服务器传递中文参数值,出现中文乱码现象的原因。 首先,当客户端浏览器表单中输入了中英文参数值并通过表单提交按钮向后台服务器提交时,浏览器会根据默认设置的编码格式对参数值进行封装,不同浏览器的默认字符编码不尽相同,而且用户可以根据自身需要进行调整设置。以IE浏览器为例,在客户端输入中文账号,在浏览器空白区域右击,展开“编码”菜单,可见浏览器当前使用的默认编码方式为Unicode(UTF8),如图38所示,也可根据需要切换简体中文(GB2312)字符编码格式。浏览器设置的字符编码不同,输入框中的数据传递给form表单的实际二进制编码内容也不同。 图38浏览器编码格式 其次,客户端封装后的数据传递到后台服务器,由HttpServletRequest对象进行数据解析,解码时也会用到字符编码,如果解码时所使用的字符编码与封装时使用的字符编码不一致,就会出现中文乱码现象。在Servlet中可以使用函数setCharacterEncoding(String encoding) 来设置Request对象编码格式,如request.setCharacterEncoding("UTF8"),需要注意的是该语句必须写在doGet()函数的首行,这是因为doGet()函数在执行函数代码之前首先检测是否有字符编码设置函数语句进行了编码设置,如果未发现则默认采用ISO88591编码格式作为首选,然后继续执行函数体内其他语句,当再次遇到setCharacterEncoding()函数设置不同代码时,将忽略该语句。 最后,客户端参数的编码封装格式还与form表单的提交方式有关,这是因为setCharacterEncoding()函数只对post方式提交的表单有效,对get方式提交的表单无效。 有的读者可能会产生疑问,为什么英文字母和阿拉伯数字不存在乱码问题,而唯独中文会出现乱码?这是因为英文字母和阿拉伯数字数量相对较少,所以在ISO88591、Unicode(UTF8)和简体中文(GB2312)等常用编码中,这些符号的编码都与其ASCII码值保持一致,也就是说在这些编码机制中,其编码都是相同的,所以无论使用何种编码进行封装和解析,都不会出现乱码现象。而中文具有其特殊性,在不同的编码格式中,同一个汉字对应的编码是不同的。比如在A编码格式中,汉字“中”存储在x行y列,我们将其编码设定为xy,而在B编码格式中,xy编码对应的x行y列存储的可能是另一个汉字或者是某个汉字的一部分,此时就会出现乱码或前后解析不符的现象。其实不光中文数据在客户端与服务器端编码不一致时会产生乱码现象,常见的CJK编码都会出现类似现象,这是由这些语言的特殊编码格式所决定的。 总结以上分析可见,客户端浏览器将表单数据进行封装然后向后台服务器传递中文参数值,服务器端由Request对象再进行数据解析,出现乱码现象与客户端浏览器编码格式、表单提交方式和服务器端Request对象进行解析时使用的CharacterEncoding编码格式三个因素有关。在实际使用过程中,只要三者设置保持同一种编码或相互兼容的编码格式,就可避免中文解析出现乱码现象。修改后的源程序文件RequestParamsServlet.java代码如下。 package cn.pju.requestapp.svlt; 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("/RequestParamsServlet") public class RequestParamsServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); String[] researcharea = request.getParameterValues("researcharea"); System.out.println("用户名: " + username); System.out.println("密码: " + password); System.out.println("研究领域: "); for (String ra : researcharea) { System.out.println(ra); } } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } } 3. 通过Request对象传递数据 在客户端的浏览器通过表单中的控件向后台Servlet传递的数据一般被当作Parameter参数保存,如果想向Request对象中插入其他非参数变量,则可使用setAttibute()方法设置参数,使用getAttribute()函数进行参数值的读取。Request对象常用的属性操作函数如表36所示。 表36Request对象常用的属性操作函数 方法说明 void setAttribute(String name, Object obj)向ServletRequest对象中写入名为name的属性并设置其值为obj; 若已存在name属性,则覆盖; 若obj值为null,则删除指定属性,效果等同于removeAttribute() Object getAttribute(String name)获取ServletRequest对象中名为name的属性值 removeAttribute(String name)在ServletRequest对象中删除名为name的属性 Enumeration getAttributeNames()返回一个包含ServletRequest对象中所有属性的Enumeration对象 修改源程序文件RequestParamsServlet.java,将注册结果写入ServletRequest对象的registOK属性,代码如下。 package cn.pju.requestapp.svlt; 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; import com.sun.org.apache.xpath.internal.operations.And; @WebServlet("/RequestParamsServlet") public class RequestParamsServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setCharacterEncoding("UTF-8"); request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); String[] researcharea = request.getParameterValues("researcharea"); System.out.println("用户名: " + username); System.out.println("密码: " + password); System.out.println("研究领域: "); for (String ra : researcharea) { System.out.println(ra); } //注册结果判断 if(username != null && !username.isEmpty() && password != null && !password.isEmpty() && researcharea != null && researcharea.length > 0 ) { request.setAttribute("registOk", true); } else { request.setAttribute("registOk", false); } if((Boolean)request.getAttribute("registOk")) { response.getWriter().println("注册成功"); } else { response.getWriter().println("注册失败"); } } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } } 输入合法的用户名、密码,选择对应的研究领域选项,单击“提交”按钮,客户端浏览器反馈结果如图39所示。 图39注册成功提示 视频讲解 3.3HttpServletResponse对象 HTTP应答与HTTP请求相似,也由3个部分构成,分别是状态行、响应头(Response Header)和响应正文。在Servlet API中,一般通过HttpServletResponse接口将服务器端的信息传递给客户端,HttpServletResponse接口专门用来封装HTTP响应消息,它是ServletResponse接口的子接口。与HTTP响应消息分为状态行、响应消息头、消息体三部分相对应,HttpServletResponse接口中定义了向客户端发送响应状态码、响应消息头、响应消息体的三类方法。 3.3.1常用方法 1. 发送状态码相关的方法 Servlet向客户端浏览器回送Response响应消息时,需要在消息中设置一个状态码。HttpServlet Response接口定义了两个常用发送状态码的方法。 1) setStatus(int status)方法 设置HTTP响应消息状态码的值。Response响应状态行中的状态描述信息直接与状态码相关,如404状态码表示找不到客户端所请求的资源。正常情况下,Web服务器会默认产生一个状态码为200的状态行。状态行由协议版本、数字形式的状态代码,及相应的状态描述组成,各元素之间以空格分隔。而HTTP版本由服务器确定,因此只要通过setStatus(int status)方法设置了状态码,即可实现状态行的发送。 2) sendError(int code)方法 当需要向客户端发送错误状态码时可使用此方法,比如sendError(404) 表示找不到客户端请求的资源。该方法提供了以下两个重载格式。 public void sendError(int code) throws java.io.IOException public void sendError(int code, String message) throws java.io.IOException 其中,sendError(int code)方法只是发送错误信息的状态码,sendError(int code, String message)方法在发送错误状态码code的同时还增加了一条用于提示说明的文本信息message,该文本信息将会显示在发送给客户端的正文内容中。 2. 发送响应消息头相关的方法 由于HTTP有多种响应头字段,所以在 HttpServletResponse接口中对应定义了一系列设置HTTP响应头字段的方法,如表37所示。 表37设置响应消息头字段的相关方法 方法说明 void addHeader(String name, String value)添加响应头的名字,以及与响应头名字对应的值。如果name对应的响应头不存在,则新增一个; 如果响应头已存在,则增加一个同名的name响应头,HTTP响应消息中允许同一名称的头字段出现多次 void setHeader(String name, String value)设置与响应头名字对应的值。如果name对应的响应头不存在,则新增一个; 如果响应头已存在,则将用新的设置值取代原来的设置值 void addIntHeader(String name, int value)添加响应头的名字,以及与响应头名字对应的整型值。函数用法与addHeader(String name, String value)类似,仅用于设置响应头对应值为整型时的情况 void setIntHeader(String name, int value)设置与响应头名字对应的整型值。函数用法与setHeader(String name,String value)类似,仅用于设置响应头对应值为整型时的情况 void setContentLength(int len)设置响应消息中实体内容的长度,单位是字节。对于HTTP来说,该方法等价于设置ContentLength响应头字段的值 void setContentType(String type)设置Servlet输出内容的MIME类型,对于HTTP而言,就是设置ContentType响应头字段的值。例如,如果发送到客户端的内容是jpeg图像数据时,就需要将响应头字段的类型设置为“image/jpeg”; 如果响应的内容为文本,需要使用setContentType()方法设置字符编码,如text/html;charset=UTF8 void setLocale(Locale loc)设置响应消息的本地化信息。即设置ContentLanguage响应头字段和ContentType头字段中的字符集编码。如果HTTP消息没有设置ContentType头字段,setLocale()方法设置的字符集编码就不会出现在任何HTTP消息的响应头中,如果调用setCharacterEncoding)或setContentType()方法指定了响应内容的字符集编码,setLocale()方法将不再具有指定字符集编码的功能 void setCharacterEncoding(String charset)设置输出内容使用的字符编码,即设置ContentType头字段中的字符集编码部分。如果没有设置ContentType头字段,setCharacterEncoding方法设置的字符集编码不会出现在HTTP消息的响应头中。SetCharacterEncoding()方法比setContentType()和setLocale()方法的优先权高,它的设置结果将覆盖setContentType()和 setLocale()方法所设置的字符码表 3. 发送响应消息体的相关方法 设置响应消息体的相关方法如表38所示。 表38设置响应消息体的相关方法 方法说明 ServletOutputStream getOutputStream()获取ServletOutputStream类型的字节输出流。该函数可以直接输出字节数组中的二进制数据,常用于输出二进制格式的响应正文 PrintWriter getWriter()获取PrintWriter类型的字符输出流对象,该对象可以直接输出字符文本内容 【例34】测试HttpServletResponse对象发送消息体相关函数。 (1) 在Eclipse中新建一个名为0304MyResponse的项目,并将整个项目的编码格式设置为UTF8。 (2) 新建一个名为MyPrintServlet的Servlet,放置在cn.pju.response包下,详细代码如下。 package cn.pju.response; import java.io.IOException; import java.io.OutputStream; 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("/MyPrintServlet") public class MyPrintServlet extends HttpServlet { private static final long serialVersionUID = 1L; public MyPrintServlet() { } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setCharacterEncoding("UTF-8"); //第一句,设置服务器端编码 response.setHeader("Content-Type","text/html;charset=UTF-8");//第二句,设置浏览器端解码 String data = "Java Web程序设计"; OutputStream out = response.getOutputStream(); out.write(data.getBytes()); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } } (3) 将项目配置到Tomcat容器上,启动服务器,在浏览器中输入访问地址http://localhost:8080/0304MyResponse/MyPrintServlet,运行结果如图310所示。 图310运行结果 (4) 修改程序代码,改用response对象的getWriter()方法获取PrintWriter对象,然后调用该对象的write(String data)方法直接输出字符型数据,代码如下。 package cn.pju.response; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; 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("/MyPrintServlet") public class MyPrintServlet extends HttpServlet { private static final long serialVersionUID = 1L; public MyPrintServlet() { } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); String data = "Java Web程序设计"; PrintWriter writer = response.getWriter(); writer.write(data); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } } 程序运行效果同图310。 需要注意的是,response对象的getOutputStream()、getWriter()函数不可同时使用,否则会发生IllegalStateException异常。 3.3.2相关应用 1. 解决中文输出乱码问题 在Web项目开发过程中,经常会用到几种常见的字符编码,有ISO88591、UTF8、GBK、GBK2312、Big5等。英文字母和阿拉伯数字在这些编码表中的数值是一致的,所以采用不同的编码方式进行英文字母输出,不会出现乱码问题。而对于CJK字符集,在不同的字符编码集合中其对应的编码是不相同的,甚至有的字符编码集合不支持CJK字符,所以Java Web项目输出中文字符时,有可能出现乱码,其主要原因就是后台程序与前端浏览器所使用的字符编码不一致。 1) Java Web项目整个项目的编码格式 在Eclipse中选中当前项目,选择右击弹出菜单中的Properties,打开项目属性对话框,弹出如图311所示的属性设置窗口,选择左侧列表中的Resource,在右侧的Text file encoding属性中选择对应的编码格式。 图311项目编码设置 2) 后台源程序文件的编码格式 如果在项目开发过程中先设置了整个项目的编码格式,则后续新建的源代码文件都将继承项目的编码格式以保持项目本身的整体一致性。如新建项目之后立刻设置了项目的编码格式为UTF8,然后再创建源代码文件,则源代码文件的默认编码格式也会是UTF8。但也有读者是先创建了项目,然后创建了几个源程序文件之后才想起要设置项目的编码格式,但此时设置了项目编码格式之后,源程序文件的编码格式不会随着项目编码格式的改变而自动改变,这就需要手动设置源程序文件的编码格式,以保持与项目编码格式的一致。 在Project Explorer窗口选中需要设置的源代码文件,在右击弹出菜单中选择Properties,在如图312所示的源程序文件属性设置窗口中设置其编码格式。 图312源程序文件属性设置 3) 程序运行时后台服务器对应的编码格式 前端用户浏览器接收到的数据都是由后台服务器发送而来的,Web服务器需要将后台的数据封装打包发送,所以后台程序也有自己的编码格式,该编码格式使用response.setCharacterEncoding("UTF8")语句进行设置。 4) 前端页面输出时的编码格式 前端客户浏览器接收到数据之后,应按照服务器预期的编码格式进行解码,然后输出显示。在程序中通过response.setHeader("ContentType","text/html;charset=UTF8")设置浏览器的期望编码格式,有些浏览器设置了自动编码解析,只要在程序中设置了期望编码,浏览器就会自动切换相应的编码进行输出显示。 5) 用户端浏览器实际使用的编码格式 客户浏览器端的实际使用编码不受源程序的限制,用户可以通过如图313所示方法自行强制调整,但通常情况下,客户端浏览器是启用编码自动检测功能的。 图313浏览器端的编码设置 如果想避免中文字符编码乱码现象出现,建议以上五种编码方式尽量全部统一。在Servlet源程序代码中,一般使用以下两种方法进行编码设置,以确保避免乱码现象出现。 (1) 分别设置服务器端和客户端的编码方式。 response.setCharacterEncoding("UTF-8");//设置服务器端HttpServletResponse使用UTF-8编码 response.setHeader("Content-Type","text/html;charset=UTF-8");//通知浏览器使用UTF-8编码 (2) 使用setContentType()函数一次性设置,该函数包含以上两行代码的相同功能而且简洁明了,所以一般经常使用。 response.setContentType("text/html;charset=UTF-8"); 在例34的两种写法中,其实就分别使用了上述两种方法进行编码设置,读者可仔细阅读比较一下。 2. 解决中文输出乱码问题 在HTTP中定义了一个Refresh头字段,用来设置指定时间内自动刷新和到期后的页面跳转。 【例35】测试HttpServletResponse对象的定时刷新功能。 在项目0304MyResponse的源程序文件夹中新建一个名为RefreshServlet的Servlet类,代码如下。 package cn.pju.response; 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("/RefreshServlet") public class RefreshServlet extends HttpServlet { private static final long serialVersionUID = 1L; public RefreshServlet() { super(); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setHeader("Refresh", "5;URL=http://www.163.com"); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } } 其中,response.setHeader("Refresh", "5;URL=http://www.163.com")语句表示设置页面5s之后跳转到http://www.163.com页面。分号前的5代表5s后进行刷新,如果在分号后添加了URL属性,则跳转到对应的页面。如果不设置URL属性,则只对当前页面进行刷新,不进行页面跳转。 例如,将以上程序修改为如下源代码,即可实现系统时间的每秒输出。 package cn.pju.response; 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("/RefreshServlet") public class RefreshServlet extends HttpServlet { private static final long serialVersionUID = 1L; public RefreshServlet() { super(); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //设置每隔2s定期刷新 response.setHeader("Refresh", "2"); //输出服务器当前时间 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); response.getWriter().println(sdf.format(new java.util.Date())); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } } 小结 本章主要介绍了HttpServletResponse对象和HttpServletRequest对象的使用,其中,HttpServletResponse对象封装了HTTP响应消息,并且提供了发送状态码、发送响应消息头、发送响应消息体的方法。使用这些方法可以解决中文输出乱码问题,实现网页的定时刷新跳转、请求重定向等。HttpServletRequest对象封装了HTTP请求消息,也提供了获取请求行、获取请求消息头、获取请求参数的方法。使用这些方法可以解决请求参数的中文乱码问题,并且使用request域对象传递数据的方法,还可以实现请求转发和请求包含。HttpServletResponse和HttpServletRequest在Web开发中至关重要,要认真学习,牢固掌握。 习题 1. 简述请求转发与重定向的异同(至少写3点)。 2. 请编写一个类,该类能够实现访问完App应用下的Servlet后,5s后跳转到index.jsp页面。 3. 编写一个系统登录界面,登录成功跳转到welcome.jsp,否则提示错误并跳转到login.jsp页面。