文件上传原理
来个例子
客户端
<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="exampleInputEmail1">邮箱</label>
<input type="email" name="email" class="form-control" id="exampleInputEmail1"
placeholder="Enter email">
</div>
<div class="form-group">
<label for="exampleInputPassword1">名字</label>
<input type="text" name="userName" class="form-control" id="exampleInputPassword1"
placeholder="Password">
</div>
<div class="form-group">
<label for="headImage">头像</label>
<input type="file" name="headImage" id="headImage">
<p class="help-block">Example block-level help text here.</p>
</div>
<div class="form-group">
<label for="photos">File input</label>
<input type="file" name="photos" id="photos" multiple>
<p class="help-block">Example block-level help text here.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox"> Check me out
</label>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
服务端
@Log4j2
@Controller
public class UpLoadController {
@PostMapping("/upload")
public String upLoad(@RequestParam("email") String email,
@RequestParam("userName") String userName,
@RequestPart("headImage") MultipartFile headImage,
@RequestPart("photos") MultipartFile[] photos) {
log.info("上传信息:email:{},userName:{},headImage:{},photos:{}", email, userName, headImage.getSize(), photos.length);
return "form/form_layouts";
}
}
设置单个文件大小限制、总体文件大小限制
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-file-size=100MB
原理
在SpringBoot启动的时候,会通过MultipartAutoConfiguration向IOC中注入一个文件解析器:StandardServletMultipartResolver,就是它负责判断和解析的
判断当前的请求是否是文件上传
请求来到DispatcherServlet中的doDispatch方法,然后在调用checkMultipart方法实现判断:就是判断当前的请示是否以"multipart/"开头,所有以我们在请求端的时候标注的enctype="multipart/form-data"是很重要的。
@Override
public boolean isMultipart(HttpServletRequest request) {
return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
}
然后调用StandardServletMultipartResolver的resolveMultipart方法将请求封装成MultipartHttpServletRequest返回
根据客户端的参数和服务端接收的参数找到对应的参数解析器
得到MultipartHttpServletRequest之后,会去HandlerMapping中获取对应的HandlerExecutionChain(里面包含的了HandlerAdapter和拦截器),在调用HandlerAdapter的handle( )方法获取参数解析器。匹配对应的参数解析器的代在:HandlerMethodArgumentResolverComposite类中的getArgumentResolver( )方法,最后得到的解析文件上传的解析器叫做RequestPartMethodArgumentResolver,判断方法就是看你参数上是否标注了RequestPart注解
//拿到系统内置的参数解析器,用单个参数挨个的匹配使用哪个文件解析器(第一次查找到之后会缓存起来)
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
解析文件
最后使用的是MultipartResolutionDelegate类中的静态方法resolveMultipartArgument获取到客户端上传的文件
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest request, @Nullable WebDataBinderFactory binderFactory) throws Exception {
......省略很多.....
Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
......省略很多.....
}
MultipartFile获取方式
获取方式
在spring-config配置了 之后
后台的获取有两种方法:
1、指定@RequestParam MultipartFile file 例如:public Map logsUpload(@RequestParam MultipartFile file,@RequestParam(value="key") String key)参数;
2、将request转化为MultipartHttpServletRequest multiRequest = (MultipartHttpServletRequest)(request);
原理是:使用spring的CommosMultipartResolver 配置MultipartResolver 用于文件上传,DispatcherServlet 将调用 MultipartResolver 的 isMultipart(request) 方法检查当前 Web 请求是否为 multipart类型。如果是,DispatcherServlet 将调用 MultipartResolver 的 resolveMultipart(request) 方法,对原始 request 进行装饰,并返回一个 MultipartHttpServletRequest 供后继处理流程使用(最初的 HttpServletRequest 被偷梁换柱成了 MultipartHttpServletRequest),否则,直接返回最初的 HttpServletRequest。也就是说请求一旦被 MultipartResolver 接手,它就会解析请求中的文件,而不必等待后续 controller 主动从 MultipartRequest 中 getFile,所以在配置了MultipartResolver后,再通过这样的方法
MultipartResolver resolver = new CommonsMultipartResolver(request.getSession().getServletContext());
MultipartHttpServletRequest multipartRequest = resolver.resolveMultipart(request);
是获取不到file的,因为控制器已经帮我们进行了转换 直接获取即可。
如果你使用该方法发觉获取没有问题,你可以看看给这个方法是不是配置了servlet,如果配置了servlet是不走这个 MultipartResolver控制,是能获取成功的。
无需进行spring-config的配置,直接在后台获取进行转换即可
MultipartResolver resolver = new CommonsMultipartResolver(request.getSession().getServletContext());
MultipartHttpServletRequest multipartRequest = resolver.resolveMultipart(request);
MultipartFile file = multipartRequest.getFile("file");
String key = multipartRequest.getParameter("key");
然后项目具体需要什么样的修改,自己结合业务斟酌即可。
java实现Multipart/form-data
新的maven和先前的版本的API不同
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5.3</version>
</dependency>
上传
package uploadTest;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UploadTest {
public static Map<String,Object> uploadFileByHTTP(File postFile,String postUrl,Map<String,String> postParam){
Logger log = LoggerFactory.getLogger(UploadTest.class);
Map<String,Object> resultMap = new HashMap<String,Object>();
CloseableHttpClient httpClient = HttpClients.createDefault();
try{
//把一个普通参数和文件上传给下面这个地址 是一个servlet
HttpPost httpPost = new HttpPost(postUrl);
//把文件转换成流对象FileBody
FileBody fundFileBin = new FileBody(postFile);
//设置传输参数
MultipartEntityBuilder multipartEntity = MultipartEntityBuilder.create();
multipartEntity.addPart(postFile.getName(), fundFileBin);//相当于<input type="file" name="media"/>
//设计文件以外的参数
Set<String> keySet = postParam.keySet();
for (String key : keySet) {
//相当于<input type="text" name="name" value=name>
multipartEntity.addPart(key, new StringBody(postParam.get(key), ContentType.create("text/plain", Consts.UTF_8)));
}
HttpEntity reqEntity = multipartEntity.build();
httpPost.setEntity(reqEntity);
log.info("发起请求的页面地址 " + httpPost.getRequestLine());
//发起请求 并返回请求的响应
CloseableHttpResponse response = httpClient.execute(httpPost);
try {
log.info("----------------------------------------");
//打印响应状态
//log.info(response.getStatusLine());
resultMap.put("statusCode", response.getStatusLine().getStatusCode());
//获取响应对象
HttpEntity resEntity = response.getEntity();
if (resEntity != null) {
//打印响应长度
log.info("Response content length: " + resEntity.getContentLength());
//打印响应内容
resultMap.put("data", EntityUtils.toString(resEntity,Charset.forName("UTF-8")));
}
//销毁
EntityUtils.consume(resEntity);
} catch (Exception e) {
e.printStackTrace();
} finally {
response.close();
}
} catch (ClientProtocolException e1) {
e1.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
} finally{
try {
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
log.info("uploadFileByHTTP result:"+resultMap);
return resultMap;
}
//测试
public static void main(String args[]) throws Exception {
//要上传的文件的路径
String filePath = "d:/test0.jpg";
String postUrl = "http://localhost:8080/v1/contract/addContract.do";
Map<String,String> postParam = new HashMap<String,String>();
postParam.put("shopId", "15");
File postFile = new File(filePath);
Map<String,Object> resultMap = uploadFileByHTTP(postFile,postUrl,postParam);
System.out.println(resultMap);
}
}
接收
@Path("addContract.do")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.TEXT_HTML)
public String addContract(@Context HttpServletRequest request, @Context HttpServletResponse response){
try{
//这里根据业务做修改
Contract contract = new Contract();
//重点是调用saveFile方法
UploadResult uploadResult = saveFile(request);
contract.setShopId(Integer.valueOf(uploadResult.getParamMap().get("shopId")));
contract.setContractUrl(uploadResult.getFilePath());
Boolean success = contractService.addContract(contract);
return JSON.toJSONString(RespApi.buildResp(200, "success", success));
} catch (ContractExeption contractExeption) {
log.warn(ExceptionUtils.getStackTrace(contractExeption));
return JSON.toJSONString(RespApi.buildResp(400, "fail to add contract", false));
}
}
public UploadResult saveFile(HttpServletRequest request) {
Map<String,String> resultMap = new HashMap<>();
String fileName = "";
try {
if (ServletFileUpload.isMultipartContent(request)) {
FileItemFactory factory = new DiskFileItemFactory();
//如果没以下两行设置的话,上传大的 文件 会占用 很多内存,
//设置暂时存放的 存储室 , 这个存储室,可以和 最终存储文件 的目录不同
/**
* 原理 它是先存到 暂时存储室,然后在真正写到 对应目录的硬盘上,
* 按理来说 当上传一个文件时,其实是上传了两份,第一个是以 .tem 格式的
* 然后再将其真正写到 对应目录的硬盘上
*/
//这个tempDir需要自己设置
File tempF = new File(tempDir);
if (!tempF.exists()) {
tempF.mkdirs();
}
factory.setRepository(tempF);
//设置 缓存的大小,当上传文件的容量超过该缓存时,直接放到 暂时存储室
factory.setSizeThreshold(1024 * 1024);
//高水平的API文件上传处理
ServletFileUpload upload = new ServletFileUpload(factory);
List<FileItem> items = null;
try {
items = upload.parseRequest(request);
} catch (FileUploadException e) {
e.printStackTrace();
}
if (items != null) {
Iterator<FileItem> iter = items.iterator();
while (iter.hasNext()) {
FileItem item = iter.next();
//属性字段
if (item.isFormField()) {
//获取表单的属性名字
String name = item.getFieldName();
String value = item.getString();
resultMap.put(name,value);
}
//文件
if (!item.isFormField() && item.getSize() > 0) {
//可以得到流
InputStream in = item.getInputStream();
fileName = processFileName(item.getName());
try {
String basePath = getRootPath();
String types = "pic";
String date = DateUtil.getNowDateStr(DateUtil.TO_DAY);
String realPath = basePath + File.separator + types + File.separator + date
+ File.separator;
//String realPath = basePath + File.separator + types + File.separator + date
// + File.separator;
System.out.println(realPath);
File file1 = new File(realPath);
if (!file1.exists()) {
file1.mkdirs();
}
String name = UUID.randomUUID().toString() + ".jpg";
fileName = types + File.separator + date + File.separator + name;
File files = new File(realPath + name);
item.write(files);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
} catch (Exception e) {
}
UploadResult uploadResult = new UploadResult();
uploadResult.setFilePath(fileName);
uploadResult.setParamMap(resultMap);
return uploadResult;
}
//返回结果的包装类
public class UploadResult {
private String filePath;
private Map<String,String> paramMap;
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public Map<String, String> getParamMap() {
return paramMap;
}
public void setParamMap(Map<String, String> paramMap) {
this.paramMap = paramMap;
}
}