Spring Boot 与 EasyExcel 携手:复杂 Excel 表格高效导入导出实战

数据的并行导出与压缩下载:EasyExcel:实现大规模数据的并行导出与压缩下载

构建高效排队导出:解决多人同时导出Excel导致的服务器崩溃

SpringBoot集成EasyExcel 3.x:

前言

在企业级应用开发中,常常需要处理复杂的 Excel 表格数据。本方案将 Spring Boot 强大的后端框架与 EasyExcel 这一高效的 Excel 处理工具进行整合,实现了复杂 Excel 表格的导入与导出功能。

对于导入功能,能够轻松应对包含多种数据类型、复杂结构以及大量数据的 Excel 文件。通过合理的配置和处理流程,确保数据的准确性和完整性,将 Excel 中的数据快速导入到系统中,为后续的数据处理和业务逻辑提供有力支持。

在导出方面,能够根据系统中的数据生成结构复杂、格式规范的 Excel 表格。可以自定义表头、样式、格式等,满足不同业务场景下对 Excel 报表的需求。无论是生成详细的业务数据报表,还是复杂的统计分析结果,都能通过这个整合方案轻松实现。

组件介绍

EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel。

Alibaba EasyExcel的核心类是EasyExcel类。

案例

1、简单的读操作

java 复制代码
/**
 * 最简单的读
 * 1. 创建excel对应的实体对象 参照{@link DemoData}
 * 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link DemoDataListener}
 * 3. 直接读即可
 */
@Test
public void simpleRead() {
    String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
    // 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
    EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();
}

2、简单的写操作

java 复制代码
/**
 * 最简单的写
 * 1. 创建excel对应的实体对象 参照{@link com.alibaba.easyexcel.test.demo.write.DemoData}
 * 2. 直接写即可
 */
@Test
public void simpleWrite() {
    String fileName = TestFileUtil.getPath() + "write" + System.currentTimeMillis() + ".xlsx";
    // 这里 需要指定写用哪个class去读,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
    // 如果这里想使用03 则 传入excelType参数即可
    EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());
}

项目基本信息

一、单个sheet&表头合并

1、添加pom.xml依赖
XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>SpringBoot-easyexcel</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>SpringBoot-easyexcel</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>

        <!-- Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- easyexcel -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>3.1.1</version>
        </dependency>

        <!-- commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.8.1</version>
        </dependency>

        <!-- lombok插件 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
2. 创建数据模型

创建一个数据模型类,用于表示Excel中的一行数据。例如,我们有一个包含用户信息的复杂Excel表格:

java 复制代码
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.metadata.data.WriteCellData;
import lombok.Builder;
import lombok.Data;

import java.util.Date;

@Data
@Builder
public class UserData{

    @ExcelProperty(value = "用户ID", index = 0)
    @ColumnWidth(10)
    private Long userId;

    @ExcelProperty(value = "用户名称", index = 1)
    @ColumnWidth(10)
    private String userName;

    @ExcelProperty(value = {"基本信息", "手机号码"}, index = 2)
    @ColumnWidth(20)
    private String userPhone;

    @ExcelProperty(value = {"基本信息", "电子邮箱"}, index = 3)
    @ColumnWidth(20)
    private String userEmail;

    @ExcelProperty(value = {"基本信息", "地址"}, index = 4)
    @ColumnWidth(20)
    private String userAddress;

    @ExcelProperty(value = "注册时间", index = 5)
    @ColumnWidth(20)
    private Date registerTime;

    @ExcelProperty(value = "性别,男:红色/女:绿色")
    @ColumnWidth(30)
    private WriteCellData<String> gender;

    /**
     * 忽略这个字段
     */
    @ExcelIgnore
    private Integer age;
}
3. 创建导出服务

创建一个服务类,用于实现Excel的导出功能:

java 复制代码
import com.alibaba.excel.EasyExcel;  
import org.springframework.stereotype.Service;  
  
import javax.servlet.http.HttpServletResponse;  
import java.io.IOException;  
import java.net.URLEncoder;  
import java.util.List;  
  
@Service  
public class ExcelExportService {  
  
    public void exportUsers(List<UserData> userDataList, HttpServletResponse response) throws IOException {  
        // 设置响应类型
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        // 设置编码格式
        response.setCharacterEncoding("utf-8");
        // 设置URLEncoder.encode 防止中文乱码
        String fileName = URLEncoder.encode("信息表", "UTF-8").replaceAll("\\+", "%20");
        // 设置响应头
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");

        EasyExcel.write(response.getOutputStream(), UserData.class).inMemory(true)
            .sheet("用户信息表")
            .doWrite(userDataList);
    }


