之前已经详细讲解了Servlet/JSP的基础知识,包括Servlet和JSP的关系、Servlet基本的编写和配置,以及一个请求/响应过程中,HTTP服务器、web容器、Servlet是如何配合工作的。

      对于一个web应用程序来说,请求/响应是其工作工程的基础,我们这里只考虑基于HTTP协议的请求/响应模型,HttpServletRequest代表请求及相关参数,HttpServletResponse代表响应及相关参数,这两个对象会随着一个请求的发起而建立,随着一个响应的结束而销毁被回收。当一个请求到来时,HttpServlet会执行service()方法,在其中判断当前HTTP的请求方式(包括GET、POST、PUT、DELETE、HEAD、OPTIONS、TRACE),并创建HttpSevletRequest和HttpServletResponse对象,传给请求方式对应的方法doXXX()。

      关于HttpSevletRequest,相关的知识无非就是请求参数的获取、编码处理、文件上传接收、请求重定向等,下面把常用的知识点整理一下,并附上demo。

获取请求参数与标头

      HttpSevletRequest里面封装了获取请求参数的方法,请求参数是以键值对的形式存储在其中的,下面是相关方法:

方法

说明

String getParameter(String key)

通过请求参数名称获取参数值

String[] getParameterValues(String key)

同上,有时候请求参数名称对应多个值

Enumeration<String> getParameterNames()

获取所有参数名称

Map<String, String[]> getParameterMap()

请求参数以Map对象返回


对于HTTP的标头(Header)信息,可以使用下面的方法来获得:

方法

说明

String getHeader(String key)

类似getParamater()

String[] getHeaders(String key)

类似getParamaterValues()

Enumeration<String> getHeaderNames()

类似getParamaterNames()


请求参数编码处理

      获取请求参数,编码是一定要考虑的,否则容易出现乱码,好在现在的框架帮我们完成了大部分编码处理的操作。什么情况下容易出现乱码呢?当客户端设置的编码和web容器使用的编码不一致时,最容易出现乱码。客户端的编码是我们自己设置的,web容器的编码一般是在一个请求中,在Content-Type表头中设置的,例如“ContentType: text/html; charset=UTF-8”就是告诉web容器:“你web容器想要获取正确的参数,就得用UTF-8解码,否则出现了问题别怪我客户端没提醒你!”。我们可以在HttpServletRequest中通过getCharacterEncoding()方法获取当前请求的编码,若客户端没有在标头中设置编码信息,这个方法将返回null,然后web容器默认使用ISO-8859-1(这是大部分浏览器默认的字符集)解码。

      下面我们只讨论POST和GET两种最常用的请求方式下的编码处理。

     POST

      客户端会将参数封装到请求中,假如客户端的编码方式是UTF-8,它在封装参数的时候,相当于执行下面这段代码:

String parameter = java.net.URLEncoder.encode(value, "UTF-8");

      然后web容器把请求和参数交给Servlet,在Servlet中取得请求参数时,若没有提前设置编码,则默认使用ISO-8859-1来解码,相当于执行了下面这段代码:

String parameter = java.net.URLEncoder.encode(value, "ISO-8859-1");

      因为编码和解码使用的字符集不一样,所以就出现了乱码。

      解决方式是在客户端发起请求的Content-Type标头中设置编码(charset=UTF-8),在Servlet中获取请求参数前,调用request.setCharacterEncoding():

request.setCharacterEncoding("GBK");

      GET

      为什么POST和GET不一样呢?因为POST和GET的传参方式不同,在HttpServletRequest的API中对serCharacterEncoding()有如下说明:

Overides the name of the character encoding userd in the body of this request
      意思就是这个方式只对请求Body中的字符编码才有用,也就是说这个方法基本上只对POST有用,因为GET传参是通过URL实现的,而URL的处理是HTTP服务器来完成的,并非Web容器,所以要使用GET方式传参,其编码处理方式就不同了。

      还是上面的例子,客户端的编码方式是UTF-8,执行下面代码,通过GET方式请求:

