文件上传是程序开发中必不可少的一个环节,对于文件上传的实现也是千奇百怪。 但是上传的基本流程基本一致。这里我们大致学习一下。
大致流程就是: 浏览器端提供了一个表单,在用户提交请求后,将文件数据和其他表单信息 编码并上传至服务器端,服务器端将上传的内容进行解码了,提取出 HTML 表单中的信息,将文件数据存入磁盘或数据库。
数据库中的文件字段其实没那么复杂,就是简单的描述文件的基本信息, 以及文件的编码值(便于后面解码下载文件), 当然还有文件在服务器中存储的位置。 这里是否删除和是否启用我们使用的类型是tinyint类型, 相信经常开发的同学应该是知道为什么使用吧。
数据名称 | 数据类型 | 数据描述 |
---|---|---|
id | bigint(0) | 主键 |
name | varchar(255) | 文件名称 |
type | varchar(255) | 文件类型 |
size | bigint(0) | 大小 |
url | varchar(255) | 文件路径 |
is_delete | tinyint(1) | 是否删除 |
enable | tinyint(1) | 是否启用 |
md5 | varchar(255) | md5值 |
对应的SQL语句
-- Table structure for sys_file
-- ----------------------------
DROP TABLE IF EXISTS `sys_file`;
CREATE TABLE `sys_file` (
`id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '文件名称',
`type` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '文件类型',
`size` bigint(0) NULL DEFAULT NULL COMMENT '大小',
`url` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '文件路径',
`is_delete` tinyint(1) NULL DEFAULT NULL COMMENT '是否删除',
`enable` tinyint(1) NULL DEFAULT NULL COMMENT '是否启用',
`md5` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT 'md5值',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 34 CHARACTER SET = utf8 COLLATE = utf8_bin COMMENT = '系统文件表' ROW_FORMAT = Dynamic;
文件上传的前端实现其实并不复杂, 我们项目是通过使用Vue实现, 所以就可以使用Element组件来实现。
组件的导入形式这里就不做过多赘述了, 之前的项目文档中有Vue引入vant
组件。 二者基本一样。
<el-upload action="http://localhost:9191/file/upload" :show-file-list="false" :on-success="handleFileUploadSuccess" style="display: inline-block">
<el-button type="primary"><i class="el-icon-a-032" style="padding-right: 6px"></i>上传</el-button>
</el-upload>
使用的组件就是upload
, 在element中的地址 : https://element.eleme.cn/#/zh-CN/component/upload
通过下面的参数解释, 可以知道action是上传文件的地址, 按照我们文章开头提到的就是将文件数据进行编码上传到服务器。当然上传至服务器的操作是通过后端来实现的。这里就是相当于调用了后端的接口让后端来处理这个请求。
参数 | 说明 | 类型 | 可选值 |
---|---|---|---|
action | 必选参数,上传的地址 | string | — |
:show-file-lis | 动态绑定的属性,设置为 false 表示在上传文件时不显示已上传文件的列表。 | false | |
:on-success | 动态绑定的属性,** 指定了文件上传成功后的回调函数。** | handleFileUploadSuccess | |
style | 为了调整上传组件的显示样式,将其显示为内联块元素,以便更好地与其它元素布局。 | display: inline-block |
上传成功我们通过调用回调函数来给用户做提示
handleFileUploadSuccess() {
this.$message.success("上传成功");
this.load();
},
通过前端的函数调用, 就将真正实现文件编码显示的功能扔给了后端来实现, 所以所有的编码解码都是通过后端来实现的。后期我也会给出文件下载的文章。 下面我将按照三层架构的形式来给出实现的步骤
通过前端给出的调用请求地址, 我们随即可以定位到对应的后端Controller层的请求内容。 当然这是我以一位读者的身份定位请求的地址,实际请求的地址应该是事先我们按照项目的需求说明以及项目开发文档来进行实现的。
@RestController
@RequestMapping("/file")
public class FileController {
@Resource
private FileService fileService;
//上传文件
@PostMapping("/upload")
public Result upload(@RequestParam MultipartFile file){
String url = fileService.upload(file);
return Result.success(url);
}
}
**@**RequestParam("file") MultipartFile file
作为方法参数来处理上传的文件。MultipartFile
对象关于MultipartFile的方法可以阅读源码得知, 这里我只给出一些我们用到的。
Service层作为处理请求的位置, 前期如果仅仅是单体项目 或者 项目的需求并不复杂, 那么使用MVC架构是可以的,但是随着微服务带给我们的高性能, 基本上有一定用户量的项目都会使用微服务架构来搭建项目, 此时模块之间的逻辑随着变得复杂,service层间调用变得越来越频繁, 代码冗余乃至架构混乱的问题就回相继显现出来。所以后期项目基本都会更换架构,比如现在很火的DDD架构。 好了说多了, 回到现在我们的项目, 通过Service层实现文件数据编码 —> 存储数据库 —-> 解码 等一系列的操作。
MultipartFile
的方法getOriginalFilename
获取用户上传的文件的原始名MultipartFile
的方法getSize
获取用户上传的文件的大小。@Service
public class FileService extends ServiceImpl<FileMapper, MyFile> {
@Resource
private FileMapper fileMapper;
public String upload(MultipartFile uploadFile){
String originalFilename = uploadFile.getOriginalFilename(); //文件原始名字
String type = originalFilename.substring(originalFilename.lastIndexOf(".")+1); //文件后缀
long size = uploadFile.getSize() / 1024; //文件大小,单位kb
String url;
MyFile myFile = new MyFile(); //用于保存于数据库的实体类Files
myFile.setName(originalFilename);
myFile.setSize(size);
myFile.setType(type);
//通过md5判断文件是否已经存在,防止在服务器存储相同文件
InputStream inputStream = null;
try {
inputStream = uploadFile.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
String md5 = SecureUtil.md5(inputStream);
QueryWrapper<MyFile> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("md5",md5);
List<MyFile> dbMyFileList = fileMapper.selectList(queryWrapper);
if(dbMyFileList.size() != 0){
//数据库中已有该md5,则拷贝其url
url = dbMyFileList.get(0) .getUrl();
myFile.setUrl(url);
}else{
//文件不存在,则保存文件
File folder = new File(Constants.fileFolderPath);
if(!folder.exists()){
folder.mkdir();
}
String folderPath = folder.getAbsolutePath()+"/"; //文件存储文件夹的位置
System.out.println("文件存储地址"+folderPath);
//将文件保存为UUID的名字,通过uuid生成url
String uuid = UUID.randomUUID().toString().replace("-", "").toLowerCase();
String finalFileName = uuid+"."+type;
File targetFile = new File(folderPath + finalFileName);
try {
uploadFile.transferTo(targetFile);
} catch (IOException e) {
e.printStackTrace();
}
url = "/file/"+finalFileName;
myFile.setUrl(url);
}
myFile.setMd5(md5);
fileMapper.insert(myFile);
System.out.println("文件"+originalFilename+" "+url);
return url;
}
我们这个项目是通过将文件保存到当前的项目文件夹中, 所以对于不同的操作系统 的当前项目所在的base地我也是做了分类, 通过PathUtils工具类实现
public class PathUtils {
public static String getClassLoadRootPath() {
String path = "";
try {
String prePath = URLDecoder.decode(PathUtils.class.getClassLoader().getResource("").getPath(),"utf-8").replace("/target/classes", "");
String osName = System.getProperty("os.name");
if (osName.toLowerCase().startsWith("mac")) {
// 苹果
path = prePath.substring(0, prePath.length() - 1);
} else if (osName.toLowerCase().startsWith("windows")) {
// windows
path = prePath.substring(1, prePath.length() - 1);
} else if(osName.toLowerCase().startsWith("linux") || osName.toLowerCase().startsWith("unix")) {
// unix or linux
path = prePath.substring(0, prePath.length() - 1);
} else {
path = prePath.substring(1, prePath.length() - 1);
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return path;
}
}
文件存储地: