大家好,我是小悟。
前情提要:Excel------那个伪装成表格的数据怪兽
想象一下,你正悠闲地喝着咖啡,产品经理突然拍着你的肩膀说:"嘿,这是客户发来的Excel文件,里面有十万条数据,明天上线前要导入系统哦!"
这时你才发现,你面对的不仅是一个.xlsx文件,而是一个披着网格外衣的"数据怪兽"。它有隐藏的工作表、合并的单元格、还有那些任性格式化的日期字段!但别怕,拿起SpringBoot这把"圣剑",我们一起来驯服这个怪兽!
战斗准备:装备你的SpringBoot武器库
第一步:添加神奇药剂(依赖)
xml
<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>
第二步:创建数据模型------给怪兽分类
typescript
import lombok.Data;
@Data
public class User {
private String name; // 姓名列
private Integer age; // 年龄列
private String email; // 邮箱列
private Date birthDate; // 生日列(Excel最擅长把日期搞乱)
private Double salary; // 工资列(希望这个数字让你开心)
// 可选:给怪兽的数据加个验证
public boolean isValid() {
return name != null && !name.trim().isEmpty()
&& email != null && email.contains("@");
}
}
第三步:创建Excel读取服务------我们的"怪兽翻译官"
java
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) + " 行数据不完整,已跳过");
}
}
}
System.out.println("成功从Excel怪兽手中解救了 " + userList.size() + " 个用户!");
return userList;
}
/**
* 把一行数据转换成User对象
* 注意:这个方法要和Excel的结构对应上!
*/
private User convertRowToUser(Row row) {
User user = new User();
try {
// 第一列:姓名(字符串)
Cell nameCell = row.getCell(0);
if (nameCell != null) {
user.setName(getCellValueAsString(nameCell));
}
// 第二列:年龄(数字)
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());
}
}
// 第五列:工资(数字)
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怪兽喜欢把数据打扮成各种类型
*/
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 {
// 避免科学计数法,也避免不必要的.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;
}
}
第四步:创建控制器------与前端通信的"传令兵"
typescript
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 {
// 调用服务读取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));
// 这里可以添加数据库保存逻辑
// 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);
}
}
/**
* 生成一些有趣的统计数据
*/
private Map<String, Object> generateSuggestions(List<User> users) {
Map<String, Object> suggestions = new HashMap<>();
if (users.isEmpty()) {
suggestions.put("note", "Excel文件是空的,或者没有找到有效数据");
return suggestions;
}
// 统计平均年龄
double avgAge = users.stream()
.filter(u -> u.getAge() != null)
.mapToInt(User::getAge)
.average()
.orElse(0);
suggestions.put("averageAge", String.format("%.1f 岁", avgAge));
// 统计最年长和最年轻
users.stream()
.filter(u -> u.getAge() != null)
.max((u1, u2) -> u1.getAge() - u2.getAge())
.ifPresent(oldest ->
suggestions.put("oldest", oldest.getName() + " (" + oldest.getAge() + "岁)"));
// 邮箱域名统计
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() {
// 这里可以创建一个模板Excel文件并返回
// 为了简洁,这里省略具体实现
return ResponseEntity.ok().body("请创建自己的模板文件".getBytes());
}
}
第五步:前端HTML页面------我们的"战斗指挥台"
xml
<!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文件,让我们一起驯服这个"数据怪兽"!</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>`;
// 显示统计信息
if (data.suggestions) {
html += `<div class="stats">`;
html += `<h4>统计信息:</h4>`;
if (data.suggestions.averageAge) {
html += `<p>平均年龄: ${data.suggestions.averageAge}</p>`;
}
if (data.suggestions.oldest) {
html += `<p>最年长: ${data.suggestions.oldest}</p>`;
}
html += `</div>`;
}
// 显示数据表格
if (data.data && data.data.length > 0) {
html += `<h4>数据预览(前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('模板下载功能需要后端实现!');
// 实际实现应该调用后端的模板下载接口
// window.location.href = '/api/excel/template';
}
// 清除结果
function clearResults() {
resultDiv.className = 'result';
resultDiv.innerHTML = '';
fileInput.value = '';
}
</script>
</body>
</html>
第六步:配置文件------设置战斗参数
yaml
spring:
servlet:
multipart:
max-file-size: 10MB # 最大文件大小
max-request-size: 10MB # 最大请求大小
# 如果你要保存到数据库
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 # 批量插入大小
第七步:高级功能------添加批量处理
scss
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);
}
});
}
}
战斗总结:我们是如何驯服Excel怪兽的?
经过这场与Excel怪兽的搏斗,我们学到了不少宝贵经验:
我们的战利品:
- Apache POI:我们的主力武器,专门对付各种Excel格式
- 智能单元格处理:Excel怪兽喜欢把数据伪装成各种类型,我们都能识别
- 批量处理能力:即使面对十万大军(数据),我们也能有序处理
- 用户友好界面:让非技术人员也能轻松使用
注意事项:
- 内存管理:大文件就像大怪兽,小心它吃光你的内存!考虑使用SAX模式
- 数据类型:Excel的日期格式是个狡猾的敌人,总想迷惑我们
- 空值处理:Excel怪兽喜欢留空白,我们要妥善处理
- 性能优化:批量操作和异步处理是我们的秘密武器
可以继续增强的功能:
- 数据验证:添加更严格的业务规则验证
- 模板验证:检查上传文件是否符合预定模板
- 错误报告:生成详细的错误报告,告诉用户哪里出错了
- 进度反馈:对于大文件,提供实时进度反馈
- 数据转换:更复杂的数据转换和清洗逻辑
建议:
每个Excel文件都是一个独特的"怪兽",可能有自己的小脾气(奇怪的格式、隐藏的工作表等)。我们的代码要足够健壮,既能处理规整的数据,也能应对意外情况。
现在,当产品经理再给你Excel文件时,你可以微笑着说:"放马过来吧,我有SpringBoot这个神器!"

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海