String parameter = java.net.URLEncoder.encode(value, "UTF-8");
      因为使用GET方式,所以web容器设置啥编码都没用了,默认使用ISO-8859-1解码:

String parameter = java.net.URLEncoder.encode(value, "ISO-8859-1");
      然后我们得在Servlet中这样解码:

String parameter = new String(value.getBytes("ISO-8859-1"), "utf-8");

下面以一个例子来演示这两种请求方式在编码处理中的差别:

test-get.html

<!DOCTYPE html>
<html>
<head>
	<meta name="content-type" content="text/html; charset=GBK">
</head>
<body>
	<form action="hello.view" method="get">
		<input type="text" name="username" />
		<button>发送GET请求</button>
	</form>
</body>
</html>
test-post.html

<!DOCTYPE html>
<html>
<head>
	<meta name="content-type" content="text/html; charset=GBK">
</head>
<body>
	<form action="hello.view" method="post">
		<input type="text" name="username" />
		<button>发送POST请求</button>
	</form>
</body>
</html>
HelloServlet.java

@WebServlet("/hello.view")
public class HelloServlet extends HttpServlet {

	public void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		String name = request.getParameter("username");
		name = new String(name.getBytes("ISO-8859-1"), "GBK");
		System.out.println("GET:" + name);
	}
	
	public void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		request.setCharacterEncoding("GBK");
		String name = request.getParameter("username");
		System.out.println("POST:" + name);
	}

}

      实际上,在Servlet中直接进行编码设置或转换,并不是最好的方法,以后学会了过滤器(Filter)时,我们就可以在过滤器中处理编码转换,将编码和逻辑解耦,这才是一种好的方式。


上传文件的接收

      在HttpServletRequest中,有getReader()方法、getInputStream()方法,可以实现获取上传的文件,但这种方式实现起来很麻烦,需要自己判断文件的起始点、标签、key等,还容易出错。于是在Servlet 3.0中,新增了Part接口,可以让我们更方便地进行文件的上传处理,举个例子:

upload.html

<!DOCTYPE html>
<html>
<head>
	<meta name="content-type" content="text/html; charset=UTF-8">
</head>
<body>
	<form action="hello.view" method="post" enctype="multipart/form-data">
		上传文件:<input type="file" name="uploadfile" />
		<input type="submit" name="upload" value="上传">
	</form>
</body>
</html>
HelloServlet.java

@MultipartConfig
@WebServlet("/hello.view")
public class HelloServlet extends HttpServlet {

	public void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		Part part = request.getPart("uploadfile");
		String fileName = getFileName(part);
		save(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 save(String fileName, Part part) throws IOException{
		InputStream in = part.getInputStream();
		File file = new File("D:/" + fileName);
		file.mkdirs();
		OutputStream out = new FileOutputStream(file);
		byte[] buffer = new byte[1024];
		int length = -1;
		while ((length = in.read(buffer)) != -1) {
			out.write(buffer, 0, length);
		}
		in.close();
		out.close();
	}
}

需要注意不要忘了在类名前加上注解@MultipartConfig,加上这个标注才可以使用Part的API。另外,@MultipartConfig还可以设置下列属性:

属性

说明

fileSizeThreshold

整数值,若上传文件大小超过这里设置的门槛,会先写入缓存文件,默认值为0

location

字符串,设置写入文件时的目录,若设置该属性,则缓存文件就是写到指定的目录,可以搭配part的write()方法使用,默认为空字符串

maxFileSize

限制上传文件大小,默认为-1L,即不限制大小

maxRequestSize

显示multipart/form-data请求个数,默认同上


注意上面的getFileName()方法中,对header的截取是通过“filename=”来实现的,为什么这里要这样硬编码呢?因为我们在upload.html的form中设置了enctype="multipart/form-data",而使用此enctype发送的每个内容区段,都会有类似以下样式的标头信息:

<span style="font-family:Microsoft YaHei;font-size:14px;">Content-Disposition: form-data; name="filename"; filename="xxxx.txt"</span>

下面是一个为@MultipartConfig设置属性的例子:

@MultipartConfig(location="D:/")
@WebServlet("/hello.view")
public class HelloServlet extends HttpServlet {

