开发者

SpringBoot中Excel处理完全指南分享

目录
  • SpringBoot中Excel处理指南
    • 1. Excel处理基础知识
      • 1.1 为什么需要在应用中处理Excel文件?
      • 1.2 Java中的Excel处理库介绍
      • 1.3 Spring Boot中集成Excel处理
    • 2. 在Spring Boot中集成Excel处理库
      • 2.1 集成Apache POI
      • 2.2 集成EasyExcel
    • 3. 使用Apache POI读取Excel文件
      • 3.1 创建数据模型
      • 3.2 创建Excel读取服务
      • 3.3 创建Controller处理Excel上传
      • 3.4 创建html上传页面
      • 3.5 处理更复杂的Excel结构
    • 4. 使用Apache POI创建和导出Excel文件
      • 4.1 创建基本Excel文件
      • 4.2 创建导出控制器
      • 4.3 创建导出页面
      • 4.4 创建复杂的Excel文件
      • 4.5 使用模板导出Excel
    • 5. 使用EasyExcel处理Excel文件
      • 5.1 使用EasyExcel读取Excel
      • 5.2 使用EasyExcel导出Excel
    • 6. 处理大型Excel文件的策略
      • 6.1 使用Apache POI SXSSF模式
      • 6.2 使用EasyExcel处理大文件
      • 6.3 使用CSV代替Excel
      • 6.4 分页导出大型数据集
    • 7. 实际应用场景和最佳实践
      • 7.1 动态列导出
      • 7.2 Excel模板填充
      • 7.3 Excel文件校验
      • 7.4 统一异常处理
    • 8. 性能优化和注意事项
      • 8.1 性能优化建议
      • 8.2 注意事项
  • 总结

    SpringBoot中Excel处理指南

    1. Excel处理基础知识

    1.1 为什么需要在应用中处理Excel文件?

    在企业应用开发中,Excel文件处理是一个非常常见的需求,主要用于以下场景:

    • 数据导入:允许用户通过Excel上传批量数据到系统
    • 数据导出:将系统数据导出为Excel供用户下载分析
    • 报表生成:生成复杂的报表并格式化为Excel
    • 数据交换:作为不同系统间交换数据的媒介
    • 批量数据处理:处理大量结构化数据

    1.2 Java中的Excel处理库介绍

    Java中处理Excel文件的主要库有以下几种:

    1.2.1 Apache POI

    Apache POI是Java中使用最广泛的Excel处理库,提供了全面的API来创建、读取和修改Office文档。

    优点

    • 功能全面,支持Excel所有功能
    • 支持.xls (HSSF - Excel 97-2003)和.xlsx (XSSF - Excel 2007+)格式
    • 社区活跃,文档丰富
    • 支持公式计算、图表、合并单元格等高级功能

    缺点

    • API相对复杂
    • 处理大文件时内存消耗大(尤其是XSSF)
    1.2.2 EasyExcel

    EasyExcel是阿里巴巴开源的Excel处理库,基于POI,但做了大量优化。

    优点

    • 内存占用低,使用SAX模式读取,避免OOM
    • API简单易用,注解驱动
    • 读写速度快
    • 适合处理大型Excel文件

    缺点

    • 功能不如POI全面
    • 灵活性相对较低
    1.2.3 JExcel

    JExcel是另一个处理Excel的Java库。

    优点

    • API较简单
    • 速度较快

    缺点

    • 仅支持旧版Excel (.xls)格式
    • 不再积极维护
    • 功能有限
    1.2.4 Apache POI SXSSF

    SXSSF是POI提供的一种流式处理模式,专为处理大型Excel文件设计。

    优点

    • 大大降低内存占用
    • 适合生成大型Excel文件

    缺点

    • 仅支持写入操作,不支持读取
    • 功能比XSSF受限

    1.3 Spring Boot中集成Excel处理

    Spring Boot本身不提供Excel处理功能,但可以轻松集成上述各种Excel处理库。本指南将主要介绍:

    1. 如何在Spring Boot项目中集成Apache POI和EasyExcel
    2. 如何实现Excel导入导出的常见功能
    3. 如何处理常见问题和优化性能

    2. 在Spring Boot中集成Excel处理库

    2.1 集成Apache POI

    2.1.1 添加依赖

    pom.XML文件中添加以下依赖:

    <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>

    如果使用Gradle,在build.gradle中添加:

    implementation 'org.apache.poi:poi:5.2.3'
    implementation 'org.apache.poi:poi-ooxml:5.2.3'
    2.1.2 创建基本配置类

    创建一个配置类来处理Excel相关的配置:

    package com.example.excel.config;
    
    import org.apache.poi.ss.usermodel.WorkbookFactory;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.multipart.MultipartResolver;
    import org.springframework.web.multipart.commons.CommonsMultipartResolver;
    
    @Configuration
    public class ExcelConfig {
        
        @Bean
        public MultipartResolver multipartResolver() {
            CommonsMultipartResolver resolver = new CommonsMultipartResolver();
            resolver.setMaxUploadSize(10485760); // 设置上传文件最大为10MB
            return resolver;
        }
    }

    2.2 集成EasyExcel

    2.2.1 添加依赖

    pom.xml文件中添加以下依赖:

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>easyexcel</artifactId>
        <version>3.2.1</version>
    </dependency>

    如果使用Gradle,在build.gradle中添加:

    implementation 'com.alibaba:easyexcel:3.2.1'
    2.2.2 创建配置类
    package com.example.excel.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.multipart.MultipartResolver;
    import org.springframework.web.multipart.commons.CommonsMultipartResolver;
    
    @Configuration
    public class EasyExcelConfig {
        
        @Bean
        public MultipartResolver multipartResolver() {
            CommonsMultipartResolver resolver = new CommonsMultipartResolver();
            resolver.setMaxUploadSize(10485760); // 设置上传文件最大为10MB
            return resolver;
        }
    }

    3. 使用Apache POI读取Excel文件

    3.1 创建数据模型

    首先,创建一个模型类来映射Excel中的数据:

    package com.example.excel.model;
    
    import lombok.Data;
    
    @Data
    public class User {
        private Long id;
        private String name;
        private Integer age;
        private String email;
        private String department;
    }

    3.2 创建Excel读取服务

    创建一个服务类来处理Excel文件读取:

    package com.example.excel.service;
    
    import com.example.excel.model.User;
    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.IOException;
    import java.io.InputStream;
    import java.util.ArrayList;
    import java.util.Iterator;
    import java.util.List;
    
    @Service
    public class ExcelService {
    
        public List<User> readUsersFromExcel(MultipartFile file) throws IOException {
            List<User> userList = new ArrayList<>();
            
            // 获取工作簿
            try (InputStream inputStream = file.getInputStream()) {
                Workbook workbook = WorkbookFactory.create(inputStream);
                
                // 获取第一个工作表
                Sheet sheet = workbook.getSheetAt(0);
                
                // 跳过标题行
                Iterator<Row> rowIterator = sheet.rowIterator();
                if (rowIterator.hasNext()) {
                    rowIterator.next(); // 跳过标题行
                }
                
                // 遍历数据行
                while (rowIterator.hasNext()) {
                    Row row = rowIterator.next();
                    User user = new User();
                    
                    // 读取单元格数据
                    user.setId((long) row.getCell(0, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK).getNumericCellValue());
                    user.setName(getCellValueAsString(row.getCell(1)));
                    user.setAge((int) row.getCell(2, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK).getNumericCellValue());
                    user.setEmail(getCellValueAsString(row.getCell(3)));
                    user.setDepartment(getCellValueAsString(row.getCell(4)));
                    
                    userList.add(user);
                }
                
                workbook.close();
            }
            
            return userList;
        }
        
        // 获取单元格的字符串值
        private String getCellValueAsString(Cell cell) {
            if (cell == null) {
                return "";
            }
            
            switch (cell.getCellType()) {
                case STRING:
                    return cell.getStringCellValue();
                case NUMERIC:
                    if (DateUtil.isCellDateFormatted(cell)) {
                        return cell.getDateCellValue().toString();
                    } else {
                        return String.valueOf((int) cell.getNumericCellValue());
                    }
                case BOOLEAN:
                    return String.valueOf(cell.getBooleanCellValue());
                case FORMULA:
                    return cell.getCellFormula();
                default:
                    return "";
            }
        }
    }

    3.3 创建Controller处理Excel上传

    创建一个Controller来处理Excel文件上传:

    package com.example.excel.controller;
    
    import com.example.excel.model.User;
    import com.example.excel.service.ExcelService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.multipart.MultipartFile;
    
    import java.io.IOException;
    import java.util.List;
    
    @RestController
    @RequestMapping("/api/excel")
    public class ExcelController {
    
        @Autowired
        private ExcelService excelService;
    
        @PostMapping("/upload")
        public ResponseEntity<List<User>> uploadExcel(@RequestParam("file") MultipartFile file) {
            try {
                List<User> users = excelService.readUsersFromExcel(file);
                return ResponseEntity.ok(users);
            } catch (IOException e) {
                e.printStackTrace();
                return ResponseEntity.badRequest().build();
            }
        }
    }

    3.4 创建HTML上传页面

    src/main/resources/templates目录下创建upload.html

    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Excel上传</title>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="external nofollow"  rel="external nofollow" >
    </head>
    <body>
        <div class="container mt-5">
            <div class="card">
                <div class="card-header">
                    <h3>上传Excel文件</h3>
                </div>
                <div cljavascriptass="card-body">
                    <form id="uploadForm" enctype="multipart/form-data">
                        <div class="form-group">
                            <label for="file">选择Excel文件:</label>
                            <input type="file" class="form-control-file" id="file" name="file" accept=".xls,.xlsx">
                        </div>
                        <button type="button" class="btn btn-primary" onclick="uploadExcel()">上传</button>
                    </form>
                    
                    <div class="mt-4">
                        <h4>上传结果:</h4>
                        <div id="resultContainer"></div>
                    </div>
                </div>
            </div>
        </div>
        
        <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
        <script>
            function uploadExcel() {
                var formData = new FormData(document.getElementById('uploadForm'));
                
                $.AJAX({
                    url: '/api/excel/upload',
                    type: 'POST',
                    data: formData,
                    processData: false,
                    contentType: false,
                    success: function(response) {
                        var resultHtml = '<table class="table table-striped">' +
                                        '<thead><tr><th>ID</th><th>姓名</th><th>年龄</th><th>邮箱</th><th>部门</th></tr></thead>' +
                                        '<tbody>';
                        
                        for (var i = 0; i < response.length; i++) {
                            var user = response[i];
                            resultHtml += '<tr>' +
                                         '<td>' + user.id + '</td>' +
                                         '<td>' + user.name + '</td>' +
                                         '<td>' + user.age + '</td>' +
                                         '<td>' + user.email + '</td>' +
                                         '<td>' + user.department + '</td>' +
                                         '</tr>';
                        }
                        
                        resultHtml += '</tbody></table>';
                        
                        $('#resultContainer').html(resultHtml);
                    },
                    error: function(error) {
                        $('#resultContainer').html('<div class="alert alert-danger">上传失败: ' + error.responseText + '</div>');
                    }
                });
            }
        </script>
    </body>
    </html>

    3.5 处理更复杂的Excel结构

    在实际应用中,Excel结构可能更复杂,如多个工作表、合并单元格、公式等。以下是处理这些情况的示例:

    public List<Department> readComplexExcel(MultipartFile file) throws IOException {
        List<Department> departments = new ArrayList<>();
        
        try (InputStream inputStream = file.getInputStream()) {
            Workbook workbook = WorkbookFactory.create(inputStream);
            
            // 读取部门信息(第一个工作表)
            Sheet departmentSheet = workbook.getSheetAt(0);
            for (int i = 1; i <= departmentSheet.getLastRowNum(); i++) {
                Row row = departmentSheet.getRow(i);
                if (row == null) continue;
                
                Department department = new Department();
                department.setId((long) row.getCell(0).getNumericCellValue());
                department.setName(row.getCell(1).getStringCellValue());
                department.setManager(row.getCell(2).getStringCellValue());
                department.setEmployees(new ArrayList<>());
                
                departments.add(department);
            }
            
            // 读取员工信息(第二个工作表)
            Sheet employeeSheet = workbook.getSheetAt(1);
            for (int i = 1; i <= employeeSheet.getLastRowNum(); i++) {
                Row row = employeeSheet.getRow(i);
                if (row == null) continue;
                
                User employee = new User();
                employee.setId((long) row.getCell(0).getNumericCellValue());
                employee.setName(row.getCell(1).getStringCellValue());
                employee.setAge((int) row.getCell(2).getNumericCellValue());
                employee.setEmail(row.getCell(3).getStringCellValue());
                
                // 获取部门ID并关联到相应部门
                long departmentId = (long) row.getCell(4).getNumericCellValue();
                for (Department dept : departments) {
                    if (dept.getId() == departmentId) {
                        dept.getEmployees().add(employee);
                        break;
                    }
                }
            }
            
            workbook.close();
        }
        
        return departments;
    }

    4. 使用Apache POI创建和导出Excel文件

    4.1 创建基本Excel文件

    以下是一个创建简单Excel文件的示例:

    package com.example.excel.service;
    
    import com.example.excel.model.User;
    import org.apache.poi.ss.usermodel.*;
    import org.apache.poi.xssf.usermodel.XSSFWorkbook;
    import org.springframework.stereotype.Service;
    
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.util.List;
    
    @Service
    public class ExcelExportService {
    
        public ByteArrayInputStream exportUsersToExcel(List<User> users) throws IOException {
            try (Workbook workbook = new XSSFWorkbook()) {
                // 创建工作表
                Sheet sheet = workbook.createSheet("用户数据");
                
                // 创建表头样式
                Font headerFont = workbook.createFont();
                headerFont.setBold(true);
                headerFont.setColor(IndexedColors.BLUE.getIndex());
                
                CellStyle headerCellStyle = workbook.createCellStyle();
                headerCellStyle.setFont(headerFont);
                headerCellStyle.setFillForegroundColor(IndexedColors.LIGHT_YELLOW.getIndex());
                headerCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
                headerCellStyle.setBorderBottom(BorderStyle.THIN);
                headerCellStyle.setBorderTop(BorderStyle.THIN);
                headerCellStyle.setBorderRight(BorderStyle.THIN);
                headerCellStyle.setBorderLeft(BorderStyle.THIN);
                
                // 创建表头行
                Row headerRow = sheet.createRow(0);
                
                // 创建表头单元格
                Cell cell0 = headerRow.createCell(0);
                cell0.setCellValue("ID");
                cell0.setCellStyle(headerCellStyle);
                
                Cell cell1 = headerRow.createCell(1);
                cell1.setCellValue("姓名");
                cell1.setCellStyle(headerCellStyle);
                
                Cell cell2 = headerRow.createCell(2);
                cell2.setCellValue("年龄");
                cell2.setCellStyle(headerCellStyle);
                
                Cell cell3 = headerRow.createCell(3);
                cell3.setCellValue("邮箱");
                cell3.setCellStyle(headerCellStyle);
                
                Cell cell4 = headerRow.createCell(4);
                cell4.setCellValue("部门");
                cell4.setCellStyle(headerCellStyle);
                
                // 设置数据单元格样式
                CellStyle dataCellStyle = workbook.createCellStyle();
                dataCellStyle.setBorderBottom(BorderStyle.THIN);
                dataCellStyle.setBorderTop(BorderStyle.THIN);
                dataCellStyle.setBorderRight(BorderStyle.THIN);
                dataCellStyle.setBorderLeft(BorderStyle.THIN);
                
                // 创建数据行
                int rowIdx = 1;
                for (User user : users) {
                    Row row = sheet.createRow(rowIdx++);
                    
                    Cell idCell = row.createCell(0);
                    idCell.setCellValue(user.getId());
                    idCell.setCellStyle(dataCellStyle);
                    
                    Cell nameCell = row.createCell(1);
                    nameCell.setCellValue(user.getName());
                    nameCell.setCellStyle(dataCellStyle);
                    
                    Cell ageCell = row.createCell(2);
                    ageCell.setCellValue(user.getAge());
                    ageCell.setCellStyle(dataCellStyle);
                    
                    Cell emailCell = row.createCell(3);
                    emailCell.setCellValue(user.getEmail());
                    emailCell.setCellStyle(dataCellStyle);
                    
                    Cell deptCell = row.createCell(4);
                    deptCell.setCellValue(user.getDepartment());
                    deptCell.setCellStyle(dataCellStyle);
                }
                
                // 自动调整列宽
                for (int i = 0; i < 5; i++) {
                    sheet.autoSizeColumn(i);
                }
                
                // 写入ByteArrayOutputStream
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                workbook.write(outputStream);
                
                return new ByteArrayInputStream(outputStream.toByteArray());
            }
        }
    }

    4.2 创建导出控制器

    package com.example.excel.controller;
    
    import com.example.excel.model.User;
    import com.example.excel.service.ExcelExportService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.core.io.InputStreamResource;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.MediaType;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.io.ByteArrayInputStream;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.List;
    
    @RestController
    @RequestMapping("/api/excel")
    public class ExcelExportController {
    
        @Autowired
        private ExcelExportService excelExportService;
    
        @GetMapping("/export")
        public ResponseEntity<InputStreamResource> exportUsers() throws IOException {
            // 生成示例数据
            List<User> users = getTestUsers();
            
            // 生成Excel文件
            ByteArrayInputStream in = excelExportService.exportUsersToExcel(users);
            
            // 设置HTTP头
            HttpHeaders headers = new HttpHeaders();
            headers.add("Content-Disposition", "attachment; filename=users.xlsx");
            
            // 返回Excel文件
            return ResponseEntity
                    .ok()
                    .headers(headers)
                    .contentType(MediaType.parseMediaType("application/vnd.ms-excel"))
                    .body(new InputStreamResource(in));
        }
        
        // 生成测试用户数据
        private List<User> getTestUsers() {
            List<User> users = new ArrayList<>();
            
            User user1 = new User();
            user1.setId(1L);
            user1.setName("张三");
            user1.setAge(28);
            user1.setEmail("zhangsan@example.com");
            user1.setDepartment("研发部");
            users.add(user1);
            
            User user2 = new User();
            user2.setId(2L);
            user2.setName("李四");
            user2.setAge(32);
            user2.setEmail("lisi@example.com");
            user2.setDepartment("市场部");
            users.add(user2);
            
            User user3 = new User();
            user3.setId(3L);
            user3.setName("王五");
            user3.setAge(45);
            user3.setEmail("wangwu@example.com");
            user3.setDepartment("行政部");
            users.add(user3);
            
            User user4 = new User();
            user4.setId(4L);
            user4.setName("赵六");
            user4.setAge(36);
            user4.setEmail("zhaoliu@example.com");
            user4.setDepartment("财务部");
            users.add(user4);
            
            User user5 = new User();
            user5.setId(5L);
            user5.setName("钱七");
            user5.setAge(29);
            user5.setEmail("qianqi@example.com");
            user5.setDepartment("人力资源部");
            users.add(user5);
            
            return users;
        }
    }

    4.3 创建导出页面

    src/main/resources/templates目录下创建export.html

    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Excel导出</title>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="external nofollow"  rel="external nofollow" >
    </head>
    <body>
        <div class="container mt-5">
            <div class="card">
                <div class="card-header">
                    <h3>导出Excel文件</h3>
                </div>
                <div class="card-body">
                    <p>点击下面的按钮导出用户数据到Excel文件:</p>
                    <a href="/api/excel/export" rel="external nofollow"  class="btn btn-primary">导出用户数据</a>
                </div>
            </div>
        </div>
    </body>
    </html>

    4.4 创建复杂的Excel文件

    以下是一个创建更复杂Excel文件的示例,包含多个工作表、合并单元格、公式等:

    public ByteArrayInputStream exportComplexExcel(List<Department> departments) throws IOException {
        try (Workbook workbook = new XSSFWorkbook()) {
            // 创建字体和样式
            Font headerFont = workbook.createFont();
            headerFont.setBold(true);
            headerFont.setFontHeightInPoints((short) 14);
            
            CellStyle headerStyle = workbook.createCellStyle();
            headerStyle.setFont(headerFont);
            headerStyle.setFillForegroundColor(IndexedColors.LIGHT_BLUE.getIndex());
            headerStyle.setFillPattern(FillPatternType.SOLID_phpFOREGROUND);
            headerStyle.setAlignment(HorizontalAlignment.CENTER);
            
            CellStyle titleStyle = workbook.createCellStyle();
            Font titleFont = workbook.createFont();
            titleFont.setBold(true);
            titleFont.setFontHeightInPoints((short) 16);
            titleStyle.setFont(titleFont);
            titleStyle.setAlignment(HorizontalAlignment.CENTER);
            
            // 创建汇总表
            Sheet summarySheet = workbook.createSheet("部门汇总");
            
            // 创建标题行
            Row titleRow = summarySheet.createRow(0);
            Cell titleCell = titleRow.createCell(0);
            titleCell.setCellValue("公司部门人员统计");
            titleCell.setCellStyle(titleStyle);
            
            // 合并标题单元格
            summarySheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 3));
            
            // 创建表头
            Row headerRow = summarySheet.createRow(1);
            String[] headers = {"部门ID", "部门名称", "部门经理", "员工数量"};
            
            for (int i = 0; i < headers.length; i++) {
                Cell cell = headerRow.createCell(i);
                cell.setCellValue(headers[i]);
                cell.setCellStyle(headerStyle);
            }
            
            // 填充部门数据
            int rowIdx = 2;
            int totalEmployees = 0;
            
            for (Department dept : departments) {
                Row row = summarySheet.createRow(rowIdx++);
                
                row.createCell(0).setCellValue(dept.getId());
                row.createCell(1).setCellValue(dept.getName());
                row.createCell(2).setCellValue(dept.getManager());
                row.createCell(3).setCellValue(dept.getEmployees().size());
                
                totalEmployees += dept.getEmployees().size();
                
                // 为每个部门创建单独的工作表
                Sheet deptSheet = workbook.createSheet(dept.getName());
                
                // 创建部门表头
                Row deptHeaderRow = deptSheet.createRow(0);
                Cell deptTitleCell = deptHeaderRow.createCell(0);
                deptTitleCell.setCellValue(dept.getName() + " - 员工列表");
                deptTitleCell.setCellStyle(titleStyle);
                deptSheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 4));
                
                // 员工表头
                Row empHeaderRow = deptSheet.createRow(1);
                String[] empHeaders = {"员工ID", "姓名", "年龄", "邮箱", "入职年限"};
                
                for (int i = 0; i < empHeaders.length; i++) {
                    Cell cell = empHeaderRow.createCell(i);
                    cell.setCellValue(empHeaders[i]);
                    cell.setCellStyle(headerStyle);
                }
                
                // 填充员工数据
                int empRowIdx = 2;
                for (User emp : dept.getEmployees()) {
                    Row empRow = deptSheet.createRow(empRowIdx++);
                    
                    empRow.createCell(0).setCellValue(emp.getId());
                    empRow.createCell(1).setCellValue(emp.getName());
                    empRow.createCell(2).setCellValue(emp.getAge());
                    empRow.createCell(3).setCellValue(emp.getEmail());
                    
                    // 使用公式计算入职年限(假设年龄减去25)
                    Cell tenureCell = empRow.createCell(4);
                    tenureCell.setCellFormula("C" + empRowIdx + "-25");
                }
                
                // 自动调整列宽
                for (int i = 0; i < 5; i++) {
                    deptSheet.autoSizeColumn(i);
                }
            }
            
            // 创建总计行
            Row totalRow = summarySheet.createRow(rowIdx);
            Cell totalLabelCell = totalRow.createCell(0);
            totalLabelCell.setCellValue("总计");
            totalLabelCell.setCellStyle(headerStyle);
            
            // 合并总计标签单元格
            summarySheet.addMergedRegion(new CellRangeAddress(rowIdx, rowIdx, 0, 2));
            
            Cell totalValueCell = totalRow.createCell(3);
            totalValueCell.setCellValue(totalEmployees);
            totalValueCell.setCellStyle(headerStyle);
            
            // 自动调整列宽
            for (int i = 0; i < 4; i++) {
                summarySheet.autoSizeColumn(i);
            }
            
            // 添加图表
            XSSFSheet chartSheet = (XSSFSheet) workbook.createSheet("部门统计图");
            
            // 复制部门数据到图表数据表
            Row chartHeaderRow = chartSheet.createRow(0);
            chartHeaderRow.createCell(0).setCellValue("部门");
            chartHeaderRow.createCell(1).setCellValue("员工数");
            
            int chartRowIdx = 1;
            for (Department dept : departments) {
                Row row = chartSheet.createRow(chartRowIdx++);
                row.createCell(0).setCellValue(dept.getName());
                row.createCell(1).setCellValue(dept.getEmployees().size());
            }
            
            // 创建图表和数据序列
            XSSFDrawing drawing = chartSheet.createDrawingPatriarch();
            XSSFClientAnchor anchor = drawing.createAnchor(0, 0, 0, 0, 4, 0, 15, 15);
            
            XSSFChart chart = drawing.createChart(anchor);
            chart.setTitleText("部门人员分布");
            chart.setTitleOverlay(false);
            
            XDDFChartLegend legend = chart.getOrAddLegend();
            legend.setPosition(LegendPosition.RIGHT);
            
            // X轴和Y轴
            XDDFCategoryAxis bottomAxis = chart.createCategoryAxis(AxisPosition.BOTTOM);
            bottomAxis.setTitle("部门");
            XDDFValueAxis leftAxis = chart.createValueAxis(AxisPosition.LEFT);
            leftAxis.setTitle("员工数");
            
            // 创建数据源
            XDDFDataSource<String> departments = XDDFDataSourcesFactory.fromStringCellRange(
                    chartSheet, new CellRangeAddress(1, chartRowIdx - 1, 0, 0));
            
            XDDFNumericalDataSource<Double> values = XDDFDataSourcesFactory.fromNumericCellRange(
                    chartSheet, new CellRangeAddress(1, chartRowIdx - 1, 1, 1));
            
            // 创建柱状图
            XDDFBarChartData barChart = (XDDFBarChartData) chart.createData(
                    ChartTypes.BAR, bottomAxis, leftAxis);
            barChart.setVaryColors(true);
            
            XDDFBarChartData.Series series = (XDDFBarChartData.Series) barChart.addSeries(departments, values);
            series.setTitle("员工数", null);
            
            chart.plot(barChart);
            
            // 写入ByteArrayOutputStream
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            workbook.write(outputStream);
            
            return new ByteArrayInputStream(outputStream.toByteArray());
        }
    }

    注意:上面的图表代码需要添加以下依赖:

    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml-full</artifactId>
        <version>5.2.3</version>
    </dependency>

    4.5 使用模板导出Excel

    在某些场景下,我们需要基于预定义的Excel模板生成文件,以下是一个示例:

    public ByteArrayInputStream exportFromTemplate(List<User> users) throws IOException {
        // 加载模板文件
        try (InputStream templateStream = getClass().getResourceAsStream("/templates/user_template.xlsx");
             Workbook workbook = WorkbookFactory.create(templateStream)) {
            
            Sheet sheet = workbook.getSheetAt(0);
            
            // 从第二行开始填充数据(第一行是表头)
            int rowIdx = 1;
            for (User user : users) {
                Row row = sheet.createRow(rowIdx++);
                
                row.createCell(0).setCellValue(user.getId());
                row.createCell(1).setCellValue(user.getName());
                row.createCell(2).setCellValue(user.getAge());
                row.createCell(3).setCellValue(user.getEmail());
                row.createCell(4).setCellValue(user.getDepartment());
            }
            
            // 更新模板中的日期单元格(假设在A1位置)
            Row headerRow = sheet.getRow(0);
            if (headerRow.getCell(6) != null) {
                Cell dateCell = headerRow.getCell(6);
                dateCell.setCellValue(new Date());
            }
            
            // 自动调整列宽
            for (int i = 0; i < 5; i++) {
                sheet.autoSizeColumn(i);
            }
            
            // 写入ByteArrayOutputStream
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            workbook.write(outputStream);
            
            return new ByteArrayInputStream(outputStream.toByteArray());
        }
    }

    5. 使用EasyExcel处理Excel文件

    EasyExcel是阿里巴巴开源的基于POI的Excel处理工具,相比原生POI,它提供了更简洁的API,并且在处理大文件时有明显的性能优势。

    5.1 使用EasyExcel读取Excel

    5.1.1 创建数据模型

    使用EasyExcel时,通常使用注解来映射Excel列:

    package com.example.excel.model;
    
    import com.alibaba.excel.annotation.ExcelProperty;
    import com.alibaba.excel.annotation.format.DateTimeFormat;
    import lombok.Data;
    
    import java.util.Date;
    
    @Data
    public class Employee {
        
        @ExcelProperty("员工ID")
        private Long id;
        
        @ExcelProperty("姓名")
        private String name;
        
        @ExcelProperty("年龄")
        private Integer age;
        
        @ExcelProperty("邮箱")
        private String email;
        
        @ExcelProperty("部门")
        private String department;
        
        @ExcelProperty("入职日期")
        @DateTimeFormat("yyyy-MM-dd")
        private Date hireDate;
        
        @ExcelProperty("薪资")
        private Double salary;
    }
    5.1.2 创建读取监听器

    EasyExcel采用事件模式读取Excel,需要创建一个监听器来处理读取的数据:

    package com.example.excel.listener;
    
    import com.alibaba.excel.context.AnalysisContext;
    import com.alibaba.excel.event.AnalysisEventListener;
    import com.example.excel.model.Employee;
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.ArrayList;
    import java.util.List;
    
    @Slf4j
    public class EmployeeReadListener extends AnalysisEventListener<Employee> {
    
        /**
         * 用于暂存读取的数据
         */
        private List<Employee> employeeList = new ArrayList<>();
        
        /**
         * 每读取一行数据就会调用一次invoke方法
         */
        @Override
        public void invoke(Employee employee, AnalysisContext context) {
            log.info("读取到一条数据: {}", employee);
            employeeList.add(employee);
            
            // 达到BATCH_COUNT时,需要存储一次数据库,防止数据几万条数据在内存,容易OOM
            if (employeeList.size() >= 5000) {
                saveData();
                // 清理内存
                employeeList.clear();
            }
        }
        
        /**
         * 所有数据解析完成后调用此方法
         */
        @Override
        public void doAfterAllAnalysed(AnalysisContext context) {
            // 确保最后一批数据被保存
            saveData();
            log.info("所有数据解析完成!");
        }
        
        /**
         * 保存数据,这里只是打印,实际应用中可以将数据存入数据库
         */
        private void saveData() {
            log.info("{}条数据,开始保存数据库!", employeeList.size());
            // 这里可以调用持久层完成数据入库
            log.info("存储数据库成功!");
        }
        
        /**
         * 获取读取到的数据
         */
        public List<Employee> getEmployeeList() {
            return employeeList;
        }
    }
    5.1.3 创建Excel读取服务
    package com.example.excel.service;
    
    import com.alibaba.excel.EasyExcel;
    import com.example.excel.listener.EmployeeReadListener;
    import com.example.excel.model.Employee;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;
    import org.springframework.web.multipart.MultipartFile;
    
    import java.io.IOException;
    import java.util.List;
    
    @Slf4j
    @Service
    public class EasyExcelService {
    
        public List<Employee> readEmployeeData(MultipartFile file) throws IOException {
            EmployeeReadListener listener = new EmployeeReadListener();
            
            EasyExcel.read(file.getInputStream(), Employee.class, listener).sheet().doRead();
            
            return listener.getEmployeeList();
        }
    }
    5.1.4 创建Controller
    package com.example.excel.controller;
    
    import com.example.excel.model.Employee;
    import com.example.excel.service.EasyExcelService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.multipart.MultipartFile;
    
    import java.io.IOException;
    import java.util.List;
    
    @Slf4j
    @RestController
    @RequestMapping("/api/easyexcel")
    public class EasyExcelController {
    
        @Autowired
        private EasyExcelService easyExcelService;
        
        @PostMapping("/upload")
        public ResponseEntity<List<Employee>> uploadExcel(@RequestParam("file") MultipartFile file) {
            try {
                List<Employee> employees = easyExcelService.readEmployeeData(file);
                return ResponseEntity.ok(employees);
            } catch (IOException e) {
                log.error("Excel读取失败", e);
                return ResponseEntity.badRequest().build();
            }
        }
    }

    5.2 使用EasyExcel导出Excel

    5.2.1 简单导出示例
    package com.example.excel.service;
    
    import com.alibaba.excel.EasyExcel;
    import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
    import com.example.excel.model.Employee;
    import org.springframework.stereotype.Service;
    
    import java.io.File;
    import java.io.IOException;
    import java.io.OutputStream;
    import java.util.List;
    
    @Service
    public class EasyExcelExportService {
    
        /**
         * 导出员工数据到Excel文件
         */
        public void exportEmployees(List<Employee> employees, OutputStream outputStream) {
            EasyExcel.write(outputStream, Employee.class)
                    .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())  // 自动调整列宽
                    .sheet("员工数据")
                    .doWrite(employees);
        }
        
        /**
         * 导出员工数据到指定文件
         */
        public void exportEmployeesToFile(List<Employee> employees, String fileName) throws IOException {
            // 确保目录存在
            File file = new File(fileName);
            if (!file.getParentFile().exists()) {
                file.getParentFile().mkdirs();
            }
            
            EasyExcel.write(fileName, Employee.class)
                    .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                    .sheet("员工数据")
                    .doWrite(employees);
        }
        
        /**
         * 导出多个Sheet的Excel
         */
        public void exportMultipleSheets(List<List<Employee>> departmentEmployees, 
                                        List<String> sheetNames, 
                                        OutputStream outputStream) {
            // 创建ExcelWriter
            try (var excelWriter = EasyExcel.write(outputStream, Employee.class)
                    .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                    .build()) {
                
                // 同一个对象多个Sheet写入
                for (int i = 0; i < departmentEmployees.size(); i++) {
                    // 获取Sheet名称
                    String sheetName = i < sheetNames.size() ? sheetNames.get(i) : "Sheet" + (i + 1);
                    
                    // 创建新的Sheet
                    var writeSheet = EasyExcel.writerSheet(i, sheetName).build();
                    
                    // 写入数据
                    excelWriter.write(departmentEmployees.get(i), writeSheet);
                }
            }
        }
    }
    5.2.2 创建Controller
    package com.example.excel.controller;
    
    import com.example.excel.model.Employee;
    import com.example.excel.service.EasyExcelExportService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.net.URLEncoder;
    import java.nio.charset.StandardCharsets;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Date;
    import java.util.List;
    
    @RestController
    @RequestMapping("/api/easyexcel")
    public class EasyExcelExportController {
    
        @Autowired
        private EasyExcelExportService exportService;
        
        @GetMapping("/export")
        public void exportEmployees(HttpServletResponse response) throws IOException {
            // 设置响应内容
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding("utf-8");
            
            // 设置文件名
            String fileName = URLEncoder.encode("员工数据", StandardCharsets.UTF_8).replaceAll("\\+", "%20");
            response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
            
            // 获取测试数据
            List<Employee> employees = getTestEmployees();
            
            // 导出Excel
            exportService.exportEmployees(employees, response.getOutputStream());
        }
        
        @GetMapping("/export-multiple-sheets")
        public void exportMultipleSheets(HttpServletResponse response) throws IOException {
            // 设置响应内容
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding("utf-8");
            
            // 设置文件名
            String fileName = URLEncoder.encode("部门员工数据", StandardCharsets.UTF_8).replaceAll("\\+", "%20");
            response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
            
            // 获取测试数据 - 三个部门的员工
            List<List<Employee>> departmentEmployees = new ArrayList<>();
            departmentEmployees.add(getEmployeesByDepartment("研发部"));
            departmentEmployees.add(getEmployeesByDepartment("市场部"));
            departmentEmployees.add(getEmployeesByDepartment("行政部"));
            
            // Sheet名称
            List<String> sheetNames = Arrays.asList("研发部员工", "市场部员工", "行政部员工");
            
            // 导出Excel
            exportService.exportMultipleSheets(departmentEmployees, sheetNames, response.getOutputStream());
        }
        
        /**
         * 生成测试员工数据
         */
        private List<Employee> getTestEmployees() {
            List<Employee> employees = new ArrayList<>();
            
            // 添加测试数据
            for (int i = 1; i <= 10; i++) {
                Employee employee = new Employee();
                employee.setId((long) i);
                employee.setName("员工" + i);
                employee.setAge(20 + i);
                employee.setEmail("employee" + i + "@example.com");
                employee.setDepartment(i % 3 == 0 ? "研发部" : (i % 3 == 1 ? "市场部" : "行政部"));
                employee.setHireDate(new Date());
                employee.setSalary(5000.0 + i * 1000);
                
                employees.add(employee);
            }
            
            return employees;
        }
        
        /**
         * 根据部门获取员工
         */
        private List<Employee> getEmployeesByDepartment(String department) {
            List<Employee> allEmployees = getTestEmployees();
            List<Employee> departmentEmployees = new ArrayList<>();
            
            for (Employee employee : allEmployees) {
                if (department.equals(employee.getDepartment())) {
                    departmentEmployees.add(employee);
                }
            }
            
            return departmentEmployees;
        }
    
        // ... 5.2.3 使用自定义样式和复杂表头
    
        /**
         * 导出自定义样式的Excel
         */
        public void exportWithCustomStyle(List<Employee> employees, OutputStream outputStream) {
            // 设置自定义拦截器来处理样式
            EasyExcel.write(outputStream, Employee.class)
                    // 自动调整列宽
                    .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                    // 设置表头样式
                    .registerWriteHandler(new AbstractRowHeightStyleStrategy() {
                        @Override
                        protected void setHeadColumnHeight(Row row, int relativeRowIndex) {
                            // 设置表头行高
                            row.setHeight((short) 500);
                        }
    
                        @Override
                        protected void setContentColumnHeight(Row row, int relativeRowIndex) {
                            // 设置内容行高
                            row.setHeight((short) 400);
                        }
                    })
                    // 设置单元格样式
                    .registerWriteHandler(new CellWriteHandler() {
                        @Override
                        public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, 
                                                  Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
                            // 设置表头样式
                            if (isHead) {
                                Workbook workbook = writeSheetHolder.getSheet().getWorkbook();
                                CellStyle style = workbook.createCellStyle();
                                Font font = workbook.createFont();
                                font.setBold(true);
                                font.setFontHeightInPoints((short) 12);
                                font.setColor(IndexedColors.WHITE.getIndex());
                                style.setFont(font);
                                style.setFillForegroundColor(IndexedColors.ROYAL_BLUE.getIndex());
                                style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
                                style.setAlignment(HorizontalAlignment.CENTER);
                                style.setVerticalAlignment(VerticalAlignment.CENTER);
                                cell.setCellStyle(style);
                            }
                        }
    
                        @Override
                        public void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, 
                                                         Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
                            // 在这里可以根据数据内容设置样式
                        }
    
                        @Override
                        public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, 
                                                   List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
                            // 内容行的样式
                            if (!isHead) {
                                // 偶数行设置背景色
                                if (relativeRowIndex % 2 == 0) {
                                    Workbook workbook = writeSheetHolder.getSheet().getWorkbook();
                                    CellStyle style = workbook.createCellStyle();
                                    style.setFillForegroundColor(IndexedColors.PALE_BLUE.getIndex());
                                    style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
                                    style.setAlignment(HorizontalAlignment.CENTER);
                                    style.setVerticalAlignment(VerticalAlignment.CENTER);
                                    cell.setCellStyle(style);
                                }
                            }
                        }
                    })
                    .sheet("员工数据")
                    .doWrite(employees);
        }
    
        /**
         * 导出复杂表头的Excel
         */
        public void exportWithComplexHead(List<Employee> employees, OutputStream outputStream) {
            // 构建复杂表头
            List<List<String>> head = new ArrayList<>();
            
            // 第一列 ID
            List<String> head1 = new ArrayList<>();
            head1.add("基本信息");
            head1.add("员工ID");
            head.add(head1);
            
            // 第二列 姓名
            List<String> head2 = new ArrayList<>();
            head2.add("基本信息");
            head2.add("姓名");
            head.add(head2);
            
            // 第三列 年龄
            List<String> head3 = new ArrayList<>();
            head3.add("基本信息");
            head3.add("年龄");
            head.add(head3);
            
            // 第四列 邮箱
            List<String> head4 = new ArrayList<>();
            head4.add("联系方式");
            head4.add("邮箱");
            head.add(head4);
            
            // 第五列 部门
            List<String> head5 = new ArrayList<>();
            head5.add("工作信息");
            head5.add("部门");
            head.add(head5);
            
            // 第六列 入职日期
            List<String> head6 = new ArrayList<>();
            head6.add("工作信息");
            head6.add("入职日期");
            head.add(head6);
            
            // 第七列 薪资
            List<String> head7 = new ArrayList<>();
            head7.add("薪资信息");
            head7.add("月薪(元)");
            head.add(head7);
            
            // 将数据转为List<List<Object>>格式
            List<List<Object>> dataList = new ArrayList<>();
            for (Employee employee : employees) {
                List<Object> data = new ArrayList<>();
                data.add(employee.getId());
                data.add(employee.getName());
                data.add(employee.getAge());
                data.add(employee.getEmail());
                data.add(employee.getDepartment());
                data.add(employee.getHireDate());
                data.add(employee.getSalary());
                dataList.add(data);
            }
            
            // 写入Excel
            EasyExcel.write(outputStream)
                    .head(head)
                    .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                    .sheet("员工数据")
                    .doWrite(dataList);
        }
    }

    6. 处理大型Excel文件的策略

    6.1 使用Apache POI SXSSF模式

    SXSSF(Streaming Xlsx Writer)是POI提供的流式写入方式,可以大大减少内存使用:

    public void exportLargeExcel(String fileName, int rowCount) throws IOException {
        try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) { // 100表示内存中保留的行数
            Sheet sheet = workbook.createSheet("大数据");
            
            // 创建表头
            Row headerRow = sheet.createRow(0)编程客栈;
            for (int i = 0; i < 10; i++) {
                headerRow.createCell(i).setCellValue("列 " + (i + 1));
            }
            
            // 创建数据行
            for (int i = 0; i < rowCount; i++) {
                Row row = sheet.createRow(i + 1);
                for (int j = 0; j < 10; j++) {
                    row.createCell(j).setCellValue("数据 " + (i + 1) + "-" + (j + 1));
                }
                
                // 每生成10000行清理一次临时文件
                if (i % 10000 == 0) {
                    ((SXSSFSheet)sheet).flushRows();
                }
            }
            
            // 写入文件
            try (FileOutputStream outputStream = new FileOutputStream(fileName)) {
                workbook.write(outputStream);
            }
            
            // 清理临时文件
            workbook.dispose();
        }
    }

    注意事项:

    1. 使用完毕后一定要调用dispose()方法清理临时文件
    2. SXSSF仅支持写入操作,不支持读取
    3. 不支持某些高级特性(如合并单元格等)

    6.2 使用EasyExcel处理大文件

    EasyExcel在设计上就考虑了大文件处理,采用SAX方式逐行读取,内存占用小:

    // 读取大文件
    public void readLargeExcel(String fileName) {
        // 使用SAX方式读取
        EasyExcel.read(fileName, Employee.class, new EmployeeReadListener())
                .sheet()
                .doRead();
    }
    
    // 写入大文件
    public void writeLargeExcel(String fileName, int batchSize) {
        // 分批获取数据
        try (ExcelWriter excelWriter = EasyExcel.write(fileName, Employee.class)
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                .build()) {
            
            // 获取WriteSheet对象
            WriteSheet writeSheet = EasyExcel.writerSheet("员工数据").build();
            
            // 模拟分批获取数据
            int totalCount = 100000; // 总数据量
            for (int i = 0; i < totalCount; i += batchSize) {
                // 获取当前批次数据
                List<Employee> data = getBatchData(i, Math.min(i + batchSize, totalCount));
                // 写入Excel
                excelWriter.write(data, writeSheet);
            }
        }
    }
    
    // 模拟分批获取数据
    private List<Employee> getBatchData(int start, int end) {
        List<Employee> list = new ArrayList<>();
        for (int i = start; i < end; i++) {
            Employee employee = new Employee();
            employee.setId((long) i);
            employee.setName("员工" + i);
            employee.setAge(20 + (i % 20));
            employee.setEmail("employee" + i + "@example.com");
            employee.setDepartment(i % 3 == 0 ? "研发部" : (i % 3 == 1 ? "市场部" : "行政部"));
            employee.setHireDate(new Date());
            employee.setSalary(5000.0 + (i % 10) * 1000);
            
            list.add(employee);
        }
        return list;
    }

    6.3 使用CSV代替Excel

    对于极大的数据集,考虑使用CSV格式替代Excel:

    public void exportToCsv(List<Employee> employees, String fileName) throws IOException {
        try (FileWriter writer = new FileWriter(fileName);
             CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT
                     .withHeader("ID", "姓名", "年龄", "邮箱", "部门", "入职日期", "薪资"))) {
                     
            for (Employee employee : employees) {
                csvPrinter.printRecord(
                    employee.getId(),
                    employee.getName(),
                    employee.getAge(),
                    employee.getEmail(),
                    employee.getDepartment(),
                    employee.getHireDate(),
                    employee.getSalary()
                );
            }
            
            csvPrinter.flush();
        }
    }

    注意:使用CSV需要添加依赖:

    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-csv</artifactId>
        <version>1.9.0</version>
    </dependency>

    6.4 分页导出大型数据集

    对于Web应用中需要导出的大型数据集,可以考虑分页导出:

    @GetMapping("/export/paged")
    public ResponseEntity<String> exportPaged() {
        // 生成唯一任务ID
        String taskId = UUID.randomUUID().toString();
        
        // 启动异步任务
        CompletableFuture.runAsync(() -> {
            try {
                // 导出文件路径
                String filePath = "/temp/" + taskId + ".xlsx";
                
                // 分页查询数据并写入Excel
                int pageSize = 1000;
                int totalPages = getTotalPages(pageSize);
                
                try (ExcelWriter excelWriter = EasyExcel.write(filePath, Employee.class)
                        .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                        .build()) {
                    
                    WriteSheet writeSheet = EasyExcel.writerSheet("员工数据").build();
                    
                    // 分页导出
                    for (int pageNum = 0; pageNum < totalPages; pageNum++) {
                        // 从数据库分页查询数据
                        List<Employee> pageData = getPageData(pageNum, pageSize);
                        // 写入Excel
                        excelWriter.write(pageData, writeSheet);
                        // 更新进度
                        updateExportProgress(taskId, (pageNum + 1) * 100 / totalPages);
                    }
                }
                
                // 更新导出状态为完成
                updateExportStatus(taskId, "COMPLETED", filePath);
                
            } catch (Exception e) {
                // 更新导出状态为失败
                updateExportStatus(taskId, "FAILED", null);
            }
        });
        
        // 返回任务ID
        return ResponseEntity.ok(taskId);
    }
    
    @GetMapping("/export/status/{taskId}")
    public ResponseEntity<Map<String, Object>> getExportStatus(@PathVariable String taskId) {
        // 获取任务状态
        Map<String, Object> status = getTaskStatus(taskId);
        return ResponseEntity.ok(status);
    }
    
    @GetMapping("/export/download/{taskId}")
    public ResponseEntity<Resource> downloadExportedFile(@PathVariable String taskId) {
        // 获取导出文件路径
        String filePath = getExportedFilePath(taskId);
        
        if (filePath == null) {
            return ResponseEntity.notFound().build();
        }
        
        // 创建文件资源
        Resource resource = new FileSystemResource(filePath);
        
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, 
                        "attachment; filename=employee_data.xlsx")
                .contentType(MediaType.parseMediaType("application/vnd.ms-excel"))
                .body(resource);
    }

    7. 实际应用场景和最佳实践

    7.1 动态列导出

    在某些业务场景中,需要根据用户选择动态确定导出的列:

    public ByteArrayInputStream exportDynamicColumns(List<Employee> employees, List<String> selectedColumns) throws IOException {
        // 定义所有可能的列
        Map<String, String> allColumns = new HashMap<>();
        allColumns.put("id", "员工ID");
        allColumns.put("name", "姓名");
        allColumns.put("age", "年龄");
        allColumns.put("email", "邮箱");
        allColumns.put("department", "部门");
        allColumns.put("hireDate", "入职日期");
        allColumns.put("salary", "薪资");
        
        try (Workbook workbook = new XSSFWorkbook()) {
            Sheet sheet = workbook.createSheet("员工数据");
            
            // 创建表头行
            Row headerRow = sheet.createRow(0);
            
            // 设置表头样式
            CellStyle headerStyle = workbook.createCellStyle();
            Font headerFont = workbook.createFont();
            headerFont.setBold(true);
            headerStyle.setFont(headerFont);
            
            // 填充表头
            int colIdx = 0;
            for (String column : selectedColumns) {
                if (allColumns.containsKey(column)) {
                    Cell cell = headerRow.createCell(colIdx++);
                    cell.setCellValue(allColumns.get(column));
                    cell.setCellStyle(headerStyle);
                }
            }
            
            // 填充数据
            int rowIdx = 1;
            for (Employee employee : employees) {
                Row row = sheet.createRow(rowIdx++);
                
                colIdx = 0;
                for (String column : selectedColumns) {
                    Cell cell = row.createCell(colIdx++);
                    
                    // 根据列名设置单元格值
                    switch (column) {
                        case "id":
                            cell.setCellValue(employee.getId());
                            break;
                        case "name":
                            cell.setCellValue(employee.getName());
                            break;
                        case "age":
                          编程客栈  cell.setCellValue(employee.getAge());
                            break;
                        case "email":
                            cell.setCellValue(employee.getEmail());
                            break;
                        case "department":
                            cell.setCellValue(employee.getDepartment());
                            break;
                        case "hireDate":
                            if (employee.getHireDate() != null) {
                                cell.setCellValue(employee.getHireDate());
                                
                                // 设置日期格式
                                CellStyle dateStyle = workbook.createCellStyle();
                                CreationHelper createHelper = workbook.getCreationHelper();
                                dateStyle.setDataFormat(createHelper.createDataFormat().getFormat("yyyy-mm-dd"));
                                cell.setCellStyle(dateStyle);
                            }
                            break;
                        case "salary":
                            cell.setCellValue(employee.getSalary());
                            break;
                    }
                }
            }
            
            // 自动调整列宽
            for (int i = 0; i < selectedColumns.size(); i++) {
                sheet.autoSizeColumn(i);
            }
            
            // 输出
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            workbook.write(outputStream);
            return new ByteArrayInputStream(outputStream.toByteArray());
        }
    }

    7.2 Excel模板填充

    使用Freemarker或其他模板引擎生成Excel:

    public ByteArrayInputStream fillTemplate(Map<String, Object> data) throws Exception {
        // 加载模板
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
        cfg.setClassLoaderForTemplateLoading(getClass().getClassLoader(), "templates");
        cfg.setDefaultEncoding("UTF-8");
        
        // 获取模板
        Template template = cfg.getTemplate("excel_template.ftl");
        
        // 输出目录
        File tempDir = new File(System.getProperty("java.io.tmpdir"));
        File tempFile = new File(tempDir, "temp_" + System.currentTimeMillis() + ".xlsx");
        
        // 填充模板
        try (Writer out = new FileWriter(tempFile)) {
            template.process(data, out);
        }
        
        // 读取填充后的文件
        try (FileInputStream fis = new FileInputStream(tempFile)) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len;
            while ((len = fis.read(buffer)) > -1) {
                baos.write(buffer, js0, len);
            }
            baos.flush();
            
            // 删除临时文件
            tempFile.delete();
            
            return new ByteArrayInputStream(baos.toByteArray());
        }
    }

    7.3 Excel文件校验

    在导入Excel文件前进行数据校验:

    public class ExcelValidationListener extends AnalysisEventListener<Employee> {
        
        private List<Employee> validEmployees = new ArrayList<>();
        private List<Map<String, Object>> errorRecords = new ArrayList<>();
        private int rowIndex = 1; // 从1开始,0是表头
        
        @Override
        public void invoke(Employee employee, AnalysisContext context) {
            rowIndex++;
            
            // 验证数据
            List<String> errors = validateEmployee(employee);
            
            if (errors.isEmpty()) {
                // 数据有效
                validEmployees.add(employee);
            } else {
                // 记录错误
                Map<String, Object> errorRecord = new HashMap<>();
                errorRecord.put("rowIndex", rowIndex);
                errorRecord.put("data", employee);
                errorRecord.put("errors", errors);
                errorRecords.add(errorRecord);
            }
        }
        
        @Override
        public void doAfterAllAnalysed(AnalysisContext context) {
            // 处理完成
        }
        
        // 验证员工数据
        private List<String> validateEmployee(Employee employee) {
            List<String> errors = new ArrayList<>();
            
            // 验证姓名
            if (employee.getName() == null || employee.getName().trim().isEmpty()) {
                errors.add("姓名不能为空");
            }
            
            // 验证年龄
            if (employee.getAge() == null) {
                errors.add("年龄不能为空");
            } else if (employee.getAge() < 18 || employee.getAge() > 65) {
                errors.add("年龄必须在18-65岁之间");
            }
            
            // 验证邮箱
            if (employee.getEmail() != null && !employee.getEmail().isEmpty()) {
                String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@" +
                        "(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
                if (!employee.getEmail().matches(emailRegex)) {
                    errors.add("邮箱格式不正确");
                }
            }
            
            // 验证部门
            if (employee.getDepartment() == null || employee.getDepartment().trim().isEmpty()) {
                errors.add("部门不能为空");
            }
            
            // 验证薪资
            if (employee.getSalary() != null && employee.getSalary() < 0) {
                errors.add("薪资不能为负数");
            }
            
            return errors;
        }
        
        public List<Employee> getValidEmployees() {
            return validEmployees;
        }
        
        public List<Map<String, Object>> getErrorRecords() {
            return errorRecords;
        }
        
        public boolean hasErrors() {
            return !errorRecords.isEmpty();
        }
    }

    7.4 统一异常处理

    为Excel处理添加统一的异常处理:

    @ControllerAdvice
    public class ExcelExceptionHandler {
    
        private static final Logger logger = LoggerFactory.getLogger(ExcelExceptionHandler.class);
        
        @ExceptionHandler(IOException.class)
        public ResponseEntity<Map<String, String>> handleIOException(IOException e) {
            logger.error("文件读写异常", e);
            
            Map<String, String> response = new HashMap<>();
            response.put("error", "文件读写异常");
            response.put("message", e.getMessage());
            
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
        }
        
        @ExceptionHandler(IllegalArgumentException.class)
        public ResponseEntity<Map<String, String>> handleIllegalArgumentException(IllegalArgumentException e) {
            logger.error("参数异常", e);
            
            Map<String, String> response = new HashMap<>();
            response.put("error", "参数异常");
            response.put("message", e.getMessage());
            
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
        }
        
        @ExceptionHandler(Exception.class)
        public ResponseEntity<Map<String, String>> handleGenericException(Exception e) {
            logger.error("Excel处理异常", e);
            
            Map<String, String> response = new HashMap<>();
            response.put("error", "Excel处理异常");
            response.put("message", e.getMessage());
            
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
        }
    }

    8. 性能优化和注意事项

    8.1 性能优化建议

    使用适当的Excel库

    • 小文件可使用Apache POI
    • 大文件请使用EasyExcel或POI的SXSSF模式
    • 极大文件考虑使用CSV格式

    避免一次性加载整个文件

    • 读取时使用流式解析
    • 写入时使用分批写入

    合理设置缓冲区大小

    • 在SXSSFWorkbook中设置合理的内存行数
    • 在批处理中选择合适的批次大小

    减少样式对象

    • 样式对象重用,而不是为每个单元格创建新样式
    • 限制使用的颜色、字体和边框样式数量

    使用异步处理

    • 将大文件处理放在后台线程中执行
    • 提供进度反馈机制

    8.2 注意事项

    内存管理

    • 注意监控JVM内存使用情况
    • 对于大文件处理,考虑增加JVM堆内存(-Xmx参数)
    • 使用完毕后及时关闭资源和清理临时文件

    安全考虑

    • 限制上传文件大小
    • 验证文件类型和内容
    • 防止恶意Excel文件(包含宏或公式)

    编码问题

    • 处理国际字符时,确保使用正确的字符编码
    • 文件名包含中文时,确保正确编码

    并发控制

    • 大文件处理时注意服务器负载
    • 限制并发处理任务数量

    临时文件清理

    • 使用SXSSF时,必须调用dispose()方法清理临时文件
    • 定期清理服务器上的临时文件

    总结

    Spring Boot提供了强大而灵活的Excel处理能力,通过结合Apache POI和EasyExcel等工具,可以轻松实现Excel文件的读取、创建和导出功能。在实际应用中,应根据具体需求和数据量选择合适的处理策略,既要保证功能完整,又要注重性能和资源使用。

    无论是简单的数据导出,还是复杂的报表生成,或是大数据量的文件处理,都可以通过本文介绍的方法灵活实现。重点是要根据实际业务场景,选择合适的技术方案,并注意性能优化和异常处理。

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