QUESTION:fileUpload组件原理和实现
ANSWER:
目录
查询了一系列博客后,发现这是由于上传文件的大小超过了可以上传的临时文件限制。
二:fileUpload组件及Java Web中文件上传的原理方法
一:异常产生
在刚学习springMVC框架时,上传文件出现了以下HTTP Status 500 – Internal Server Error:
Exception
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.apache.commons.fileupload.FileUploadBase$IOFileUploadException: Processing of multipart/form-data request failed. C:\Program Files\apache-tomcat-8.5.38\temp\upload_2f146db1_be01_4311_a3d8_e6c7cd69c8d3_00000002.tmp (拒绝访问。)
org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1013)
org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:908)
javax.servlet.http.HttpServlet.service(HttpServlet.java:661)
org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)
javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
Root Cause
org.apache.commons.fileupload.FileUploadBase$IOFileUploadException: Processing of multipart/form-data request failed. C:\Program Files\apache-tomcat-8.5.38\temp\upload_2f146db1_be01_4311_a3d8_e6c7cd69c8d3_00000002.tmp (拒绝访问。)
org.apache.commons.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:351)
org.apache.commons.fileupload.servlet.ServletFileUpload.parseRequest(ServletFileUpload.java:115)
com.xy.controller.UserController.fileUpload(UserController.java:36)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:498)
org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:189)
org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038)
org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:908)
javax.servlet.http.HttpServlet.service(HttpServlet.java:661)
org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)
javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
Root Cause
java.io.FileNotFoundException: C:\Program Files\apache-tomcat-8.5.38\temp\upload_2f146db1_be01_4311_a3d8_e6c7cd69c8d3_00000002.tmp (拒绝访问。)
java.io.FileOutputStream.open0(Native Method)
java.io.FileOutputStream.open(FileOutputStream.java:270)
java.io.FileOutputStream.<init>(FileOutputStream.java:213)
java.io.FileOutputStream.<init>(FileOutputStream.java:162)
org.apache.commons.io.output.DeferredFileOutputStream.thresholdReached(DeferredFileOutputStream.java:178)
org.apache.commons.io.output.ThresholdingOutputStream.checkThreshold(ThresholdingOutputStream.java:224)
org.apache.commons.io.output.ThresholdingOutputStream.write(ThresholdingOutputStream.java:128)
org.apache.commons.fileupload.util.Streams.copy(Streams.java:107)
org.apache.commons.fileupload.util.Streams.copy(Streams.java:70)
org.apache.commons.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:347)
org.apache.commons.fileupload.servlet.ServletFileUpload.parseRequest(ServletFileUpload.java:115)
com.xy.controller.UserController.fileUpload(UserController.java:36)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:498)
org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:189)
org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038)
org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:908)
javax.servlet.http.HttpServlet.service(HttpServlet.java:661)
org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)
javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
Note The full stack trace of the root cause is available in the server logs.
查询了一系列博客后,发现这是由于上传文件的大小超过了可以上传的临时文件限制。
二:fileUpload组件及Java Web中文件上传的原理方法
2.1基本原理
给出官网文档:upLoad。
组件FileUpload依赖于Commons IO组件,因此在继续之前,要确保在你的工程classpath中有描述页中提到的相应版本。(这里FileUpload版本为:commons- fileupload-1.2.1,Commons IO版本为:commons-io-1.4)。
最简单的示例(官网):
// Create a factory for disk-based file items
DiskFileItemFactory factory = new DiskFileItemFactory();
// Configure a repository (to ensure a secure temp location is used)
ServletContext servletContext = this.getServletConfig().getServletContext();
File repository = (File) servletContext.getAttribute("javax.servlet.context.tempdir");
factory.setRepository(repository);
// Create a new file upload handler
ServletFileUpload upload = new ServletFileUpload(factory);
// Parse the request
List<FileItem> items = upload.parseRequest(request);
原理:
上传的文件要求包括一个根据RFC 1867(在HTML中基于表单的文件)编码的选项列表清单。组件FileUpload可以解析这个请求,并给你的应用程序提供一份独立上传的项目清单。无论每个项目背后如何执行都实现了FileItem接口。
这里将描述组件FileUpload库的普通API,这些API比较简单。不过,对于最终的实现,你可以参考最新的API流。
每一个文件项目都有一些属性,这些可能在你的应用程序中应用到。比如:每一个项目有一个名称name和内容类型content type,并提供了一个 InputStream访问其数据。另一方面,你处理项目的方法可能有所不同,这个依赖于是否这个项目是一个规则的表单域,即:这个数据是来自普通的表单文本,还是普通的HTML域或是一个上传文件。在FileItem接口中提供了处理这些问题的方法,可以更加方便的去访问这些数据。
组件FileUpload使用FileItemFactory工厂创建新的文件项目。这个给了组件FileUpload很大的灵活性。这个工厂拥有怎样创建项目的最终控制权。工厂执行过程中上传项目文件的临时数据可以存储在内存中或硬盘上。这个依赖于上传项目的大小(即:数据的字节)。不过这种行为可以在你的应用程序中适当的自定制。
解析:
在实现上传项目之前,当然需要解析这个请求。确保这个请求的确是一个正确的上传文件,组件FileUpload为了使这个判断简单,提供了一个静态的方法去做这个事情。
// 检测我们是否一个文件上传的请求
boolean isMultipart = ServletFileUpload.isMultipartContent(request);
示例:
最简单的使用情况如下:
* 上传项目只要足够小,就应该将其保存在内存中。
* 较大的项目应该被写入到硬盘的临时文件中。
* 应该避免有非常大的上传项目。
* 设置项目默认的在内存中所占的空间,限制最大的上传请求,并设定临时文件 的位置。
处理这种情况下的请求非常的简单:
// 创建磁盘工厂
FileItemFactory factory = new DiskFileItemFactory();
// 创建处理工具
ServletFileUpload upload = new ServletFileUpload(factory);
// 解析
List <FileItem> items = upload.parseRequest(request);
这就是我们真正需要的全部代码。
处理的结果是生成了一个文件项目列表,每个文件项目实现一个FileItem接口。下面将介绍如何处理这些项目。
控制:
如果你的使用情况和上面描述的例子很接近,但是你需要在一点更多的控制限定文件的大小或临时文件的存放位置。你可以很容易的自定义上传实例或文件项目或两者的行为。下面例子展示了几种配置选项:
// 创建磁盘工厂
DiskFileItemFactory factory = new DiskFileItemFactory();
// 设置参数
factory.setSizeThreshold(yourMaxMemorySize);
factory.setRepository(yourTempDirectory);
// 创建处理工具
ServletFileUpload upload = new ServletFileUpload(factory);
// 设置最大允许的尺寸
upload.setSizeMax(yourMaxRequestSize);
// 解析
List <FileItem> items = upload.parseRequest(request);
当然,每一个配置处理方法都是独立于其他方法的,但是如果你想一次性配置这个工厂,你可以使用工厂的另一个重载方法。像这样:
DiskFileItemFactory factory = new DiskFileItemFactory( yourMaxMemorySize, yourTempDirectory);
详细的接口文档:http://commons.apache.org/proper/commons-fileupload/javadocs/api-release/index.html
2.2实现代码
使用fileUpload固定步骤:
创建工厂类:DiskFileItemFactory factory=new DiskFileItemFactory();
创建解析器:ServletFileUpload upload=new ServletFileUpload(factory);
使用解析器解析request对象:List<FileItem> list=upload.parseRequest(request);
首先导入相关包。见上文。
1.jsp代码:
<%--
Created by IntelliJ IDEA.
User: 杨路恒
Date: 2019/8/17 0017
Time: 11:24
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>文件上传</title>
</head>
<body>
<form action="UserController/fileUpload1" method="post" enctype="multipart/form-data">
选择文件:<input type="file" name="upload"><br>
上传:<input type="submit" value="上传" name="uploadFile">
</form>
</body>
</html>
2.配置springmvc-xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--扫描spring的包-->
<context:component-scan base-package="com.xy"></context:component-scan>
<!--视图解析器对象-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="internalResourceViewResolver">
<property name="prefix" value="/WEB-INF/pages/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<!--前端控制器,哪些静态资源不拦截-->
<mvc:resources location="/css/" mapping="/css/**"/> <!-- 样式 -->
<mvc:resources location="/images/" mapping="/images/**"/> <!-- 图片 -->
<mvc:resources location="/js/" mapping="/js/**"/> <!-- javascript -->
<!--开启springMVC的支持,类型转换器生效-->
<mvc:annotation-driven></mvc:annotation-driven>
</beans>
3.配置web.xml:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--前端控制器-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--中文乱码过滤器-->
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
4.控制器代码:
package com.xy.controller;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.util.List;
@Controller
@RequestMapping("/UserController")
public class UserController {
/**
* 文件上传
* @return
*/
@RequestMapping("/fileUpload1")
public String fileUpload(HttpServletRequest request) throws Exception {
System.out.println("文件上传");
//使用fileupload组件上传文件
//上传路径
String path=request.getSession().getServletContext().getRealPath("/uploads");
//判断该路径是否存在
File file=new File(path);
if (!file.exists()){
file.mkdirs();
}
//解析request对象获取上传文件项
DiskFileItemFactory diskFileItemFactory=new DiskFileItemFactory();
// diskFileItemFactory.setSizeThreshold(1024*1024); //设置内存缓冲区大小,系统默认值10KB
File temp=new File(request.getSession().getServletContext().getRealPath("/temp"));
temp.mkdirs();
System.out.println("建立成功");
diskFileItemFactory.setRepository(temp);
System.out.println(System.getProperty("java.io.tmpdir"));
ServletFileUpload servletFileUpload=new ServletFileUpload(diskFileItemFactory);
servletFileUpload.setFileSizeMax(100*1024*1024);
//解析request
List<FileItem> items=servletFileUpload.parseRequest(request);
for (FileItem item:items
) {
//判断当前item是否是上传文件对象
if (item.isFormField()){
//说明普通表单向
}
else {
//说明上传文件项
//获取文件名称
String fileName=item.getName();
//完成文件上传
item.write(new File(path,fileName));
System.out.println("写入成功");
//删除临时文件,系统会自动删除
// item.delete();
}
}
return "success";
}
}
5.success代码:
<%--
Created by IntelliJ IDEA.
User: 杨路恒
Date: 2019/8/17 0017
Time: 11:21
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>文件上传成功</h1>
</body>
</html>
1.普通表单提交默认enctype="application/x-www-form-urlencoded";但是当表单中存在文件类型时,需要设置enctype="multipart/form-data",它不对字符进行编码,用于发送二进制的文件(即所有文件类型,如视频、图片、音乐、文档都可以用此类型entype);还有一种enctype="text/plain"用于发送纯文本内容。
2.表单请求方式必须为post。
3.接收时不能再用request.getParameter(),而是request.getInputStream()解析二进制流,得到ServletInputStream对象。
2.3重要方法
1.DiskFileItemFactory:
DiskFileItemFactory有两个方法 :setSizeThreshold和setRepository
setRepository方法用于设置当上传文件尺寸大于setSizeThreshold方法设置的临界值时,将文件以临时文件形式保存在磁盘上的存放目录。有一个对应的获得临时文件夹的 File getRespository() 方法。
注意:当从没有调用此方法设置临时文件存储目录时,默认采用系统默认的临时文件路径,可以通过系统属性 java.io.tmpdir 获取。如下代码:
System.getProperty("java.io.tmpdir");
Tomcat系统默认临时目录为“<tomcat安装目录>/temp/”。
setSizeThreshold方法说明:
Apache文件上传组件在解析上传数据中的每个字段内容时,需要临时保存解析出的数据,以便在后面进行数据的进一步处理(保存在磁盘特定位置或插入数据库)。因为Java虚拟机默认可以使用的内存空间是有限的,超出限制时将会抛出“java.lang.OutOfMemoryError”错误。如果上传的文件很大,例如800M的文件,在内存中将无法临时保存该文件内容,Apache文件上传组件转而采用临时文件来保存这些数据;但如果上传的文件很小,例如600个字节的文件,显然将其直接保存在内存中性能会更加好些。
setSizeThreshold方法用于设置是否将上传文件已临时文件的形式保存在磁盘的临界值(以字节为单位的int值),如果从没有调用该方法设置此临界值,将会采用系统默认值10KB。对应的getSizeThreshold() 方法用来获取此临界值。
2.FileItem:
String getFieldName():获取表单项的name的属性值。
String getName():获取文件字段的文件名。如果是普通字段,则返回null
String getString():获取字段的内容。如果是普通字段,则是它的value值;如果是文件字段,则是文件内容。
String getContentType():获取上传的文件类型,例如text/plain、image。如果是普通字段,则返回null。
long getSize():获取字段内容的大小,单位是字节。
boolean isFormField():判断是否是普通表单字段,若是,返回true,否则返回false。
InputStream getInputStream():获得文件内容的输入流。如果是普通字段,则返回value值的输入流。
2.4注意事项:
1.文件名中文乱码处理:servletFileUpload.setHeaderEncoding("utf-8") 或 request.setCharacterEncoding("utf-8");
2.表单普通字段中文乱码处理:new String(str.getBytes("iso-8859-1","utf-8"));
3.设置内存缓冲区的大小,默认为10KB:diskFileItemFactory.setSizeThreshold(1024*1024);
4.指定临时文件目录,如果单个文件的大小超过内存缓冲区,该文件将会临时缓存在此目录下diskFileItemFactory.setRepository(file);
5.设置单个文件大小限制,如果有某个文件超过此大小,将抛出FileUploadBase.FileSizeLimitExceededException:servletFileUpload.setFileSizeMax(1024*1024*10);
6.设置所有文件,也就是请求大小限制,如果文件总和超过此大小,将抛出FileUploadBase.SizeLimitExceededException:servletFileUpload.setSizeMax(1024*1024*20);
7.利用UUID生成伪随机字符串作为文件名避免重复:UUID.randomUUID().toString();
8.将文件写到硬盘上。写完之后,系统会自动将放在临时文件目录的该文件删除:fileItem.write(new File(path,fileName));
fileUpload用来上传文件,主要任务解析request:
parseRequest(request)