JavaWeb之文件上传和下载-Ajax技术

文件上传和下载

文件的上传和下载是日常开发中常见需求及功能,在Servlet3.0以前,官方并没有提供相关的API,如果要实现文件的上传需要借助apache的开源工具common-fileupload,实现文件的上传相对比较麻烦,但是Servlet3.0以后官方提供了文件上传的相关实现,在本章节内将详细了解文件的上传。

MultipartConfig注解

Servlet3.0 提供了MultipartConfig注解,用于配置文件上传的相关属性,例如上传路径,上传文件大小等,查看MultipartConfig源码。

1
2
3
4
5
6
7
8
9
10
11
12
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MultipartConfig {

String location() default "";

long maxFileSize() default -1L;

long maxRequestSize() default -1L;

int fileSizeThreshold() default 0;
}

下面对各个属性进行解释:

  • location:上传文件的临时目录
  • maxFileSize:上传文件大小的限制,如果超出该限制则抛出异常
  • maxRequestSize:最大请求限制,如果超出该限制则抛出异常
  • fileSizeThreshold:设置文件缓存的临界点,超过则先保存到临时目录

需要注意的是在控制文件上传的Servlet上必须要加该注解,否则程序将抛出异常

Part接口

了解了MultipartConfig以后,还需要了解Part类,Part接口的作用是获取文件名,向目录中写入上传文件等,下面先了解一下Part接口中常用的方法:

方法 描述
public void write(String fileName) throws IOException; 将文件写入指定的磁盘位置
public long getSize(); 获取上传文件的大小
public String getName(); 获取file空间的name属性
public InputStream getInputStream() throws IOException; 获取文件输入流
public String getSubmittedFileName(); 获取上传文件的名称

文件上传

了解了MultipartConfig注解和Part接口后,就可以实现文件的上传功能。

单文件上传

  1. 新建JSP页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Insert title here</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/upload" method="post" enctype="multipart/form-data">
文件:<input type="file" name="file">
<input type="submit" value="提交">
</form>
</body>
</html>
  1. 新建Servlet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package cn.bytecolleg.controller;

import java.io.IOException;
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;

@WebServlet("/upload")
@MultipartConfig(maxFileSize = 1024*1024*50)
public class UploadController extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doPost(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
//接受数据
Part part = request.getPart("file");
String path = "D:/upload/";
//获取文件名称
String name = part.getSubmittedFileName();
System.out.println(name);
//写文件
part.write(path+name);
response.getWriter().write("上传成功");
}
}

在上例中只配置了上传文件的最大限制为50M。如果超出此限制则抛出异常。另外在页面的表单上,一定要添加enctype=”multipart/form-data”属性,否则Servlet将无法获取上传的文件。

多文件上传

通常情况下还会遇到多文件上传,多文件上传和单文件上传的区别有如下两点:

  1. 在页面的input需要配置可以选择多文件
  2. 在Servlet中接收文件时需要调用request对象getParts()方法,该方法会获取所有上传的文件

JSP页面如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Insert title here</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/upload" method="post" enctype="multipart/form-data">
文件:<input type="file" name="file" multiple="multiple">
<input type="submit" value="提交">
</form>
</body>
</html>

Servlet页面如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package cn.bytecolleg.controller;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

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;

@WebServlet("/upload")
@MultipartConfig(maxFileSize = 1024*1024*50)
public class UploadController extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doPost(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
//接受数据
List<Part> list = new ArrayList<>();
list = (List<Part>) request.getParts();
String path = "D:/upload/";
//获取文件名称
for (int i = 0; i < list.size(); i++) {
Part part = list.get(i);
String name = part.getSubmittedFileName();
System.out.println(name);
//写文件
part.write(path+name);
}
response.getWriter().write("上传成功");
}
}

文件下载

文件下载核心是读取文件,如果文件是在项目的webapp目录下,直接访问对应的链接就可以下载,但是通常情况下都不会把文件放置在webapp目录下,这是因为随着文件的增加,项目部署和启动的时间也会增加,一般做法是将文件放置在硬盘中其他目录下,客户端通过链接访问控制下载的Servlet,有Servlet找到对应的文件,并将文件以流的形式发送给客户端。这么做的优点是减小了项目的体积,缩短启动和部署的时间,其次也可以更好的控制下载速度,防止同一时间下载请求过高导致服务器崩溃。控制下载的核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Insert title here</title>
</head>
<body>
<a href="${pageContext.request.contextPath}/download?filename=a.zip">a.zip</a>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package cn.bytecolleg.controller;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* Servlet implementation class DownController
*/
@WebServlet("/download")
public class DownloadController extends HttpServlet {
private static final long serialVersionUID = 1L;

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取要下载的文件名
String name = request.getParameter("filename");
//在对应目录下寻找该文件
String path = "D:/upload/";

FileInputStream fis = new FileInputStream(path+name);
byte[] b = new byte[1024];
//为了防止文件名中文不显示或者乱码, 对文件名进行编码
response.setHeader("Content-Disposition","attachment;filename="+ URLEncoder.encode(name,"UTF-8"));

ServletOutputStream sos = response.getOutputStream();
int i = 0;
while((i=fis.read(b))>-1) {
sos.write(b);
}
//关闭资源
sos.close();
fis.close();
}

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}

}

