文件上传是程序开发中必不可少的一个环节,对于文件上传的实现也是千奇百怪。 但是上传的基本流程基本一致。这里我们大致学习一下。

大致流程就是:
浏览器端提供了一个表单,在用户提交请求后,将文件数据和其他表单信息 编码并上传至服务器端,服务器端将上传的内容进行解码了,提取出 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语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 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组件。 二者基本一样。

基本使用

1
2
3
<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

上传成功我们通过调用回调函数来给用户做提示

1
2
3
4
handleFileUploadSuccess() {
this.$message.success("上传成功");
this.load();
},

后端实现思路

通过前端的函数调用, 就将真正实现文件编码显示的功能扔给了后端来实现, 所以所有的编码解码都是通过后端来实现的。后期我也会给出文件下载的文章。
下面我将按照三层架构的形式来给出实现的步骤

Controller层接受请求

通过前端给出的调用请求地址, 我们随即可以定位到对应的后端Controller层的请求内容。 当然这是我以一位读者的身份定位请求的地址,实际请求的地址应该是事先我们按照项目的需求说明以及项目开发文档来进行实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
@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);
}
}

  1. 表单提交:
    • 当用户提交带有文件输入的表单时,浏览器会向服务器发送多部分请求。
    • 表单的 enctype 属性设置为 **”multipart/form-data”**,表示表单数据包含二进制数据,包括文件。
  2. 通过MultipartFile接受请求过来的数据
  3. Controller层的方法处理:
    • 在 Spring MVC 的方法中,使用 **@**RequestParam("file") MultipartFile file作为方法参数来处理上传的文件。
    • Spring 自动将上传的文件绑定到 MultipartFile 对象

关于MultipartFile的方法可以阅读源码得知, 这里我只给出一些我们用到的。

  • getOriginalFilename(): 返回客户端文件系统中的原始文件名。
  • getSize(): 返回文件的字节大小。
  • getContentType(): 返回文件的 MIME 类型。

Service层处理请求

Service层作为处理请求的位置, 前期如果仅仅是单体项目 或者 项目的需求并不复杂, 那么使用MVC架构是可以的,但是随着微服务带给我们的高性能, 基本上有一定用户量的项目都会使用微服务架构来搭建项目, 此时模块之间的逻辑随着变得复杂,service层间调用变得越来越频繁, 代码冗余乃至架构混乱的问题就回相继显现出来。所以后期项目基本都会更换架构,比如现在很火的DDD架构。
好了说多了, 回到现在我们的项目, 通过Service层实现文件数据编码 —> 存储数据库 —-> 解码 等一系列的操作。

实现逻辑

  1. 通过MultipartFile的方法getOriginalFilename获取用户上传的文件的原始名
  2. 解析文件名, 对其中的文件名后缀解析出文件的类型
  3. 通过MultipartFile的方法getSize获取用户上传的文件的大小。
  4. 获取用户存储文件的流对象, 通过流对象对输入文件的流进行 MD5 哈希计算
  5. 因为数据库中存储了对应的md5, 所以我们进行比较, 看是否文件已存在。 防止重复存储相同的文件消耗服务器资源。
  6. 将用户上传的资源进行存储到服务器。
  7. 我们这里并没有进行编码(压缩) – 解码的步骤, 因为该项目中的文件内容仅用于存储用户的头像, 而且也并不打算部署到服务器, 所以就省略了这个步骤, 当然实现起来也并不难, 只需要再通过一个方法来对存储的文件进行转换为字节码的形式即可。
  8. 通过UUID生成字符串, 保存文件名到服务器中
  9. 最后, 创建File实体类的对象, 将我们前面得到的文件的类型,文件名,文件大小 ,md5的值等保存到数据库中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@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工具类实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
}
}

文件存储地:
image.png