javaWeb-HttpServletResponse

Http的响应正是由本文中的主角HttpServletResponse来完成的,下面就让我们一起学习如何对一个Http请求作出正确的(适当的,处理成功或失败,无权限,参数错误等)的响应。其局部(省去了Servlet容器)的执行过程如下图所示: image

设置响应的状态码

我们在开发中并没有设置response的状态码为200呀,怎么这里的200是哪里来的?我们程序正常执行(完整的处理了Http请求,没发生bug)的时候,Web服务器会默认成神一个状态码200。还有,在我们url路径输错的时候返回的404错误,调用servlet发生异常直接抛出返回的500错误等,都是web服务器帮我们默认产生的。

在开发的过程中,500错误使我们遇到最多的错误了,如空指针异常,sql异常,状态异常等导致的请求中断,对于这些异常,如果直接将报错信息暴露给用户,那我们的系统体验就会非常的差。为了增加系统的用户友好程度,我们必须对异常进行处理,但是也需要将错误信息正确的提示给用户,让其可以有下一步处理或者联系客服。

这样我们就需要来设置状态码,让前端可以根据状态码及其他返回信息进行相应的页面处理了。那么问题来了,我们如何手动的设置状态码呢?HttpServletResponse中为我们提供了以下几个方法:

方法描述
void setStatus(int sc)设置此次给客户端响应的状态码
void sendError(int sc)使用指定的状态码向客户端返回一个错误响应,并且清空缓存区
void sendError(int sc,String msg)使用指定的状态码和状态描述向客户端返回一个错误响应,并清空缓存区

其中比较重要的方法为setStatus(int sc),我们可以通过其方便的设置给客户端响应的状态码;对于sendError()的两个方法,会将Servlet之前写入缓冲区的数据全部清除,但是其他也有较好的使用场景,就是对同一种异常设置专门的错误提示页面,比如用户未登录,可以在过滤器(Filter)中判断此种情况,并且直接调用sendError跳转至相应的页面,在此页面上友好的向用户提示错误信息,but此功能完全可以使用重定向来完成,且重定向可以获取更多的请求和响应信息,因此算一个比较鸡肋的功能吧。

我们简单的对setStatus、sendError进行测试,这里我们先建个servlet,命名为ResponseTestServlet,其doGet代码如下:

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws            				ServletException, IOException {
  //设置返回客户端的contentType
  response.setContentType("text/html;charset=utf-8");
  //设置状态码
  response.setStatus(500);
  //response.sendError(500);
  //获取输出流
  PrintWriter out = response.getWriter();
  out.println("虽然我的状态为500,但是信息正常输出了");
}

我们直接在浏览器中调用ResponseTestServlet,其运行结果如下图第一张所示:

image

image

当我们执行response.sendError(500)时,我觉得大家应该都已经预料到结果页面了,其结果如上图的第二张所示。那我们应该如何设置错误码对应的错误页面呢?这里我们需要在web.xml中增加如下配置,需要注意的是,配置的路径必须以‘/’开头,即必须是绝对路径===打包发布时的路径

<error-page>
  <error-code>500</error-code>
  <location>/index.jsp</location>
</error-page>

在此浏览器上执行,其运行结果如下图所示 image

如何根据错误信息,设置合理的状态码呢?这就需要我们知道每个状态码表示的含义。Http错误码总共分为5类,即1xx,2xx,3xx,4xx,5xx,分别表示通知信息,成功信息,重定向信息,客户端错误,服务端错误,下面举例一些常见的:

namediscribtion释义
200sc_ok此次请求已经成功
301sc_moved_permanently请求的网页已永久移动到新位置
302sc_moved_temporarily临时移动,请求地址不变
401sc_unauthorized未授权,用户需登录
403sc_forbidden服务器拒接了此次请求(权限问题)
404sc_not_found服务器没找到URI匹配的
405sc_method_not_allowed调用的方法不允许使用(get、post不匹配)
500sc_internal_server_error服务器内部发生异常,请求中断
502sc_bad_gateway网关错误(如nginx),无法收到服务器响应
504sc_gateway_timeout请求超时,在约定时间内没有收到http响应