在上例的DownServlet中,首先获取了要下载文件的名称,然后在对应的目录中寻找该文件,并使用流读取文件。读取到文件后获取ServletOutputStream对象,使用该对象向客户端输出文件流,需要注意的是输出前必须设置响应头,告诉客户端浏览器保存文件。

Ajax技术

Ajax是Asynchronous JavaScript and XML的缩写,意思是异步的JavaScript与XML。Ajax并不是一门新的语言或技术,它是JavaScript、XML、CSS、DOM等多种已有技术的组合,可以实现客户端的异步请求操作。从而可以实现在不需要刷新页面的情况下与服务器进行通信,减少了用户的等待时间,减轻了服务器和带宽的负担,提供更好的服务响应。

在传统的Web应用模式中,页面中用户的每一次操作都将触发一次返回Web服务器的HTTP 请求,服务器进行相应的处理(获得数据、运行与不同的系统会话)后,返回一个HTML页面给客户端。

而在Ajax应用中,页面中用户的操作将通过Ajax引擎与服务器端进行通信,然后将返回结果提交给客户端页面的Ajax引擎,再由Ajax引擎来决定将这些数据插入到页面的指定位置。

与传统的Web应用不同,Ajax在用户与服务器之间引入一个中间件XMLHttpRequest,该中间件可以在不提交整个页面的前提下进行局部提交,Ajax具有如下几个优点:

  1. 减轻服务器的负担。Ajax的原则是“按需求获取数据”,这可以最大程度地减少冗余请求和响应对服务器造成的负担。
  2. 可以把一部分以前由服务器负担的工作转移到客户端,利用客户端闲置的资源进行处理,减轻服务器和带宽的负担,节约空间和成本。
  3. 无刷新更新页面,Ajax使用XMLHttpRequest对象发送请求并得到服务器响应,在不需要重新载入整个页面的情况下,就可以通过DOM及时将更新的内容显示在页面上。

XMLHttpRequest对象

Ajax是XMLHttpRequest对象和JavaScript、XML、CSS、DOM等多种技术的组合。

Ajax使用的技术中,最核心的技术就是XMLHttpRequest,它是一个具有应用程序接口的JavaScript对象,能够使用超文本传输协议(HTTP)连接一个服务器,是微软公司为了满足开发者的需要,于1999年在IE 5.0浏览器中率先推出的。通过XMLHttpRequest对象,Ajax可以像桌面应用程序一样只同服务器进行数据层面的交换,而不用每次都刷新页面,也不用每次都将数据处理的工作交给服务器来完成,这样既减轻了服务器负担又加快了响应速度、缩短了用户等待的时间。

初始化XMLHttpRequest对象

在使用XMLHttpRequest对象发送请求和处理响应之前,首先需要初始化该对象,由于XMLHttpRequest不是一个W3C标准,所以对于不同的浏览器,初始化的方法也是不同的。通常情况下,初始化XMLHttpRequest对象只需要考虑两种情况,一种是IE浏览器,另一种是非IE浏览器

IE浏览器创建XMLHttpRequest对象代码如下:

1
var xhr = new ActiveXObject("Mircrosoft.XMLHTTP");

非IE浏览器创建XMLHttpRequest对象代码如下:

1
var xhr = new XMLHttpRequest();

为了提高程序的兼容性,可以创建一个跨浏览器的XMLHttpRequest对象。创建一个跨浏览器的XMLHttpRequest对象其实很简单,只需要判断一下不同浏览器的实现方式,如果浏览器提供了XMLHttpRequest类,则直接创建一个实例,否则实例化一个ActiveX对象,具体代码如下:

1
2
3
4
5
6
var xhr = null;
if(window.XMLHttpRequest){
xhr = new XMLHttpRequest();
}else if(window.ActiveXObject){
xhr = new ActiveXObject("Mircrosoft.XMLHTTP");
}

在上面的代码中,调用window.ActiveXObject将返回一个对象或是null,在if语句中,会把返回值看做true或false(如果返回的是一个对象,则为true,如果返回null,则为false)。

XMLHttpRequest对象的常用方法

XMLHttpRequest对象提供了一些常用的方法,通过这些方法可以对请求进行操作。下面对XMLHttpRequest对象的常用方法进行介绍。

  • open()方法:用于设置进行异步请求目标的URL、请求方法以及其他参数信息。

open()方法的参数说明如表:

参数 说明
method 用于指定请求的类型,一般为GET或者POST
URL 用于指定请求地址,可以传递参数
asyncFlage 可选参数,用于指定是否异步请求,默认情况下位true
username 可选参数,用于指定请求用户名,可省略
password 可选参数,用于指定请求密码,可省略
  • send(content)方法:用于向服务器发送请求,如果请求声明为异步,该方法将立即返回,否则将等到接受响应为止,用于指定发送的数据,可以是DOM对象的实例、输入流或字符串。如果没有参数需要传递,可以设置为null。
  • setRequestHeader()方法:用于为请求的HTTP头设置值。setRequestHeader()方法的具体语法格式如下:
1
setRequestHeader("header","value")

header:用于指定HTTP头。value:用于为指定的HTTP头设置值。setRequestHeader()方法必须在调用open()方法之后才能调用。例如,在发送POST请求时,需要设置Content-Type请求头的值为“application/x-www-form-urlencoded”,这时就可以通过setRequestHeader()方法进行设置。

XMLHttpRequest对象的常用属性

XMLHttpRequest对象提供了一些常用属性,通过这些属性可以获取服务器的响应状态及响应内容,下面将对XMLHttpRequest对象的常用属性进行介绍。

  1. onreadystatechange属性:onreadystatechange属性用于指定状态改变时所触发的事件处理器。在Ajax中,每个状态改变时都会触发这个事件处理器,通常会调用一个JavaScript函数。
  2. readyState属性:属性用于获取请求的状态。该属性共包括5个属性值,如表所示。
含义
0 未初始化
1 正在加载
2 已加载
3 交互中
4 完成
  1. responseText属性:responseText属性用于获取服务器的响应,表示为字符串
  2. responseXML属性:responseXML属性用于获取服务器的响应,表示为XML。这个对象可以解析为一个DOM对象。
  3. status属性:status属性用于返回服务器的HTTP状态码,常用的状态码如表所示。
含义
200 表示成功
202 表示请求被接受,但尚未成功
400 错误的请求
404 文件未找到
500 内部服务器错误

Ajax实战

Ajax可以通过XMLHttpRequest对象实现采用异步方式在后台发送请求。通常情况下,Ajax发送请求有两种,一种是发送GET请求,另一种是发送POST请求。但是无论发送哪种请求,都需要经过以下5个步骤。

  1. 初始化XMLHttpRequest对象。
  2. 创建一个与服务器的连接
  3. 为XMLHttpRequest 对象指定一个返回结果处理函数
  4. 向服务器发送请求。
  5. 解析响应结果。

例如注册的业务时需要检测用户名是否已经被注册,在Ajax出现之前,必须将表单提交以后将用户填写的用户名发送的服务端,服务端检测结束后再将结果返回给客户端,这个过程是一个很糟糕的体验,假如填写的信息比较多,信息提交后如果用户名已经被注册,服务器返回给客户端结果后,之前填写的数据都将重新填写。当Ajax技术出现后可以在不提交表单的前提下进行局部提交,并且不会出现提交表单时页面的闪白。下面就检查用户名是否重复进行示范:

客户端

register.html页面如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>注册页面</title>
</head>
<body>
<form action="" method="post">
<input id="username" name="username" type="text" onblur="check()" placeholder="用户名"><br>
<input name="password" type="password" placeholder="密码"><br>
<input type="submit" value="注册">
</form>
</body>
</html>
<script>
function check() {
//获取用户名
var username = document.getElementById("username").value;
//获取XMLHttpRequest对象
var xhr = new XMLHttpRequest();
//发送GET请求
xhr.open("post","${pageContext.request.contextPath}/check");
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.send("username="+username);
//①
xhr.onreadystatechange = function (){
if(xhr.readyState==4){
if(xhr.status==200){
alert(xhr.responseText)
}
}
}
}
</script>

在页面中定义了一个表单,用于填写用户注册的用户名和密码,并且在填写用户名的input上绑定了事件,当该input失去焦点是就会触发该事件,并调用check函数。

在check函数中首次创建了XMLHttpRequest对象,使用open函数设置了请求方式和请求地址,因为请求方式是post。因此还需要设置请求头(get请求则不需要),接着讲获取到的用户名使用send函数发送至服务器。

在代码①处,当XMLHttpRequest的readyState状态发生变化时都会调用后面的函数,在函数内判断XMLHttpRequest的状态以及服务器响应状态,当readyState值为4并且服务器成功响应后弹出服务端响应的内容。

服务端

服务端验证用户名的Servlet代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package cn.bytecollege.controller;

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("/check")
public class CheckUsername extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doPost(request,response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
String username = request.getParameter("username");
//模拟从数据库查询
PrintWriter writer = response.getWriter();
if(username.equals("admin")){
writer.write("post用户名重复");
}else{
writer.write("post用户名可用");
}
}
}

服务端在接收到请求以后,先获取用户名,获取到用户名以后模拟从数据库查询数据,如果用户名是admin向客户度响应用户名重复。否则响应用户名可用。