Servlet基础之HttpServletResponse详解

​ 在上一文中,我们详细的介绍了HttpServletRequest对象,并且重点的介绍一个Http请求中包含请求行、请求头、请求体三部分,并且讲解了如何通过HttpServletRequest中封装的方法来方便的获取对应的头信息及用户的请求参数。

资源分配图

​ 在上文中我们也讲到了Servlet的功能和重要性,其在MVC架构中充当Controller的角色,因此其不仅要获取客户端的数据(用户输入的表单数据、查询参数等),并在处理结束后,给客户端一个响应,也正如我们上图中所示,Servlet需要对客户端的HTTP请求进行一个HTTP响应。而Http的响应正是由本文中的主角HttpServletResponse来完成的,下面就让我们一起学习如何对一个Http请求作出正确(适当的,处理成功或失败、无权限、参数错误等)的响应。其局部(省去了Servlet容器)的执行过程如下图所示:

资源分配图

1.设置响应的状态码

​ 这里我们通过一个截图来看下什么是响应的状态码,滴、滴、滴(😅,用图就用全套,此部分图片来自上篇文章),下图最大的红框中的绿色小灯泡旁的200字样,就是我们这里说的状态码了,绿灯表示其为响应成功。

资源分配图

​ 有些同学可能会比较疑惑了,我在开发的过程中并没有设置response的状态码为200呀,怎么这里的200是哪来的?这里需要给大家说一下了,在我们程序正常执行(完整的处理了Http请求,没发生bug)的时候,Web服务器会默认产生一个状态码为200。还有,在我们url路径输错的时返回的404错误,调用servelt发生异常直接抛出返回的500错误等,都是web服务器帮我们默认产生的。

资源分配图资源分配图

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

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

资源分配图

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

​ 我们简单的对setStatus、sendError进行测试,这里我们新建个Servelt,命名为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,其运行结果如下图图左所示:

资源分配图资源分配图

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

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

​ 在此在浏览器上执行,其运行结果如下图所示(偷个懒,没有写错误显示页面😅):

资源分配图

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

Name discribtion 释义
200 SC_OK 此次请求已经成功
301 SC_MOVED_PERMANENTLY 请求的网页已永久移动到新位置
302 SC_MOVED_TEMPORARILY 临时移动、请求地址不变
401 SC_UNAUTHORIZED 未授权、用户需登录
403 SC_FORBIDDEN 服务器拒绝了此次请求(权限问题)
404 SC_NOT_FOUND 服务器没找到URI匹配的
405 SC_METHOD_NOT_ALLOWED 调用的方法不允许使用(get、post不匹配)
500 SC_INTERNAL_SERVER_ERROR 服务器内部发生异常,请求中断
502 SC_BAD_GATEWAY 网关错误(如Nginx),无法收到服务器的响应
504 SC_GATEWAY_TIMEOUT 请求超时,在约定时间内没有收到Http响应

2.设置响应消息头

​ 在上文中,我们在chrom调试工具中查看了Http请求的请求头、请求体,Chrome提供的信息不止于此,我们来看下图,可以看到,Response的Headers信息。

资源分配图

​ 这也说明了,相应于Request中的请求头,Response也有对应的响应头,这些响应头主要如下图所示:

资源分配图

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

资源分配图

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

​ 这里,为了演示上面这些方法,我们将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,但是信息正常输出了");
  //添加类型为String的header
  response.addHeader("Location", "#");
  //添加类型为long的header
  response.addDateHeader("Date", new Date().getTime());
  //创建一个Cookie
  Cookie cookie = new Cookie("name", "李子树");
  //添加一个cookie
  response.addCookie(cookie);
}

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

资源分配图

3.发送响应消息体

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

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

资源分配图

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

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

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

资源分配图

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

资源分配图资源分配图

​ 因为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给的响应。

资源分配图

​ 这么看,ServletOutputStream和PrintWriter似乎没什么区别,ServletOutputStream一样可以输出字符串呀,但是,注意了,当我们吧输出内容改为中文,代码修改为out.print("我们不一样!");,让我在来看下执行结果:

资源分配图

​ 啥情况,居然发生了错误?代码中不是通过setContentType设置了编码格式为UTF-8了么,为什么页面中会提示不是ISO 8859-1字符?这里我们在回过头来看一句话,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"));
}

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

资源分配图

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

4.总结

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

参考阅读:Http1.1状态码定义


​ 又到了分隔线以下,本文到此就结束了,本文内容全部都是由博主自己进行整理并结合自身的理解进行总结,如果有什么错误,还请批评指正。

​ Java web这一专栏会是一个系列博客,喜欢的话可以持续关注,如果本文对你有所帮助,还请还请点赞、评论加关注。

​ 有任何疑问,可以评论区留言。

展开阅读全文

Git 实用技巧

11-24
这几年越来越多的开发团队使用了Git,掌握Git的使用已经越来越重要,已经是一个开发者必备的一项技能;但很多人在刚开始学习Git的时候会遇到很多疑问,比如之前使用过SVN的开发者想不通Git提交代码为什么需要先commit然后再去push,而不是一条命令一次性搞定; 更多的开发者对Git已经入门,不过在遇到一些代码冲突、需要恢复Git代码时候就不知所措,这个时候哪些对 Git掌握得比较好的少数人,就像团队中的神一样,在队友遇到 Git 相关的问题的时候用各种流利的操作来帮助队友于水火。 我去年刚加入新团队,发现一些同事对Git的常规操作没太大问题,但对Git的理解还是比较生疏,比如说分支和分支之间的关联关系、合并代码时候的冲突解决、提交代码前未拉取新代码导致冲突问题的处理等,我在协助处理这些问题的时候也记录各种问题的解决办法,希望整理后通过教程帮助到更多对Git操作进阶的开发者。 本期教程学习方法分为“掌握基础——稳步进阶——熟悉协作”三个层次。从掌握基础的 Git的推送和拉取开始,以案例进行演示,分析每一个步骤的操作方式和原理,从理解Git 工具的操作到学会代码存储结构、演示不同场景下Git遇到问题的不同处理方案。循序渐进让同学们掌握Git工具在团队协作中的整体协作流程。 在教程中会通过大量案例进行分析,案例会模拟在工作中遇到的问题,从最基础的代码提交和拉取、代码冲突解决、代码仓库的数据维护、Git服务端搭建等。为了让同学们容易理解,对Git简单易懂,文章中详细记录了详细的操作步骤,提供大量演示截图和解析。在教程的最后部分,会从提升团队整体效率的角度对Git工具进行讲解,包括规范操作、Gitlab的搭建、钩子事件的应用等。 为了让同学们可以利用碎片化时间来灵活学习,在教程文章中大程度降低了上下文的依赖,让大家可以在工作之余进行学习与实战,并同时掌握里面涉及的Git不常见操作的相关知识,理解Git工具在工作遇到的问题解决思路和方法,相信一定会对大家的前端技能进阶大有帮助。
©️2020 CSDN 皮肤主题: 像素格子 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值