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 文件的寫操作