    private static WriteCellData<String> buildCellData(String gender) {
        // 设置单个单元格多种样式
        WriteCellData<String> cellData = new WriteCellData<>();
        // 设置单个单元格的填充类型
        cellData.setType(CellDataTypeEnum.RICH_TEXT_STRING);
        
        RichTextStringData richTextStringData = new RichTextStringData();
        cellData.setRichTextStringDataValue(richTextStringData);
        richTextStringData.setTextString(gender);
        WriteFont writeFont = new WriteFont();
        if ("男".equalsIgnoreCase(gender)) {
         //设置颜色为红色
            writeFont.setColor(IndexedColors.RED.getIndex());
        } else if ("女".equalsIgnoreCase(gender)) {
         //设置颜色为绿色
            writeFont.setColor(IndexedColors.GREEN.getIndex());
        }
        //应用颜色字体
        richTextStringData.applyFont(writeFont);
        return cellData;
    }  
}
4. 创建导入服务

导入的数据模型

java 复制代码
import java.util.Date;

import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.metadata.data.WriteCellData;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@Builder
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class UserData {
    private Long userId;

    private String userName;

    private Integer age;

    private String userPhone;

    private String userEmail;

    private String userAddress;

    private Date registerTime;

    private String gender;
}

创建一个服务类,用于实现Excel的导入功能:

java 复制代码
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.util.ListUtils;
import com.alibaba.excel.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;

@Slf4j
@Service
public class ExcelImportService {

    /**
     * 每隔一定数量的数据存储到数据库,可根据实际情况调整。
     */
    private static final int BATCH_COUNT = 100;

    /**
     * 手机号正则表达式校验。
     */
    private static final Pattern PHONE_REGEX = Pattern.compile("^1[0-9]{10}$");

    /**
     * 缓存要导入的数据列表。
     */
    private List<UserData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

    /**
     * 错误信息列表。
     */
    private final List<String> errorMsgList = new ArrayList<>();

    // 假设这里有一个数据访问层的接口引用
    private UserDataRepository userDataRepository;

    public ExcelImportService(UserDataRepository userDataRepository) {
        this.userDataRepository = userDataRepository;
    }

    /**
     * 导入 Excel 文件并保存数据到数据库。
     *
     * @param excelFilePath Excel 文件路径。
     */
    public void importExcel(String excelFilePath) {
        EasyExcel.read(excelFilePath, UserData.class, new CustomAnalysisEventListener()).sheet().doRead();
    }

    class CustomAnalysisEventListener extends AnalysisEventListener<UserData> {

        @Override
        public void invoke(UserData userData, AnalysisContext analysisContext) {
            log.info("解析到一条数据:{}", userData);
            int rowIndex = analysisContext.readRowHolder().getRowIndex();
            String name = userData.getUserName();
            String phone = userData.getUserPhone();
            String gender = userData.getGender();
            String email = userData.getUserEmail();
            Integer age = userData.getAge();
            String address = userData.getUserAddress();
            // 只有全部校验通过的对象才能被添加到下一步
            if (nameValid(rowIndex, name) && phoneValid(rowIndex, phone) && genderValid(rowIndex, gender) &&
                    emailValid(rowIndex, email) && ageValid(rowIndex, age) && addressValid(rowIndex, address)) {
                cachedDataList.add(userData);
            }
            // 达到 BATCH_COUNT 了,存储到数据库并清理列表
            if (cachedDataList.size() >= BATCH_COUNT) {
                saveDataToDatabase();
                cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
            }
        }

        @Override
        public void doAfterAllAnalysed(AnalysisContext analysisContext) {
            log.info("所有数据解析完成!全部校验通过的数据有{}条", cachedDataList.size());
            // 保存剩余数据到数据库
            saveDataToDatabase();
        }

        @Override
        public void onException(Exception exception, AnalysisContext context) throws Exception {
            if (exception instanceof RuntimeException) {
                throw exception;
            }
            int index = context.readRowHolder().getRowIndex() + 1;
            errorMsgList.add("第" + index + "行解析错误");
        }

