对自己说:
前面主要讲了java web 基础知识以及Servlet接受GET请求数据,码了3W字,基本抄书还不不敢公开,从书上第三章开始,尽可能根据自己的理解来记笔记。
有实例源码,人麻了
目录
过程分析:浏览器发送 POST 请求数据和 Web 服务器接收 POST 请求数据
小露身手:request 请求对象接收 application/x-www-form-urlencoded 格式的 POST 请求数据
小露身手:向 Servlet 添加 multipart/form-data 支持
HTTP响应数据与HTTPServletResponse之间的关系
场景4步骤 重定向、定时刷新、请求转发、请求包含等的使用以及文本型数据封装到响应体
场景5使用Servlet的response响应对象,在浏览器页面中显示图片或者下载图片
场景7 向Servlet程序的doGet()方法新增如下代码
第三章 Servlet 接受 POST 请求数据
FORM表单
Form 表单由表单标签、表单控件和表单按钮三部分组成。
1. 表单标签
外观上:类似于 Excel 工作表的虚框。表单标签像编剧,默默存在,决定剧情
功能上:定义了数据的发送方式、处理数据、数据的 MIME。表单标签的语法格式如下:
<form method="post" action="处理程序" enctype="multipart/form-data" > 这里是表单控件的代码和表单按钮的代码 </form>
重点属性讲解如下:
method:设置 FORM 表单数据的发送方式,值为 get 或 post,默认为GET
action:设置FORM表单里输入的数据发送给哪个程序处理。若不设置,或者值为空字符串(即 action="")时,表示表单数据发送给自己(当前程序)处理。action 设置出错,将会导致 404 错误。
enctype:设置表单数据的内容格式(实际上是MIME)。若不设置,默认值为 application/x-www-form-urlencoded。如果希望通过表单上传文件,enctype 必须设置为 multipart/form-data,并且 method 必须设置为 post。
2.表单控件
外观上:表单控件在浏览器上可见(隐藏域除外)。表单控件像演员
功能上:允许浏览器用户输入数据或者选择数据。表单控件包括单行文本框、密码框、隐藏域、复选框、单选按钮、文件上传框、多行文本框和下拉选择框等
单行文本框:
用户名: <input type="text" name="name" value="victor" id="ID值"/>
显示效果为:
属性讲解:
type="text":定义单行文本输入框
name:定义表单控件的名字,几乎所有的表单控件都有名字,Servlet 程序通过 name 的值区分各个表单控件。
value:定义初始值
id:设置了唯一标识符,唯一标记 HTML 页面上的元素。在同一个 HTML 页面上,必须确保ID值唯一,不能重复
说明:如果没有设置 type 属性,那么 type 属性默认值为 text。
密码框:
密码:<input type="password" name="password" value="1234"/>
显示效果为:
属性讲解:
type="password":定义密码框
隐藏域:
<input type="hidden" name="userID" value="6"/>
隐藏域,顾名思义,隐藏域在浏览器上不可见
属性讲解:同上,举一反三
复选框:
<input name="inserest" type="checkbox" value="music"/>音乐 <input name="inserest" type="checkbox" value="game" checked/>游戏 <input name="inserest" type="checkbox" value="film" checked/>电影
显示效果:
重点讲解:
type="checkbox":定义复选框
checked:表示该复选框默认被选中,该属性无须设置值
单选按钮:
<input name="sex" type="radio" value="male" checked/>男 <input name="sex" type="radio" value="female"/>女
显示效果:
重点讲解:
name:定义表单控件的名字。要想保持单选按钮之间互相 “排斥”,必须保证单选按钮的 name相同。
type="radio":定义单选按钮
checked:参考复选框的 checked
文件上传框:
<input type="file" name="myFiles" multiple/>
显示效果,注:点击选择文件弹出的框在网页界面的左上角:
重点属性讲解:
type="file":定义了文件上传框
multiple:表示允许选中多个文件(按住 Ctrl 键来选中多个文件)。若无,表示只能选中一个文件。
注意:表单标签 <form>的 enctype 属性必须设置为 multipart/form-data,method 属性必须设置为 post,才能上传文件。
多行文本框:
备注:<textarea name="remark" cols="30" row="4">示例代码</textarea>
显示效果:
重点属性讲解:
cols:定义多行文本框的宽度(单位是px)
rows:定义多行文本框的高度(单位是px)
content:多行文本框默认显示的文字内容(这里是"示例代码")
下拉选择框:
下拉选择框分为单选下拉选择框(类似于单选按钮)和多选下拉框(类似复选框)
<select name="hobby" size="3" multiple> <option value="music" selected>音乐</option> <option value="game" selected>游戏</option> <option value="film">电影</option> </select>
显示效果:
<select>标签用于定义下拉选择框,重点属性讲解如下:
size:定义下拉选择框的高度,默认值为1
multiple属性:同上文件选取
<option>标签用于定义下拉选择框,重点属性讲解如下:
value:指定某个选项的值。若没有指定,则选项的值为<option>和</option>之间的内容。
3.表单按钮
单击按钮触发执行 action 属性指定的处理程序。常用的表单按钮有提交按钮(submit)和重置按钮(reset)
提交按钮:
提交按钮用于将FORM表单输入的数据发送到<form>标签 action 属性指定的处理程序。有两种方法可以创建提交按钮。
<input type="submit" name="login" value="普通提交按钮"/> <button type="submit" name="login1">普通提交按钮</button>
显示效果为:
值得说明的是:第二行的提交按钮上可以显示一些特殊字符
重置按钮:
重置按钮并不是将表单控件输入的信息清空,而是将表单控件恢复到初始值状态(或者默认状态),初始值由表单控件的 value 值决定。下面两种方式都可以创建重置按钮:<input type="reset" name="cancel" value="重新输入"/> <button type="reset" name="login">重新输入</button>
显示效果:
小露身手:利用 FORM 表单模拟发送 GET 请求数据
准备工作:
在 Eclipse 中创建 Dynamic Web Project 项目 post
将 Servlet-api.jar 包导入 Web 项目
步骤:
(1)在 WebContent 目录下创建 form0.jsp 程序,输入如下代码:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="ISO-8859-1"> <title>Insert title here</title> </head> <body> <form action="ABCServlet" method="get"> 用户名:<input type="text" name="name" value="张三" /><br/> 兴趣爱好:<input type="checkbox" name="hobby" value="shopping" checked>购物 <input type="checkbox" name="hobby" value="music" checked>音乐<br/> <input type="submit" value="发送"><input type="reset" value="重置"> </form> </body> </html>
(2)将第二章 get 项目的 ABCServlet 程序和 BCDServlet 程序复制到 post 项目中
具体步骤:打开 Project Explore 视图 -> 展开刚刚创建的WEB项目 -> 展开 Java Resources -> 右击 src -> 弹出 New Java Package 窗口。Java package 文本框处输入 controller
然后将 get 项目的 ABCServlet 和 BCDServlet 程序复制到 post 的 controller 包中
(3)部署本项目,启动 Tomcat。
(4)打开浏览器,输入网址 http://localhost:8080/post/form0.jsp,执行 form0.jsp 程序,执行结果如图所示:
(5)单击发送按钮,触发 ABCServlet 的 doGet() 方法执行
分析:form0.jsp 利用 FORM 表单向 ABCServlet 发送了 GET 请求,并且发送的数据与第二章直接在浏览器地址栏上输入的URL发送的数据完全相同。因此,本次 Tomcat 控制台输出的信息几乎和第二章的完全相同(部分请求头信息不一样,例如请求中新增了referer请求头)
URL 路径定位方法
前引:ABCServlet.java 的 urlPatterns 是 “/ABCServlet”,能不能将 form0.jsp 里 <form> 标签里的 action 属性值 “ABCServlet” 修改 “/ABCServlet” 呢?答案明显是不能的,否则会出现404错误,如何设置正确的路径,让一个程序能够访问另一个资源呢?
URL路径可以分为绝对路径和相对路径,而相对路径可以再分为:server-relative 路径和page-relative 路径。
URL绝对路径
URL绝对路径通常在访问系统外部资源时才使用,访问系统内部资源一般使用URL相对路径。
URL相对路径
使用URL相对路径访问某个 WEB 服务器的目的资源时,Web开发人员必须用清楚两个问题:起始目录在哪儿;目的资源在哪儿(目的路径)。
1.server-relative 路径
该路径的典型特征是以 “/” 开头。“/”的含义又可以细分为如下两种情形
情形1:A 通过浏览器访问目的资源 B
典型的有:通过表单<form>标签里的action属性、超链接<a>标签里的 href 属性、CSS样式表<link>标签里的 href 的属性、JavaScript<script>标签里的 src 属性、图片<img>标签的 src 属性、重定向 redirect、定时刷新 refresh,甚至直接在浏览器地址栏中输入目的资源的URL。
该情形中的 “/” :表示将 Web 服务器的根目录作为初始目录。例如本地 Web 服务器根目录对应的 URL 是 http://localhost:8080。
例如:form0.jsp中的action属性属于情形该情形,server-relative 路径应该是 /post/ABCServlet(ABCServlet 相对于 Web 服务器根目录的目录层次是 /post/ABCServlet)
回顾:获取Web项目虚拟路径的方法是 request.getContextPath()
情形2:Web 项目的程序 A 直接访问该项目的目的资源 B
典型的有请求转发和请求包含
该情形的共同特征是:A 访问 B 是在 Web 项目内完成,与浏览器没有任何关系。
该情形的 “/”:表示将 Web 项目的虚拟路径作为起始目录(Web 项目的虚拟路径对应 Web 项目根目录,注意不是 Web 服务器的根目录)
例如:ABCServlet 将 request 请求对象请求转发至目的资源 BCDServlet ,此时server-relative路径为:/BCDServlet(起始目录:http://localhost:8080/post 目的路径:http://localhost:8080/post/BCDServlet;计算路径将两者相减得出BCDServlet相对于Web项目虚拟路径的目录层次即server-relative)
将操场当做 Web 服务器,操场的每条跑道看做 Web 服务器上部署的 Web 项目;跑道上的每名运动员看做 Servlet 程序,接力棒看做 request 请求对象,场外工作人员看做浏览器,操场上的每条跑道可以同时进行接力比赛,就像 Web 服务器可以同时部署多个 Web 项目;接力棒只能在同一个跑道内传递和接收,就像 request 请求对象只能在同一个 Web 项目内派发。
2. page-relative 路径
page-relative 路径将当前程序所在的 URL 路径作为起始目录,page-relative 路径的典型特征是不以 “/” 开头
示例1:form0.jsp 中的 action 属性怎么输入 page-relative 路径呢?
起始目录:form0.jsp 对应的 URL 路径是 http://localhost:8080/post/form0.jsp,因此 form0.jsp 文件的起始目录是 http://localhost:8080/post/
目的路径:form0.jsp 中的 action 属性要访问的目的资源是 ABCServlet,而 ABCServlet 对应的 URL 路径是:http://localhost:8080/post/ABCServlet
计算路径:http://localhost:8080/post/ABCServlet - http://localhost:8080/post/
计算结果:ABCServlet 相对于 form0.jsp 的目录层次是 ABCServlet.
位于同一目录,使用 page-relative 路径时,它们之间可以通过名称直接访问
page-relative 路径的技巧
- 使用目录分隔符时,尽量使用 “ / ” (而不是 “ \ ”),这样更由于程序需在不同操作系统(Windows 和 Linux 等)间的移植
- 可以使用 “ . ” 表示当前目录
- 位于同一目录的两个程序,“直呼其名”即可访问
例如 http://localhost:8080/1/2/a.jsp(如果 a.jsp 在2楼)访问 http://localhost:8080/1/2/b.jsp。因此 page-relative 路径是 “b.jsp”,也可以写作 “./b.jsp”- A 访问下级目录中的资源 B,直接指定下级目录层次和资源 B 的文件名即可。
例如 http://localhost:8080/1/a.jsp 访问 http://localhost:8080/1/2/3/b.jsp。因此 a.jsp 访问 b.jsp 的 page-relative 路径是 “ 2/3/b.jsp ”,也可以写成 “./2/3/b.jsp”- 可以使用 “../”表示当前目录的上级目录
- A 访问上级目录中的资源 B,需要使用“ ../ ”
小露身手:URL 路径定位方法
场景1 使用绝对路径,访问目的资源
步骤:
(1)form1.jsp文件创建 <body></body>中添加如下代码:
<% String contextPath = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getHeader("host") + contextPath; %> <form action="<%=basePath %>/ABCServlet" method="get" > 用户名:<input type="text" name="name" value="张三" /><br/> 兴趣爱好:<input type="checkbox" name="hobby" value="shopping" checked>购物 <input type="checkbox" name="hobby" value="music" checked>音乐<br/> <input type="submit" value="发送"><input type="reset" value="重置"> </form>
说明1:form1.jsp 的目的资源时 ABCServlet ,而 ABCServlet 的 URL 绝对路径是 http://localhost:8080/post/ABCSevlet,格式如下:
请求协议://接收请求的主机域名或者IP地址 + :+端口号/虚拟路径/ABCServlet
说明2:Java 变量 basePath 的值是一个封装了 ABCServlet 的URL绝对路径的字符串
说明3:代码片段“<%=basePath %>” 的功能是,将 Java 变量 basePath 的值输出到当前页面。
运行,检测URL路径是否正确。
场景2 通过浏览器,使用 server-ralative 相对路径访问目的资源
步骤:
(1)form2.jsp文件创建 <body></body>中添加如下代码:
<% String contextPath = request.getContextPath(); %> <form action="<%=contextPath %>/ABCServlet" method="get" > 用户名:<input type="text" name="name" value="张三" /><br/> 兴趣爱好:<input type="checkbox" name="hobby" value="shopping" checked>购物 <input type="checkbox" name="hobby" value="music" checked>音乐<br/> <input type="submit" value="发送"><input type="reset" value="重置"> </form>
说明:form2.jsp 的目的资源文件是 ABCServlet。server-relative 相对路径的起始目录是 Tomcat 根目录,对应的 URL 是 http://localhost:8080,目的资源 ABCServlet 所对应的 URL 路径是 http://localhost:8080/post/ABCServlet。因此,form2.jsp 使用路径 “/post/ABCServlet”,即可访问 ABCServlet。
结论:通过浏览器使用 server-relative 相对路径访问目的资源时,需要指定 Web 项目的虚拟路径 contextPath。
场景3 使用 page-relative 路径,访问目的资源
步骤:
(1)form3.jsp 创建,并将代码片段 action = "./ABCServlet"
说明:千万不要忽视 “ / ”前面的 “ . ”,访问的路径就变成 server-relative 路径,变成了从Tomcat 服务器根目录访问 ABCServlet。场景4 浏览器端访问目的资源的方法比较
步骤:
(1)如图,并在 Folder Name 处输入 test(2)将form0~3.jsp,复制到 test目录,并依次运行这4个jsp程序。
(3)单击发送按钮,测试URL路径是否正确结论:A 使用 page-relative 路径访问目的资源 B 时,如果A的当前目录发生了变化,目录层次也要跟着发生变化。考虑到项目移植,不建议使用 page-relative 路径访问目的资源。使用URL绝对路径 和 server-relative 路径,不用在意自己所处的位置。另外由于server-relative 路径简单明了,推荐使用 server-realative 路径访问目的资源B,综上所述,推荐使用场景2 中的URL路径定位方法。
过程分析:浏览器发送 POST 请求数据和 Web 服务器接收 POST 请求数据
准备工作
若要让浏览器发出 POST 请求,需要借助 FORM 表单,准备工作如下。
- 复制 form2.jsp,让新文件命名为 form4.jsp
- 将 form4.jsp 中的代码片段 method="get"修改为 method="post"
- 在浏览器输入 http://localhost:8080/post/form4.jsp
ps:这是浏览器向Web服务器发送的第一次 http 请求,该请求是GET请求。
过程分析:浏览器向 Web 服务器发送 POST 请求数据
- 浏览器显示了 form4.jsp的Form表单
- 单击发送,浏览器自动将Form表单数据“封装”成POST请求数据(这因为FORM表单的method 设置为 post)
- 浏览器主机与Web服务器建立网络连接(TCP/IP连接)
- 浏览器将“封装”好的POST请求数据发送到Web服务器。浏览器等待 Web 服务器响应。
POST请求数据的构成
常见的POST请求头及其含义如下:
Content-Length:浏览器通知 Web 服务器,POST请求体长度(单位是字节)。POST请求头必有Content-length。如果POST请求数据中没有请求体,那么Content-Length的值为0。
注意:POST请求体长度与字符编码有直接关系,如果没有指定字符集,大部分浏览器默认使用UTF-8编码。
Content-Length用于标记POST请求体的结束。
Cache-Control:max-age=0,表示若请求头中包含此内容,浏览器使用浏览器的缓存资源之前,先进行缓存资源是否有效的验证。说明1:输入网址,按Enter键,浏览器不一定会向 Web 服务器发送请求。有事为了环节压力,Web服务器可以控制浏览器,将浏览器上次访问的静态资源(例如 JavaScript文件、CSS文件、图片文件)缓存到浏览器本地磁盘。浏览器再次发出请求时,先自行判断这些本地资源是否在有效期内,若在有效期内,直接从本地获取即可,没必要从Web服务器下载这些静态资源。
请求2:在请求头和响应头中,都可以定义Cache-Control,但意义不同。
请求头中的 Cache-Control,浏览器发出HTTP请求时,浏览器判断它是否使用本地缓存资源。
响应头中的 Cache-Control,Web服务器用它控制浏览器,让浏览器设置缓存资源的有效期。Origin:浏览器通知 Web 服务器,本次HTTP请求从哪里发起,其值仅包括协议、域名、端口号。
Content-Type:浏览器通知 Web 服务器,POST 请求体的内容格式(实际上是MIME),要么是 application/x-www-form-urlencoded,要么是 multipart/form-data,取决于<form>标签 enctype 的值。
说明:Content-Length 请求头和 Content-Type 请求头是POST请求数据所特有的,GET请求数据没有这两个请求头。其他请求头,例如 Referer、Origin、Cache-Control以及Cookie,并不是POST请求数据所特有的,GET请求数据也可以有这些请求头。
Referer:引用页或者推荐人。浏览器通知Web服务器,本次HTTP请求来自哪个URL。Referer主要用于统计访问本网站
Cooike:浏览器通知 Web 服务器,本次HTTP请求的Cooike信息,有关Cooike和Session的更多知识在第六章。
空行:POST 请求数据的空行和GET请求数据的空行功能相同
POST请求体:两种内容格式 application/x-www-form-urlencoded 和 multipart/form-data,由<form>标签的 enctype 属性值决定。
GET和POST对比:
- 请求行:POST请求类型是POST,GET是GET
- POST请求特有
- 请求参数:GET请求的请求参数位于请求行,通过查询字符串发送到 Web 服务器,查询字符串被浏览器进行 URL 编码。POST请求的请求参数位于POST请求体
- 其他对比:GET没有请求体,POST可以没有请求体
小露身手:request 请求对象接收 application/x-www-form-urlencoded 格式的 POST 请求数据
准备工作
(1)向 ABCServlet 类的 doPost()方法中加 doGet(request,response);
(2)重新运行 form4.jsp,打开表单,单击 “发送” 按钮,POST请求数据由 doGet() 方法到控制台上。
场景1 GET请求与POST请求的区别之乱码问题
1. 浏览器编码方式和 Tomcat 服务器解码方式相同,因此使用request请求对象获取GET请求数据时,不会出现中文字符乱码问题
2. POST请求体的字符编码并不是由浏览器决定的,而是由JSP程序的 contentType 的charset 属性值决定的。POST请求体的字符编码无法统一,造成了 Tomcat 无法采用统一的字符解码方案。
解决方案:在 Servlet 程序中手动调用 request.setCharacterEncoding(String charset),将接收到的POST请求体里的参数,按照 charset 值解码
场景2 GET请求与POST请求的区别之请求转发不同
结论:POST请求的请求转发触发doPost()方法运行;同理doGet();
浏览器 向ABCServlet 用了doPost() 方法,调用了本身的 doGet()方法,后请求转发了BCDServlet,则调用的是BCDServlet 的doPost()方法。
场景3 GET请求与POST请求的区别之请求头不同
前文提及,不加赘述。
场景4 GET请求与POST混合发送的请求数据
准备工作:form2.jsp复制改为form5.jsp改为:
<form action="<%=contextPath %>/ABCServlet?hobby=gaming&hobby=study" method="post" >
form5调用。
结论1:还是调用 doPost() 方法,表单发送的数据是POST而不是GET
结论2:采用GET请求和POST请求混合发送时,request 请求对象能够接收所有的请求数据
小露身手:向 Servlet 添加 multipart/form-data 支持
form2-->form6,再修改为:
<form action="<%=contextPath %>/ABCServlet" method="Post" enctype="multipart/form-data">
场景1: 向 Tomcat 添加 multipart/form-data 格式支持
<Context docBase="get" path="/get" reloadable="true" source="org.eclipse.jst.jee.server:get"/><Context docBase="post" path="/post" reloadable="true" source="org.eclipse.jst.jee.server:post" allowCasualMultipartParsing="true"/></Host>
场景2:Servlet 添加 multipart/form-data 格式支持
(1)向ABCServlet 添加 multipart/form-data 格式支持
@WebServlet("/ABCServlet")
@javax.servlet.annotation.MultipartConfig
public class ABCServlet extends HttpServlet {场景3:创建支持多文件上传的FORM表单
步骤:复制form6 --> form7,在<form>加如下代码:
<input type="file"name="myFiles" multiple/><br/>
注意1:form的method必须是post,enctype必须设置为multipart/form-data
注意2:句中添加了 multiple,支持多文件选择
实践任务 Servlet 接收 POST 请求数据
1.目的
掌握 FORM 表单发送 GET 和 POST 请求数据的方法
掌握 URL 路径定位的方法
掌握获取 multipart / form - data 内容格式的 POST 请求体参数的方法
掌握多文件上传的实现方法
了解单文件和多文件混合上传的实现方法
2.任务
所有:小露身手
本实践任务:使用 request 请求对象实现单文件和多文件混合上传
说明:本人无的难点在于,如何防止单个文件被上传两次
3.步骤
(1)复制 form7.jsp,将新文件命名为 form8.jsp
(2)将 form8.jsp 修改,以下是修改过的 form8.jsp 代码
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="ISO-8859-1"> <title>Insert title here</title> </head> <body> <% String contextPath = request.getContextPath(); %> <form action="<%=contextPath %>/ABCServlet" method="Post" enctype="multipart/form-data"> 用户名:<input type="text" name="name" value="张三" /><br/> 兴趣爱好:<input type="checkbox" name="hobby" value="shopping" checked>购物 <input type="checkbox" name="hobby" value="music" checked>音乐<br/> 只能选择单个文件:<input type="file" name="myFile" /><br/> 可以选择多个文件:<input type="file" name="myFiles" multiple /><br/> <input type="submit" value="发送"><input type="reset" value="重置"> </form> </body> </html>
(3)将 ABCServlet 的 doPost() 方法,修改如下:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // doGet(request, response); System.out.println("执行ABCServlet的doPost()方法!"); request.setCharacterEncoding("UTF-8"); doGet(request,response); //我用form4.jsp运行,contentType="text/html; charset=UTF-8"他上面用UTF-8 就只能用UTF-8(兼容) //场景3 GET请求与POST请求的区别之请求头不同 System.out.println(request.getContentLength());//没有Post请求体返回0,是GET请求体返回-1 System.out.println(request.getContentType()); if("application/x-www-form-urlencoded".equals(request.getContentType())) { System.out.println("application/x-www-form-urlencoded格式的数据接收完毕!"); return; } String uploadDIR = "/upload/"; String uploadPath = request.getServletContext().getRealPath(uploadDIR); javax.servlet.http.Part singlePart = request.getPart("myFile"); String singleFileName = ""; //确保单文件上传框中选择了文件 if(singlePart != null) { singleFileName = singlePart.getSubmittedFileName(); if(singleFileName !=null && !"".equals(singleFileName)) { System.out.println("单个上传的文件是:" + singleFileName); singlePart.write(uploadPath + singleFileName); singlePart.delete(); } } java.util.Collection<javax.servlet.http.Part> parts = request.getParts(); for (javax.servlet.http.Part multipart : parts) { String multiFileName = multipart.getSubmittedFileName(); //通过判断文件名,确保单文件上传框中的文件,不会重复上传 if (multiFileName !=null && !multiFileName.equals(singleFileName)) { System.out.println("多文件上传的文件有:" + multiFileName); multipart.write(uploadPath + multiFileName); multipart.delete(); } } System.out.println("ABCServlet的doPost()方法执行完毕!"); }
第四章 Servlet 生成 HTTP 响应数据
HTTP响应数据与HTTPServletResponse之间的关系
HTTPServletResponse 响应对象的主要功能:在Web服务器端产生HTTP响应数据所需的响应行、响应头列表、响应体。
Web 服务器返回的响应针对的都是浏览器发向Web服务器的 HTTP 请求。
HTTP 响应行
响应状态码
响应状态码:针对当前的HTTP请求,Web服务器通知浏览器处理当前 HTTP 请求的状态码。响应状态码由 3 位数字构成,其中首位数字定义了状态码的类型。响应状态码一共分为5种类型分别是:100+ 、 200+ 、 300+ 、 400+ 、 500+
使用 response 响应对象设置响应状态码
void response.setStatus(int):设置响应状态码
void response.sendError(int,"状态码对应的信息"):设置状态响应码并设置简要描述说明:大多数情况下,Web开发人员没有必要记住这些响应状态码,也无须手动设置响应状态码;可以用 int response.getStatus()来获取当前 Servlet 程序的响应状态码的方法
HTTP响应头列表
常见的响应头
- Content-Length:80
响应体长度,单位是字节- Content-Type:text/html;charset=UTF-8
响应体的内容格式- Content-Disposition:attachment;filename=aaa.zip
以文件下载方式下载响应体内容- Transfer-Encoding:chunked
将响应体分隔成若干数据块,分块传输到浏览器。当使用Response响应对象实现文件下载功能,Web服务器无法确定文件真实长度,无法将Content-Length 响应体写入HTTP响应头列表。Web服务器会自动将HTTP响应头列表中添加一个 Transfer-Encoding 响应头,目前 Transfer-Encoding 响应头的值只能是 chunked(分块编码)。
值得注意的是,Transfer-Encoding和Content-Length是互斥的,不会出现在同一个HTTP响应头列表- Location:https://www.baidu.com
重定向到 Location指定的网址
Location响应头需要和302响应状态码配合使用,才能实现重定向功能- Refresh:10
每隔10s,刷新一次页面(定时刷新功能)
提供了类似于重定向的功能示例:Refresh:10;url=http://www.baidu.com(代码通知浏览器,10秒后打开百度页面)- Set-Cookie:cookie=value
Web服务器通知浏览器,将响应头的 Cookie 信息保存到浏览器主机内存或外存- Connection:keep-alive
Web服务器和浏览器之间的连接状态- Date:Tue,31 Dec 2019 04:25:57 GMT
HTTP响应数据的生成时间- Content-Encoding:gzip
响应体数据的压缩类型- Content-Language:zh-cn
响应体的语言。zh-ch 表示简体中文使用 response 响应对象设置、添加响应头
response 响应对象提供了两种设置、添加响应头的方法:一种通用方法、一种便捷方法。
- 使用通用的方法设置、添加响应头(能够设置、添加浏览器支持的所有响应头)
void addDateHeader(String name,long date):添加时间格式的响应头,响应头值是长整数。
void addHeader(String name,String value):添加字符串格式的响应头,响应头值是长整数。
void addIntHeader(String name,int value):添加整数格式的响应头,响应头值是长整数。
void setDateHeader(String name,long date):设置时间格式的响应头,响应头值是长整数。
void setHeader(String name,String value):设置字符串格式的响应头,响应头值是长整数。
void setIntHeader(String name,int value):设置整数格式的响应头,响应头值是长整数。
说明1:HTTP响应头中,有些响应头,例如 Content-Length,若存在,也只能存在一个,但有些例如 Set-Cookie,若存在,可以存在多个,set方法设置响应头的时候,若响应头已存在,则会覆盖已有的响应头;若使用add方法,则会据需继续添加,不会覆盖。所以,响应头若能重复,则使用add方法,若不能重复,则使用set方法。
Content-Length既是有且唯一又值为整数,示例:response.setIntHeader(“Content-Length”,0);
同理,response.addHeader(“Set-Cookie”,“userName=zhangsan;Path=/;HttpOnly”);
response.addHeader(“Set-Cookie”,“password=123456;Path=/;HttpOnly”);
说明2:response提供若干获取当前Servlet程序响应头的方法,列举如下——
boolean containsHeader(String name):判断 HTTP响应头列表中是否存在响应头名为 name的响应头
String getHeader(String name):返回响应头名为 name的响应头。若不存在,返回null。
java.util.Collection<String> getHeaderNames():返回HTTP响应头列表中所有的响应头名
java.util.Collection<String> getHeaders(String name):由于响应头名可以重名,该方法返回响应头名为 name 的所有响应头值- 使用便捷方法设置、添加响应头
便捷方法只能设置个别常用的响应头
(1)Web服务器通知浏览器,设置响应体的长度
response.setContentLength(0);
相当于response.setIntHeader(“Content-Length”,0);
(2)Web服务器通知浏览器,设置响应体的MIME和字符集
response.setContentType(“text / html,charset=UTF-8”);
相当于response.setContentType(“Content-Type”,“text / html,charset=UTF-8”);
(3)Web服务器通知浏览器,添加Cookie响应头
Cookie cookie = new Cookie(“useName”,“zhangsan”);
cookie.setMaxAge(60*60*24*7);//设置Cooike的生命周期
response.addCooike(cookie);
相当于response.addHeader("Set-Cookie","userName=zhangsan;Max-Age=604800;Expires=Sun,29-Sep-2019 14:21:24 GMT");
(4)Web服务器通知浏览器,设置重定向响应头,代码如下:
response.sendRedirect("https://www.baidu.com");
等效于如下通用方法(两行代码):
response.sendStatus(302);
response.setHeader("Location","https://www.baidu.com");
ps:仅仅是第二行代码不会导致重定向的发生,第二行代码产生的响应码是200,第一行的代码保证了重定向的发生,第二行代码指明了重定向的地址。
重定向、定时刷新和请求转发的比较
共同点:请求访问的是A,但看到的是B的内容;
他们后续代码会继续执行,有时会造成歧义,这就好比上一位运动员以及将接力棒给下一位运动员,但上一位运动员仍然继续在跑道上前行,通常情况下,这三者代码后面要紧跟return语句,防止后续代码继续执行。
1.重定向和定时刷新的区别(1)定时刷新有“定时”功能;重定向没有定时功能。
(2)定时刷新到新页面前,可以在当前页面输出数据(第一次响应结束后),严格的来讲,重定向不可以。
(3)重定向Location必须和状态码例如302一起使用。定时刷新的响应状态码是200
(4)定时刷新时,浏览器和web服务器要建立两次tcp/ip连接,而重定向可能只需要一次(当重定向前后访问的是同一个Web服务器的两个Servlet程序时只用建立一次TCP/IP连接,浏览器向Web服务器发送两次HTTP请求)
(5)若不使用定时功能,尽量使用重定向
2.请求转发与重定向的区别
(1)请求转发后,HTTP请求尚未处理完成,需要交由下一个Servlet程序继续处理HTTP请求直至最后一个Servlet程序返回响应。
(2)请求转发只能在同一个Web项目内进行请求的转发和呼应,重定向可以将Location设置成任意网址
(3)若请求转发和重定向都在同一Web项目内进行,那么区别是什么呢?
请求次数和响应次数不一样
请求转发的流程:请求A-请求转发至B-B做出响应-请求/响应结束(此时只用一个HTTP请求)
而重定向会有两次请求,好比,我打电话给A,服务员帮我转接,结果A换电话号码了,然后我换号码打,发送新的请求(第二次一定是GET请求)最后接通(4)第一次响应的时机不一样
请求转发前生成的HTTP响应数据,是要被response对象的缓存延时发送到浏览器端的,直至最后的Servlet程序向浏览器返回运行结果,第一次响应结束重定向前生成的HTTP响应数据,会随着Location响应头和302状态码,返回浏览器第一次响应结束
因此他们第一次响应时机的不同,导致Cookie使用也不同。
(5)表单重复提交问题不一样
请求转发意味着请求尚未结束,浏览器地址栏不会发生变化。请求转发期间,由于浏览器误认为请求尚未结束,若刷新浏览器页面,可能会导致表单重复提交的问题。而重定向则不会。
(6)若需要将POST请求转化成GET请求,必须使用重定向。
请求方式一旦确定,请求转发期间,请求类型就无法改变
重定向后第二次请求一定是GET请求(7)数据共享方式不同
重定向:重定向的两个程序,通常借助查询字符串实现数据的共享,也可以通过Cookie或者Session实现数据的共享。
请求转发:请求转发的两个程序还可以通过请求参数以及request请求对象上绑定的属性,实现数据的共享。尤其是request请求对象的属性可以存放任意对象(8)URL路径定位方式不同
重定向:由于重定向后,新的HTTP请求来自浏览器,因此重定向的Location若使用server-relative路径,必须包含Web项目的虚拟路径
请求转发:若使用server-relative路径,不能包含Web项目的虚拟路径
使用response响应对象生成HTTP响应体
使用response响应对象既可以将文本型数据封装到HTTP响应体中,也可以将二进制数据封装在HTTP响应体。但需要注意,HTTP响应体的数据只能是文本型数据和二进制数据二选一。ps:以后将通过实验来对知识点进行展开
response响应对象的缓存
response缓存
向response缓存添加文本型数据
java.io.PrintWriter response.getWriter():返回可以将文本型数据添加到 response 缓存的 PrintWriter 对象。
使用 PrintWriter 对象的 write()、print()、println()、或者append()方法可以将文本型数据添加到response缓存。write只能添加字符串,print可以将各种类型的数据转化成字符串后再添加到response缓存中,(回车和换行符会被html解析成空格符)append支持方法连缀。
说明1: 默认情况下,PrintWriter将文本型数据编码成对应的ISO-8859-1编码数据,故不支持中文。
说明2:针对上述情况建议使用如下代码:response.getCharacterEncoding("UTF-8")
注意:必须在第一次向response缓存中添加文本型数据前设置文本型数据的字符编码。
说明3: 下面方法返回response缓存中的文本型数据的字符编码
String response.getCharacterEncoding(),和上述只少了输入参数,返回null时,表示默认为OSI-8859-1.
向response缓存添加字节型数据
说明1:添加字节数据前,设置响应头Content-Type,提前告知浏览器,字节数据的内容格式MIME
说明2:若字节数据全是文本型数据,刻在Content-Type响应头设置charset,指定字节数据的字符编码,如果字节数据中包含了图片、音频等二进制数据,就无须在Content-Type响应头中设置charset。
注意:response.getWriter()方法用于向response缓存添加文本型数据,response.getOutputStream()方法用于向response缓存添加字节数据,response缓存中不能同时包含二者。
response响应对象一石三鸟的代码
(1)向response缓存添加数据之前,提前设置数据的内容格式(MIME),代码如下:
response.setContentType("text/html")
注意:String response.setContentType() 该方法返回response缓存数据的内容格式,若为NULL则表示由浏览器决定响应数据的内容格式,和上一小节代码有相同之处。(2)一石三鸟的代码
以下代码实现一石三鸟:1.将文本型数据编码成UTF-8编码的数据后,添加到response缓存2.通知浏览器,HTTP响应数据是HTML格式的文本型数据;第三通知浏览器,按照UTF-8编码解码HTTP响应数据
response.setContentType("text/html;charset=UTF-8")
注:response缓存中所有数据的字符编码都是一致的。缓存的第一条数据的字符编码决定后来数据的字符编码,必须在第一次向response缓存中添加文本型数据前设置文本型数据的字符编码,之后设置是无效的。
实践任务 Servlet生成HTTP响应数据
场景1步骤:
1. 创建动态Web项目,项目名称是response
2.展开Java Resource,右键src-->单击Servlet--> 弹出Create Servlet窗口。Java package文本框输入controller,Class name文本框处输入CBAServlet,单击“完成”按钮
3.精简代码,只保留CBAServlet.java的doGet方法,并将代码修改为如下代码,为浏览器用户提供各种功能。
package controller; 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("/CBAServlet") public class CBAServlet extends HttpServlet { private static final long serialVersionUID = 1L; public CBAServlet() { super(); // TODO Auto-generated constructor stub } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getParameter("action"); if(action == null || "".equals(action)) { response.setContentType("text/html;charset=UTF-8"); response.getWriter() .append("本页面是程序首页,为浏览器用户提供了若干功能<br/>") .append("在URL后添加<font color='red'>查询字符串</font>,测试response各功能<br/>") .append("?action=statusCode 测试响应状态码,在Tomcat控制器查看本程序的响应状态码<br/>") .append("?action=header 注意使用F12,打开浏览器使用开发者工具<br/>") .append("?action=refresh 查看Tomcat控制台,refresh后的代码是否执行<br/>") .append("?action=redirect 查看Tomcat控制台,redirect后的代码是否执行<br/>") .append("?action=forward 查看Tomcat控制台,请求转发后的代码是否执行<br/>") .append("?action=include 查看Tomcat控制台,请求包含后的代码是否执行<br/>") .append("?action=contentType 不指定浏览器解码方式,出现乱码了吗?<br/>") .append("?action=chars 字符数据里不能掺杂字节数据,否则异常<br/>") .append("?action=bytes 字节数据里不能掺杂字符数据,否则异常<br/>") .append("?action=picture&fileName = 学习强国.jpg 通过response预览图片<br/>") .append("?action=picture&subAction=download&fileName=学习强国.jpg 通过response下载图片文件<br/>") .append("?action=img&filename=学习强国.jpg 使用<img>标签预览图片<br/>") .append("?action=imgServlet&fileName=学习强国.jpg 使用<img>标签和response的结合预览图片<br/>") ; return; } System.out.println("开始测试"+action+"功能"); //所有新增的功能代码添加到此处 //通知浏览器用户更多功能尚未开发 response.setContentType("text/html;charset = utf-8"); response.getWriter().print("该功能尚未提供,期待您的开发升级!<br/>"); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub doGet(request, response); } }
运行结果如下:
本实践任务编写的CBAServlet程序,可以根据不同的参数,产生不同的处理结果,关键技巧向Servlet的doGet()方法传递action查询字符串,action参数控制了Servlet程序的运行流程,必须要掌握此技巧
场景2步骤 使用response响应对象设置状态响应码
(1)向Servlet程序的doGet()方法新增如下代码
//所有新增的功能代码添加到此处 if(action.equals("statusCode")) { System.out.println("设置前的状态码是:" + response.getStatus()); response.sendError(404,"您访问的页面“跑道火星上去”了!"); System.out.println("设置后的状态码是:"+response.getStatus()); return; }
执行结果如下:注意网址
技巧1:该页面是友好的错误提示页面
技巧2:在Web部署描述符文档(Web.xml配置文件)配置错误页面,这样出错后,浏览器用户看到的不再是Web服务器提供的默认错误页面
(3)创建Web部署描述符文档
web.xml配置文件位于WEB-INF目录下,若目录没有web.xml配置文件,就右击Web项目名,选择Java EE Tools,单击Generate Deployment Descriptor Stub 即可在WEB-INF目录下创建web.xml配置文件
(4)在web.xml配置文件中配置友好的404错误提示页面
error添加后如下:
(5)创建友好的404错误提示页面404.jsp
打开 Project Explore视图->展开abc项目->右击WebContent目录->选择New->单击JSP File->File Name文本框处输入404.jsp->单击Next按钮->使用默认模板->单击Finish按钮->在WebContent目录下创建404.jsp程序,将该程序代码修改为如下代码。
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <!DOCTYPE html> <html> <head> <title>不要着急......</title> </head> <body> <center> <h1>对不起,您访问的页面暂时丢失了</h1> </center> </body> </html>
场景3 使用response响应对象设置响应头列表
(1)向Servlet程序的doGet()方法新增如下代码
if(action.equals("header")) { response.setHeader("Content-Type", "text/html;charset=utf-8"); response.setIntHeader("Content-Length", 0); response.setDateHeader("Date", System.currentTimeMillis()); System.out.println("包含Set-Cookie响应头吗?"+response.containsHeader("Set-Cookie")); response.addHeader("Set-Cookie", "username=zhangsan;path=/;HttpOnly"); response.addHeader("Set-Cookie", "password=123456;path=/;HttpOnly"); System.out.println("包含Set-Cookie响应头吗?"+response.containsHeader("Set-Cookie")); System.out.println("Set-Cookie响应头是"+response.getHeader("Set-Cookie")); java.util.Collection<java.lang.String> headerNames = response.getHeaderNames(); for(String headerName : headerNames) { System.out.print(headerName + "是:"); java.util.Collection<java.lang.String> headerValues = response.getHeaders(headerName); for(String headerValue:headerValues) { System.out.println(headerValue); } } return; }
(2)输入网址 localhost:8080/response/CBAServlet?action=header 打开F12开发者模式,先F12再开网址
场景4步骤 重定向、定时刷新、请求转发、请求包含等的使用以及文本型数据封装到响应体
上一章节的form0.jsp复制到WebContent目录
doGet()方法新增如下代码
if(action.equals("refresh")) { //定时刷新,是浏览器端发出的HTTP请求 同下重定向,需要完整虚拟路径 //会先出字,然后跳转到form0.jsp response.setHeader("Content-Type", "text/html;charset=utf-8"); String url = request.getContextPath() + "/" + "form0.jsp"; response.getWriter().append("观察refresh前的字符串是否输出到网页上"); response.setHeader("refresh", "5;url=" + url); response.getWriter().append("观察refresh后的字符串是否输出到网页上"); System.out.println("refresh后的语句"); return; } if(action.equals("redirect")) {//重定向不需要设置字符编码,原因是重定向第一次仅包含Location响应头 //和302响应状态码,第一次响应没有HTTP响应体,而定时刷新第一次响应可以包含HTTP响应体 String url = request.getContextPath() + "/" + "form0.jsp"; response.getWriter().append("观察 redirect 前的字符串是否输出到网页上!"); response.sendRedirect(url); response.getWriter().append("观察 redirect 后的字符串是否输出到网页上!"); System.out.println("redirect后的语句"); return ; } if(action.equals("forward")) { response.setHeader ("Content-Type","text/html;charset=UTF-8"); response.getWriter().append("观察 forward 前的字符串是否输出到网页上!"); request.getRequestDispatcher("/form0.jsp").forward (request,response); response.getWriter().append("观察 forward 后的字符串是否输出到网页上!"); System.out.println("forward 后的语句"); return ; } if(action.equals ("include")){ response.setHeader("Content-Type","text/html;charset=UTF-8"); response.getWriter().append ("观察 include 前的字符串是否输出到网页上!"); request.getRequestDispatcher("/form0.jsp").include(request,response) ; response.getWriter().append ("观察 include 后的字符串是否输出到网页上!"); System.out.println ("include 后的语句"); return ; } if(action.equals("chars")) { response.setContentType("text/html;charset=utf-8"); response.getWriter().write("write数据"); //response.getWriter()返回的是PrintWriter,这是一个打印输出流 //response.getWriter().writer(),只能打印输出文本格式的(包括html标签),不可以打印对象 response.getWriter().print(2020); response.getWriter().print("print数据"); java.util.List<String> l = new java.util.ArrayList<String>(); l.add("test"); response.getWriter().print(l); response.getWriter().println("println数据"); response.getWriter().append("append数据1") .append("append数据2") .append("append数据3"); String data = "使用OutputStream将字节数据封装到响应体<br/>"; byte[] dataByteArray = data.getBytes("utf-8"); response.getOutputStream().write(dataByteArray); return; } if(response.equals("bytes")) { response.setContentType("text/html;charset=utf-8"); String data = "使用OutputStream将字节数据封装到响应体"; byte[] dataByteArray = data.getBytes("utf-8"); response.getOutputStream().write(dataByteArray); response.getOutputStream().write(dataByteArray,0,dataByteArray.length); response.getWriter().write("write数据");//字节数据封装到响应体之后,不能再封装字符 return; }
场景5使用Servlet的response响应对象,在浏览器页面中显示图片或者下载图片
步骤:代码如下
if(action.equals("picture")) { String fileName = request.getParameter("fileName"); String path = this.getServletContext().getRealPath("download/" + fileName); java.io.File file = new java.io.File(path); java.io.FileInputStream fis = new java.io.FileInputStream(file); String mime = request.getServletContext().getMimeType(fileName); if(mime == null) { mime = "application/octet-stream"; } response.setContentType(mime); response.setContentLength((int)file.length()); String subAction = request.getParameter("subAction"); if(subAction != null && subAction.equals("download")) { System.out.println("开始测试" + subAction + "功能"); //解决下载时文件名乱码问题 String newFileName = new String(fileName.getBytes("UTF-8"),"ISO-8859-1");//不理解 response.setHeader("Content-Disposition", "attachment;fileName=" + newFileName); } javax.servlet.ServletOutputStream sos = response.getOutputStream(); byte[] bytes = new byte[1024*4]; int len = 0; while((len = fis.read(bytes))!=-1){ sos.write(bytes,0,len); } fis.close(); sos.close(); return; }
创建download文件夹,然后放素材进去
然后是下载:
?action=picture&subAction=download&fileName=学习强国.jpg 通过response下载图片文件
场景7 向Servlet程序的doGet()方法新增如下代码
if(action.equals("img")) { String fileName = request.getParameter("fileName"); String contextPath = request.getContextPath(); String src = contextPath + "/download/" + fileName; String img = "<img src='" + src + "' />";//先src内部地址搞出来,再用单括号括起来 response.setContentType("text/html;charset=utf-8"); response.getWriter().append("图片和文字共存:<br/>").append("img"); return; }
场景8<img>标签结合response缓存,在浏览器显示图片
(1)向Servlet程序的doGet()方法新增如下代码
if(action.equals("imgServlet")) { String fileName = request.getParameter("fileName"); String contextPath = request.getContextPath();//获取项目根路径 String servletURLPattern = request.getHttpServletMapping().getPattern(); //Servlet 映射是从HttpServletRequest实例获得的 //getPattern()—返回激活 servlet 请求的 URL 模式 String src = contextPath + servletURLPattern + "?action=picture&fileName=" + fileName; String img = "<img src='" + src + "' />"; response.setContentType("text/html;charset=utf-8"); response.getWriter().append("图片和文字共存:并且图片的物理路径可以是任意位置:<br/>").append(img); return; }
需要注意的是,第一次浏览器接收到<img>标签后,紧接着向CBAServlet发送了第二次请求,第二次请求的url是:localhost:8080/response/CBAServlet?action=picture&fileName=JAVAEE 开发.jpg