JSP&Servlet学习笔记
请求与响应
从容器到HttpServlet
- 在前面第二章中,我们看到了Web容器对Web应用程序要求的目的架构,以及相关的部署规范。实际上,对于第一个Servlet程序中HttpServletRequest、HttpServletResponse的使用并没有着墨,这是有关请求与响应的处理。
Web容器做了什么
- 第二章中,已经看过Web容器做的事情是创建Servlet实例,并完成Servlet名称注册以及URL模式的对应。在请求来到时,Web容器会转发给正确的Servlet来处理请求。
- 当浏览器请求HTTP服务器时,是使用HTTP来传送请求与相关信息(标头、请求参数、Cookie等)。HTTP是基于TCP/IP之上的协议,信息基本上都是通过文字信息来传送,然而Servlet本质上是个Java对象,运行于Web容器(一个Java写的应用程序)中。
- 当请求来到HTTP服务器,而HTTP服务器转交请求给容器时,容器会创建一个代表当此请求的HttpServletResponse对象,并将请求相关信息交给该对象。同时,容器会创建一个HttpServletResponse对象,作为稍后要对客户端进行相应的Java对象。
- 如果查询HttpServletResponse、HttpServletRequest的API文件说明,会发现他们都是接口,实现这些接口的相关类就是容器提供的。Web容器本身就是一个Java所编写的应用程序。
- 接着,容器会根据读取的@WebServlet标注或web.xml设置,找出处理该请求的Servlet,调用它的service()方法,将创建的HttpServletRequest、HttpServletResponse对象传入作为参数,service()方法中会根据HTTP请求的方式,调用对应的doXXX()方法,例如doGet()方法。
- 再然后,在doGet()方法中,可以使用HttpServletRequest对象、HttpServletResponse对象。例如,使用getParameter()取得请求参数,使用getWriter()取得输出用的PrintWriter对象,并进行各项响应处理。对PrintWriter做的输出操作,最后由容器转换为HTTP响应,再由HTTP服务器对浏览器进行相应。之后容器将HttpServletRequest对象、HttpServletResponse对象销毁回收,该次请求响应结束。
- 两次请求时创建的请求、响应对象和之前的无关,符合HTTP基于请求、响应、无状态的模型。所以对这两个对象的设置是无法延续到下一次请求的。
doXXX()方法
- 前面提到很多次doXXX()方法。我们可以看到Servlet接口的service()方法签名(Signature)其实接受的是ServletRequest、ServletResponse:
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
- 前面提到过,当初在定义Servlet时,期待的是Servlet不仅使用于HTTP,所以请求、相应对象的基本行为是规范在ServletRequest、ServletResponse(包是javax.servlet),而与HTTP相关的行为,则分别是由两者的子接口HttpServletRequest、HttpServletResponse(包是javax.servlet.http)定义。
- Web容器创建的确实是HttpServletResponse、HttpServletRequest的实现对象,而后调用Servlet接口的service()方法。在HttpServlet中的实现service()如下:
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest)req;
response = (HttpServletResponse)res;
} catch (ClassCastException var6) {
throw new ServletException(lStrings.getString("http.non_http"));
}
this.service(request, response);
}
- 上面调用的service(request, response),其实是HttpServlet新定义的方法:
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod();
long lastModified;
if (method.equals("GET")) {
lastModified = this.getLastModified(req);
if (lastModified == -1L) {
this.doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader("If-Modified-Since");
} catch (IllegalArgumentException var9) {
ifModifiedSince = -1L;
}
if (ifModifiedSince < lastModified / 1000L * 1000L) {
this.maybeSetLastModified(resp, lastModified);
this.doGet(req, resp);
} else {
resp.setStatus(304);
}
}
} else if (method.equals("HEAD")) {
lastModified = this.getLastModified(req);
this.maybeSetLastModified(resp, lastModified);
this.doHead(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {
this.doPut(req, resp);
} else if (method.equals("DELETE")) {
this.doDelete(req, resp);
} else if (method.equals("OPTIONS")) {
this.doOptions(req, resp);
} else if (method.equals("TRACE")) {
this.doTrace(req, resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[]{
method};
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(501, errMsg);
}
}
- 这也是为什么在继承了HttpServlet之后,必须实现与HTTP方法对应的doXXX()方法来处理请求。HTTP定义了GET、POST、PUT、DELETE、HEAD、OPTIONS、TRACE等请求方式,而HttpServlet中对应的方法有:
- doGet()
- doPost()
- doPut()
- doDelete()
- doHead()
- doOptions()
- doTrace()
- 如果客户端发出了没有实现的请求又会怎样。以HttpServlet的doGet()为例:
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_get_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(405, msg);
} else {
resp.sendError(400, msg);
}
}
关于HttpServletRequest
- 当HTTP转发给Web容器处理时,Web容器会收集相关信息,并产生HttpServletRequest对象,可以使用这个对象取得HTTP请求中的信息。可以在Servlet中进行请求处理,或者是将请求装啊(或包含)另一个Servlet进行处理。各个Servlet在同一请求周期中所需共享的资料,则可以设置在请求对象中成为属性。
处理请求参数与标头
-
请求来到服务器时,Web容器会创建HttpServletRequest实例来包装请求中的相关信息,HttpServletRequest定义了取得一些通用请求信息的方法。例如,可以使用以下方法来取得请求参数。
getParameter():
指定请求参数名称来获得对应的值。例如:String username = request.getParameter("name");
,如果是类似于“123”这样的字符串值,则必须使用Integer.parseInt()这类的方法将其转换为基本类型。如果请求中没有所指定的请求参数名称,则会返回null。getParameterValues():
如果窗体上有可复选的元件,如复选框、列表等,则同一个请求参数名称会有多个值(此时会出现类似于param=10¶m=20¶m=30),可以用getParameterValues()方法取得一个String数组,数组元素代表所有被选取选项的值。例如:String[] values=request.getParameterValues("param");
getParameterNames():
如果想知道请求中有多少个请求参数,则可以使用该方法,此时返回一个Enumeration对象,其中包括所有的请求参数名称。例如:
Enumeration<String> e = req.getParameterNames(); while(e.hasMoreElements()) { String param=e.nextElement(); }
getParameterMap():
将请求参数以Map对象返回,Map中的键时请求参数名称,值时请求参数值,以字符串数组类型发安徽(这是考虑到同一请求参数有多个值的情况)。
-
对于HTTP的标头(Header)信息,可以使用如下方法来取得:
getHeader():
使用方式与getParameter()类似,指定标头名称后可返回字符串值,代表浏览器所送出的标头信息。getHeaders():
使用方式与getParameterValues()类似,指定标头名称后可返回Enumeration,元素为字符串。getHeaderNames():
使用方式与getParameterNames():类似,取得素有标头名称,以Enumeration返回,内含所有标头字符串名称。
-
下面这个范例示范了如何取得并显示浏览器送出的标头信息:
package chapter3;
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 java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;
@WebServlet("/header.view")
public class HeaderServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>HeaderServlet</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>HeaderServlet at "+ req.getContextPath()+ "</h1>");//获得应用程序环境路径
Enumeration<String> names =req.getHeaderNames();//取得所有标头名称
while(names.hasMoreElements())
{
String name = names.nextElement();
out.println(name + ":" + req.getHeader(name) + "<br>");//取得标头值
}
out.println("</body>");
out.println("</html>");
out.close();
}
}
- 输出结果如下:
HeaderServlet at /war_exploded
host:localhost:8080
connection:keep-alive
upgrade-insecure-requests:1
user-agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36
accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
sec-fetch-site:none
sec-fetch-mode:navigate
sec-fetch-user:?1
sec-fetch-dest:document
accept-encoding:gzip, deflate, br
accept-language:zh-CN,zh;q=0.9
cookie:JSESSIONID=8E509B24A94D4E898F25B757D2174910; JSESSIONID=F40A6289F39DEB1688E4C0F2EBBB7764; Idea-66598eb4=8e297653-4e36-4790-97fd-bd21e4da0c6a
- 如果标头值本身是个整数或日期的字符串表示法,则可以使用getIntHeader()或getDateHeader()方法分别取得转换为int或Date的值。如果getIntHeader()无法转换为int,会抛出NumberFormatException。另一个则会抛出IllegalArgumentException。
请求参数编码处理
- 请求参数的编码处理,基本上分为POST和GET的情况来说明。先来看POST的情况。
POST请求参数编码处理
- 如果客户端没有在Content-Type标头中设置字符编码信息(例如浏览器可以设置Content-Type:text/html;charset=UTF-8),此时使用HttpServletRequest的getCharacterEncoding()的返回值是null。在这个情况下,容器若使用的默认编码处理是ISO-8859-1(大部分浏览器默认的字符集),而客户端使用UTF-8发送非ASCII字符的请求参数,Servlet直接使用getParameter()等方法取得该请求参数值,就会是不正确的结果也就是得到了乱码。
- 要在取得任何请求参数前执行setCharacterEncoding()方法才有用。
GET请求参数编码处理
- 在HttpServletRequest的API文件中,对setCharacterEncoding()的说明清楚提到:Overrides the name of the character encoding used in the body of this request
- 也就是说,这个方法对于请求Body中的字符编码才有作用,也就是基本上这个方法只对POST产生作用,当请求是用GET发送时,则没有定义这个方法是否会影响Web容器处理编码的方式。
- 如果使用Tomcat并采用GET,或没有设置serCharacterEncoding(),且已取得一个请求参数字符串,另一个处理编码的方式就是通过String的getBytes()指定编码来取得该字符串的字节数组,然后再重新构造为正确编码的字符串。
- 例如,若浏览器使用UTF-8处理字符,Web容器默认使用ISO-8859-1编码,则正确处理编码的方式为:
String name = req.getParameter("name");
String name = new String(name.getBytes("ISO-8859-1"), "UTF-8");
- 以下使用范例来示范,如何正确处理编码作为总结。这里准备两个窗体,一个使用GET,一个使用POST。
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html"; charset="Big5">
<title>get</title>
</head>
<body>
<form method="get" action="encoding">
名称:<input type="text" name="nameGet"><br><br>
<button>送出 GET 请求</button>
</form>
</body>
</html>
- 处理请求的Servlet如下:
package chapter3;
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 java.io.IOException;
@WebServlet("/encoding")
public class EncodingServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("nameGet");
name = new String(name.getBytes("ISO-8859-1"), "BIG5");
System.out.println("GET:" + name);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("BIG5");
String name = req.getParameter("namePost");
System.out.println("POST:" +name);
}
}
- 因为网页是BIG5编码,所以Servlet在转换编码时,指定的就是BIG5编码。至于输出中文字符至浏览器则是另一个话题,所以上例先将取得的字符显示在文字模式主控台,可以观察是否显示正确的中文字符。
getReader()、getInputStream()读取body内容
- HttpServletRequest上定义有getReader()方法,可以取得一个BufferedReader,通过该对象,可以读取请求的Body数据,例如,可以使用下面这个范例来读取请求Body内容。
package chapter3;
import org.jetbrains.annotations.NotNull;
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 java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet("/body.view")
public class BodyServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String body = readBody(req);
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Servlet BodyView</title>");
out.println("</head>");
out.println("<body>");
out.println(body);
out.println("</body>");
out.println("</html>");
}
private String readBody(@NotNull HttpServletRequest request) throws IOException
{
BufferedReader reader = request.getReader();
String input = null;
String requestBody = "";
while((input = reader.readLine()) != null)
{
requestBody = requestBody + input + "<br>";
}
return requestBody;
}
}
- 可以用下面的窗体对Servlet发出请求
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html" charset="UTF-8">
<title></title>
</head>
<body>
<form action="/body.view" method="post">
名称:<input type="text" name="user"><br>
密码:<input type="password" name="passwd"><br><br>
<input type="submit" name="login" value="发送">
</form>
</body>
</html>
- 窗体发送时,如果<form>标签没有设置enctype属性,则默认值就是application/x-www-form-urlencoded,如果要上传文件,则enctype属性要设置为multipart/form-data。如果使用下面窗体选择一个文件发送:
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>upload files</title>
</head>
<body>
<form action="body.view" method="post" enctype="multipart/form-data">
选择文件:<input type="file" name="filename" value=""><br>
<input type="submit" value="Upload" name="upload">
</form>
</body>
</html>
需要注意:如果想运行这个url,则需要修改tomcat/config/web.xml配置文件,在最后加上
<welcome-file>test.html</welcome-file>
,并在该项目中也要加入这一行代码,表示,欢迎界面优先选取test.html,此后,我们运行的网页只需要拖进WEB-INF文件夹,并改名为test.html即可。
- 通过这个窗口发送一个PNG图片,会在网页上看到:
------WebKitFormBoundaryMWB4nLrgKOYE8hRg
Content-Disposition: form-data; name="filename"; filename="1.png"
Content-Type: image/png
‰PNG
IHDR5™?ψgAMA±üa cHRMz&€„ú€èu0ê`:˜pœºQ<�bKGDÿÿÿ ½§“ pHYsÄÄ•+tIMEå
1+@6SFIDATxÚíÝoliž'öï#ù_{úÏôkºg¦9CÍ\i×÷bƒlTÜÞÜÞð‡¥@!±¾@_€’œ_D¢ÓHÚzÀö!s¢|/œ±H$€¯\ãìCDˆ‡KŽ—ÝÇn€»\¼ËÚq–s½3-w·{ºÛÝãöIO^T±X,)’âŸ*Ö÷Ã~¬zê©?$zž§žRJ‘gŒ
»DDDDT‡ù‘·8ó3í}Íu9ÆgœqÆgœqÆFDDDä)ìß$"""òægDDDDÞr¤³Å…v…‰ˆˆˆ|¥ó±dl?#"""ò–Ûϼ¥€ˆˆˆè@Ýv<²ýŒˆˆˆÈ[œígÚûšú¦Ú¸ãŒ÷#~…#)~ô^ÑSï;ÆgÜûñç?3¾MÙ¿I=rEˆK²0ìZõѽÄÏL¢Àê6qbÿ&‘·0?#"""òægDDDDÞÂüŒˆˆˆÈ[˜ŸR.-DTˆh27ìšQægD¾R^¨ëz§/9iÉÙÊÊvAÊB&>ì=""¢ÎüL{_s]ŽqÆû'7ZÒ=ÍҒ꺞+jsÊ!·P®”RÂ]¯_I«é|ñ óÚûŽqÆ÷~œóŸÑ0qþ³z•´š¼¸Û”©X³xy=rÙ›HœÇÙÓÙ‹kØ(dâÆ2 ®fŠq-2™Õ@YÙ¾žÂzÄ\>¶)#·ÅrÖ(x:±y®8³ddƒÊÊöõT€–¬.Ø(d”õº¢Ì¬NKŠåìt¢äL›Åƒ‹óŸç?#òµüBTˆ4nd}rÖ,Ž¬/HY±ìÕu¡ÔÍ„:(ÉBqñnrrgY¤,ÈíÈóëzmùTjfÛ\Rjs±ÅëÒXr#tñ'PI«ËØ(ÁL\s)
- 总之这是一堆奇怪的字符,我们可以使用以下的Servlet来处理一个上传的文件:
package chapter3;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.DataInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@WebServlet("/upload.do")
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
//读取请求Body
byte[] body = readBody(req);
//取得所有Body内容的字符串表示
String textBody = new String(body, StandardCharsets.ISO_8859_1);
//取得上传文件的名称
String filename = getFilename(textBody);
//取得文件开始与结束的位置
Position p = getFilePosition(req, textBody);
//输出至文件
writeTo(filename, body, p);
}
static class Position
{
int begin;
int end;
Position(int begin, int end)
{
this.begin = begin;
this.end = end;
}
}
private byte[] readBody(HttpServletRequest request) throws IOException {
int formDataLength = request.getContentLength();
DataInputStream dataStream =
new DataInputStream(request.getInputStream());//取得ServletInputStream对象
byte[] body = new byte[formDataLength];
int totalBytes = 0;
while (totalBytes < formDataLength)
{
int bytes = dataStream.read(body, totalBytes, formDataLength);
totalBytes += bytes;
}
return body;
}
private Position getFilePosition(HttpServletRequest request, String textBody) {
//获得文件区段边界信息
String contentType = request.getContentType();
String boundaryText = contentType.substring(
contentType.lastIndexOf("=") + 1);
//获得实际上传文件的起始与结束位置
int pos = textBody.indexOf("filename=\"");
pos = textBody.indexOf("\n", pos) + 1;
pos = textBody.indexOf("\n", pos) + 1;
pos = textBody.indexOf("\n", pos) + 1;
int boundaryLoc = textBody.indexOf(boundaryText, pos) - 4;
int begin = ((textBody.substring(0, pos)).getBytes(StandardCharsets.ISO_8859_1)).length;
int end = ((textBody.substring(0, boundaryLoc)).getBytes(StandardCharsets.ISO_8859_1)).length;
return new Position(begin, end);
}
private String getFilename(String reqBody)
{
String filename = reqBody.substring(
reqBody.indexOf("filename=\"") + 10);
filename = filename.substring(0, filename.indexOf("\n"));
filename = filename.substring(
filename.lastIndexOf("\\") + 1, filename.indexOf("\"")
);
return filename;
}
private void writeTo(String filename, byte[] body, Position p) throws IOException
{
FileOutputStream fileOutputStream = new FileOutputStream("/home/lancibe/Desktop" + filename);
fileOutputStream.write(body, p.begin, (p.end - p.begin));
fileOutputStream.flush();
fileOutputStream.close();
}
}
getPart()、getParts()取得上传文件
- 上一届最后一个程序我们可以看到,上传文件的处理过程相当琐碎,在Servlet 3.0中,新增了Part接口,可以更方便的进行文件上传处理。可以通过HttpServletRequest的getPart()方法取得Part实现对象。例如一个上传窗体如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>upload</title>
</head>
<body>
<form action="upload2.do" method="post" enctype="multipart/form-data">
上传照片:<input type="file" name="photo" /><br><br>
<input type="submit" value="上传" name="upload"/>
</form>
</body>
</html>
- 可以编写一个Servlet来进行文件上传的处理,这次使用getPart()来处理上传的文件
package chapter3;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@MultipartConfig //Tomcat中必须设置此标准才能使用getPart()相关API
@WebServlet("/upload2.do")
public class UploadServlet2 extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Part part = req.getPart("photo");//使用getPart()取得Part对象
String filename = getFilename(part);
writeTo(filename, part);
}
private String getFilename(Part part)
{
String header = part.getHeader("Content-Disposition");
String filename = header.substring(header.indexOf("filename=\"") +10, header.lastIndexOf("\""));
return filename;
}
private void writeTo(String filename, Part part) throws IOException
{
InputStream in = part.getInputStream();
OutputStream out = new FileOutputStream("/home/lancibe/Desktop" + filename);
byte[] buffer = new byte[1024];
int length = -1;
while ((length = in.read(buffer)) != -1)
{
out.write(buffer, 0, length);
}
in.close();
out.close();
}
}
- @MultipartConfig可以设置Servlet处理上传文件的相关信息,在上例中仅使用了标注没设置属性,表示属性采用默认值。所有可用的属性如下:
fileSizeThreshold:
整数值设置,若上传文件大小超过设置门槛,会先写入缓存文件,默认值为0。location:
字符串设置,设置写入文件时的目录,如果设置这份属性,则缓存文件就是写到指定的目录,也可搭配Part的write()方法使用,默认为空字符串。maxFileSize:
限制上传文件大小,默认为-1L,表示不限制大小。maxRequestSize:
限制multipart/form-data请求个数,默认值为-1L,表示不限个数。
- 在Tomcat中要在Servlet上设置@MultipartConfig才能取得Part对象,苟泽getPart()会得到null的结果。调用getPart()时要指定名称取得对应的Part对象。上一节提到过,multipart/form-data发送的每个内容区段都会有标头信息。
- 如果想取得这些标头信息,可以使用Part对象的getHeader()方法,指定标头名称来取得对应的值。所以想要取得上传的文件名称,就是取得Content-Disposition标头的值,然后取得filename属性的值。最后再利用Java IO API写入文件中。
- Part有个方便的write()方法,可以直接将上传文件指定文件名写入磁盘中,write()可指定文件名,写入的路径是相对于@MultipartConfig的location设置的路径。
package chapter3;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.IOException;
@MultipartConfig(location = "/home/lancibe/Desktop")
@WebServlet("/upload2plus.do")
public class UploadServlet2plus extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
Part part = req.getPart("photo");
String filename = getFilename(part);
part.write(filename);
}
private String getFilename(Part part)
{
String header = part.getHeader("Content-Disposition");
String filename = header.substring(
header.indexOf("filename=\"") + 10,
header.lastIndexOf("\"")
);
return filename;
}
}
- 在这个范例中,设置了@MultipartConfig的location属性。由于上传的文件名可能有中文,所以调用setCharacterEncoding()设置正确的编码。最后使用Part的write()直接将文件写入location属性指定的目录,这可以大大简化文件IO的处理。
- 如果有多个文件要上传,可以使用getParts()方法,这会返回一个Collection<Part>,其中时每个上传文件的Part对象。
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>upload3</title>
</head>
<body>
<form action="upload3.do" method="post" enctype="multipart/form-data">
文件一:<input type="file" name="file1" value=""><br>
文件二:<input type="file" name="file2" value=""><br>
文件三:<input type="file" name="file3" value=""><br>
<input type="submit" value="上传" name="upload">
</form>
</body>
</html>
- 可以使用以下的Servlet来处理文件上传请求:
package chapter3;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.IOException;
@MultipartConfig(location = "/home/lancibe/Desktop")
@WebServlet("/upload3.do")
public class UploadServlet3 extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
for(Part part : req.getParts())
{
if(part.getName().startsWith("file"))
{
String filename = getFilename(part);
part.write(filename);
}
}
}
private String getFilename(Part part)
{
String header = part.getHeader("Content-Disposition");
String filename = header.substring(
header.indexOf("filename=\"") + 10,
header.lastIndexOf("\"")
);
return filename;
}
}
- 可以使用web.xml来设置@MultipartConfig对应的信息,可以这样:
...
<servlet>
<servlet-name>UploadServlet</servlet-name>
<servlet-class>chapter.UploadServlet</servlet-class>
<multipart-config>
<location>/home/lancibe/Desktop</location>
</multipart-config>
</servlet>
使用RequestDispatcher调派请求
- 在Web应用程序中,经常需要多个Servlet来完成请求。例如,将另一个Servlet的请求处理流程包含(Include)进来,或将请求转发(Forward)给别的Servlet处理。如果有这类的请求,可以使用HttpServletRequest的getRequestDispatcher()方法取得RequestDispatcher接口的实现对象实例,调用时指定转发或包含的相对URL网址。如:
RequestDispatcher dispatcher = request.getRequestDispatcher("some.do");
使用include()方法
- RequestDispatcher的include()方法,可以将另一个Servlet的操作流程包括至目前的Servlet操作流程之中。例如:
package chapter3;
import javax.servlet.RequestDispatcher;
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 java.io.IOException;
import java.io.PrintWriter;
@WebServlet("/some.view")
public class Some extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
PrintWriter out = resp.getWriter();
out.println("Some do one...");
RequestDispatcher dispatcher = req.getRequestDispatcher("other.view");
dispatcher.include(req, resp);
out.println("Some do two...");
out.close();
}
}
- other.view实际上会依照URL模式取得对应的Servlet。调用include()时,必须分别传入实现ServletReq、ServletResponse接口的对象,可以是service()方法传入的对象,或者是自定义的对象或封装器。如果被include()的Servlet是这么编写的:
package chapter3;
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 java.io.IOException;
import java.io.PrintWriter;
@WebServlet("/other.view")
public class OtherServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
PrintWriter out = resp.getWriter();
out.println("other do one...");
}
}
- 则网页上见到的响应顺序是
Some do one...
other do one...
Some do two...
- 在取得RequestDispatcher时,也可以包括查询字符串。如:
req.getRequestDispatcher("other.view?data=123456").include(req,resp);
- 那么在包含(或转发,如果使用的是forward())的Servlet中就可以使用getParameter(“data”)取得请求参数值。
请求范围属性
- 在include()或者forward()时包括请求参数的做法,仅适用于传递字符串值给另一个Servlet,在调派请求的过程中,如果有必须共享的“对象”,可以设置给请求对象成为属性,成为请求范围属性(Request Scope Attribute),HttpServletRequest上与请求范围属性有关的几个方法如下:
setAttribute():
指定名称与对象设置属性。getAttribute():
指定名称取得属性getAttributeNames():
取得所有属性名称removeAttribute():
指定名称移除属性
- 例如,有个Servlet会根据某些条件查询数据:
List<Book> books = bookDAO.query("ServletJSP");
request.setAttribute("books", books);
request.getRequestDispatcher("result.view").include(req, resp);
- 假设result.view这个URL是负责相应的Servlet实例,则它可以用HttpServletRequest对象的getAttribute()取得查询结果
Lisk<Book> books = (List<Book>) req.getAttribute("books");
- 由于请求对象仅在此次请求周期内有效,在请求、响应之后,请求对象会被销毁回收,设置在请求对象中的属性自然也就消失了,所以通过setAttribute()设置的属性才称为请求范围属性。
使用forward方法
- RequestDispatcher有个forward()方法,调用时同样传入请求与响应对象,这表示要将请求处理转发给别的Servlet,对客户端的响应同时也转发给另一个Servlet。
若要调用forward()方法,目前的Servlet不能有任何响应确认(Commit),如果在目前的Servlet中通过响应对象设置了一些响应但未确认(响应缓冲区未满或未调用任何清除方法),则所有响应设置会被忽略,如果已经有响应确认且调用的forward()方法,则会抛出IllegalStateException异常。
- 在被转发请求的Servlet中,也可以通过一下请求范围属性名称取得对应信息:
- javax.servlet.forward.request_uri
- javax.servlet.forward.context_path
- javax.servlet.forward.servlet_path
- javax.servlet.forward.path_info
- javax.servlet.forward.query_string
- 在了解请求调派的处理方式之后,这里先来做一个简单的Model2架构引用程序,一方面应用刚才学习到的请求调派处理,另一方面初步了解Model2的基本流程。首先看控制器(Controller),他通常由一个Servlet来实现:
package chapter3;
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 java.io.IOException;
@WebServlet("/hello.do")
public class HelloController extends HttpServlet {
private HelloModel model = new HelloModel();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("user");
String message = model.doHello(name);
req.setAttribute("message", message);
req.getRequestDispatcher("hello.view").forward(req, resp);
}
}
- HelloController会收集请求参数并委托一个HelloModel对象处理,HelloController中不会有任何HTML出现。HelloModel对象处理的结果,会设置为请求对象中的属性,之后呈现画面的Servlet可以从请求对象中取得该属性。接着将请求的响应工作转发给hello.view来负责。
- 至于HelloModel类的设计很简单,利用一个HashMap,针对不同用户设置不同信息:
package chapter3;
import java.util.HashMap;
import java.util.Map;
public class HelloModel {
private Map<String, String> messages = new HashMap<>();
public HelloModel()
{
messages.put("fei", "Hello");
messages.put("xun", "Welcome");
messages.put("Lancibe", "Hi");
}
public String doHello(String user)
{
String message = messages.get(user);
return message + "," + user + "!";
}
}
- 这是一个再简单不过的类。要注意的是,HelloModel对象处理完的结果返回给HelloController,HelloModel类中不会有任何html的出现。也没有任何与前端呈现技术或后端存储技术的API出现,是一个纯粹的Java对象。
- HelloController得到HelloModel对象的返回值之后,将流程转发给HelloView呈现画面
package chapter3;
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 java.io.IOException;
@WebServlet("/hello.view2")
public class HelloView extends HttpServlet {
private String htmlTemplate =
"<html>"
+"<head>"
+"<meta http-equiv='Content-type'"
+" content='text/html;charset=UTF-8'>"
+"<title>%s</title>"
+"</head>"
+"<body>"
+"<h1>%s</h1>"
+"</body>"
+"</html>";
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String user = req.getParameter("user");
String message = (String) req.getAttribute("message");
String html = String.format(htmlTemplate, user, message);//产生html结果
resp.getWriter().print(html);//输出html结果
}
}
- 在HelloView中分别取得user请求参数以及之前在HelloController中设置在请求对象中的message属性,这里特地使用字符串来组成Html样板,在取得请求参数与属性后,分别设置样板中的两个%s占位符号,然后在输出至浏览器。这可以与同等作用的JSP作对比:
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<html>
<head>
<meta http-equiv="Cnotent-Type" content="text/html;charset=UTF-8">
<title>${param.user}</title>
</head>
<body>
<h1>${message}</h1>
</body>
</html>
- 这个JSP网页中动态的部分,是利用Expression Language功能(之后学习JSP时就会说明),分别取得user请求参数以及先前Servlet中设置在请求对象中的message属性。最主要的是注意到,JSP中没有任何Java代码的出现。
关于HttpServletResponse
- 可以使用Http来对浏览器进行响应。大部分的情况下,会使用setContentType()设置响应类型,使用getWriter()取得PrintWriter对象,而后使用PrintWriter的println()等方法输出HTML内容。
- 还可以进一步使用setHeader()、addHeader()等方法进行响应标头的设置,或者是使用sendRedirect()、sengError()方法,对客户端要求重定向网页,或是传送错误状态信息。若必要,也可以使用getOutputStream()取得ServletOutputStream,直接使用串流对象对浏览器进行字节数据的响应。
设置响应标头、缓冲区
- 可以使用HttpServletResponse对象上的setHeader()、addHeader()来设置响应标头,前者设置标头名称与值,后者则可以在同一个标头名称上附加值。如果标头的值是整数,则可以使用setIntHeader()、addIntHeader()方法,如果标头的值是个日期,则可以使用setDateHeader()、addDateHeader()方法
要注意所有的标头设置,必须在响应确认之前,在响应确认之后设置的标头会被容器忽略。
- 容器可以(但不是必要)对响应进行缓冲,通常容器都会对响应进行缓冲。可以操作HttpServletResponse以下有关缓冲的几个方法:
- getBufferSize()
- setBufferSize()
- isCommitted()
- reset()
- resetBuffer()
- flushBuffer()
- setBufferSize()必须在调用HttpServletResponse的getWriter()或getOutputStream()方法之前调用,所取得的Writer或ServletOutputStream才会套用这个设置。否则会抛出IllegalStateException异常。
- 在缓冲区未满之前,设置的响应相关内容都不会真正传至客户端,可以使用isCommitted()来看是否响应已经确认。如果想要重置所有响应信息,可以调用reset()方法,这会连同已设置的标头一并清除,调用resetBuffer()会重置响应内容,但不会清除已设置的标头内容。
- flushBuffer()会清楚所有缓冲区中已设置的响应信息至客户端,reset()、resetBuffer()必须在响应未确认前调用。
- HttpServletResponse对象若被容器关闭,则必须清除所有响应内容,响应对象被关闭的时机有以下几种:
- Servlet的service()方法已结束,响应的内容长度超过HttpServletResponse的setContentLength()所设置的长度。
- 调用了sendRedirect()方法
- 调用了sendError()方法
- 调用了AsyncContext的complete()方法
使用getWriter()输出字符
- 如果要对浏览器输出HTML,在先前的范例中,都会通过HttpServletResponse的getWriter()取得PrintWriter对象,然后指定字符串进行输出。
- 要注意的是,在没有设置任何内容类型或编码之前,HttpServletResponse使用的字符编码默认是ISO-8859-1。也就是说,如果直接输出中文,在浏览器上就会看到乱码。有几个方法可以影响HttpServletResponse输出的编码处理。
设置Locale
- 浏览器如果有发送Accept-language标头,则可以使用HttpServletRequest的getLocale()来取得一个Locale对象,代表客户端可接受的语系。
- 可以使用HttpServletResponse的setLocale()来设置地区信息,地区信息就包括了语系与编码信息。语系信息通常通过响应标头Content-Language来设置,而serLocale()也会设置HTTP响应的Content-Language标头。如:
resp.setLocale(Locale.CHINA);
- 这会改变HTTP响应的Content-Language,而字符编码处理设置为BIG5,。可以使用HttpServletResponse的getCharacterEncoding()方法取得编码设置。
- 可以在web.xml中设置默认的区域与编码对应。
<locale-encoding-mapping-list>
<locale-encoding-mapping>
<locale>zh_CN</locale>
<encoding>UTF-8</encoding>
</locale-encoding-mapping>
</locale-encoding-mapping-list>
使用setCharacterEncoding()或setContentType()
- 也可以调用HttpServletResponse的setCharacterEncoding()设置字符编码:
resp.setCharacterEncoding("UTF-8");
- 如果使用了HttpServletResponse的setContentType时,指定charset,charset的值会自动用来调用setCharacterEncoding()。例如,以下不仅设置内容类型为text/html,也会自动调用setCharacterEncoding(),设置编码为UTF-8:
resp.setContentType("text/html;charset=UTF-8")
- 如果使用该方式,则setLocale()就会被忽略。
- 因为浏览器需要指定如何处理响应,所以必须告知内容类型,setContentType()方法在相应中设置content-type标头,只要指定MIME类型就可以了。由于编码设置与内容类型通常都要设置,所以调用setContentType()设置内容类型时,同时指定charset属性是个方便且常见的做法。
- 常见的MIME设置有text/html,application/pdf,application/jar,application/x-zip,image/jpeg等,例如:
<mime-mapping>
<extension>pdf</extension>
<mime-type>application/pdf</mime-type>
</mime-mapping>
- 在介绍HttpServletRequest时,曾说明过如何正确取得中文请求参数,结合这个说明,以下范例可以通过窗体发送中文请求参数值,Servlet可以正确的接受处理并显示在浏览器中。可以使用窗体发送名称、邮件与复选项的喜爱宠物类型。窗体部分如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>宠物类型调查</title>
</head>
<body>
<form action="pet" method="post">
姓名:<input type="text" name="user" value=""/><br>
邮件:<input type="text" name="email" value=""/><br>
喜爱的宠物代表:<br>
<select name="type" size="6" multiple="true">
<option value="猫">猫</option>
<option value="狗">狗</option>
<option value="鱼">鱼</option>
<option value="鸟">鸟</option>
<option value="王八">王八</option>
</select><br>
<input type="submit" value="发送"/>
</form>
</body>
</html>
- 故意将下拉菜单设置成中文,看看稍后是否可以正确接收并显示中文。注意网页编码使用UTF-8,下面是Servlet部分:
package chapter3;
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 java.io.IOException;
import java.io.PrintWriter;
@WebServlet("/pet")
public class Pet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>感谢填写</title>");
out.println("</head>");
out.println("<body>");
out.println("联系人:<a href='mailto:"+
req.getParameter("email") +"'>"+
req.getParameter("user") + "</a><br>");
out.println("喜爱的宠物类型");
out.println("<ul>");
for(String type : req.getParameterValues("type"))
{
out.println("<li>" + type + "</li>"); //取得复选框请求参数值
}
out.println("</ul>");
out.println("</body>");
out.println("</html>");
out.close();
}
}
- 执行结果符合预期。
- 为了可以接受中文请求参数值,使用了setCharacterEncoding()方法来指定请求对象处理字符串编码的方式,这个动作必须在取得任何请求参数之前进行。
使用getOutputStream()输出二进制字符
- 有些时候,需要直接对浏览器进行字节输出,这时可以使用HttpServletResponse的getOutputStream()方法取得ServletOutputStream实例,它是OutputStream的子类。
- 举例来说,我们可能会想实现用户必须输入正确的密码,才可以进行下载等操作。
package chapter3;
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 java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@WebServlet("/download.do")
public class Download extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String passwd = req.getParameter("passwd");
if("123456".equals(passwd))
{
resp.setContentType("application/html");//设置内容类型
InputStream in = getServletContext().getResourceAsStream("/WEB-INF/test.html");//取得输入串流
OutputStream out = resp.getOutputStream();//取得输出串流
writeBytes(in, out);//读取文件并输出至浏览器
}
}
private void writeBytes(InputStream in, OutputStream out) throws IOException
{
byte[] buffer = new byte[1024];
int length = -1;
while((length = in.read(buffer)) != -1)
{
out.write(buffer, 0, length);
}
in.close();
out.close();
}
}
- 当输入密码正确时,就会读取指定的PDF文件并对浏览器进行相应。因为对浏览器输出的是PDF文件,所以设置内容类型必须为application/pdf,这样若浏览器有外挂PDF阅读器,则会直接使用阅读器打开PDF文案,而对于不知如何处理的内容类型,浏览器通常会出现另存为的提示。
使用sendRedirect()、sendError()
-
前面介绍过forward()方法,这个方法会将请求转发至指定的URL,这个动作是在Web容器中进行的,浏览器并不会知道请求被转发,地址栏也不会有所变化
-
在转发过程中,都还是在同一个请求周期,这也是为什么RequestDispatcher是由调动HttpServletRequest的getRequestDispatcher()方法取得,所以在HttpServletRequest中使用setAttribute()设置的属性对象,都可以在转发中共享。
-
可以使用HttpServletResponse的sendRedirect()要求浏览器重新请求另一个URL,又称为重定向(Redirect),使用时可以指定绝对URL或相对URL。如:
resp.sendRedirect("http://chapter3");
- 该方法会在响应中设置HTTP状态码301以及Location标头,浏览器接收到这个表头,会重新使用GET方法请求指定的URL,因此地址栏上会发现URL的变更。
- 同样的,如果在处理请求的过程中发现一些错误,也可以使用sendError()方法:
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
- 其中,SC_NOT_FOUND会令服务器响应404状态码,这类常数定义在HttpServletResponse接口上。