设置响应消息头

在上文中,我们在chrome调试工具中查看了Http请求的请求头,请求体,Chrome提供的信息不止于此,我们来看下图,可以看到,Response的Headers信息 image这也说明了,相应于Request中的请求头,Response也有对应的响应头,这些响应头主要如下表所示:

响应消息头名称说明
Server一种标明Web服务器软件及其版本号的头标
Content-Type返回文档时采用的MIME类型
Transfer-Encoding表示为了达到安全传输或者数据压缩等目的而对实体进行的编码。如chunked编码,该编码将实体分块传送并逐块标明长度,直到长度为0块表示传输结束,这在实体长度未知时特别有用(比如由数据库动态产生的数据)
Date发送Http消息的日期
Content-Encoding用于说明页面在传递过程中已经采用的编码方式
Content-length响应内容的长度,以字节为单位
Expires特定的一段时间,这段时间应该将文档认作为过期,不应该再继续缓存
Refresh多少秒后浏览器应该重新载入页面
Cache-Control用来指定响应遵循的缓存机制,若取值no-cache值表示阻止浏览器缓存页面
Location浏览器应该重新连接到的URl.
Content-Disposition通过这个报头,可以请求浏览器询问用户将响应存储到磁盘上给定名称的文件中
Set-Cookie浏览器应该记下来的cookie。推荐使用addCookie

当然,为了方便的设置响应头中对应的信息,HttpServletResponse也提供了一系列的方法,主要相关方法如下:

响应方法说明
setContentType(String type)设定Content-Type消息头
setContentLength(int length)设定Content-Length消息头
setCharacterEncoding(String charset)设定返回给客户端的内容的编码格式
addHeader(String name,String value)新增String类型的值到名为name的header
addIntHeader(String name,int value)新增int类型的值到名为name的header
addDateHeader(String name,long date)以毫秒数新增类型为Date的header
addCookie(Cookie c)为Set-Cookie消息头增加一个值

需要注意的是,addHeader,addIntHeader,addDateHeader都有一个对应的setxxx方法,两者的区别就如同集合和列表,setxxxx方法不允许出现重复的header,而addxxxx方法可以;setContentType、setCharacterEncoding方法皆是指返回给客户端的内容的编码方式的,推荐直接使用setContentType设置客户端内容的MIME类型及编码方式,比如setContentType("text/html;charset=UTF-8")等价于setContentType("text/html");setCharacterEncoding("charset=UTF-8")两条语句同时执行。

这里,为了演示上面这些方法,我们将Response TestServlet中的doGet方法修改如下:

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws            				ServletException, IOException {
  //设置返回客户端的contentType
  response.setContentType("text/html;charset=utf-8");
  //设置状态码
  //response.setStatus(500);
  //response.sendError(500);
  PrintWriter out = response.getWriter();
  //out.println("虽然我的状态为500,但是信息正常输出了");
  //添加类型为String的header
  response.addHeader("Location", "#");
  //添加类型为long的header
  response.addDateHeader("Date", new Date().getTime());
  //创建一个Cookie
  Cookie cookie = new Cookie("name", "李子树");
  //添加一个cookie
  response.addCookie(cookie);
}

其执行结果如下图所示,我们添加的header都能看到: image

关于Content-Disposition

这里单单说一下Content-Disposition。在常规的Http应答中,Content-Disposition响应头指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。

在multipart/form-data类型的应答消息体中,Content-Disposition消息头可以被用在multipart消息体的子部分中,用来给出其对应字段的响应信息。各个子部分由Content-Type中定义的分隔符分隔。用在消息体自身则无实际意义。

Content-Disposition消息头最初是在MIME标准中定义的,Http表单及Post请求只用到了其所有参数的一个子集。只有form-data以及可选的name和filename三个参数可以应用到http场景中。

语法

作为消息主题中的消息头

在Http场景中,第一个参数或者是inline(默认值,表示回复中的消息体会以页面的一部分或者整个页面的形式展示),或者是attachment(意味着消息体应该被下载到本地;大多数浏览器会呈现一个“保存为”的对话框,将filename的值预填为下载后的文件名,假如它存在的话)。

 Content-Disposition:inline
 Content-Disposition:attachment
 Content-Disposition:attachment;filename="filename.jpg"

