SpringBoot讀取Excel文件的完整指南
前情提要:Excel——那個偽裝成表格的數(shù)據(jù)怪獸
想象一下,你正悠閑地喝著咖啡,產(chǎn)品經(jīng)理突然拍著你的肩膀說:“嘿,這是客戶發(fā)來的Excel文件,里面有十萬條數(shù)據(jù),明天上線前要導入系統(tǒng)哦!”
這時你才發(fā)現(xiàn),你面對的不僅是一個.xlsx文件,而是一個披著網(wǎng)格外衣的“數(shù)據(jù)怪獸”。它有隱藏的工作表、合并的單元格、還有那些任性格式化的日期字段!但別怕,拿起SpringBoot這把“圣劍”,我們一起來馴服這個怪獸!
戰(zhàn)斗準備:裝備你的SpringBoot武器庫
第一步:添加神奇藥劑(依賴)
<dependencies>
<!-- 主武器:SpringBoot基礎裝備 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 對付Excel的專屬神器:Apache POI -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
<!-- 輔助裝備:簡化代碼的Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
第二步:創(chuàng)建數(shù)據(jù)模型——給怪獸分類
import lombok.Data;
@Data
public class User {
private String name; // 姓名列
private Integer age; // 年齡列
private String email; // 郵箱列
private Date birthDate; // 生日列(Excel最擅長把日期搞亂)
private Double salary; // 工資列(希望這個數(shù)字讓你開心)
// 可選:給怪獸的數(shù)據(jù)加個驗證
public boolean isValid() {
return name != null && !name.trim().isEmpty()
&& email != null && email.contains("@");
}
}
第三步:創(chuàng)建Excel讀取服務——我們的"怪獸翻譯官"
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Service
public class ExcelReaderService {
/**
* 讀取Excel文件的主要方法
* @param file 上傳的Excel文件
* @return 用戶列表
* @throws Exception 如果Excel怪獸太難對付
*/
public List<User> readExcelFile(MultipartFile file) throws Exception {
// 安全檢查:先確認這不是個空文件陷阱
if (file.isEmpty()) {
throw new RuntimeException("文件是空的!Excel怪獸使用了隱身術!");
}
// 檢查文件類型:.xlsx還是.xls?
String fileName = file.getOriginalFilename();
if (fileName == null || (!fileName.endsWith(".xlsx") && !fileName.endsWith(".xls"))) {
throw new RuntimeException("這不是一個合法的Excel文件!可能是偽裝的怪獸!");
}
List<User> userList = new ArrayList<>();
// 嘗試打開Excel文件(打開怪獸的嘴)
try (InputStream inputStream = file.getInputStream();
Workbook workbook = WorkbookFactory.create(inputStream)) {
// 獲取第一個工作表(Excel怪獸可能有多個頭)
Sheet sheet = workbook.getSheetAt(0);
// 遍歷每一行(像翻閱怪獸的日記)
for (int i = 1; i <= sheet.getLastRowNum(); i++) { // 從1開始,跳過表頭
Row row = sheet.getRow(i);
// 跳過空行(怪獸的空白記憶)
if (row == null) {
continue;
}
User user = convertRowToUser(row);
if (user.isValid()) {
userList.add(user);
} else {
System.out.println("第 " + (i+1) + " 行數(shù)據(jù)不完整,已跳過");
}
}
}
System.out.println("成功從Excel怪獸手中解救了 " + userList.size() + " 個用戶!");
return userList;
}
/**
* 把一行數(shù)據(jù)轉(zhuǎn)換成User對象
* 注意:這個方法要和Excel的結構對應上!
*/
private User convertRowToUser(Row row) {
User user = new User();
try {
// 第一列:姓名(字符串)
Cell nameCell = row.getCell(0);
if (nameCell != null) {
user.setName(getCellValueAsString(nameCell));
}
// 第二列:年齡(數(shù)字)
Cell ageCell = row.getCell(1);
if (ageCell != null) {
if (ageCell.getCellType() == CellType.NUMERIC) {
user.setAge((int) ageCell.getNumericCellValue());
} else {
// 嘗試從字符串解析
String ageStr = getCellValueAsString(ageCell);
if (ageStr.matches("\\d+")) {
user.setAge(Integer.parseInt(ageStr));
}
}
}
// 第三列:郵箱(字符串)
Cell emailCell = row.getCell(2);
if (emailCell != null) {
user.setEmail(getCellValueAsString(emailCell));
}
// 第四列:生日(日期)
Cell birthCell = row.getCell(3);
if (birthCell != null) {
if (birthCell.getCellType() == CellType.NUMERIC &&
DateUtil.isCellDateFormatted(birthCell)) {
user.setBirthDate(birthCell.getDateCellValue());
}
}
// 第五列:工資(數(shù)字)
Cell salaryCell = row.getCell(4);
if (salaryCell != null) {
if (salaryCell.getCellType() == CellType.NUMERIC) {
user.setSalary(salaryCell.getNumericCellValue());
}
}
} catch (Exception e) {
System.err.println("處理第 " + (row.getRowNum()+1) + " 行時遇到問題: " + e.getMessage());
}
return user;
}
/**
* 智能獲取單元格值作為字符串
* Excel怪獸喜歡把數(shù)據(jù)打扮成各種類型
*/
private String getCellValueAsString(Cell cell) {
if (cell == null) {
return "";
}
switch (cell.getCellType()) {
case STRING:
return cell.getStringCellValue().trim();
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue().toString();
} else {
// 避免科學計數(shù)法,也避免不必要的.0
double num = cell.getNumericCellValue();
if (num == Math.floor(num)) {
return String.valueOf((int) num);
} else {
return String.valueOf(num);
}
}
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
case FORMULA:
try {
return cell.getStringCellValue();
} catch (Exception e) {
try {
return String.valueOf(cell.getNumericCellValue());
} catch (Exception ex) {
return cell.getCellFormula();
}
}
default:
return "";
}
}
/**
* 高級功能:讀取多個工作表
*/
public Map<String, List<User>> readMultipleSheets(MultipartFile file) throws Exception {
Map<String, List<User>> result = new HashMap<>();
try (InputStream inputStream = file.getInputStream();
Workbook workbook = new XSSFWorkbook(inputStream)) {
// 遍歷所有工作表
for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
Sheet sheet = workbook.getSheetAt(i);
String sheetName = sheet.getSheetName();
List<User> users = new ArrayList<>();
for (Row row : sheet) {
if (row.getRowNum() == 0) continue; // 跳過表頭
User user = convertRowToUser(row);
if (user.isValid()) {
users.add(user);
}
}
result.put(sheetName, users);
System.out.println("工作表 '" + sheetName + "' 中讀取到 " + users.size() + " 個用戶");
}
}
return result;
}
}
第四步:創(chuàng)建控制器——與前端通信的"傳令兵"
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/excel")
public class ExcelController {
@Autowired
private ExcelReaderService excelReaderService;
/**
* 上傳并讀取Excel文件
* POST /api/excel/upload
*/
@PostMapping("/upload")
public ResponseEntity<Map<String, Object>> uploadExcel(
@RequestParam("file") MultipartFile file) {
Map<String, Object> response = new HashMap<>();
try {
// 調(diào)用服務讀取Excel
List<User> users = excelReaderService.readExcelFile(file);
// 構建響應
response.put("success", true);
response.put("message", "文件讀取成功!");
response.put("totalRecords", users.size());
response.put("data", users);
response.put("suggestions", generateSuggestions(users));
// 這里可以添加數(shù)據(jù)庫保存邏輯
// userRepository.saveAll(users);
return ResponseEntity.ok(response);
} catch (Exception e) {
response.put("success", false);
response.put("message", "讀取文件時出錯: " + e.getMessage());
response.put("error", e.toString());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 生成一些有趣的統(tǒng)計數(shù)據(jù)
*/
private Map<String, Object> generateSuggestions(List<User> users) {
Map<String, Object> suggestions = new HashMap<>();
if (users.isEmpty()) {
suggestions.put("note", "Excel文件是空的,或者沒有找到有效數(shù)據(jù)");
return suggestions;
}
// 統(tǒng)計平均年齡
double avgAge = users.stream()
.filter(u -> u.getAge() != null)
.mapToInt(User::getAge)
.average()
.orElse(0);
suggestions.put("averageAge", String.format("%.1f 歲", avgAge));
// 統(tǒng)計最年長和最年輕
users.stream()
.filter(u -> u.getAge() != null)
.max((u1, u2) -> u1.getAge() - u2.getAge())
.ifPresent(oldest ->
suggestions.put("oldest", oldest.getName() + " (" + oldest.getAge() + "歲)"));
// 郵箱域名統(tǒng)計
Map<String, Long> emailDomains = users.stream()
.filter(u -> u.getEmail() != null && u.getEmail().contains("@"))
.map(u -> u.getEmail().split("@")[1])
.collect(Collectors.groupingBy(domain -> domain, Collectors.counting()));
if (!emailDomains.isEmpty()) {
suggestions.put("emailDomains", emailDomains);
}
return suggestions;
}
/**
* 下載Excel模板
* GET /api/excel/template
*/
@GetMapping("/template")
public ResponseEntity<byte[]> downloadTemplate() {
// 這里可以創(chuàng)建一個模板Excel文件并返回
// 為了簡潔,這里省略具體實現(xiàn)
return ResponseEntity.ok().body("請創(chuàng)建自己的模板文件".getBytes());
}
}
第五步:前端HTML頁面——我們的"戰(zhàn)斗指揮臺"
<!DOCTYPE html>
<html>
<head>
<title>Excel文件讀取器 - 怪獸馴服界面</title>
<style>
body {
font-family: 'Arial', sans-serif;
max-width: 800px;
margin: 40px auto;
padding: 20px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.container {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
}
.upload-area {
border: 3px dashed #4CAF50;
border-radius: 10px;
padding: 40px;
text-align: center;
margin: 20px 0;
transition: all 0.3s;
cursor: pointer;
}
.upload-area:hover {
background-color: #f0fff0;
border-color: #45a049;
}
.upload-area.dragover {
background-color: #e8f5e9;
border-color: #2e7d32;
}
#fileInput {
display: none;
}
.btn {
background-color: #4CAF50;
color: white;
padding: 12px 24px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin: 10px;
transition: background-color 0.3s;
}
.btn:hover {
background-color: #45a049;
}
.result {
margin-top: 30px;
padding: 20px;
border-radius: 8px;
background-color: #f8f9fa;
display: none;
}
.result.success {
display: block;
border-left: 5px solid #4CAF50;
}
.result.error {
display: block;
border-left: 5px solid #f44336;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
border: 1px solid #ddd;
padding: 12px;
text-align: left;
}
th {
background-color: #4CAF50;
color: white;
}
tr:nth-child(even) {
background-color: #f2f2f2;
}
.loading {
text-align: center;
padding: 20px;
display: none;
}
.loading.show {
display: block;
}
.stats {
background-color: #e8f5e9;
padding: 15px;
border-radius: 8px;
margin: 15px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>Excel文件讀取器</h1>
<p>上傳你的Excel文件,讓我們一起馴服這個"數(shù)據(jù)怪獸"!</p>
<div class="upload-area" id="dropArea">
<h2>拖放文件到這里</h2>
<p>或者</p>
<button class="btn" onclick="document.getElementById('fileInput').click()">
選擇Excel文件
</button>
<input type="file" id="fileInput" accept=".xlsx,.xls" onchange="handleFileSelect()">
<p style="margin-top: 15px; color: #666;">
支持 .xlsx 和 .xls 格式,請確保第一行是表頭
</p>
</div>
<div class="loading" id="loading">
<h3>正在讀取Excel文件...</h3>
<p>正在與Excel怪獸搏斗中,請稍候!</p>
<div style="margin: 20px;">
<div style="width: 100%; background-color: #ddd; border-radius: 5px;">
<div id="progressBar" style="width: 0%; height: 20px; background-color: #4CAF50; border-radius: 5px; transition: width 0.3s;"></div>
</div>
</div>
</div>
<div class="result" id="result"></div>
<div style="margin-top: 30px; text-align: center;">
<button class="btn" onclick="downloadTemplate()">下載模板文件</button>
<button class="btn" onclick="clearResults()">清除結果</button>
</div>
</div>
<script>
const dropArea = document.getElementById('dropArea');
const fileInput = document.getElementById('fileInput');
const loading = document.getElementById('loading');
const resultDiv = document.getElementById('result');
const progressBar = document.getElementById('progressBar');
// 拖放功能
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, unhighlight, false);
});
function highlight() {
dropArea.classList.add('dragover');
}
function unhighlight() {
dropArea.classList.remove('dragover');
}
dropArea.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
fileInput.files = files;
handleFileSelect();
}
// 處理文件選擇
async function handleFileSelect() {
const file = fileInput.files[0];
if (!file) return;
if (!file.name.match(/\.(xlsx|xls)$/i)) {
showError('請選擇Excel文件 (.xlsx 或 .xls)');
return;
}
// 顯示加載動畫
loading.classList.add('show');
resultDiv.className = 'result';
progressBar.style.width = '30%';
const formData = new FormData();
formData.append('file', file);
try {
progressBar.style.width = '60%';
const response = await fetch('/api/excel/upload', {
method: 'POST',
body: formData
});
progressBar.style.width = '90%';
const data = await response.json();
progressBar.style.width = '100%';
if (data.success) {
showSuccess(data);
} else {
showError(data.message);
}
} catch (error) {
showError('上傳失敗: ' + error.message);
} finally {
setTimeout(() => {
loading.classList.remove('show');
progressBar.style.width = '0%';
}, 500);
}
}
// 顯示成功結果
function showSuccess(data) {
resultDiv.className = 'result success';
let html = `<h3>文件讀取成功!</h3>`;
html += `<p>共讀取 <strong>${data.totalRecords}</strong> 條記錄</p>`;
// 顯示統(tǒng)計信息
if (data.suggestions) {
html += `<div class="stats">`;
html += `<h4>統(tǒng)計信息:</h4>`;
if (data.suggestions.averageAge) {
html += `<p>平均年齡: ${data.suggestions.averageAge}</p>`;
}
if (data.suggestions.oldest) {
html += `<p>最年長: ${data.suggestions.oldest}</p>`;
}
html += `</div>`;
}
// 顯示數(shù)據(jù)表格
if (data.data && data.data.length > 0) {
html += `<h4>數(shù)據(jù)預覽(前10條):</h4>`;
html += `<table>`;
html += `<tr><th>姓名</th><th>年齡</th><th>郵箱</th><th>生日</th><th>工資</th></tr>`;
data.data.slice(0, 10).forEach(user => {
html += `<tr>`;
html += `<td>${user.name || ''}</td>`;
html += `<td>${user.age || ''}</td>`;
html += `<td>${user.email || ''}</td>`;
html += `<td>${user.birthDate || ''}</td>`;
html += `<td>${user.salary || ''}</td>`;
html += `</tr>`;
});
html += `</table>`;
if (data.data.length > 10) {
html += `<p>... 還有 ${data.data.length - 10} 條記錄未顯示</p>`;
}
}
resultDiv.innerHTML = html;
}
// 顯示錯誤
function showError(message) {
resultDiv.className = 'result error';
resultDiv.innerHTML = `
<h3>讀取失敗!</h3>
<p>${message}</p>
<p>請檢查:</p>
<ul>
<li>文件是否為Excel格式</li>
<li>文件是否損壞</li>
<li>文件結構是否符合要求</li>
</ul>
`;
}
// 下載模板
async function downloadTemplate() {
alert('模板下載功能需要后端實現(xiàn)!');
// 實際實現(xiàn)應該調(diào)用后端的模板下載接口
// window.location.href = '/api/excel/template';
}
// 清除結果
function clearResults() {
resultDiv.className = 'result';
resultDiv.innerHTML = '';
fileInput.value = '';
}
</script>
</body>
</html>
第六步:配置文件——設置戰(zhàn)斗參數(shù)
spring:
servlet:
multipart:
max-file-size: 10MB # 最大文件大小
max-request-size: 10MB # 最大請求大小
# 如果你要保存到數(shù)據(jù)庫
datasource:
url: jdbc:mysql://localhost:3306/excel_db
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
# 自定義配置
excel:
upload:
max-size: 10485760 # 10MB
allowed-extensions: .xlsx,.xls
batch:
insert-size: 1000 # 批量插入大小
第七步:高級功能——添加批量處理
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ExcelBatchService {
@Autowired
private UserRepository userRepository;
/**
* 批量保存用戶,提高性能
*/
@Transactional
public void batchSaveUsers(List<User> users, int batchSize) {
List<User> batch = new ArrayList<>(batchSize);
for (int i = 0; i < users.size(); i++) {
batch.add(users.get(i));
if (batch.size() == batchSize || i == users.size() - 1) {
userRepository.saveAll(batch);
batch.clear();
// 顯示進度
System.out.printf("已保存 %d/%d 條記錄 (%.1f%%)%n",
i + 1, users.size(), (i + 1) * 100.0 / users.size());
}
}
}
/**
* 異步處理大文件
*/
@Async
public CompletableFuture<List<User>> processLargeFileAsync(MultipartFile file) {
return CompletableFuture.supplyAsync(() -> {
try {
// 這里處理大文件,可以使用SAX模式讀取
return readLargeExcelFile(file);
} catch (Exception e) {
throw new RuntimeException("處理文件失敗", e);
}
});
}
}
戰(zhàn)斗總結:我們是如何馴服Excel怪獸的?
經(jīng)過這場與Excel怪獸的搏斗,我們學到了不少寶貴經(jīng)驗:
我們的戰(zhàn)利品
- Apache POI:我們的主力武器,專門對付各種Excel格式
- 智能單元格處理:Excel怪獸喜歡把數(shù)據(jù)偽裝成各種類型,我們都能識別
- 批量處理能力:即使面對十萬大軍(數(shù)據(jù)),我們也能有序處理
- 用戶友好界面:讓非技術人員也能輕松使用
注意事項
- 內(nèi)存管理:大文件就像大怪獸,小心它吃光你的內(nèi)存!考慮使用SAX模式
- 數(shù)據(jù)類型:Excel的日期格式是個狡猾的敵人,總想迷惑我們
- 空值處理:Excel怪獸喜歡留空白,我們要妥善處理
- 性能優(yōu)化:批量操作和異步處理是我們的秘密武器
可以繼續(xù)增強的功能
- 數(shù)據(jù)驗證:添加更嚴格的業(yè)務規(guī)則驗證
- 模板驗證:檢查上傳文件是否符合預定模板
- 錯誤報告:生成詳細的錯誤報告,告訴用戶哪里出錯了
- 進度反饋:對于大文件,提供實時進度反饋
- 數(shù)據(jù)轉(zhuǎn)換:更復雜的數(shù)據(jù)轉(zhuǎn)換和清洗邏輯
建議
每個Excel文件都是一個獨特的"怪獸",可能有自己的小脾氣(奇怪的格式、隱藏的工作表等)。我們的代碼要足夠健壯,既能處理規(guī)整的數(shù)據(jù),也能應對意外情況。
現(xiàn)在,當產(chǎn)品經(jīng)理再給你Excel文件時,你可以微笑著說:"放馬過來吧,我有SpringBoot這個神器!"
到此這篇關于SpringBoot讀取Excel文件的完整指南的文章就介紹到這了,更多相關SpringBoot讀取Excel內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
String字符串轉(zhuǎn)BigDecimal時,報NumberFormatException異常的解決
這篇文章主要介紹了String字符串轉(zhuǎn)BigDecimal時,報NumberFormatException異常的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07

