在上一文中,我们详细的介绍了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状态码定义
又到了分隔线以下,本文到此就结束了,本文内容全部都是由博主自己进行整理并结合自身的理解进行总结,如果有什么错误,还请批评指正。
有任何疑问,可以评论区留言。