Java接口同时上传文件和json数据, Java MultipartFIle向接口上传文件
1.问题描述
最近有个数据对接的项目,第三方请求接口同时提供上传文件、数据,后台这边根据业务逻辑进行处理。
2.思路分析
前端向后台传文件(文件流)只能用表单form-data,无法用Json形式上传,后端接口之间对接也是如此。因此和对方约定好 接口为:
"files": xxx{"param":"abc" }
参数名 | 类型 | 可为空 | 描述 |
---|---|---|---|
param | 字符串 | 否 | 参数 |
files | MultipartFile[] | 否 | 文件 |
注意:
1)由于接口并非一个完整的JSON数据,因此不可以加@RequestBody注解;
2)文件参数使用@RequestParam注解,json参数使用@RequestPart注解。@RequestParam和@RequestPart的区别是:@RequestParam适用于name-valueString类型的请求域,@RequestPart适用于复杂的请求域(像JSON,XML),Json对象只能是Alibaba的fastjson对象(JsonObject)。
后台这边的方法为:
@RequestMapping(value = "/upload", method = {RequestMethod.GET, RequestMethod.POST})public JSONObject upload(@RequestParam("files") MultipartFile[] files, @RequestPart("json") JSONObject requestJson){StringBuilder msg = new StringBuilder();JSONObject response = new JSONObject();if (files == null || files.length == 0) {msg.append("文件上传失败");response.put("code", -1);response.put("msg", msg.toString());return response;}if (files.length > 1) {msg.append("只能上传一个文件");response.put("code", -1);response.put("msg", msg.toString());return response;}MultipartFile file = files[0];File dest = null;try{String originalFilename = file.getOriginalFilename();//获取文件后缀String prefix = originalFilename.substring(originalFilename.lastIndexOf("."));dest = File.createTempFile(originalFilename, prefix);file.transferTo(dest);//删除临时文件file.deleteOnExit();//其他处理逻辑msg.append("文件上传成功");response.put("code", -1);response.put("msg", msg.toString());return response;}catch{log.error("文件:{}上传失败,", file.getOriginalFilename(), e);msg.append("文件上传失败");response.put("code", -1);response.put("msg", msg.toString());return response;}}
3.出现bug
使用MultipartFile报错:java.io.IOException: java.io.FileNotFoundException(系统找不到指定的路径)。
经过多方查找资料,得知原因为:spring框架MultipartFile上传文件报错,MultipartFile的transferTo()方法会监测传入其中的File对象的路径是否为绝对路径,如果不是绝对路径,会自动拼接application里设置的路径 和 \work\Tomcat\localhost\ROOT。
详情可查阅该文章 https://blog.csdn.net/qq_37289717/article/details/97792608
4.Apipost测试
在本地运行项目后,通过apipost输入文件、json数据,模拟第三方访问系统后台。
form-data中的Content-Type为手动设置,默认不会展示;File参数需将参数类型(Text/File)设定为File,json参数需将内容类型设置为“application/json”,如下图所示。
5.补充概念
1.1 File常见方法
java.io.File类是文件和路径名称的抽象表示,主要用于文件和目录的创建、查找、删除等。
File类的构造方法
public File(String pathname):通过将给定的路径名字符串转换为抽象类路径来创建新的实例public File(String parent,String child):从父路径字符串和子路径字符串创建新的File实例public File(File parent,String child):从父抽象路径名和子路径名字符串创建新的File实例
注意:一个File代表的是硬盘中的一个路径或者一个文件。无论该路径下是否存在文件或目录,都不影响File对象的创建。
常见方法
public String getAbsolutePath():返回File的绝对路径名(字符串)public String getPath():将此File转换为路径名 字符串,获取构造路径(就是获取你构造方法时候放入的路径)public String getName():返回此File表示的文件或目录的名称public long length():返回此File表示的文件的字节大小,不能获取目录的字节大小public boolean exists(): 此File标识的文件或目录是否实际存在public boolean isDirectory():此File标识的是否为文件夹public boolean isFile():此file表示的是否为文件//创建和删除的方法public boolean creatNewFile():当且仅当不存在该名称的文件时,创建一个新的空文件public boolean mkdir():创建由File表示的目录public boolean mkdirs():创建由File表示的目录,包括任何必须但不存在的父目录public boolean delete():删除由此File表示的文件或目录,只能删除文件或者空文件夹,不能删除非空文件夹//遍历目录的方法public String[] list():返回一个String数组,表示该File目录中的所有子文件或目录的名称public File[] listFiles():返回一个File数组,表示File目录中的所有子文件或目录的路径
1.2 删除文件或文件夹的7种方法
摘抄出处https://cloud.tencent.com/developer/article/1703463
1.2.1 删除文件或文件夹的四种基础方法
下面的四个方法都可以删除文件或文件夹,它们的共同点是:当文件夹中包含子文件的时候都会删除失败,也就是说这四个方法只能删除空文件夹。
注意:传统IO中的File类和NIO中的Path类既可以代表文件,也可以代表文件夹。
- File类的delete()
- File类的deleteOnExit()
- Files.delete(Path path)
- Files.deleteIfExists(Path path);
四种方法差异
成功的返回值 | 是否能判别文件夹不存在导致失败 | 是否能判别文件夹不为空导致失败 | 备注 | |
---|---|---|---|---|
File类的delete() | true | 不能(返回false) | 不能(返回false) | 传统IO |
File类的deleteOnExit() | void | 不能,但不存在就不会去执行删除 | 不能(返回void) | 传统IO,这是个坑,避免使用 |
Files.delete(Path path) | void | NoSuchFileException | DirectoryNotEmptyException | NIO,笔者推荐使用 |
Files.deleteIfExists(Path path) | true | false | DirectoryNotEmptyException | NIO |
- 由上面的对比可以看出,传统IO方法删除文件或文件夹,再删除失败的时候,最多返回一个false。通过这个false无法发掘删除失败的具体原因,是因为文件本身不存在删除失败?还是文件夹不为空导致的删除失败?
- NIO 的方法在这一点上,就做的比较好,删除成功或失败都有具体的返回值或者异常信息,这样有利于我们在删除文件或文件夹的时候更好的做程序的异常处理
- 需要注意的是传统IO中的deleteOnExit方法,笔者觉得应该避免使用它。它永远只返回void,删除失败也不会有任何的Exception抛出,所以我建议不要用,以免在你删除失败的时候没有任何的响应,而你可能误以为删除成功了。
//false只能告诉你失败了 ,但是没有给出任何失败的原因@Testvoid testDeleteFileDir1(){ File file = new File("D:\data\test"); boolean deleted = file.delete(); System.out.println(deleted);}//void ,删除失败没有任何提示,应避免使用这个方法,就是个坑@Testvoid testDeleteFileDir2(){ File file = new File("D:\data\test1"); file.deleteOnExit();}//如果文件不存在,抛出NoSuchFileException//如果文件夹里面包含文件,抛出DirectoryNotEmptyException@Testvoid testDeleteFileDir3() throws IOException { Path path = Paths.get("D:\data\test1"); Files.delete(path); //返回值void}//如果文件不存在,返回false,表示删除失败(文件不存在)//如果文件夹里面包含文件,抛出DirectoryNotEmptyException@Testvoid testDeleteFileDir4() throws IOException { Path path = Paths.get("D:\data\test1"); boolean result = Files.deleteIfExists(path); System.out.println(result);}
归根结底,建议大家使用java NIO的Files.delete(Path path)
和Files.deleteIfExists(Path path);
进行文件或文件夹的删除。
1.2.2 如何删除整个目录或者目录中的部分文件
上述四个API删除文件夹的时候,如果文件夹包含子文件,就会删除失败。那么,如果我们确实想删除整个文件夹,该怎么办?
privatevoid createMoreFiles() throws IOException { Files.createDirectories(Paths.get("D:\data\test1\test2\test3\test4\test5\")); Files.write(Paths.get("D:\data\test1\test2\test2.log"), "hello".getBytes()); Files.write(Paths.get("D:\data\test1\test2\test3\test3.log"), "hello".getBytes());}
1.2.2.1 walkFileTree与FileVisitor
- 使用walkFileTree方法遍历整个文件目录树,使用FileVisitor处理遍历出来的每一项文件或文件夹
- FileVisitor的visitFile方法用来处理遍历结果中的“文件”,所以我们可以在这个方法里面删除文件
- FileVisitor的postVisitDirectory方法,注意方法中的“post”表示“后去做……”的意思,所以用来文件都处理完成之后再去处理文件夹,所以使用这个方法删除文件夹就可以有效避免文件夹内容不为空的异常,因为在去删除文件夹之前,该文件夹里面的文件已经被删除了。
@Testvoid testDeleteFileDir5() throws IOException { createMoreFiles(); Path path = Paths.get("D:\data\test1\test2"); Files.walkFileTree(path,new SimpleFileVisitor<Path>() { // 先去遍历删除文件 @Override public FileVisitResult visitFile(Path file,BasicFileAttributes attrs) throws IOException {Files.delete(file);System.out.printf("文件被删除 : %s%n", file);return FileVisitResult.CONTINUE; } // 再去遍历删除目录 @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {Files.delete(dir);System.out.printf("文件夹被删除: %s%n", dir);return FileVisitResult.CONTINUE; }} );}
//输出结果,体现文件的删除顺序文件被删除 : D:\data\test1\test2\test2.log文件被删除 : D:\data\test1\test2\test3\test3.log文件夹被删除 : D:\data\test1\test2\test3\test4\test5文件夹被删除 : D:\data\test1\test2\test3\test4文件夹被删除 : D:\data\test1\test2\test3文件夹被删除 : D:\data\test1\test2
我们既然可以遍历出文件夹或者文件,我们就可以在处理的过程中进行过滤。比如:
- 按文件名删除文件或文件夹,参数Path里面含有文件或文件夹名称
- 按文件创建时间、修改时间、文件大小等信息去删除文件,参数BasicFileAttributes 里面包含了这些文件信息。
1.2.2.2 Files.walk
如果你对Stream流语法不太熟悉的话,这种方法稍微难理解一点,但是说实话也非常简单。
- 使用Files.walk遍历文件夹(包含子文件夹及子其文件),遍历结果是一个
Stream
- 对每一个遍历出来的结果进行处理,调用Files.delete就可以了。
@Testvoid testDeleteFileDir6() throws IOException { createMoreFiles(); Path path = Paths.get("D:\data\test1\test2"); try (Stream<Path> walk = Files.walk(path)) {walk.sorted(Comparator.reverseOrder()) .forEach(DeleteFileDir::deleteDirectoryStream); }}private static void deleteDirectoryStream(Path path) { try {Files.delete(path);System.out.printf("删除文件成功:%s%n",path.toString()); } catch (IOException e) {System.err.printf("无法删除的路径 %s%n%s", path, e); }}
问题:怎么能做到先去删除文件,再去删除文件夹? 。 利用的是字符串的排序规则,从字符串排序规则上讲,“D:\data\test1\test2”一定排在“D:\data\test1\test2\test2.log”的前面。所以我们使用“sorted(Comparator.reverseOrder())”把Stream顺序颠倒一下,就达到了先删除文件,再删除文件夹的目的。
//结果输出,体现文件的删除顺序。删除文件成功:D:\data\test1\test2\test3\test4\test5删除文件成功:D:\data\test1\test2\test3\test4删除文件成功:D:\data\test1\test2\test3\test3.log删除文件成功:D:\data\test1\test2\test3删除文件成功:D:\data\test1\test2\test2.log删除文件成功:D:\data\test1\test2
1.2.2.3 传统IO-递归遍历删除文件夹
传统的通过递归去删除文件或文件夹的方法就比较经典了
//传统IO递归删除@Testvoid testDeleteFileDir7() throws IOException { createMoreFiles(); File file = new File("D:\data\test1\test2"); deleteDirectoryLegacyIO(file);}private void deleteDirectoryLegacyIO(File file) { File[] list = file.listFiles();//无法做到list多层文件夹数据 if (list != null) {for (File temp : list) { //先去递归删除子文件夹及子文件 deleteDirectoryLegacyIO(temp); //注意这里是递归调用} } if (file.delete()) { //再删除自己本身的文件夹System.out.printf("删除成功 : %s%n", file); } else {System.err.printf("删除失败 : %s%n", file); }}
需要注意的是:
listFiles()
方法只能列出文件夹下面的一层文件或文件夹,不能列出子文件夹及其子文件。- 先去递归删除子文件夹,再去删除文件夹自己本身
1.3 MultipartFile
Java中的流
1、“流”是一个抽象的概念,它是对输入输出设备的一种抽象理解,在java中,对数据的输入输出操作都是以“流”的方式进行的。
2、“流”具有方向性,输入流、输出流是相对的。当程序需要从数据源中读入数据的时候就会开启一个输入流,相反,写出数据到某个数据源目的地的时候也会开启一个输出流。
3、数据源可以是文件、内存或者网络等。
MultipartFile是SpringMVC提供简化上传操作的工具类。在不使用框架之前,都是使用原生的HttpServletRequest来接收上传的数据,文件是以二进制流传递至后端,需要后端转换为File类。使用MultipartFile工具类后,文件上传的操作就便捷许多。
MultipartFile工具类全部的接口方法
package org.springframework.web.multipart;import java.io.File;import java.io.IOException;import java.io.InputStream;import java.nio.file.Files;import java.nio.file.Path;import org.springframework.core.io.InputStreamSource;import org.springframework.core.io.Resource;import org.springframework.lang.Nullable;import org.springframework.util.FileCopyUtils;public interface MultipartFile extends InputStreamSource {//getName() 返回参数的名称String getName();//获取源文件的昵称@NullableString getOriginalFilename();//getContentType() 返回文件的内容类型@NullableString getContentType();//isEmpty() 判断是否为空,或者上传的文件是否有内容boolean isEmpty();//getSize() 返回文件大小 以字节为单位long getSize();//getBytes() 将文件内容转化成一个byte[] 返回byte[] getBytes() throws IOException;//getInputStream() 返回InputStream读取文件的内容InputStream getInputStream() throws IOException;default Resource getResource() {return new MultipartFileResource(this);}//transferTo是复制file文件到指定位置(比如D盘下的某个位置),不然程序执行完,文件就会消失,程序运行时,临时存储在temp这个文件夹中void transferTo(File var1) throws IOException, IllegalStateException;default void transferTo(Path dest) throws IOException, IllegalStateException {FileCopyUtils.copy(this.getInputStream(), Files.newOutputStream(dest));}}
InputStreamSource 这个接口本质上返回的还是一个InputStream 流对象
package org.springframework.core.io;import java.io.IOException;import java.io.InputStream;public interface InputStreamSource {//定位并打开资源,返回资源对应的输入流。//每次调用都会返回新的输入流,调用者在使用完毕后必须关闭该资源。 InputStream getInputStream() throws IOException;}
详情可见 https://blog.csdn.net/weixin_45393094/article/details/112056436