目录
在现代Web应用中,文件上传是一个常见的功能,无论是图片、文档还是其他类型的文件,用户经常需要将它们上传到服务器。而文件上传可以分为两种方式,一种是上传到对象存储OSS,另一种是上传到物理存储。本文将详细介绍如何在Java Web应用中实现文件上传到物理存储模块。
其中实现这个Java文件上传模块后,也需要前端页面的配合。
根据文件上传前端页面三要素:
- 表单中的表单项的 type 必须为 file
- 表单提交方式必须为 Post 方式
- 表单属性指定 enctype="multipart/form-data"
这里讲解的是后端,前端方面可自行简单实现。
这里给出简单文件上传页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Angindem File Upload</title>
<!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
<style>
.container {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.showImage{
width: 500px;
height: 570px;
}
.showImage-Child{
width: 500px;
height: 570px;
}
</style>
</head>
<body>
<div class="container table">
<div class="context text-center">
<h1>Angindem File Upload</h1>
<div class="row">
<div class="box">
<div class="thumbnail">
<div class="showImage">
</div>
<div class="caption">
<h3>Thumbnail label</h3>
<input type="file" id="file">
<p>
<a href="#" class="btn btn-primary" role="button" style="margin-right: 50px;">上传文件</a>
<a href="#" class="btn btn-default" role="button">删除文件</a>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>
<script>
if (localStorage.getItem('imageUrl') !== null) {
document.querySelector('.showImage').innerHTML = `<img src="${localStorage.getItem('imageUrl')}" alt="..." class="showImage-Child" style="object-fit: cover">`;
}
document.querySelector('.btn-primary').addEventListener('click', async () => {
if (localStorage.getItem('imageUrl') !== null){
alert("请先删除已上传的文件,再重新上传");
return ;
}
const fileInput = document.getElementById('file');
if (fileInput.files.length === 0) {
alert("请选择上传文件");
return;
}
const formData = new FormData();
formData.append("file", fileInput.files[0]); // "file" 是后端接收文件的字段名
const response = await fetch("/upload", {
method: "POST",
body: formData // `FormData` 对象会自动设置正确的 `Content-Type` 头
});
const result = await response.json();
console.log(result); // 处理服务器返回的结果
alert("文件上传成功!!!");
localStorage.setItem('imageUrl', result.data);
document.querySelector('.showImage').innerHTML = `<img src="${result.data}" alt="..." class="showImage-Child" style="object-fit: cover">`;
});
document.querySelector('.btn-default').addEventListener('click', async () => {
const imageSrc = document.querySelector('.showImage-Child').src;
const formData = new FormData();
formData.append('savePath', imageSrc); // 添加savePath字段到表单数据中
const response = await fetch("/delFile", {
method: "POST",
body: formData // 发送表单数据
});
const result = await response.json();
alert("文件删除成功!!!");
console.log(result); // 处理服务器返回的结果
document.querySelector('.showImage').innerHTML = "";
localStorage.clear();
});
</script>
</html>
文件上传模块
添加起步依赖
基于Maven的项目,需要在pom.xml
文件中添加Spring Boot的起步依赖,以及用于文件上传的支持:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
使用 MultipartFile 接受文件
package com.angindem.controller;
import com.angindem.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@RestController
public class FileController {
@PostMapping("/upload")
public Result upload(MultipartFile images){
log.info("上传的文件为:{}",images);
System.out.println("成功接收到文件");
return Result.success();
}
}
通过Debug可以看到
文件保存的路径,
显示是TMP临时文件。程序运行完后,TMP文件会自动回收删除。
接受文件后transferTo()方法存储到本地目录
接受到文件后, MultipartFile 自带存储方法,参数为 File 文件对象,通过 File 对象存储到指定位置。
由于File 对象 创建,参数为 其指定存储位置+文件全名.存储文件类型
我们需要知道上传文件的文件类型。所以同时 MultipartFile 自带了getOriginalFilename() 获取原始文件名方法
package com.angindem.controller;
import com.angindem.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
@Slf4j
@RestController
public class FileController {
@PostMapping("/upload")
public Result upload(MultipartFile images) throws IOException {
log.info("上传的文件为:{}",images);
System.out.println("成功接收到文件");
String originalFilename = images.getOriginalFilename(); // 获取原始文件名
// 存储位置:D:\images\
String path = "D:\\images\\" + originalFilename;
// 保存
images.transferTo(new File(path));
return Result.success();
}
}
最后成功存储:
处理同文件名产生文件覆盖
当我们将文件名完全相同文件上传的时候,会发现,同文件名的时候,会发送文件名覆盖。
所以我们这里需要处理同文件名的情况。
UUID(通用唯一识别码)
我们可以通过 UUID 做文件名,这样唯一名,就不会产生文件同名覆盖的情况了。
使用方法:
String uuid = UUID.randomUUID().toString();
调用Java内置UUID工具类即可获得。
同时我们需要获得源文件的文件扩展名。
所以我们对源文件名进行处理即可获得文件扩展名即可。
package com.angindem.controller;
import com.angindem.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
@Slf4j
@RestController
public class FileController {
@PostMapping("/upload")
public Result upload(MultipartFile images) throws IOException {
log.info("上传的文件为:{}",images);
System.out.println("成功接收到文件");
String originalFilename = images.getOriginalFilename(); // 获取原始文件名
// 获取 uuid
String uuid = UUID.randomUUID().toString();
// 获取文件扩展名下标
int index = originalFilename.lastIndexOf(".");
// 截取获得文件扩展名
String extName = originalFilename.substring(index);
// 拼凑获得新文件名
String newFileName = uuid + extName;
// 存储位置:D:\images\
String path = "D:\\images\\" + newFileName;
// 保存
images.transferTo(new File(path));
return Result.success();
}
}
最后效果,可以看到避免了相同文件名覆盖问题:
上传文件过大导致的报错
当我们上传单个文件超过 1M 的时候,可以发现出现了报错。
这是由于在SpringBoot中,文件上传,默认单个文件允许最大大小为1M。如果需要上传大文件,对SpringBoot在 application.properties 进行配置即可。
配置如下:
# 配置单个文件最大上传大小
spring.servlet.multipart.max-file-size=10MB
# 配置单个请求最大上传大小(一次请求可以上传多个文件)
spring.servlet.multipart.max-request-size=100MB
重新上传,可以发现提交成功
文件删除操作
从Java 7开始,java.nio.file
包提供了更多的文件操作工具,包括删除文件。使用Files
类删除文件通常更推荐,因为它提供了更多的文件操作功能。
删除示例如下:
public static Result delFile(String urlReadPath) {
String fileName = urlReadPath.split("=")[1];
String savePath = getSavePath() + fileName;
Path filePath = null;
try {
filePath = Paths.get(savePath);
if (Files.exists(filePath) && !Files.isDirectory(filePath)) {
Files.delete(filePath);
return Result.success();
}
}catch (Exception ex){
return Result.success();
}
return Result.success();
}
测试效果:
点击删除按钮
删除成功!!!
通过相对路径读取物理存储文件
有时候,我们Windows有ABCD盘,当我们开发完成小项目后,需要部署到服务器上,此时服务器上可能就不会有ABCD盘的区分,所以这里我们可以读取当前项目目录将物理存储路径相对应,方便我们读取查看文件。
结合上面,我们可以统一写一个关于文件的工具类
文件工具类
package com.angindem.utils;
import com.angindem.result.Result;
import lombok.Setter;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
public class FileUtils {
@Setter
private static String PATH = "D:\\images\\";
public static String saveFile(MultipartFile images) throws IOException {
String originalFilename = images.getOriginalFilename();
String newFileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));
String savePath = PATH + newFileName;
images.transferTo(new File(savePath));
return savePath;
}
public static Result delFile(String savePath) {
Path filePath = null;
try {
filePath = Paths.get(savePath);
if (Files.exists(filePath) && !Files.isDirectory(filePath)) {
Files.delete(filePath);
return Result.success();
}
}catch (Exception ex){
return Result.success();
}
return Result.success();
}
}
使用user.dir
得到项目相对路径
我们可以当用户访问主路径的时候调用 System 类使用user.dir
获取当前项目放置的路径
// 获取项目主目录路径
String projectPath = System.getProperty("user.dir");
访问主路经,立即获取当前项目运行位置
@GetMapping("/")
public Result ProjectMain(){
// 获取项目主目录
String projectPath = System.getProperty("user.dir");
projectPath += "\\images\\";
log.info("当前项目相对路径:{}",projectPath );
FileUtils.setPATH(projectPath);
return Result.success(FileUtils.getPATH());
}
打包测试
访问主路经可以发现,此时已经获取到了相对项目的文件保存路径。
访问index.html尝试上传文件
发现报错,看来存储文件到指定目录的前提是该目录必须得存在。
我们现在创建一个目录,重新尝试。
文件成功上传!!!
上传操作生效,删除操作同样生效。
Java创建目录
防止一开始的运行找不到目录的报错,这里可以补充创建目录
File dir = new File(PATH);
if(!dir.exists() || !dir.isDirectory()){
dir.mkdir();
}
前后端分离,前端读取图片
由于绝对路径的存储方式,导致无法读取图片。
这时候,我们需要对后端处理通过数据流的方式反馈给前端。
反馈数据流实例:
@GetMapping("/views")
public void views(@RequestParam String fileName, HttpServletResponse response) throws IOException {
//"http://xxxx/views?fileName=xxx"
if (fileName == null) return ; // 当空请求,不响应输出流
String[] split = fileName.split("="); // 获取到文件URL后,通过分割字符串获取文件名
String name = "/" + split[split.length-1]; // 最后一个字符串就是文件名
File file = new File(FileUtils.getSavePath() + name); // 根据绝对路径读取文件 + 文件名
//设置输出流格式
ServletOutputStream outputStream = response.getOutputStream();
response.addHeader("Content-Disposition","attachment;filename="+ URLEncoder.encode(name,"UTF-8"));
//任意类型的二进制流数据
response.setContentType("application/octet-stream");
//读取文件字节流
outputStream.write(FileUtil.readBytes(file));
// 清除刷新缓冲区输出流
outputStream.flush();
// 关闭输出流
outputStream.close();
}
总结工具类:
package com.angindem.utils;
import com.angindem.result.Result;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
@Slf4j
public class FileUtils {
@Setter
@Getter
private static String PATH = "D:\\images\\";
public static String getSavePath() {
// 获取项目主目录
String projectPath = System.getProperty("user.dir");
// 处理 \ 转义字符
// projectPath = projectPath.replace("\\", "\\\\") + "\\images\\";
projectPath += "\\images\\";
log.info("当前项目相对路径:{}", projectPath);
PATH = projectPath;
return PATH;
}
public static String saveFile(MultipartFile images) throws IOException {
getSavePath();
String originalFilename = images.getOriginalFilename();
String newFileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));
String savePath = PATH + newFileName;
File dir = new File(PATH);
if (!dir.exists() || !dir.isDirectory()) {
dir.mkdir();
}
images.transferTo(new File(savePath));
return newFileName;
}
public static Result delFile(String urlReadPath) {
String fileName = urlReadPath.split("=")[1];
String savePath = getSavePath() + fileName;
Path filePath = null;
try {
filePath = Paths.get(savePath);
if (Files.exists(filePath) && !Files.isDirectory(filePath)) {
Files.delete(filePath);
return Result.success();
}
}catch (Exception ex){
return Result.success();
}
return Result.success();
}
}
测试访问成功