	public void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		Part part = request.getPart("uploadfile");
		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;
	}
}

对于多文件上传,只要在同一个form中,可以参考下面这个例子:

<body>
	<form action="hello.view" method="post" enctype="multipart/form-data">
		文件1:<input type="file" name="file1" />
		文件2:<input type="file" name="file2" />
		文件3:<input type="file" name="file3" />
		<input type="submit" name="upload" value="上传">
	</form>
</body>

	public void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		for (Part part: request.getParts()) {
			//加个判断是因为form表单中的上传按钮,也会被算作part
			if(part.getName().startsWith("file")){
				String fileName = getFileName(part);
				part.write(fileName);
			}
		}
	}

之前说过我们可以在web.xml中注册Servlet而不使用注解的方式,同样,@MultiConfig也可以放在web.xml中:

	<servlet>
		<servlet-name>SimpleServlet</servlet-name>
		<servlet-class>com.web.SimpleServlet</servlet-class>
		<multipart-config>
			<location>D:/</location>
		</multipart-config>
	</servlet>

使用RequestDispatcher调派请求

       web应用程序中,一个请求经常需要多个servlet协作完成,这就需要考虑多个servlet如何传递/交接请求、如何传递参数等。HttpServletRequest提供了getRequestDispatcher()方法取得RequestDispatcher接口的实现对象实例,调用时需要传入被调用的servlet名称:

RequestDispatcher dispatcher = req.getRequestDispatcher("test.view");


      RequestDispatcher接口包含两个方法:

  •  include(ServletRequest,ServletResponse):请求转发后,原先的servlet还可以继续输出响应信息,接受转发的servlet对请求的响应会按代码顺序并入原先的servlet响应对象中;
  •  forward(ServletRequest,ServletResponse):必须在响应提交给客户端之前调用,否则抛出异常;原先的servlet会被终止,在请求中没有提交的内容将被清除,而接受转发的servlet负责对请求作出响应。

      下面是一个例子:

@WebServlet("/hello.view")
public class HelloServlet extends HttpServlet {

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		PrintWriter out = resp.getWriter();
		out.println("这是第一段输出的内容");
		RequestDispatcher dispatcher = req.getRequestDispatcher("test.view");
		dispatcher.include(req, resp);
		out.println("这是第n+1段输出的内容(n >= 0)");
		out.close();
	}
	
	public void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		PrintWriter out = resp.getWriter();
		out.println("这段内容将不会被输出");
		RequestDispatcher dispatcher = req.getRequestDispatcher("test.view");
		dispatcher.forward(req, resp);
		out.println("这段内容将不会被输出");
//		out.close(); 这段代码会抛出异常
	}
}

      调派请求时传递参数

      由客户端传递过来的参数都被封装在HttpServletRequest里面,所以肯定会被传递到下一个servlet里面啦。假如我们在第一个servlet里有一些数据,想要传递到第二个servlet里,那就需要用到HttpServletRequest的setAttribute()方法了,这个用起来很简单,下面是一个例子:

@WebServlet("/hello.view")
public class HelloServlet extends HttpServlet {

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		PrintWriter out = resp.getWriter();
		RequestDispatcher dispatcher = req.getRequestDispatcher("test.view");
		List<String> list = new ArrayList<String>();
		req.setAttribute("userIds", list);
		dispatcher.include(req, resp);
		out.close();
	}
	
	public void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		PrintWriter out = resp.getWriter();
		RequestDispatcher dispatcher = req.getRequestDispatcher("test.view");
		List<String> list = new ArrayList<String>();
		req.setAttribute("userIds", list);
		dispatcher.forward(req, resp);
	}
}

      这里需要注意的一点是,对于我们传递的参数,仅在此次请求周期内有效,在请求/响应之后,参数就会被销毁,所以传递的参数的生命周期是随着请求/响应的生命周期而存在/消亡的。





Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