作为multipart body中的消息头

在http场景中。第一个参数总是固定不变的form-data;附加的参数不区分大小写,并且拥有参数值,参数名与参数值用等号('=')连接,参数值用引号括起来。参数之间用分号(';')分隔。

Content-Disposition: form-data
Content-Disposition: form-data;name="fieldName"
Content-Disposition: form-data;name="filedName";filename="filename.jpg"

指令

name

后面是一个表单字段名的字符串。每一个字段名会对应一个子部分。在同一个字段名对应多个文件的情况下(例如,带有multiple属性的<input type=file> 元素),则多个子部分共用同一个字段名。如果name参数的值为‘charset’,意味着这个子部分表示的的不是一个HTML字段,而是在为明确指定字符集信息的情况下各部分使用的默认字符集

filename

后面要是传送的文件的初始名称的字符串。这个参数总是可选的,而且不能盲目使用:路径信息必须舍掉,同时要进行一定的转换以符合服务器文件系统规则。这个参数主要用来提供展示性信息。当与Content-Disposition:attachment一同使用的时候,它被用作“保存为”对话框中呈现给用户的默认文件名。

filename*

"filename"和"filename*"两个参数的唯一区别在于,"filename*"采用了RFC_5987中规定的编码方式。当""filename"和""filename*"同时出现的时候,应该优先采用""filename*",假如二者都是支持的话。

示例

以下是一则可以触发“保存为”对话框的服务器应答:

200 OK
Content-Type: text/html; charset=utf-8
Content-Disposition: attachment; filename="cool.html"
Content-Length: 22

<HTML>Save me!</HTML>

这个简单的Html文件会被下载到本地而不是在浏览器中展示。大多数浏览器默认会将cool.html作为文件名。

以下是一个Html表单的示例,展示了在multipart/form-data格式的报文中使用Content-Disposition消息头的情况:

POST /test.html HTTP/1.1
Host: example.org
Content-Type: multipart/form-data;boundary="boundary"

--boundary
Content-Disposition: form-data; name="field1"

value1
--boundary
Content-Disposition: form-data; name="field2"; filename="example.txt"

value2
--boundary--

发送响应消息体

前面说了这么多,到了这里才是真正的重头戏!这里才是直观的响应给客户看到的内容,在早期jsp还没有诞生的时代,许多动态页面是通过在Serlvet中使用HttpServletResponse输出到页面上的,就算到现在,教材上仍有这部分的演示代码。下面,就让我们一起来看下HttpServletResponse是如何发送消息体到客户端的。

首先我们来看两个HttpServletResponse提供的两个方法:

方法描述
ServletOutputStream getOutputStream()返回一个适合写入二进制数据ServletOutputStream对象,并通过调用flush()提交这次响应
PrintWriter getWriter()返回一个PrintWriter对象,该对象可以将字符文本发送到客户端

从中我们可以看到,getOutputStream()方法返回ServletOutPutStream对象,更适合向客户端写入二进制数据,并且Servlet容器不会对这些二进制数据进行编码,因此我们常用ServletOutputStream向客户端发送如图片,文件等内容;对于getWriter()方法返回的PrintWriter对象,里面封装了更多的写入字符文本的函数,并且我们上文提到的setContentType()方法设置的MIME类型对其输出内容有效,因此也可以很好的解决中文乱码的问题。

还有一点需要注意的是,这两个方法在一个response对象中不可以同时调用,否则会抛出一个IllegalStateException,也就是非法状态异常,因为输出流只能有一个(如果可以多次获取的话,客户端又如何确认哪个Http响应是最后一个呢)。

下面我们对来简单的介绍下ServletOutputStream对象和PrintWriter对象中的方法,我们首先来看下ServletOutputStream这个对象(抽象类)的概述(Outline),可以看到,其重载了几乎可以输出各种数据类型的print()、println()方法,但是通过查看源码可以发现,这些方法都是通过其父类OutputStream(java.io.OutputStream)的write()方法进行消息体的输出。

