1、文件上传
1.1 功能描述
在页面选择一个文件,后端处理:
1、上传到阿里云 OSS
2、将文件的 URL、ContentType 等信息保存到数据库
1.2 页面搭建
前端使用的框架为 Vue + ElementUI + Axios
template 代码如下 (略去了template以及唯一的根标签):
<el-upload
ref="fileUploadForm"
action=""
:multiple="false"
:auto-upload="false"
:show-file-list="false"
:file-list="fileList"
:http-request="handleFileUploadSubmit"
:on-change="handleFileListChanged"
>
<el-row>
<el-col style="display: flex; text-align: center; width: 400px">
<el-input :value="currentFileName" placeholder="请选择文件" readonly/>
<el-button type="info" round style="margin-left: 20px" size="middle">选择文件</el-button>
</el-col>
</el-row>
</el-upload>
<el-row style="margin-top: 30px; margin-bottom: 30px">
<el-col style="text-align: center">
<el-button round @click="clearFileList">清空</el-button>
<el-button type="success" round style="margin-left: 20px" @click="submitFile">上传</el-button>
</el-col>
</el-row>
<el-progress
type="circle"
:color="progressColor"
:width="150"
:stroke-width="15"
:percentage="uploadPercentage"
:format="formatUploadPercentage"
/>
script 代码如下:
<script lang="js">
import apiService from 'axios'
export default {
name: 'UploadDemo',
data() {
return {
// 当前已选择的文件的文件名
currentFileName: '',
// 待上传的文件列表
fileList: [],
// 上传进度(百分比,0-100)
uploadPercentage: 0,
// 进度条的颜色
progressColor: 'rgb(0, 0, 255)'
};
},
methods: {
/**
* 文件状态改变时的操作
* @param {Object} selectedFile 选择的文件对象
* */
handleFileListChanged(selectedFile) {
// TODO 选择了文件时可以做一些文件类型的校验之类的操作
// 文件列表修改为选择的文件
this.fileList = [selectedFile];
// 提示框展示为选择的文件的文件名
this.currentFileName = selectedFile.name;
},
/**
* 清空待上传的文件列表
* */
clearFileList() {
// 待上传的文件列表清空
this.fileList = [];
// 提示框展示的文件名清空
this.currentFileName = '';
},
/**
* 点击【上传】按钮时要进行的操作
* */
submitFile() {
// TODO 还可以进行文件校验等操作
if (this.fileList.length < 1) {
this.$alert('请选择文件后再上传!', '提示', {
type: 'warning'
});
return;
}
// 校验无误将要上传,这里会调用 http-request 所绑定的方法
this.$refs.fileUploadForm.submit();
},
/**
* 发送文件上传的网络请求
* */
handleFileUploadSubmit({ file }) {
// 将进度条的颜色重置为蓝色
this.progressColor = 'rgb(0, 0, 255)';
// 创建一个表单,用于存放请求数据(主要是文件)
const fileFormData = new FormData();
// 在表单中添加要发送给接口的数据
fileFormData.append('username', 'Alice');
fileFormData.append('excelFile', file);
// 使用 axios 发送文件上传请求,并监听文件上传进度
apiService.post('http://192.168.199.203:2021/business/file/upload', fileFormData, {
onUploadProgress: progressEvent => {
if (progressEvent.lengthComputable) {
const percentage = progressEvent.loaded / progressEvent.total;
// 此处的"percentage"达到1时,并不意味着已经得到了接口的响应结果,因此只有当得到响应时才将进度置百
if (percentage < 1) {
this.uploadPercentage = percentage * 100;
}
}
}
}).then(({data: respObj}) => {
if (respObj.result !== 0) {
// 文件上传失败,将进度条置为红色
this.progressColor = 'rgb(255, 0, 0)';
this.$alert('上传失败!', '提示', {type: 'error'});
console.error(respObj.message);
return;
}
// 已成功得到接口的响应结果,将进度条的颜色置为绿色,并将进度置为100%
this.progressColor = 'rgb(0, 255, 0)';
this.uploadPercentage = 100;
this.$alert('上传成功!', '提示', {type: 'success'});
console.log('文件信息: ', respObj.file);
}).catch(error => {
// 请求失败,将进度条置为红色
this.progressColor = 'rgb(255, 0, 0)';
this.$alert('上传失败!', '提示', {type: 'error'});
console.error(error);
});
},
/**
* 进度条内容格式化
* @param {number} percentage 进度
* */
formatUploadPercentage(percentage) {
// 进度百分比保留两位小数展示,达到100%时展示"上传成功"
return percentage === 100 ? '上传完成' : `${percentage.toFixed(2)}%`;
}
</script>
页面效果如下:
1.3 后端接收
/file/upload 接口的实现如下:
/**
* 文件上传接口
* 注:当前服务并不直接进行文件的存储操作,而是交给另一个专门用来做这个工作的 oss 服务
* 使用 RestTemplate 调用该服务时:
* 1、需要使用 MultiValueMap 封装数据(是 java.util.Map 的一个子接口)
* 2、文件数据需要封装为 ByteArrayResource 或 FileSystemResource (均为 AbstractResource 的子类)
* @param username 操作文件的用户名
* @param multipartFile 由 MultipartFile 封装的上传的文件
* @return 包含了操作结果信息的 Map
*/
@PostMapping(value = {"/upload"})
public Map<String, Object> fileUpload(@RequestPart(value = "username") String username,
@RequestPart(value = "excelFile") MultipartFile multipartFile) {
log.info("username: " + username);
log.info("fileName: " + multipartFile.getOriginalFilename());
Map<String, Object> respMap = new HashMap<>(2);
try {
/*
* 将文件转化为输入流资源
* 为了避免生成临时文件,这里使用了 ByteArrayResource,且这里需要重写其中的 getFilename() 方法
* */
ByteArrayResource byteArrayResourceOfFile = new ByteArrayResource(multipartFile.getBytes()) {
@Override
public String getFilename() {
return multipartFile.getOriginalFilename();
}
};
// 创建 MultiValueMap 实例,这里使用 LinkedMultiValueMap 这一实现类来进行实例化
MultiValueMap<String, Object> multiValueMap = new LinkedMultiValueMap<>(2);
multiValueMap.add("username", username);
multiValueMap.add("file", byteArrayResourceOfFile);
// 使用 RestTemplate 调用目标服务,获得上传成功后文件的 URL
String ossProjectServer = "http://192.168.199.203:3021/oss/file/upload";
FileEntity fileEntity = restTemplate.postForObject(ossProjectServer, multiValueMap, FileEntity.class);
if (fileEntity == null) {
log.error("上传失败!");
respMap.put("result", -1);
respMap.put("message", "上传失败!");
return respMap;
}
// 上传成功,将文件信息存入数据库
String fileId = fileDao.saveFile(fileEntity);
log.info("fileId: " + fileId);
// 将结果返回
respMap.put("result", 0);
respMap.put("file", fileEntity);
return respMap;
} catch (Exception e) {
e.printStackTrace();
respMap.put("result", -1);
respMap.put("message", e.getMessage());
return respMap;
}
}
用于将文件上传到 OSS 的服务接口如下:
@PostMapping(value = {"/upload"})
public FileEntity fileHandler(@RequestPart(value = "username") String username,
@RequestPart(value = "file") MultipartFile multipartFile) {
OSS ossClient = null;
try (
// 获取要上传的文件的输入流
InputStream fileInputStream = multipartFile.getInputStream()
) {
// 创建 OSSClient 实例
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, secretAccessKey);
// 获取当前日期信息并准备 UUID 和时间戳
LocalDate now = LocalDate.now();
String datePrefix = now.getYear() + "/" + now.getMonthValue() + "/" + now.getDayOfMonth();
String uuIdAndTimestamp = UUID.randomUUID().toString() + "-" + System.currentTimeMillis();
/*
* 对象名称的规则如下:
* 用户名/年份/月份/日期/uuid-时间戳-文件名.扩展名
* */
String objectName
= username + "/" + datePrefix + "/" + uuIdAndTimestamp + "-" + multipartFile.getOriginalFilename();
log.info("objectName: " + objectName);
// 将文件上传到 OSS
PutObjectResult putObjectResult = ossClient.putObject(bucketName, objectName, fileInputStream);
if (putObjectResult != null) {
log.info("putObjectResult.getETag(): " + putObjectResult.getETag());
}
// 获取由 OSS 识别后的文件类型
OSSObject ossObject = ossClient.getObject(bucketName, objectName);
ObjectMetadata objectMetadata = ossObject.getObjectMetadata();
String contentType = objectMetadata.getContentType();
// 获取文件路径
String createdUrl = "http://" + bucketName + "." + endpoint.substring(endpoint.indexOf("oss")) + "/" + objectName;
// 将自定义的 FileEntity 实体类返回
return new FileEntity()
.setFileName(multipartFile.getOriginalFilename())
.setFileUrl(createdUrl)
.setContentType(contentType)
.setUsername(username);
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
注:如果在 ossClient.putObject(bucketName, objectName, fileInputStream) 时,出现了
java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty
的错误,最简单直接的方式是将 OSS 的 endpoint 中的 "https" 改为 "http"。
2、文件下载
2.1 功能描述
文件服务器中存有如下的 Excel 文件:
数据库中有
tb_student
表:
在页面点击"下载成绩单"按钮后,后端将从数据库中查到的学生信息写入到 Excel 文件中,并返回文件的字节数组。
2.2 后端搭建
/file/download 接口的实现如下:
@PostMapping(value = {"/download"})
public Map<String, Object> fileDownload(@RequestBody Map<String, String> dataMap) {
String fileId = dataMap.get("fileId");
log.info("fileId: " + fileId);
Map<String, Object> respMap = new HashMap<>(2);
if (fileId == null || "".equals(fileId)) {
respMap.put("result", -1);
respMap.put("message", "文件Id不能为空");
return respMap;
}
InputStream urlResourceInputStream = null;
Workbook workbook = null;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try {
// 获取文件的输入流
FileEntity fileEntity = fileDao.getFileUrl(fileId);
UrlResource urlResource = new UrlResource(fileEntity.getFileUrl());
urlResourceInputStream = urlResource.getInputStream();
// 从数据库中查询出所有的学生成绩信息
List<StudentEntity> studentEntities = studentDao.listAll();
// 根据文件输入流创建 Workbook 实例
workbook = WorkbookFactory.create(urlResourceInputStream);
// 获取 Excel 文件中的第一个 Sheet 页
Sheet firstSheet = workbook.getSheetAt(0);
// 创建"宋体"字体
Font font1 = workbook.createFont();
font1.setFontName("宋体");
// 创建"Times New Roman"字体
Font font2 = workbook.createFont();
font2.setFontName("Times New Roman");
// 创建单元格样式1: "宋体" + 居中
CellStyle cellStyle1 = workbook.createCellStyle();
cellStyle1.setFont(font1);
cellStyle1.setAlignment(HorizontalAlignment.CENTER);
cellStyle1.setAlignment(HorizontalAlignment.CENTER_SELECTION);
// 创建单元格样式2: "Times New Roman" + 居中
CellStyle cellStyle2 = workbook.createCellStyle();
cellStyle2.setFont(font2);
cellStyle2.setAlignment(HorizontalAlignment.CENTER);
cellStyle2.setAlignment(HorizontalAlignment.CENTER_SELECTION);
// 创建单元格样式3: "Times New Roman" + 居中 + 保留两位小数
CellStyle cellStyle3 = workbook.createCellStyle();
cellStyle3.setFont(font2);
cellStyle3.setAlignment(HorizontalAlignment.CENTER);
cellStyle3.setAlignment(HorizontalAlignment.CENTER_SELECTION);
cellStyle3.setDataFormat(HSSFDataFormat.getBuiltinFormat("0.00"));
// 数据长度
int studentsCount = studentEntities.size();
// 遍历学生成绩信息,将数据写入单元格
for (int i = 0; i < studentsCount; i++) {
// 获取当前索引的学生成绩信息
StudentEntity studentEntity = studentEntities.get(i);
// 创建Excel"行",索引"+1"是因为要跳过表头行
Row currentRow = firstSheet.createRow(i + 1);
// 创建存储"学号"数据的单元格并赋值
Cell studentIdCell = currentRow.createCell(0, CellType.STRING);
studentIdCell.setCellValue(String.valueOf(studentEntity.getStudentId()));
studentIdCell.setCellStyle(cellStyle2);
// 创建存储"姓名"数据的单元格并赋值
Cell studentNameCell = currentRow.createCell(1, CellType.STRING);
studentNameCell.setCellValue(studentEntity.getStudentName());
studentNameCell.setCellStyle(cellStyle1);
// 创建存储"性别"数据的单元格并赋值
Cell studentGenderCell = currentRow.createCell(2, CellType.STRING);
studentGenderCell.setCellValue(studentEntity.getStudentGender() == 1 ? "男" : "女");
studentGenderCell.setCellStyle(cellStyle1);
// 创建存储"班级"数据的单元格并赋值
Cell classNameCell = currentRow.createCell(3, CellType.STRING);
classNameCell.setCellValue(studentEntity.getClassName());
classNameCell.setCellStyle(cellStyle1);
// 创建存储"语文成绩"数据的单元格并赋值
Cell chineseScoreCell = currentRow.createCell(4, CellType.NUMERIC);
chineseScoreCell.setCellValue(studentEntity.getChineseScore());
chineseScoreCell.setCellStyle(cellStyle2);
// 创建存储"数学成绩"数据的单元格并赋值
Cell mathScoreCell = currentRow.createCell(5, CellType.NUMERIC);
mathScoreCell.setCellValue(studentEntity.getMathScore());
mathScoreCell.setCellStyle(cellStyle2);
// 创建存储"英语成绩"数据的单元格并赋值
Cell englishScoreCell = currentRow.createCell(6, CellType.NUMERIC);
englishScoreCell.setCellValue(studentEntity.getEnglishScore());
englishScoreCell.setCellStyle(cellStyle2);
// 创建存储"总分"数据的单元格并赋值
Cell totalScoreCell = currentRow.createCell(7, CellType.FORMULA);
// 总分的数据使用 Excel 中的公式自动计算出
totalScoreCell.setCellFormula("SUM(E" + (i + 2) + ":G" + (i + 2) + ")");
totalScoreCell.setCellStyle(cellStyle2);
// 创建存储"平均分"数据的单元格并赋值
Cell averageScoreCell = currentRow.createCell(8, CellType.FORMULA);
// 平均分的数据也使用 Excel 中的公式自动计算出
averageScoreCell.setCellFormula("AVERAGE(E" + (i + 2) + ":G" + (i + 2) + ")");
// 设置数据格式: 保留两位小数
averageScoreCell.setCellStyle(cellStyle3);
}
// 写入到字节数组输出流
workbook.write(byteArrayOutputStream);
respMap.put("result", 0);
// 默认的文件名
respMap.put("name", "成绩单.xls");
// 文件类型
respMap.put("type", fileEntity.getContentType());
// 输出流转为字节数组返回
respMap.put("data", byteArrayOutputStream.toByteArray());
return respMap;
} catch (Exception e) {
e.printStackTrace();
respMap.put("result", -1);
respMap.put("message", e.getMessage());
return respMap;
} finally {
try {
if (workbook != null) {
workbook.close();
}
if (urlResourceInputStream != null) {
urlResourceInputStream.close();
}
byteArrayOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.3 页面接收
template 代码如下 (简化为一个下载按钮,参数略):
<template>
<div>
<hr style="margin-top: 50px"/>
<el-button type="primary" @click="handleFileDownload">下载成绩单</el-button>
</div>
</template>
methods 代码如下:
/**
* 处理文件下载操作
* */
handleFileDownload() {
apiService.post('http://192.168.199.203:2021/business/file/download', {
fileId: '123456789'
}).then(({data: respObj}) => {
if (respObj.result !== 0) {
this.$alert('下载失败!', { type: 'error' });
console.error(respObj.message);
return;
}
this.downloadByteArrayFile(respObj.data, '成绩单.xls', respObj.type);
}).catch(error => {
this.$alert('下载失败!', { type: 'error' });
console.error(error);
});
},
/**
* 将 Base64 编码后的数据解码后由浏览器下载
* @param encodedData Base64编码后的数据
* @param fileName 文件名
* @param contentType MIME
*/
downloadByteArrayFile(encodedData, fileName, contentType) {
const rawData = atob(encodedData);
const blobPartsArray = [];
const sliceLength = 128;
for (let i = 0; i < rawData.length; i += sliceLength) {
const sliceString = rawData.substr(i, i + sliceLength);
const unicodeByteArray = new Array(sliceLength);
for (let j = 0; j < sliceLength; j++) {
unicodeByteArray[j] = sliceString.charCodeAt(j);
}
blobPartsArray.push(new Uint8Array(unicodeByteArray));
}
const blob = new Blob(blobPartsArray, {
type: contentType
});
const objectURL = URL.createObjectURL(blob);
const hyperLinkElement = document.createElement('a');
hyperLinkElement.setAttribute('href', objectURL);
hyperLinkElement.setAttribute('download', fileName);
hyperLinkElement.click();
}
3、遗留问题
- https 资源的获取
- xlsx 格式的 Excel 文件的写操作