        @Override
        public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
            int totalRows = context.readSheetHolder().getApproximateTotalRowNumber() - 1;
            int maxNum = 2000;
            if (totalRows > maxNum) {
                errorMsgList.add("数据量过大,单次最多上传2000条");
                throw new RuntimeException("数据量过大,单次最多上传2000条");
            }
        }
    }

    /**
     * 将缓存的数据保存到数据库。
     */
    private void saveDataToDatabase() {
        userDataRepository.saveAll(cachedDataList);
    }

    public List<String> getErrorMsgList() {
        return errorMsgList;
    }

    /**
     * 名称的校验。
     *
     * @param rowIndex 行数。
     * @param name     名称。
     */
    private Boolean nameValid(Integer rowIndex, String name) {
        if (StringUtils.isBlank(name)) {
            errorMsgList.add("第" + rowIndex + "行,'姓名'不能为空");
            return false;
        }
        return true;
    }

    /**
     * 手机号的校验。
     *
     * @param rowIndex 行数。
     * @param phone    手机号。
     */
    private Boolean phoneValid(int rowIndex, String phone) {
        if (StringUtils.isBlank(phone)) {
            errorMsgList.add("第" + rowIndex + "行,'手机号'不能为空");
            return false;
        }
        return true;
    }

    /**
     * 性别的校验。
     *
     * @param rowIndex 行数。
     * @param gender   性别。
     */
    private Boolean genderValid(int rowIndex, String gender) {
        if (StringUtils.isBlank(gender)) {
            errorMsgList.add("第" + rowIndex + "行,'性别'不能为空");
            return false;
        }
        return true;
    }

    /**
     * 地址的校验。
     *
     * @param rowIndex 行数。
     * @param address  地址。
     */
    private Boolean addressValid(int rowIndex, String address) {
        // 校验地址是否为空
        if (StringUtils.isBlank(address)) {
            errorMsgList.add("第 " + rowIndex + " 行,'地址'不能为空");
            return false;
        }
        return true;
    }

    /**
     * 年龄的校验。
     *
     * @param rowIndex 行数。
     * @param age      年龄。
     */
    private Boolean ageValid(int rowIndex, Integer age) {
        // 校验年龄是否为空
        if (Objects.isNull(age)) {
            errorMsgList.add("第 " + rowIndex + " 行'年龄'不能为空");
            return false;
        }
        return true;
    }

    /**
     * 邮箱的校验。
     *
     * @param rowIndex 行数。
     * @param email    邮箱。
     */
    private Boolean emailValid(int rowIndex, String email) {
        // 校验邮箱是否为空
        if (StringUtils.isBlank(email)) {
            errorMsgList.add("第 " + rowIndex + " 行'邮箱'不能为空");
            return false;
        }
        return true;
    }
}
5. 创建控制器

创建一个控制器类,用于处理Excel的导入与导出请求:

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.web.bind.annotation.*;  
import org.springframework.web.multipart.MultipartFile;  
  
import javax.servlet.http.HttpServletResponse;  
import java.io.IOException;  
import java.util.List;  
  
@RestController  
@RequestMapping("/excel")  
public class ExcelController {  
  
    @Autowired  
    private ExcelExportService excelExportService;  
  
    @Autowired  
    private ExcelImportService excelImportService;

    @Autowired  
    private UserService userService;  
  
    
    @GetMapping("/export")  
    public void exportUsers(HttpServletResponse response,  
                            @RequestParam(value = "data", required = false) List<UserData> userDataList) throws IOException { 

        // 判断userDataList是否为空,为空就去数据库查询数据,后执行导出操作 
        if (userDataList == null || userDataList.isEmpty()) {  
            // 从数据库或其他地方获取数据  
            userDataList = userService.findAll();
            for (UserData userData: userDataList) {
                // 修改性别单元格的样式
                excelExportService.buildCellData(userData.getGender)
            }  
        }
  
        excelExportService.exportUsers(userDataList, response);  
    }  
  
    @PostMapping("/import")  
    public String importUsers(@RequestParam("file") MultipartFile file) {  
        try {  
            String filePath = "/path/to/upload/" + file.getOriginalFilename();  
            file.transferTo(new java.io.File(filePath));  
            excelImportService.importExcel(filePath);  
            return "导入成功";  
        } catch (Exception e) {  
            e.printStackTrace();  
            return "导入失败";  
        }  
    }  
}
6. 运行项目并测试

启动Spring Boot项目,然后你可以通过以下URL进行测试:

  • 导出Excel文件http://localhost:8080/excel/export(可以传递一个data参数,包含要导出的用户数据)
  • 导入Excel文件http://localhost:8080/excel/import(上传一个Excel文件)'

二、多个sheet导出

1、创建数据模型

城市实体类

java 复制代码
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class City{

    @ExcelProperty(value = "城市名称", index = 0)
    @ColumnWidth(10)
    private String cityName;

    @ExcelProperty(value = "城市介绍", index = 1)
    @ColumnWidth(60)
    private String cityDesc;
}

公司实体类

