最近参与了一个小项目,主要负责的是文件的跨服务器传送,其中包含按要求对文件进行压缩后传送,实现的时候使用的是Restful接口,实现的过程中遇到了很多关于文件File的操作,感觉实现的时候有思路知道该如何如何实现这个功能,但是具体的代码实现还是得百度搜索,想着之后也会常用不如总结一下,后续遇到相关操作继续更新~
SpringBoot中通常前端上传文件到后端,对应接口的文件参数类型为MultipartFile,后端获取到这个MultipartFile对象之后需要将其转为File对象,然后存到本地/文件服务器上。
try {
// 将文件存入指定目录中(需要确保文件确实存在)
File sendFile = new File(path);
// file为MultipartFile类型
file.transferTo(sendFile);
} catch (IOException e) {
e.printStackTrace();
}
// 需要确保path对应的文件存在
File file = new File(path);
FileInputStream input = new FileInputStream(file);
MultipartFile multipartFile =new MockMultipartFile(“file”, file.getName(), “text/plain”, IOUtils.toByteArray(input));
拓展一个小知识点:
使用RestTemplate发送文件的方法,将文件使用FileSystemResource转为文件流进行发送,注意需要设置请求头的类型为MULTIPART_FORM_DATA,设置成整个格式主要是因为接收方的参数类型也一般为MultipartFile
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
//设置提交方式都是表单提交
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
//设置表单信息
MultiValueMap params = new LinkedMultiValueMap<>();
params.add("file", new FileSystemResource(new File(filePath)));
HttpEntity> requestEntity = new HttpEntity<>(params, headers);
restTemplate.postForEntity(
"http://xxx:xx/xxxxx",requestEntity, Object.class);
注意一个特别小的点:
File对象获取文件名使用getName()方法获取
MultipartFile对象获取文件名需要使用getOriginalFilename()获取
上述两种操作都涉及文件的创建,普通的File file = new File(path);一定要明确这个只是创建了一个文件对象,根据file对象的相关方法对file进行操作,但是方法执行的前提是这个file真实存在,这条语句并不能在文件不存在的时候自动创建,所以需要我们抽取一个方法创建文件。
考虑到有时候文件的目录即文件夹不存在,所以一般会先判断文件夹是否存在,如果不存在的话mkdirs()方法创建文件夹,之后再判断file文件是否存在,如果不存在则创建(注意会特意给这个file对象赋予写权限,以防无权限而报错)
public static File createNewFile(String path, String fileName) throws IOException {
File fileDir = new File(path);
if (!fileDir.exists() && !fileDir.isDirectory()) {
// fileDir的mkdir()方法只是创建最近一层的目录,如果根目录中有不存在的会创建最近一级目录失败。例如directory/test/路径,mkdir()方法是创建test目录,如果directory不存在,test会创建失败
fileDir.mkdirs();
}
File file = new File(path + fileName);
if (file.exists()) {
file.delete();
}
// 文件设置可写
file.setWritable(true);
file.createNewFile();
return file;
}
这里拓展说一下一般项目中不会固定文件的路径,因为Windows和Linux的文件路径表达方式不同,所以一般会使用项目的绝对路径。
/**
* 文件路径分隔符 /
*/
public static final String FILE_SEPARATOR = System.getProperty("file.separator");
/**
* 文件存储路径
*/
public static final String FILE_DIRECTORY = System.getProperty("user.dir") + FILE_SEPARATOR + "Directory";
要么是在项目配置文件resouces/application.yml中写文件存储的相对路径,文件夹置入项目目录下,然后使用@Value或者properties对象加载获取
// application.yml配置path的存储路径
path: D:\test\test.txt
// 调用
@Value("${path}")
private String path;
// 通过properties对象获取
public class testProperties {
Properties p = new Properties();
InputStream in = testProperties.class.getClassLoader().getResourceAsStream("application.yml");
p.load(in);
String name = p.getProperty("className");
}
判断是否为图片的主要思路就是摘取文件名,看是否满足以png、jpeg、jpg、gif结尾,使用正则表达式匹配即可
/**
* 常用图片格式 ^表示正则表达式开始,$表示正则表达式结束,这个正则表达式是 .叠加上jpeg/jpg/png/gif中的任意一个
*/
public static final String REG_IMG_FORMAT = "^.+(.JPEG|.jpeg|.JPG|.jpg|.PNG|.png|.GIF|.gif)$";
/**
* 判断MultipartFile对象是否为图片
* @param file
* @return
*/
public static boolean isImage(MultipartFile file) {
Matcher matcher = Pattern.compile(Const.REG_IMG_FORMAT).matcher(file.getOriginalFilename());
return matcher.find();
}
/**
* 判断File对象是否为图片
* @param file
* @return
*/
public static boolean isImage(File file) {
Matcher matcher = Pattern.compile(Const.REG_IMG_FORMAT).matcher(file.getName());
return matcher.find();
}
用户上传的图片名千奇百怪,为了避免不同用户上传相同文件名的文件而导致前一个文件被覆盖,通常会对用户上传至后端的文件名进行改造。本人常用的方式是使用随机生成的UUID,然后使用空字符串替换掉其中的"-",然后叠加原本文件的后缀,保证文件的类型。那么如何获取文件后缀是一个问题,常用的方法是利用后缀的特征(以"."结尾)字符串的lastIndexOf(".")方法获取到最后一个"."在这个字符串中的索引值,然后使用substring方法截取从这个索引值开始一直到字符串末尾的值。
/**
* 获取文件后缀 .+后缀
* @param file
* @return
*/
public static String getSuffix(MultipartFile file){
// 获取最后一个.所在的索引值
int pos = file.getOriginalFilename().lastIndexOf(".");
if (pos == -1){
return null;
}
// 例如test.png,如果此处为pos+1,返回的是png;直接是pos的话返回.png
return file.getOriginalFilename().substring(pos);
}
一般前端传递文件对象到后端后,后端的基本操作就是先把这个文件保存下来,然后根据需要再对其进行其他操作。上传的方法就是将MultipartFile写入到指定路径的File对象中,方法同第一点
@RestController
@RequestMapping("test")
public class TestController {
@PostMapping("/file")
public String send(@ApiParam(name = "file", value = "文件", required = true, type = "MultipartFile")
@RequestParam("file") MultipartFile file) {
// 重构文件名(UUID+.+图片后缀)
String fileName = UUID.randomUUID().toString().replace("-","") + FileUtil.getSuffix(file);
try {
// 将文件存入指定目录中
File sendFile = FileUtil.createNewFile(path, fileName);
file.transferTo(sendFile);
return "文件上传至本地成功";
} catch (IOException e) {
return "文件上传至本地失败";
e.printStackTrace();
}
}
}
如果是批量多文件上传,需要改动的地方就是在转文件的外面加一层文件遍历即可。参考:https://www.jianshu.com/p/be1af489551c
public String multifileUpload(HttpServletRequest request){ // 或者此处参数可以为List files
List files = ((MultipartHttpServletRequest)request).getFiles("fileName");
if(files.isEmpty()){
return "false";
}
// 设置文件存储路径
String path = "F:/test" ;
for(MultipartFile file:files){
String fileName = file.getOriginalFilename();
if(file.isEmpty()){
return "false";
}else{
File dest = new File(path + "/" + fileName);
if(!dest.getParentFile().exists()){ //判断文件父目录是否存在
dest.getParentFile().mkdirs();
}
try {
file.transferTo(dest);
}catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
return "false";
}
}
}
return "true";
}
此操作涉及文件的读和写,使用java的io流,读使用input流,写使用output流。其中如果是图片、音频、视频等二进制文件类型,使用字节流读写;如果是文本、数字等文件类型,使用字符流读写。参考:https://blog.csdn.net/jiangxinyu/article/details/7885518
// 字节读写 FileInputStream + FileOutputStream
// 源文件
File sourceFile = new File("D:\\test\\test.txt);
// 目标文件
File targetFile = new File("D:\\test\\test1.txt);
// 将接收的文件保存到接收端
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
try {
inputStream = new FileInputStream(file);
outputStream = new FileOutputStream(targetFile);
byte[] bts = new byte[1024]; //读取缓冲
while ((inputStream.read(bts)) != -1) {
outputStream.write(bts);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (outputStream != null) {
outputStream.close();
}
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 字节读写+缓冲流 BufferedInputStream + BufferedOutputStream(减少io次数)
// 源文件
File sourceFile = new File("D:\\test\\test.txt);
// 目标文件
File targetFile = new File("D:\\test\\test1.txt);
// 将接收的文件保存到接收端
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
inputStream = new FileInputStream(file);
bis = new BufferedInputStream(inputStream);
outputStream = new FileOutputStream(targetFile);
bos = new BufferedOutputStream(outputStream);
byte[] bts = new byte[1024]; //读取缓冲
while ((bis.read(bts)) != -1) {
bos.write(bts);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
bos.close();
outputStream.close();
bis.close();
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 字符读写 FileReader + FileWriter
// 源文件
File sourceFile = new File("D:\\test\\test.txt);
// 目标文件
File targetFile = new File("D:\\test\\test1.txt);
// 将接收的文件保存到接收端
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
FileReader reader = null;
FileWriter writer = null;
try {
inputStream = new FileInputStream(file);
outputStream = new FileOutputStream(targetFile);
reader = new FileReader(new InputStreamReader(inputStream));
writer = new FileWriter(new OutputStreamWriter(outputStream));
byte[] bts = new byte[1024]; //读取缓冲
while ((reader.read(bts)) != -1) {
writer.write(bts);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
writer.close();
outputStream.close();
reader.close();
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 字符读写+缓冲流 BufferedReader + BufferedWriter
// 源文件
File sourceFile = new File("D:\\test\\test.txt);
// 目标文件
File targetFile = new File("D:\\test\\test1.txt);
// 将接收的文件保存到接收端
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
FileReader reader = null;
FileWriter writer = null;
BufferedReader bufferedReader = null;
BufferedWriter bufferedWriter = null;
try {
inputStream = new FileInputStream(file);
reader = new FileReader(new InputStreamReader(inputStream));
bufferedReader = new BufferedReader(reader);
outputStream = new FileOutputStream(targetFile);
writer = new FileWriter(new OutputStreamWriter(outputStream));
bufferedWriter = new BufferedWriter(writer);
String data;
while ((data = bufferedReader.readLine()) != null) {
bufferedWriter.append(data); // 一行一行的读然后追加
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
bufferedWriter.close();
writer.close();
outputStream.close();
bufferedReader.close();
reader.close();
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
注意上述读写文件的时候,使用的是byte字节读取,数组大小为1024,这样对于有些文件小于1024的,会扩展为1024字节大小。所以实际中建议使用int参数
File file = new File("D:\\test\\test.txt");
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
File targetFile = new File("D:\\test\\test1.txt");;
try {
inputStream = new FileInputStream(file);
outputStream = new FileOutputStream(targetFile);
int read;
// 此处不可使用byte并初始化为1024,因为有可能本身文件大小不足1024
while ((read = inputStream.read()) != -1) {
outputStream.write(read);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (outputStream != null) {
outputStream.close();
}
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
实际项目中会有这样的场景:由于上传的图片比较大,在前端展示的时候会展示缩略图,等待用户点击查看大图的时候才会显示原图。缩略图的实现就是后端对上传的图片进行了压缩,然后返回给前端压缩后的图片(base64编码后的字节流)。使用Thumbnailator开源库对图片进行压缩。
项目导入依赖
net.coobird
thumbnailator
0.4.8
使用thumbnailator对图片进行按比例压缩,参考:https://blog.csdn.net/jie_liang/article/details/78582637
//filePath是原地址,outPath是压缩后的文件地址
public static void imageCompress(String filePath,String outPath) throws IOException {
Thumbnails.of(filePath)
.scale(0.5f) //图片大小压缩,1为原图大小,0.1是十分之一
.outputQuality(0.1f) //图片质量压缩,1为原图质量,0.1位十分之一
.toFile(outPath);
}
实际中可能会先判断原文件本身大小,超过一定范围再进行压缩。
/**
* 根据指定大小和指定精度压缩图片
*
* @param srcPath
* 源图片地址
* @param desPath
* 目标图片地址
* @param desFilesize
* 指定图片大小,单位kb
* @param accuracy
* 精度,递归压缩的比率,建议小于0.9
* @return
*/
private static void commpressPicCycle(String srcPath, String desPath, long desFileSize,
double accuracy) throws IOException {
File srcFileJPG = new File(desPath);
long srcFileSizeJPG = srcFileJPG.length();
// 2、判断大小,如果小于指定大小,不压缩;如果大于等于指定大小,压缩
if (srcFileSizeJPG <= desFileSize * 1024) {
return;
}
// 计算宽高
BufferedImage bim = ImageIO.read(srcFileJPG);
int srcWdith = bim.getWidth();
int srcHeigth = bim.getHeight();
int desWidth = new BigDecimal(srcWdith).multiply(
new BigDecimal(accuracy)).intValue();
int desHeight = new BigDecimal(srcHeigth).multiply(
new BigDecimal(accuracy)).intValue();
Thumbnails.of(srcPath).size(desWidth, desHeight)
.outputQuality(accuracy).toFile(desPath);
// 此处也可继续使用递归调用compressPicCycle方法,限制图片大小不大于desFileSize*1024
}
将文件转为图片对象:
BufferedImage image = ImageIO.read(new FileInputStream(new File("D:\\test\\test.png)));
获取图片的像素方法:
/**
* 根据File图片对象返回图片像素
* @param file
* @return
* @throws IOException
*/
private String getSizeOfImage(File file) throws IOException {
BufferedImage image = ImageIO.read(new FileInputStream(file));
return image.getWidth() + "*" + image.getHeight();
}
一般对于图片的存储都是将图片的存储路径保存在数据库中,查询的时候先查询到路径,然后再根据这个路径去找图片,最后将这个图片流传给前端进行展示。存储在数据库存在的问题是如果换一个部署环境,之前的图片就丢失了即数据库无法迁移,所以对图片质量和数据可靠性要求较高的项目中,通常会直接存储图片对象。
但往往直接存储图片对象对于数据库压力过高,尤其对于图片数量特别多的情况下,图片太大会导致数据库性能降低。一般会采取将图片使用base64进行转码,然后存储到数据库中,然后查询这个base64编码传递给前端,前端解码后进行展示。
public String getImageType(MultipartFile file){
int pos = file.getOriginalFilename().lastIndexOf(".");
if (pos == -1){
return null;
}
return file.getOriginalFilename().substring(pos+1);
}
public String getImageBase64(MultipartFile icon){
BufferedImage image = null;
String imageStr = null;
String imgType = getImageType(icon);
try {
image = ImageIO.read(icon.getInputStream());
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ImageIO.write(image, imgType, bos);
byte[] imageBytes = bos.toByteArray();
imageStr = new String(Base64.getEncoder().encode(imageBytes)).replace("\r\n","");
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
return "data:image/"+imgType+";base64,"+imageStr;
}
//输入
File file = null;
FileInputStream fis = null;
BufferedInputStream bin = null;
//输出
File outfile = null;
FileOutputStream fos = null;
BufferedOutputStream bos = null;
ZipOutputStream zos = null;
ZipEntry ze = null;
try {
//输入-获取数据
file = new File("D:\\test\test.txt");
fis = new FileInputStream(file);
bin = new BufferedInputStream(fis);
//输出-写出数据
outfile = new File("D:\\test\test.zip");
fos = new FileOutputStream(outfile);
bos = new BufferedOutputStream(fos, 1024); //the buffer size
zos = new ZipOutputStream(bos); //压缩输出流
ze = new ZipEntry(file.getName()); //实体ZipEntry保存
zos.putNextEntry(ze);
byte[] bts = new byte[1024]; //读取缓冲
while(bin.read(bts) != -1){ //每次读取1024个字节
zos.write(bts); //每次写len长度数据,最后前一次都是1024,最后一次len长度
}
System.out.println("压缩成功");
} catch (Exception e) {
e.printStackTrace();
} finally{
try { //先实例化后关闭
zos.closeEntry();
zos.close();
bos.close();
fos.close();
bin.close();
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
多个文件压缩,则读取遍历压缩这些文件到同一个压缩文件中即可
for (File srcFile : srcFiles) {
byte[] buf = new byte[1024];
zos.putNextEntry(new ZipEntry(srcFile.getName()));
int len;
file = new File(srcFile);
fis = new FileInputStream(file);
bin = new BufferedInputStream(fis);
while ((len = bin.read(buf)) != -1){
zos.write(buf, 0, len);
}
zos.closeEntry();
}
可使用file.length()获取文件大小,如果需要对文件大小进行类型转换,例如1024B输出1KB,使用转换工具类,参考:https://www.jianshu.com/p/4610944ee9be
public static String readableFileSize(long size) {
if (size <= 0) return "0";
final String[] units = new String[]{"B", "KB", "MB", "GB", "TB"};
// size的1024的log值,即1024的多少次方为size值,这个log值指向units数组的下标
int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
// size除以1024的这个log值进行进制转换,叠加下标指向的单位
return new DecimalFormat("#,##0.#").format(size / Math.pow(1024, digitGroups))+ units[digitGroups];
}
有时候项目中涉及一些重要数据文件的传送,传送的过程中需要对文件进行加密,以防数据被窃取。参考:https://www.linuxidc.com/Linux/2018-07/153311.htm
既然提到了文件上传,对应的用户可以点击下载按钮下载指定的文件,如何实现文件下载?大体思路就是读取指定的文件,然后将转为字节流输出即可。参考:https://www.pianshen.com/article/7655351311/
public String download(HttpServletResponse response) throws UnsupportedEncodingException {
String filepath = "files/" + filename;
// 如果文件名不为空,则进行下载
if (filename != null) {
File file = new File(filepath);
// 如果文件存在,则进行下载
if (file.exists()) {
// 配置文件下载
// 下载文件能正常显示中文
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
// 实现文件下载
byte[] buffer = new byte[1024];
FileInputStream fis = null;
BufferedInputStream bis = null;
try {
fis = new FileInputStream(file);
bis = new BufferedInputStream(fis);
OutputStream os = response.getOutputStream();
int i = bis.read(buffer);
while (i != -1) {
os.write(buffer, 0, i);
i = bis.read(buffer);
}
System.out.println("Download successfully!");
return "successfully";
} catch (Exception e) {
System.out.println("Download failed!");
return "failed";
} finally {
if (bis != null) {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
return "";
}
在网上找到了简单的方法(封装好文件读写的过程) FileUtils.copyFile(file,response.getOutputStream()); 参考https://blog.csdn.net/taiguolaotu/article/details/95945400
public void upload(@RequestParam("filename")String filename,HttpServletResponse response) {
File file = new File("D:/file/"+filename);
try {
// 设置文件内容字符,使中文可正常读取
response.setHeader("content-disposition", "attchment;filename="+URLEncoder.encode(filename,"utf-8"));
if (file.exist()) { // 判断文件存在
try {
FileUtils.copyFile(file,response.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
一般项目中如果图片比较多,并且图片重要性没有那么大的话,最好是将图片存储到项目运行服务器本地,另外将文件存储路径存到数据库。返回给前端有两种处理方法,一种是从数据库中读取文件路径然后从文件路径中读取到图片然后转为base64编码传给前端(转成io流);另一种方法是读取文件路径将其映射为url返回给前端,前端根据url然后直接获取图片。
相对于第一种方法来讲,第二种方法更好些,因为图片传递也是比较耗费IO资源的。
1.读取MulipartFile转为File存入本地,并将文件路径存入到实体对象,后续存入到数据库(数据库中的图片字段类型为VARCHAR即可)
//获取文件的后缀名
String suffixName = FileUtil.getSuffix(icon);
//为上传的文件生成一个随机名字
String filename = UUID.randomUUID() + suffixName;
// Linux下获取文件存储路径
String filePath = "/home/icon/";
File userIconFile = null;
try {
userIconFile = FileUtil.createNewFile(filePath, filename);
icon.transferTo(userIconFile);
if (!userIconFile.exists()) {
return new CommonResult(HttpCode.SERVER_ERROR, "上传头像失败", null);
} else {
// 存入数据库
user.setIcon(filename);
}
} catch (IOException e) {
e.printStackTrace();
return new CommonResult(HttpCode.SERVER_ERROR, "上传头像失败", null);
}
/**
* 创建一个新文件,如果文件存在则删除后重建
* @param path
* @param fileName
* @return
* @throws IOException
*/
public static File createNewFile(String path, String fileName) throws IOException {
File fileDir = new File(path);
if (!fileDir.exists() && !fileDir.isDirectory()) {
fileDir.mkdirs();
}
File file = new File(path + fileName);
if (file.exists()) {
file.delete();
}
// 文件设置可写
file.setWritable(true);
file.createNewFile();
return file;
}
2. 查询数据库返回文件路径,然后编写映射的url返回给前端
/**
* 获取文件映射路径
* @param path icon存储路径
* @param fileName icon图片名
* @return
*/
public static String getFileUrl(String path, String fileName) {
return "http://localhost:8080/iamge" + path + "/" + fileName;
}
3. 配置映射,项目获取到image然后匹配成对应的文件存储基本父路径/home,然后在本地服务器中找文件并返回
package com.baidu.sdjh.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* 本地文件路径映射为url
* @author cici
*/
@Configuration
public class StaticFileConfig extends WebMvcConfigurerAdapter {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry){
// url映射路径
registry.addResourceHandler("/image/**").addResourceLocations("file:" + "/home/");
}
}
注:在Linux系统和Windows系统中文件的分隔符不同,在写代码的时候需要考虑到这一点,可以使用FILE_SEPARATOR = System.getProperty("file.separator")获取当前系统的文件分隔符,另外如果项目在windows系统中运行需要注意转为url的时候检测url中分隔符都得为/而非\
最近状态有点缓慢低迷,该支棱起来的时候就得支棱起来,多做项目、多学习他人优秀思路和代码、多做总结。