一、文件上传
进行文件上传时需要做如下前提准备:
- 网页表单的请求方式必须为POST请求,并且form项的enctype属性要设置为multipart/form-data(表示以二进制方式提交请求信息)
- 使用file的表单域:<input type="file" name="file"/>(注意name属性必须设置,不然浏览器不会发送上传的数据)
当表单请求的编码方式变成以二进制的方式传输信息时,通过String request.getParameter方法是获取不到请求信息的。我们需要通过request对象获得输入流,然后通过输入流来读取到信息。这样后端就拿到上传的文件了,只不过有时候我们还需要解析等操作,用此方式是比较麻烦的。Apache组织提供了一个开源的组件commons-fileupload。该组件可将“multipart/form-data”类型请求的各种表单域解析出来,并实现一个或多个文件上传,同时也可以限制上传文件的大小等内容。
首先需要导入两个jar包:commons-fileupload和commons-io。
commons-fileupload可以解析请求,得到一个 FileItem对象组成的List,它把所有的请求信息都解析为FileItem对象,无论是一个普通的文本域还是一个文件域。(对于这一点可以利用FileItem对象的isFormField()方法来判断是否是一个表单域(若不是,则说明是一个文件域))。如下是得到这个List<FileItem>的简单方式:
FileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
List<FileItem> items = upload.parseRequest(request);
当然在需要的时候我们可以增加一些设置,如下:
DiskFileItemFactory factory = new DiskFileItemFactory();
//用于设置临时文件的的临界值100KB如不指定则系统默认为10KB
factory.setSizeThreshold(1024*100);
//创建临时存放文件夹
File tempDirectory = new File("f:\\tempDirectory");
//当上传文件尺寸大于setSizeThreshold方法设置的临界值时,将文件以临时文件形式保存在磁盘上。
//如果不设置则默认采用系统默认的临时文件路径,该路径可以通过Stsyem.getProperty("java.io.tmpdir")来获取。
factory.setRepository(tempDirectory);
ServletFileUpload upload = new ServletFileUpload(factory);
//设置编码方式为UTF-8,注意这个解决的是上传的文件路径的乱码
upload.setHeaderEncoding("UTF-8");
//设置上传文件的总大小
upload.setSizeMax(1024 * 1024 * 10);
//设置单个上传文件的大小
upload.setFileSizeMax(1024 * 1024 * 3);
List<FileItem> items = upload.parseRequest(request);
Apahce文件上传组件在解析上传数据中的每个字段内容时,需要临时保存解析的数据,以便在后面进行数据的进一步处理。由于java虚拟机默认可以使用的内存空间是有限的,超出限制则会抛出java.lang.OutOfMemoryError错误。若上传的文件很大,在内存中将无法保存该文件内容,所以采用临时文件来保存这些数据,但如果文件很小,则直接加载到内存中性能会好一些。
当拿到这个List<FileItem>之后,就可以遍历它针对普通的表单域和文件域做出不同的处理。示例如下:
List<FileItem> items = upload.parseRequest(request);
int size = items.size();
for(int index = 0; index < size; index++) {
FileItem item = (FileItem) items.get(index);
//下面只是介绍FileItem的一些常用方法。
//如果是一个普通的表单域
if(item.isFormField()) {
//获取参数名
String name = item.getFieldName();
//获取参数值(以UTF-8的编码方式获取)
String value = item.getString("UTF-8");
}else {
//这个是<input>标签的name属性值
String fieldName = item.getFieldName();
//获取上传文件的文件名(带绝对路径)
String fileName = item.getName();
//获取文件真实大小(单位B)
long sizeInBytes = item.getSize();
//这里可以使用自定义方法writeFile(file, item)将上传的文件写到目标文件中去,但是建议直接使用FileItem对象的write方法。
item.write(file);
//用于删除临时文件。务必调用。
item.delete();
}
}
private void writeFile(File file, FileItem item) throws IOException {
//获得该文件的输入流
InputStream in = item.getInputStream();
byte[] buff = new byte[1024];
int len = 0;
OutputStream out = new FileOutputStream(file);
while((len = in.read(buff)) != -1) {
out.write(buff, 0, len);
}
out.close();
in.close();
}
综上就是文件上传组件的基本使用。这里需要注意的是
中文乱码问题:
- upload.setHeaderEncoding("UTF-8"):这个解决的是上传的中文路径的乱码。
- item.getString("UTF-8"):这个解决的是普通表单项的乱码。
拷贝文件时直接调用FileItem对象的write(File file)即可。
上传完成时务必删除临时文件。
文件上传实战案例
需求:
- 在upload.jsp 页面上使用jQuery实现“新增一个附件",“删除附件"。但至少需要保留一个.。
- 对文件的扩展名和文件的大小进行验证。以下的规则是可配置的:
- 文件的扩展名必须为-pptx, docx, doc
- 每个文件的大小不能超过1M
- 总的文件大小不能超过5M.
jsp定义如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script type="text/javascript" src="${pageContext.request.contextPath}/scripts/jQuery.js"></script>
<script type="text/javascript">
$(function(){
var i = 2;
//获取#addFile
$("#addFile").click(function(){
$(this).parent().parent().before("<tr class='file' ><td>File"
+ i + ":</td><td><input type='file' name='file"
+ i + "'/></td></tr>"
+ "<tr class='desc'><td>Desc"
+ i + ":</td><td><input type='text' name='desc"
+ i + "'/><button type='button' id='delete"
+ i + "'>删除</button></td></tr>");
i++;
//获取新添加的按钮
$("#delete" + (i - 1)).click(function(){
var $tr = $(this).parent().parent();
$tr.prev("tr").remove();
$tr.remove();
//对i重写排序
$(".file").each(function(index){
var n = index + 1;
$(this).find("td:first").text("File" + n);
$(this).find("td:last input").attr("name","file" + n);
});
$(".desc").each(function(index){
var n = index + 1;
$(this).find("td:first").text("Desc" + n);
$(this).find("td:last input").attr("name","desc" + n);
});
i--;
});
});
});
</script>
</head>
<body>
<c:if test="${!empty requestScope.sizeLimitExceeded}">
${requestScope.sizeLimitExceeded}
</c:if>
<c:if test="${!empty requestScope.fileSizeLimitExeeded}">
${requestScope.fileSizeLimitExeeded}
</c:if>
<form action="fileUploadServlet" method="post" enctype="multipart/form-data">
<table>
<tr class="file">
<td>File1:</td>
<td><input type="file" name="file1" /></td>
</tr>
<tr class="desc">
<td>Desc1:</td>
<td><input type="text" name="desc1" /></td>
</tr>
<tr>
<td> <input type="submit" value="提交" /> </td>
<!--button的type 属性,IE的默认是 “button”,非IE默认是 “submit”。 所以如果不指定type则点击button会提交表单-->
<td><button type="button" id="addFile">新增一个附件</button></td>
</tr>
</table>
</form>
</body>
</html>
这里需要注意的是:File和Desc是一一对应的,所以为了后端能顺利映射成功,将JSP页面中的这两个输入项的name需要对应起来(比如:file1对应desc1)
后端的处理,可以配置扩展名,可以配置单个文件和总文件大小,我们利用properties文件进行配置,配置方式如下:
exts=pptx,docx,doc
file.max.size=1048576
total.file.max.size=5242880
工具类PropertiesParseUtil负责解析properties文件,将其键值对加载至内存:
public class PropertiesParseUtil {
private static final Map<String, String> map = new ConcurrentHashMap<String, String>();
private PropertiesParseUtil() {};
public static void parseProperties(String filepPath) {
InputStream in = PropertiesParseUtil.class.getClassLoader().getResourceAsStream(filepPath);
Properties properties = new Properties();
try {
properties.load(in);
Enumeration<Object> keys = properties.keys();
while(keys.hasMoreElements()) {
String key = (String) keys.nextElement();
map.put(key, properties.getProperty(key));
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static String getProperty(String propertyName) {
return map.get(propertyName);
}
}
配合监听器,当web服务启动时就解析properties文件:
public class FileUploadListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
PropertiesParseUtil.parseProperties("/upload.properties");
}
}
定义JavaBean,用来封装:文件名,文件存放路径,文件描述信息。(用于映射数据库中对应的三个字段)
public class FileUploadBean {
private String fileName;
private String filePath;
private String fileDesc;
public FileUploadBean(){}
public FileUploadBean(String fileName, String filePath, String fileDesc) {
this.fileName = fileName;
this.filePath = filePath;
this.fileDesc = fileDesc;
}
//省略getter和setter方法
}
核心类FileUploadServlet
1.首先我们需要获得FileItem的集合,并对配置的参数进行设置,如下:
@SuppressWarnings("unchecked")
private List<FileItem> getFileItemList(HttpServletRequest request) throws FileUploadException {
String fileMaxSizeValue = PropertiesParseUtil.getProperty("file.max.size");
String totalFileMaxSizeValue = PropertiesParseUtil.getProperty("total.file.max.size");
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(1024 * 1024 * 5);
File tempDirectory = new File(DEFAULT_DIRECTORY);
factory.setRepository(tempDirectory);
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setSizeMax(Integer.parseInt(totalFileMaxSizeValue));
upload.setFileSizeMax(Integer.parseInt(fileMaxSizeValue));
upload.setHeaderEncoding("UTF-8");
return upload.parseRequest(request);
}
2.拿到List<FileItem>之后,需要将每个FileItem解析为FileUploadBean,即解析出FileUploadBean集合;其次将扩展名合法的文件上传至后端,并且收集那些扩展名不合法的文件。
//这里IllegalExtNameList用于收集扩展名不合规格的文件
private List<FileUploadBean> buildFileUploadBeans(List<FileItem> fileItems, List<String> IllegalExtNameList) throws Exception {
Map<String, String> descMap = dealFormField(fileItems);
return dealFileField(fileItems, descMap, IllegalExtNameList);
}
这里需要分两步做的原因是:文件描述项本身是普通域而文件是文件域,这两者获取到的内容是不一样的,所以我先获取了每个文件的描述项,将其封装进HashMap中(键:每个文件的desc对应标签的name属性值,值:描述内容) ,由于将JSP页面中的这两个输入项的name是对应的(比如:file1对应desc1),所以在解析文件生成FileUploadBean对象时可以正确构建其中的文件描述项。
private Map<String, String> dealFormField(List<FileItem> fileItems){
Map<String, String> descMap = new HashMap<String, String>();
try {
for(FileItem item : fileItems) {
if(item.isFormField()) {
String key = item.getFieldName();
String value = item.getString("UTF-8");
descMap.put(key, value);
}
}
} catch (UnsupportedEncodingException e) {
//这个异常时getString抛出的,此处UTF-8是支持的
e.printStackTrace();
}
return descMap;
}
dealFileField用于生成FileUploadBean集合,并处理用户上传的文件(存储扩展名合法的文件,收集扩展名不合法的文件名用于反馈给用户)。
private List<FileUploadBean> dealFileField(List<FileItem> fileItems, Map<String,String> descMap, List<String> IllegalExtNameList) throws Exception {
String exts = PropertiesParseUtil.getProperty("exts");
List<String> legalExtNameList = Arrays.asList(exts.split(","));
List<FileUploadBean> beans = new ArrayList<FileUploadBean>();
for(FileItem item : fileItems) {
if(!item.isFormField()) {
String fieldName = item.getFieldName();
String index = fieldName.substring(fieldName.length() - 1);
//截取文件名
String fileName = item.getName();
int i = fileName.lastIndexOf('\\');
fileName = fileName.substring(i + 1);
//截取文件扩展名
int j = fileName.lastIndexOf('.');
String extName = fileName.substring(j + 1);
//效验文件扩展名
if(!legalExtNameList.contains(extName)) {
IllegalExtNameList.add(fileName);
continue;
}
//随机生成目标文件存放路径
String targetFilePath = createTargetFilePath(extName);
item.write(new File(targetFilePath));
item.delete();
beans.add(new FileUploadBean(fileName, targetFilePath, descMap.get("desc" + index)));
}
}
return beans;
}
在存储上传的文件时,并没有用原来的文件名,而是随机生成了文件名,并将其存放至默认目录中。如下:
private String createTargetFilePath(String extName) {
Random random = new Random();
int randomNumber = random.nextInt(1000000);
return TARGET_FILE_PATH + "\\" + System.currentTimeMillis() + randomNumber + "." + extName;
}
3.当请求过来时,我们只需要按照上面的思路处理就可以了,如下:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String path = null;
try {
List<FileItem> fileItems = getFileItemList(request);
List<String> IllegalExtNameList = new ArrayList<String>();
List<FileUploadBean> beans = buildFileUploadBeans(fileItems, IllegalExtNameList);
saveBeans(beans); //将信息保存至数据库
FileUtils.deleteDirectory(new File(DEFAULT_DIRECTORY)); //删除临时文件夹
path = "/app/sucess.jsp";
//设置扩展名不合法异常
setIllegalExtNameException(request, IllegalExtNameList);
} catch (Exception e) {
path = "/app/upload.jsp";
//设置上传文件大小越界异常
setFileUploadException(request, e);
}
request.getRequestDispatcher(path).forward(request, response);
}
4.向用户反馈信息,当文件大小越界,或者扩展名不合法时,都应该将该消息告知给用户,所以首先将这些消息设置进request域中,并通过请求转发的方式跳转页面(如果出现异常则所有文件都不能成功上传,因为这个异常是在ServletFileUpload对象歇息request的时候就出现的)。
设置扩展名不合法消息:
private void setIllegalExtNameException(HttpServletRequest request, List<String> IllegalExtNameList) {
if(IllegalExtNameList.size() > 0) {
StringBuilder extException = new StringBuilder();
extException.append("当前支持的扩展名有:" + PropertiesParseUtil.getProperty("exts") + "\n");
extException.append("下列文件扩展名不合法:\n");
for(String fileName : IllegalExtNameList) {
extException.append("\t" + fileName + "\n");
}
request.setAttribute("extException", extException.toString());
}
}
设置文件大小越界消息:
private void setFileUploadException(HttpServletRequest request, Exception e) {
//总文件大小超出异常
if(e instanceof FileUploadBase.SizeLimitExceededException) {
Pattern pattern = Pattern.compile("\\D*(\\d+)\\D*(\\d+)\\)");
Matcher matcher = pattern.matcher(e.getMessage());
if(matcher.matches()) {
request.setAttribute("sizeLimitExceeded", "您的总文件大小" + matcher.group(1) + "超过了规定大小" + matcher.group(2));
}
}
//单个文件大小超出异常
if(e instanceof FileUploadBase.FileSizeLimitExceededException) {
request.setAttribute("fileSizeLimitExeeded", "文件大小超出限制,允许上传的单个文件的最大大小为:" + PropertiesParseUtil.getProperty("file.max.size"));
}
}
最终的成功页面:
<body>
<c:if test="${!empty requestScope.extException}">
<h3>只有部分文件上传成功,${requestScope.extException }</h3>
</c:if>
<c:if test="${empty requestScope.extException}">
<h3>文件上传成功</h3>
</c:if>
<br>
<a href="${pageContext.request.contextPath }/app/upload.jsp">Return...</a>
</body>
下面是这几种情况的运行结果:
二、文件下载
比如我现在有一个txt文件在浏览器端展示,如果用户需要下载这个txt文件可以这样操作:将这个文件的路径包裹在超链接标签内,然后访问那个txt文件,然后点“另存为”就可以下载,如下:
当然上面的方式也是一种下载方式,是静态下载。如果我们直接点击超链接就会发现网页直接显示了txt文件中的内容。这是因为:浏览器可以自行识别解析txt格式的文件,你再直接点击超链接访问mp4文件它也是直接可以播放的。如果直接访问的是浏览器不能识别解析的文件格式,那么它才会下载。我们现在需要做的就是直接访问txt文件不让浏览器自行解析,而是让其将文件发送传输给客户端浏览器,也就是下载。具体做法很简单,如下:
前端页面:
<body>
<a href="./downloadServlet">下载mp4文件</a>
</body>
downloadServlet如下:
public class DownloadServlet extends HttpServlet{
private static final long serialVersionUID = 2730011332467612775L;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1.通知客户端浏览器:这是一个需要下载的文件,不能再按普通的html文件的方式打开。
response.setContentType("application/x-msdownload");
String fileName = "视频文件.mp4";
/*
* 2.通知浏览器,不再有浏览器处理该文件,而是交由用户自行处理
* 注意该文件名是中文,将该文件名按照UTF-8格式编码,以解决下载文件名称乱码问题
*/
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
OutputStream out = response.getOutputStream();
String realPath = getServletContext().getRealPath("resources/" + fileName);
InputStream in = new FileInputStream(realPath);
byte[] buffer = new byte[1024];
int len = 0;
while((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
in.close();
//输出流是不用关的,还需要将响应信息交给浏览器端
}
}
这样我们直接访问视频文件就会下载下来: