1. 文件上传
在web开发中文件上传是一个十分重要的操作,通过网页上传文件只能通过表单的 file 表单输入域进行。但是一般情况下通过表单上传的数据都是文本数据,通过键值对传送。就像不可能使用字符流成功拷贝图片一样,显然不能将二进制的文件数据转化为字符串的value进行传送。所以,要传送二进制数据,我们就需要一种新的编码方式 multipart/form-data,通过form表单的enctype属性进行设置 enctype='multipart/form-data'
。
故而前端页面如下:
<form action="响应路径..." method="post" enctype="multipart/form-data">
<input type="file" name="upfile" /><br/>
<input type="text" name="text" value="其他数据" /><br/>
<input type="radio" name="sex" value="男"/>男<br/>
<input type="radio" name="sex" value="女"/>女<br/>
<button type="submit">提交</button><br/>
</form>
multipart/form-data 不是以key-value形式编码,那它是如何编码的呢?
容易发现 servlet 中无法通过getParameter获取参数了,那我们只能通过request.getInputStream()
获取请求数据的输入流,打印一下看看这 multipart/form-data 格式的数据到底长啥样?
servlet如下:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取输入流并转化为字符流
ServletInputStream inputStream = request.getInputStream();
InputStreamReader isr = new InputStreamReader(inputStream,"utf-8");
BufferedReader br = new BufferedReader(isr);
// 打印请求体的数据
String buff;
while((buff=br.readLine())!=null){
System.out.println(buff);
}
br.close();
}
输出结果如下:
可以发现数据被 ------WebKitFormBoundaryhl5cYVh7Z4LDjf3x
分成了几部分。
分析可知每一部分都对应这表单中的一个输入域,也就相当于原来的一组 key-value。
分割行的下一行 Content-Disposition: form-data; name="text"
中name保存了表单输入域的name值。
如果是文件还会多一个filename
项,也就是上传文件的文件名。同时下一行指明文件的类型 Content-Type: image/png
,若是普通的表单输入域则不会有这一行。
再往下都是一个空行分割,然后就是表单域对应的值。如上例中的 5-14 行就是上传的文件的数据了,只不过这里被我强转为了字符串输出,出现乱码。
解析数据,完成上传。
了解了请求数据的结构也就好办了,只要我们将其中的二进制截出来放到文件里就行了。
但是还有一个问题:每次请求的分隔符 ------WebKitFormBoundaryhl5cYVh7Z4LDjf3x
都是不一样的,那么如何获得这个分隔符的?
原来这东西已经包含在请求头中一起被提交了,我们只需要从请求头中拿出来就好
要注意:这里少两个 -- ,而上边 的请求体结束时最后一行最后也是 --。
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取分隔符
String header = request.getHeader("Content-Type");
String sp = header.split("boundary=")[1];
// 存储路径
String dir = "C:\\Users\\默尘\\Desktop\\新建文件夹\\";
String fileName=null;
BufferedOutputStream bos = null;
// 从输入流中截取文件数据
ServletInputStream inputStream = request.getInputStream();
int readFlag = 3;
for(boolean flag=true;flag;){
// 读一行
int c;
List<Integer> line = new ArrayList<>();
while((c=inputStream.read())!='\n'){
if(c==-1){
flag=false;
break;
}
line.add(c);
}
// data中保存二进制数据
byte[] data = tobyteArray(line);
// lineStr中保存转码后的字符串
String lineStr = new String(data,"utf-8");
// 根据字符串进行判断
if(lineStr.contains("filename")){
readFlag=2; // 表示现在进入文件域了,准备读...
fileName = lineStr.substring(lineStr.indexOf("filename=\"")+10,lineStr.length()-2);
fileName = URLDecoder.decode(fileName,"utf-8");
if(fileName.contains("''")) {
fileName = fileName.split("''")[1];
}
bos = new BufferedOutputStream(new FileOutputStream(dir+fileName));
System.out.println("文件名:" + fileName);
}else if(readFlag==2 && "\r".equals(lineStr)){
readFlag=1; // 表示下一行就是文件内容,可以读了。
}else if(lineStr.contains(sp) && readFlag==1){// 分割行,退出读状态
bos.close();
readFlag = 3;
}else if(readFlag==1){ // 如果处于读状态 就拷贝数据
bos.write(data);
bos.write(10); // 把 \n 补上
}
}
}
private byte[] tobyteArray(List<Integer> line){
byte[] tmp = new byte[line.size()];
for(int i=0;i<tmp.length;i++){
tmp[i] = line.get(i).byteValue();
}
return tmp;
}
使用 FileUpload 工具类完成文件上传
可以发现,从文本与二进制夹杂的输入流中截取数据是非常麻烦的,而且这还只是提取文件而已,普通文本域的数据并未提取。
所以,为了便于文件上传,可以借助一些工具类。
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 存储路径
String dir = "C:\\Users\\默尘\\Desktop\\新建文件夹\\";
try {
//1.创建磁盘文件项工厂
DiskFileItemFactory factory = new DiskFileItemFactory();
//2.创建文件上传核心类
ServletFileUpload upload = new ServletFileUpload(factory);
//2.1 设置上传文件名的编码
upload.setHeaderEncoding("utf-8");
//2.2 判断表单是否是文件上传表单
boolean multipartContent = upload.isMultipartContent(req);
//2.3 是文件上传表单
if (multipartContent) {
//3. 解析request ,获取文件项集合
List<FileItem> list = upload.parseRequest(req);
if (list != null) {
//4.遍历获取表单项
for (FileItem item : list) {
//5. 判断是不是一个普通表单项
boolean formField = item.isFormField();
if (formField) {
//普通表单项
String fieldName = item.getFieldName();
String value = item.getString("utf-8");//设置编码
System.out.println(fieldName + "=" + value);
} else {
//文件上传项
//文件名
String fileName = item.getName();
//避免图片名重复
String newFileName = new Random().nextInt() + "_" + fileName;
//获取输入流
InputStream in = item.getInputStream();
//创建输出流 输出到H盘
FileOutputStream fos = new FileOutputStream(dir+newFileName);
//使用Java9新特性,copy文件
in.transferTo(fos);
//关闭流
fos.close();
in.close();
}
}
}
}
} catch (FileUploadException e) {
e.printStackTrace();
}
}
相关类说明:
- 导入依赖:
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.2.1</version>
</dependency>
- DiskFileItemFactory类
磁盘文件项工厂, 读取文件时相关的配置,比如: 缓存的大小 , 临时目录的位置 - ServletFileUplaod类
用于解析请求对象。是文件上传的核心类isMultipartContent(request);
判断请求是不是 multipart/form-data 格式的。parseRequest(request);
解析请求,返回 FileItem 的集合。setHeaderEncoding("UTF-8");
设置每个域中信息头的编码;若不设置,文件名有中文时,可能乱码。 - FileItem类
由ServletFileFactory解析request得到,每个FileItem对象都代表表单中的一个输入域。isFormField()
判断输入域是否为 普通文本域getFieldName()
获取 \<input /\> 中的name值getString("utf-8")
获取普通文本域的 value,指定其编码以防止出现乱码。getName()
获取上传的文件名getInputStream()
获得上传文件的输入流delete()
删除零时文件
使用@MultipartConfig注解完成文件上传
使用工具类虽然简化了代码且功能强大,但还是要导包,也挺麻烦的,servlet3.0之后,servlet就支持了文件上传功能。
首先,要在Servlet上添加注解:@MultipartConfig
然后就可以通过 getParameter( ) 方法获取一般的表单域的数据。通过 Part对象 就可以获取文件数据
通过request的以下方法就可以获取Part对象:
request.getPart(String name); // 通过表单域的name获取每个文件对应的Part对象
request.getParts(); // 获取所有的 Part对象
每个Part对象代表一个表单输入域。
注意:
- 当多文件上传时一个part只能代表一个文件,这时候就只能使用 getParts() 获取所有的Part再处理,但是普通文本域也会被一起获取到,处理时要注意区分。
Part对象的使用:
// 获取文件信息
public long getSize(); // 获取文件的大小
public String getContentType(); // 获取文件的的类型
// 获取Part头部信息
public String getName(); // 获取表单域的name属性
public Colloction<String> getHearderNames(); //获取所有 Part头 的name
public String getHeader(String name); // 根据 Part头的名字 获取对应的值
public Colloction<String> getHeaders(String name); // 作用同上,用于值有多个的情况
// 文件操作
public void write(String filePath); // 将上传的文件写入到指定的文件中。
public InputStream getInputStream(); // 获取上传文件的输入流
public void delete(); // 删除临时文件
注意:
- Part头是指每一部分数据之前的内容,一行为一个Part头,如:Content-Disposition
- Part对象并没有获取文件名的函数,只能自己从Part头信息中截取。
private String getFileName(Part part){
String header = part.getHeader("Content-Disposition");
return header.substring(header.indexOf("filename=\"")+10,header.lastIndexOf("\""));
}
举例:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
// 存储路径
String dir = "C:\\Users\\默尘\\Desktop\\新建文件夹\\";
// 根据name获取 Part
Part upfile = request.getPart("upfile");
// 上传文件 的 信息
String fileName = getFileName(upfile);
System.out.println("文件名称:\t"+fileName);
System.out.println("文件大小:\t"+upfile.getSize()+" B");
System.out.println("文件类型:\t"+upfile.getContentType());
// part 头部信息
System.out.println("part的name\t"+upfile.getName());
Collection<String> headerNames = upfile.getHeaderNames();
System.out.println("headers:");
for (String headerName : headerNames) {
String value = upfile.getHeader(headerName);
System.out.println("\t"+headerName+" : "+value);
}
// 存储文件
upfile.write(dir+fileName);
System.out.println("over");
}
private String getFileName(Part part){
String header = part.getHeader("Content-Disposition");
return header.substring(header.indexOf("filename=\"")+10,header.lastIndexOf("\""));
}
关于@MultipartConfig注解:
常用属性如下:
属性名 | 类型 | 说明 |
location | String | 指定容器临时存放文件 的目录 |
maxFileSize | long | 允许上传的文件的最大字节数 |
fileSizeShreshold | int | 但上传的数据大于该值时,就会将数据写到磁盘 |
也可以通过web.xml配置
<servlet>
<servlet-name>sss</servlet-name>
<servlet-class>xxxxx</servlet-class>
<multipart-config>
<location></location>
<file-size-threshold></file-size-threshold>
<max-file-size></max-file-size>
</multipart-config>
</servlet>