前面有一篇简单版的文件上传,是为了让大家知道文件上传是在干什么,但是在正式的开发中文件上传是一个稍微有些麻烦的东西,需要从页面层开发到数据层,如果你常常听人说文件上传会知道有一些相关的名词,比如切片、秒传、断点续传、md5、合并等名词。但其实一个完整的文件上传开发起来核心点永远只有那么几个。因此,本篇知识点给大家写了一个完整的文件上传流程,本身是用于大文件上传,不过当你看明白了代码,知晓了文件上传的核心要点,你就会发现,大、小文件的上传区别就两点,一是是否分片,二是是否并发,其他的都差不多。
当然考虑到适用性和大家的理解能力,本篇写的上传流程并没有偏业务代码,比如检查文件大小、图片像素等等这种偏业务的代码通通没有,只写了上传文件的主体核心流程,同时代码也上传到了github----》https://github.com/wangyang159/boot-jsp
看本例代码的时候有个容易产生误区的点要注意,本例代码只针对单个文件,如果你是多文件,则遍历文件调用上传就行,千万不要在单文件并发的基础上,再套一层多文件并发,先不说性能和开发难度的问题,浏览器就撑不住,浏览器对于请求的个数都是有一个上限的,而且一般不高,你可以去查一查,最高的应该是谷歌派系,也只是最高支持6个。所以我的代码中并发度是两个,给其他请求留了4个,同理你要是在单文件并发上在套一层多文件同时并发,那你的代码就成垃圾了,能不能用都成问题。不过一般在正式开发中涉及到文件上传,小文件一般传递很快,只需要把大文件的一个md5对应多个切片的js数据集合,换成小文件场景下一对一的数据集,就是说把小文件当成大文件的切片,再对应的改一下发送请求的代码,从而就可以实现小文件的并发发送,而大文件肯定会有个数和大小的限制的,不会让你很多个大文件一起上传,比如腾讯旗下的产品大多就限制单个大文件最大4G,多个挨个上传,所以遍历大文件单个并发上传就够了。
由于整个流程细节很多就不给大家分解了,完整的代码如果看不明白可以留言给我。为了方便大家理解,最好是先看一下流程图。
看完流程图对整个流程有个影响,就可以看代码了。
1、首先是前端的表单页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>切片上传</title>
</head>
<body>
<form id="addFrm" enctype="multipart/form-data" method="post">
文件上传:<input type="file" name="file" id="file"><br/>
<input type="button" onclick="saveObject()" value="文件上传"/>
</form>
<script type="text/javascript" src="/sy.js"></script>
<script type="text/javascript" src="spark-md5.min.js"></script>
<script type="text/javascript" src="jquery-3.2.1.js"></script>
<script type="text/javascript">
var readyfilemeg = []
document.querySelector('#file').addEventListener('change', e => {
const fileblock = [];
let fileblock_index = 0;
const file = e.target.files[0];
const sliceLength = 5;
const chunkSize = Math.ceil(file.size / sliceLength);
const fileReader = new FileReader();
const md5 = new SparkMD5();
let file_index = 0;
const loadFile = () => {
const slice = file.slice(file_index, file_index + chunkSize);
fileblock[fileblock_index]=slice;
fileblock_index++;
file_index += chunkSize;
fileReader.readAsBinaryString(slice);
}
loadFile();
fileReader.onload = e1 => {
md5.appendBinary(e1.target.result);
if ( file_index < file.size ) {
loadFile();
} else {
readyfilemeg["filemd5"]=md5.end();
readyfilemeg["fileblocks"]=fileblock;
readyfilemeg["fileblocksize"]=fileblock_index;
readyfilemeg["filename"]=file.name;
readyfilemeg["filesize"]=file.size;
console.log(readyfilemeg)
}
};
});
function saveObject() {
$.ajax({
method:"post",
dataType:"json",
url:"/minupload",
data:{"fileMd5":readyfilemeg["filemd5"]},
success:function (result) {
if(result){
alert("上传成功")
}else {
concurRequst(readyfilemeg,2).then(resps=>{
let reduce = 0;
for (let i = 0; i < resps.length; i++) {
reduce+=parseInt(resps[i]);
}
if(reduce != 0){
alert("上传失败请重新上传文件")
}else{
$.ajax({
method:"post",
dataType:"json",
url:"/allhb",
data:{"fileMd5":readyfilemeg["filemd5"],"fileSize":readyfilemeg["filesize"],"fileName":readyfilemeg["filename"]},
success:function (result) {
if(result){
alert("上传成功")
}else{
alert("上传失败")
}
}
})
}
});
}
}
})
}
</script>
</body>
</html>
2、页面上发送单独写了一个js,用来写上传时的方法
function concurRequst(datas,maxNum) {
return new Promise(resolve => {
if(datas == null){
resolve([])
return
}
const result = [];
let index = 0;
let fileblocks = datas["fileblocks"];
async function request() {
if(index === fileblocks.length){
resolve(result);
return;
}
const fileblock = fileblocks[index];
const i = index;
index++;
let f = new FormData();
f.append("fileBlock",fileblock);
f.append("fileMd5",datas["filemd5"]);
f.append("fileBlockSize",fileblocks.length);
f.append("blockIndex",i+1);
f.append("fileName",datas["filename"]);
try {
const resp = await fetch("http://localhost:91/upload",{
method : "post",
body : f
})
result[i]=resp.headers.get("meg")
}catch (err){
result[i] = err
}finally {
request()
}
}
const t = Math.min(maxNum,fileblocks.length)
for (let i = 0 ; i < t ; i++){
request()
}
})
}
3、前端用了两个框架,第一个是jquery,大家应该都有,第二个是spark-md5,是一个计算文件md5的js框架,它可以在git上下载到https://github.com/satazor/js-spark-md5 代码中用它配合文件切分,提高转换大文件md5效率的同时,也完成了切片的需求,如果大家自己写代码的时候如果上传的是一般打下的文件,不需要切片的话,可以直接整个文件读取md5,就像下面这样
document.querySelector('#file').addEventListener('change', e => {
const file = e.target.files[0];
const fileReader = new FileReader()
fileReader.readAsBinaryString(file);
fileReader.onload = e => {
const md5 = SparkMD5.hashBinary(e.target.result);
console.log(md5);
}
});
4、前端就上面这些了,下面就是后端,首先是后端的数据Bean,需要对应数据库中的切片信息表和文件信息表
package com.wy.bootjsp.bean;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
public class FileBlock {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String fileMd5;
private Integer fileBlockSize;
private Integer blockIndex;
private String fileName;
private String blockPathName;
public FileBlock() {
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getFileMd5() {
return fileMd5;
}
public void setFileMd5(String fileMd5) {
this.fileMd5 = fileMd5;
}
public Integer getFileBlockSize() {
return fileBlockSize;
}
public void setFileBlockSize(Integer fileBlockSize) {
this.fileBlockSize = fileBlockSize;
}
public Integer getBlockIndex() {
return blockIndex;
}
public void setBlockIndex(Integer blockIndex) {
this.blockIndex = blockIndex;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getBlockPathName() {
return blockPathName;
}
public void setBlockPathName(String blockPathName) {
this.blockPathName = blockPathName;
}
@Override
public String toString() {
return "FileBlock{" +
"id=" + id +
", fileMd5='" + fileMd5 + '\'' +
", fileBlockSize=" + fileBlockSize +
", blockIndex=" + blockIndex +
", fileName='" + fileName + '\'' +
", blockPathName='" + blockPathName + '\'' +
'}';
}
}
package com.wy.bootjsp.bean;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
public class FileMeg {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String fileMd5;
private Integer fileBlockSize;
private String fileName;
private String pathName;
private Long fileSize;
public FileMeg() {
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getFileMd5() {
return fileMd5;
}
public void setFileMd5(String fileMd5) {
this.fileMd5 = fileMd5;
}
public Integer getFileBlockSize() {
return fileBlockSize;
}
public void setFileBlockSize(Integer fileBlockSize) {
this.fileBlockSize = fileBlockSize;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public Long getFileSize() {
return fileSize;
}
public void setFileSize(Long fileSize) {
this.fileSize = fileSize;
}
public String getPathName() {
return pathName;
}
public void setPathName(String pathName) {
this.pathName = pathName;
}
@Override
public String toString() {
return "FileMeg{" +
"id=" + id +
", fileMd5='" + fileMd5 + '\'' +
", fileBlockSize=" + fileBlockSize +
", fileName='" + fileName + '\'' +
", pathName='" + pathName + '\'' +
", fileSize=" + fileSize +
'}';
}
}
并且你要准备一个数据库建表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `blockmeg`;
CREATE TABLE `blockmeg` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`filemd5` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '原文件的md5',
`fileblocksize` int(11) NULL DEFAULT NULL COMMENT '原文件被切分的总块数',
`blockindex` int(11) NULL DEFAULT NULL COMMENT '该块数对比原文件分块的顺序',
`filename` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '原文件姓名',
`blockpathname` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '块数据保存路径',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 79 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '切片信息表' ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `filemeg`;
CREATE TABLE `filemeg` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`filemd5` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件的md5',
`fileblocksize` int(11) NULL DEFAULT NULL COMMENT '文件被切分的总块数',
`filename` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件名',
`pathname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件存储路径',
`filesize` bigint(20) NULL DEFAULT NULL COMMENT '文件总大小',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '切片信息表对应的原文件信息表' ROW_FORMAT = Dynamic;
5、随后是控制器层
package com.wy.bootjsp.controller;
import com.wy.bootjsp.bean.FileBlock;
import com.wy.bootjsp.bean.FileMeg;
import com.wy.bootjsp.service.FileBlockService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.List;
@Controller
public class FileUploadContrller {
@Resource
private FileBlockService fileBlockService;
@RequestMapping("/upload")
@ResponseBody
public void test(HttpServletResponse response, @RequestParam("fileBlock") MultipartFile fileBlock, FileBlock fileBlockMeg){
Integer blockId = fileBlockService.getFileBlockByMd5AndIndex(fileBlockMeg);
if(blockId != null){
response.addHeader("meg","0");
return;
}
String path = null;
try {
path = fileBlockService.saveFile(fileBlock, fileBlockMeg);
} catch (IOException e) {
response.addHeader("meg","1");
e.printStackTrace();
return;
}
fileBlockMeg.setBlockPathName(path);
fileBlockService.insertFileBlockMeg(fileBlockMeg);
response.addHeader("meg","0");
}
@RequestMapping("/minupload")
@ResponseBody
public Boolean minonload(String fileMd5){
Integer id = fileBlockService.getFileMegByMd5(fileMd5);
if(id != null){
return true;
}else{
return false;
}
}
@RequestMapping("/allhb")
@ResponseBody
public Boolean allhb(String fileMd5,long fileSize,String fileName) throws InterruptedException {
Thread.sleep(1000);
List<FileBlock> fileBlocksPath = fileBlockService.getFileBlocksPath(fileMd5);
File[] fileBlocks = new File[fileBlocksPath.size()];
for (int i = 0 ; i < fileBlocksPath.size() ; i++){
FileBlock f = fileBlocksPath.get(i);
fileBlocks[f.getBlockIndex()-1] = new File(f.getBlockPathName());
}
String savePath = fileBlockService.allhb(fileBlocks, fileMd5 + "." + fileName.split("\\.")[1]);
if(!savePath.equals("error")){
FileMeg fileMeg = new FileMeg();
fileMeg.setFileMd5(fileMd5);
fileMeg.setFileName(fileName);
fileMeg.setFileSize(fileSize);
fileMeg.setFileBlockSize(fileBlocks.length);
fileMeg.setPathName(savePath);
fileBlockService.insertFileMeg(fileMeg);
return true;
}else{
return false;
}
}
}
6、随后是service业务层
package com.wy.bootjsp.service;
import com.wy.bootjsp.bean.FileBlock;
import com.wy.bootjsp.bean.FileMeg;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.List;
public interface FileBlockService {
Integer getFileBlockByMd5AndIndex(FileBlock fileBlock);
String saveFile (MultipartFile file,FileBlock fileBlock) throws IOException;
Integer getFileMegByMd5(String fileMd5);
void insertFileBlockMeg(FileBlock fileBlock);
List<FileBlock> getFileBlocksPath(String fileMd5);
String allhb(File[] files,String savaFileName);
void insertFileMeg(FileMeg fileMeg);
}
package com.wy.bootjsp.service.impl;
import com.wy.bootjsp.bean.FileBlock;
import com.wy.bootjsp.bean.FileMeg;
import com.wy.bootjsp.mapper.FileBlockMapper;
import com.wy.bootjsp.service.FileBlockService;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.*;
import java.util.List;
@Service
public class FileBlockSerivceImpl implements FileBlockService {
@Resource
private FileBlockMapper fileBlockMapper;
@Override
public Integer getFileBlockByMd5AndIndex(FileBlock fileBlock) {
return fileBlockMapper.getFileBlockByMd5AndIndex(fileBlock);
}
@Override
public String saveFile(MultipartFile file,FileBlock fileBlock) throws IOException {
String fileName = fileBlock.getFileMd5() + fileBlock.getBlockIndex() + ".ext";
File destFile = new File("D:\\pic", fileName);
if (!destFile.getParentFile().exists()) {
destFile.mkdirs();
}
file.transferTo(destFile);
return destFile.getPath();
}
@Override
public Integer getFileMegByMd5(String fileMd5) {
return fileBlockMapper.getFileMegByMd5(fileMd5);
}
@Override
public void insertFileBlockMeg(FileBlock fileBlock) {
fileBlockMapper.insertFileBlockMeg(fileBlock);
}
@Override
public List<FileBlock> getFileBlocksPath(String fileMd5) {
return fileBlockMapper.getFileBlocksPath(fileMd5);
}
@Override
public String allhb(File[] files,String savaFileName) {
String resultPath = "D:\\pic\\"+savaFileName;
FileInputStream in = null;
FileOutputStream out = null;
try {
File target = new File(resultPath);
out = new FileOutputStream(target);
for(File f : files) {
byte[] buf = new byte[1024];
int len = 0;
in = new FileInputStream(f);
while ((len = in.read(buf)) != -1) {
out.write(buf,0,len);
}
if (in != null) {
in.close();
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
return "error";
} catch (IOException e) {
e.printStackTrace();
return "error";
}finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return resultPath;
}
@Override
public void insertFileMeg(FileMeg fileMeg) {
fileBlockMapper.insertFileMeg(fileMeg);
}
}
7、最后是数据层
package com.wy.bootjsp.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.wy.bootjsp.bean.FileBlock;
import com.wy.bootjsp.bean.FileMeg;
import java.util.List;
public interface FileBlockMapper extends BaseMapper<FileBlock> {
Integer getFileBlockByMd5AndIndex(FileBlock fileBlock);
Integer getFileMegByMd5(String fileMd5);
void insertFileBlockMeg(FileBlock fileBlock);
List<FileBlock> getFileBlocksPath(String fileMd5);
void insertFileMeg(FileMeg fileMeg);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wy.bootjsp.mapper.FileBlockMapper">
<select id="getFileBlockByMd5AndIndex" resultType="Integer">
select id from blockmeg where filemd5=#{fileMd5} and blockindex=#{blockIndex}
</select>
<insert id="insertFileBlockMeg">
insert into blockmeg(filemd5,fileblocksize,blockindex,filename,blockpathname) values(
#{fileMd5},#{fileBlockSize},#{blockIndex},#{fileName},#{blockPathName}
)
</insert>
<select id="getFileMegByMd5" resultType="Integer">
select id from filemeg where filemd5=#{fileMd5}
</select>
<resultMap id="fileBlockMap" type="com.wy.bootjsp.bean.FileBlock">
<id column="id" property="id"></id>
<result column="blockpathname" property="blockPathName" />
<result column="blockindex" property="blockIndex" />
</resultMap>
<select id="getFileBlocksPath" resultMap="fileBlockMap">
select id,blockpathname,blockindex from blockmeg where filemd5=#{fileMd5}
</select>
<insert id="insertFileMeg">
insert into filemeg(id,filemd5,fileblocksize,filename,pathname,filesize) values(
#{id},#{fileMd5},#{fileBlockSize},#{fileName},#{pathName},#{fileSize}
)
</insert>
</mapper>
其他pom、springboot配置文件那些,大家自己把代码拉下来自己看就行
|