image

下面我们来看下PrintWriter对象的概述,其方法较多,我们只截取部分主要方法,如下图所示,printWriter中提供的输出方法更多,其输出方法都是通过Writer(java.io.Writer)类中的write()方法来进行的消息体的输出。

image

image

因为PrintWriter的输出功能在前面已经使用N遍了,下面我们主要演示下如何通过ServletOutputStream来输出内容下面我们简单的通过代码演示下ServletOutputStream的使用,我们再ResponseTestServlet中的doGet中代码修改如下(注释之前的部分):

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws            				ServletException, IOException {
  //设置返回客户端的contentType
	response.setContentType("text/html;charset=utf-8");
  
  //...
  
  ServletOutputStream out = response.getOutputStream();
  //通过ServletOutputStream向客户端输出值
  out.print("We Are Young Man!");
}

其执行结果如下图所示,浏览器中输出了Servlet给的响应。 image

这么看,ServletOutputStream和PrintWriter似乎没什么区别,ServletOutputStream一样可以输出字符串呀,但是,当我们把输出的内容改为中文,代码修改为out.print("我们不一样!"),再看下输出结果: image

这里我们回头来看一句话,ServletOutputStream输出二进制数据,并且Servlet容器不会对这些二进制数据进行编码,这里就是说,你输入的二进制流是什么,Servlet不会对你的输出流编码,因此上面setContentType是无效的。那么为什么会产生异常呢,我们来看下ServletOutPutStream中print(String s)的源码。(注意:println方法中调用的print方法)

public void print(String s) throws IOException {
  if (s==null) s="null";
  int len = s.length();
  for (int i = 0; i < len; i++) {
    char c = s.charAt (i);

    // XXX NOTE:  This is clearly incorrect for many strings,
    // but is the only consistent approach within the current
    // servlet framework.  It must suffice until servlet output
    // streams properly encode their output.
    //
    if ((c & 0xff00) != 0) {        // high order byte must be zero
      String errMsg = lStrings.getString("err.not_iso8859_1");
      Object[] errArgs = new Object[1];
      errArgs[0] = Character.valueOf(c);
      errMsg = MessageFormat.format(errMsg, errArgs);
      throw new CharConversionException(errMsg);
    }
    write (c);
  }
}

注意下中间的一段注释,明确的告知了这个方法对许多字符是不正确的,iso 8859-1编码方式完全不支持中文,因此这里在转化的过程中会直接抛出异常,我们再上个运行结果上看到的报错信息的根由也是如此。

通过源码我们也可以看到,print并没有进行转码,只是判断一个字节的高地址的一个字节(8位)是否为0(注:iso 8859-1只使用了一个字节来进行编码),一次来判断字符是否是iso 8859-1字符集中的字符。那这样的话,servletOutputStream就真的无法输出中文了吗?

如果Servlet容器不对二进制数据进行任何处理,那么我们是不是可以换个思路?直接将String转为指定编码方式的byte,并通过ServletOutputStream中的write(byte b[])方法将字符数组输出到客户端。对应的,我们将上面的代码修改为

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws            				ServletException, IOException {
  //设置返回客户端的contentType
	response.setContentType("text/html;charset=utf-8");
  
  //...
  
  ServletOutputStream out = response.getOutputStream();
  //通过ServletOutputStream向客户端输出值
  //通过getBytes获取字节数组,并指定编码方式
  out.print("我们不一样!".getBytes("UTF-8"));
}

其运行结果也如下所示: image

我们可以看到,浏览器中正常的显示了中文输出。但是如果我们的每个含有中文的字符串都需要使用这种方式输出,那不是太麻烦了。这里也是我们再描述ServletOutputStream时说的,其适合(suitable)输出二进制数据。因此在对客户端的Http请求进行相应式,我们也要选择合理的输出方式。

总结

本文主要讲解了Servlet如何对Http请求进行响应,Http响应对应Http请求的三个部分内容,分别为响应行,响应头和消息体,以及对应的如何通过HttpServletResponse设置对应的状态码、响应头,并详细的解释了getOutputStream()和getWriter()的区别以及其使用场景

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值