java 复制代码
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class Company{
    @ExcelProperty(value = "公司名称", index = 0)
    @ColumnWidth(10)
    private String companyName;

    @ExcelProperty(value = "公司创始人", index = 1)
    @ColumnWidth(10)
    private String companyBoss;

    @ExcelProperty(value = "公司总基地", index = 2)
    @ColumnWidth(10)
    private String companyBase;

    @ExcelProperty(value = "公司简介", index = 3)
    @ColumnWidth(50)
    private String companyDesc;
}
2、创建导出服务

创建一个服务类,用于实现Excel的导出功能:

java 复制代码
import com.alibaba.excel.EasyExcel;  
import org.springframework.stereotype.Service;  
  
import javax.servlet.http.HttpServletResponse;  
import java.io.IOException;  
import java.net.URLEncoder;  
import java.util.List;  
  
@Service  
public class ExcelExportAllService {  

        public void exportUsers(List<UserData> userDataList,List<CityList> cityList,List<CompanyList> companyList, HttpServletResponse response) throws IOException {  

            // 设置响应类型
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            // 设置编码格式
            response.setCharacterEncoding("utf-8");
            // 设置URLEncoder.encode 防止中文乱码
            String fileName = URLEncoder.encode("信息表", "UTF-8").replaceAll("\\+", "%20");
            // 设置响应头
            response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
            // 多个sheet的输出需要使用ExcelWriter类,这里想要下载成功,需要输出到OutputStream中
            try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).inMemory(true).build()) {

                // 创建用户信息表的sheet,写入用户信息数据,1代表sheet的位置是第一个
                WriteSheet userInfoSheet = EasyExcel.writerSheet(0, "用户信息表").head(UserData.class).build();
                excelWriter.write(userDataList, userInfoSheet);

                // 创建城市信息表的sheet,写入城市信息数据,2代表sheet的位置是第二个
                WriteSheet cityInfoSheet = EasyExcel.writerSheet(1, "城市信息表").head(City.class).build();
                excelWriter.write(cityList, cityInfoSheet);

                // 创建公司信息表的sheet,写入公司信息数据,3代表sheet的位置是第三个
                WriteSheet companyInfoSheet = EasyExcel.writerSheet(2, "公司信息表").head(Company.class).build();
                excelWriter.write(companyList, companyInfoSheet);
           }
        }
}
3、创建控制器

创建一个控制器类,用于处理Excel的导入与导出请求

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.web.bind.annotation.*;  
import org.springframework.web.multipart.MultipartFile;  
  
import javax.servlet.http.HttpServletResponse;  
import java.io.IOException;  
import java.util.List;  
  
@RestController  
@RequestMapping("/excelAll")  
public class ExcelAllController {  
  
    @Autowired  
    private ExcelExportAllService excelExportAllService;  

    @Autowired  
    private UserDataService userService;

    @Autowired  
    private CompanyService companyService;  

    @Autowired  
    private CityService cityService;  
  
    
    @GetMapping("/export")  
    public void exportUsers(HttpServletResponse response,  
                            @RequestParam(value = "userDataList", required = false) List<UserData> userDataList,
                            @RequestParam(value = "companyList", required = false) List<Company> companyList,
                            @RequestParam(value = "cityList", required = false) List<City> cityList
                            ) throws IOException { 

        // 判断userDataList是否为空,为空就去数据库查询数据,后执行导出操作 
        if (userDataList == null || userDataList.isEmpty()) {  
            // 从数据库或其他地方获取数据  
            userDataList = userService.findAll(); 
        }

        if (companyList== null || companyList.isEmpty()) {  
            // 从数据库或其他地方获取数据  
            companyList= companyService.findAll(); 
        }

        if (cityList== null || cityList.isEmpty()) {  
            // 从数据库或其他地方获取数据  
            cityList= cityService.findAll(); 
        }
  
        excelExportAllService.exportUsers(userDataList,cityList,companyList, response);  
    }  
  
}
4. 运行项目并测试

启动Spring Boot项目,然后你可以通过以下URL进行测试:

  • 导出Excel文件http://localhost:8080/excelAll/export

多个sheet导出

相关推荐
考虑考虑4 小时前
Jpa使用union all
java·spring boot·后端
用户3721574261354 小时前
Java 实现 Excel 与 TXT 文本高效互转
java
浮游本尊5 小时前
Java学习第22天 - 云原生与容器化
java
渣哥7 小时前
原来 Java 里线程安全集合有这么多种
java
间彧7 小时前
Spring Boot集成Spring Security完整指南
java
间彧7 小时前
Spring Secutiy基本原理及工作流程
java
Java水解8 小时前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
洛小豆11 小时前
在Java中,Integer.parseInt和Integer.valueOf有什么区别
java·后端·面试
前端小张同学11 小时前
服务器上如何搭建jenkins 服务CI/CD😎😎
java·后端
ytadpole11 小时前
Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查
java·后端