SpringBoot读取Excel文件,一场与“表格怪兽”的搏斗记

大家好,我是小悟。

前情提要: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怪兽的搏斗,我们学到了不少宝贵经验:

我们的战利品:

  1. Apache POI:我们的主力武器,专门对付各种Excel格式
  2. 智能单元格处理:Excel怪兽喜欢把数据伪装成各种类型,我们都能识别
  3. 批量处理能力:即使面对十万大军(数据),我们也能有序处理
  4. 用户友好界面:让非技术人员也能轻松使用

注意事项:

  1. 内存管理:大文件就像大怪兽,小心它吃光你的内存!考虑使用SAX模式
  2. 数据类型:Excel的日期格式是个狡猾的敌人,总想迷惑我们
  3. 空值处理:Excel怪兽喜欢留空白,我们要妥善处理
  4. 性能优化:批量操作和异步处理是我们的秘密武器

可以继续增强的功能:

  1. 数据验证:添加更严格的业务规则验证
  2. 模板验证:检查上传文件是否符合预定模板
  3. 错误报告:生成详细的错误报告,告诉用户哪里出错了
  4. 进度反馈:对于大文件,提供实时进度反馈
  5. 数据转换:更复杂的数据转换和清洗逻辑

建议:

每个Excel文件都是一个独特的"怪兽",可能有自己的小脾气(奇怪的格式、隐藏的工作表等)。我们的代码要足够健壮,既能处理规整的数据,也能应对意外情况。

现在,当产品经理再给你Excel文件时,你可以微笑着说:"放马过来吧,我有SpringBoot这个神器!"

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。

您的一键三连,是我更新的最大动力,谢谢

山水有相逢,来日皆可期,谢谢阅读,我们再会

我手中的金箍棒,上能通天,下能探海

相关推荐
SimonKing2 小时前
支付宝H5支付接入实战:Java一站式解决方案
java·后端·程序员
摇滚侠2 小时前
Java 零基础全套视频教程,日期时间 API,笔记147-148
java·开发语言·笔记
不惑_2 小时前
Windows安装Java
java·开发语言·windows
程序员侠客行2 小时前
Mybatis的Executor和缓存体系
java·后端·架构·mybatis
毕设源码-赖学姐2 小时前
【开题答辩全过程】以 基于Java的化学实验室信息管理系统为例,包含答辩的问题和答案
java·开发语言
带刺的坐椅2 小时前
通用流程编排框架,Solon Flow v3.8.0 隆重发布
java·solon·flowable·flow·drools
小王师傅662 小时前
【轻松入门SpringBoot】actuator健康检查(中)
java·spring boot·spring
咕噜咕噜啦啦2 小时前
Java速通(应用程序)
java·开发语言
爱学习的小可爱卢2 小时前
JavaEE进阶——Spring Bean与Java Bean的核心区别
java·后端·java-ee