《优化接口设计的思路》系列:第十一篇—表格的导入导出接口优化

系列文章导航

第一篇---接口参数的一些弯弯绕绕

第二篇---接口用户上下文的设计与实现

第三篇---留下用户调用接口的痕迹

第四篇---接口的权限控制

第五篇---接口发生异常如何统一处理

第六篇---接口防抖(防重复提交)的一些方式

第七篇---接口限流策略

第八篇---分页接口的设计和优化

第九篇---用好缓存,让你的接口速度飞起来

第十篇---网站的静态资源怎么获取?

第十一篇---表格的导入导出接口优化

本文参考项目源码地址: summo-springboot-interface-demo

由于文章经常被抄袭,开源的代码甚至被当成收费项,所以源码里面不是全部代码,有需要的同学可以留个邮箱,我给你单独发!

一、前言

大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。

作为一名从业已达六年的老码农,我的工作主要是开发后端Java业务系统,包括各种管理后台和小程序等。在这些项目中,我设计过单/多租户体系系统,对接过许多开放平台,也搞过消息中心这类较为复杂的应用,但幸运的是,我至今还没有遇到过线上系统由于代码崩溃导致资损的情况。这其中的原因有三点:一是业务系统本身并不复杂;二是我一直遵循某大厂代码规约,在开发过程中尽可能按规约编写代码;三是经过多年的开发经验积累,我成为了一名熟练工,掌握了一些实用的技巧。

在后端开发中,我们经常处理增删改查的接口,这些操作已经非常熟悉了。然而,有时产品经理会要求增加一个表格数据的导入和导出功能,让用户可以离线处理数据。这类操作也很常见,但处理起来不太简单。尽管一些前端表格组件可以直接实现这类功能,但往往不够灵活,因为前端的数据通常已经过处理。如果要获取原始数据,还是得依靠后端处理。

开源的框架也有,如Apache POI、Alibaba EasyExcel等,我使用的最多的是Alibaba EasyExcel框架,非常好用,这里介绍一下我平时是怎么使用它的。

二、组件介绍

首先放出官网地址,欢迎大家star(目前已经24K): https://alibaba-easyexcel.github.io/docs/current/

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

64M内存20秒读取75M(46W行25列)的Excel(3.0.2+版本)

Alibaba EasyExcel的核心类是EasyExcel

java 复制代码
  /**
     * 最简单的读
     * <p>1. 创建excel对应的实体对象 参照{@link DemoData}
     * <p>2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link DemoDataListener}
     * <p>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();
    }
java 复制代码
/**
     * 最简单的写
     * <p>1. 创建excel对应的实体对象 参照{@link com.alibaba.easyexcel.test.demo.write.DemoData}
     * <p>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());
    }

三、尝试一下

maven导入

xml 复制代码
<!-- easyexcel -->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>easyexcel</artifactId>
  <version>3.1.1</version>
</dependency>

1. 表格导出

(1)测试类代码

ExportController.java

java 复制代码
package com.example.springbooteasyexcel.controller;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.example.springbooteasyexcel.data.Mock;
import com.example.springbooteasyexcel.sheet.CitySheet;
import com.example.springbooteasyexcel.sheet.CompanySheet;
import com.example.springbooteasyexcel.sheet.UserSheet;
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;

@RestController
@RequestMapping("/export")
public class ExportController {

    /**
     * @param response
     * @url <a>http://localhost:8080/export/test1</a>
     * 在Excel中写入单个sheet
     */
    @RequestMapping("/test1")
    public void test1(HttpServletResponse response) {
        //从HttpServletResponse中获取OutputStream输出流
        try {
            // 设置响应类型
            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");
            // 写出Excel
            EasyExcel.write(response.getOutputStream(), UserSheet.class).inMemory(true).sheet("用户信息表").doWrite(Mock.userList());
        } catch (IOException e) {
            throw new RuntimeException("数据或文件损坏,无法下载");
        }
    }

    /**
     * 在Excel中写入多个sheet
     *
     * @url <a>http://localhost:8080/export/test2</a>
     */
    @RequestMapping("/test2")
    public void test2(HttpServletResponse response) throws Exception {
        // 设置响应类型
        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(UserSheet.class).build();
            excelWriter.write(Mock.userList(), userInfoSheet);
            // 创建城市信息表的sheet,写入城市信息数据,2代表sheet的位置是第二个
            WriteSheet cityInfoSheet = EasyExcel.writerSheet(1, "城市信息表").head(CitySheet.class).build();
            excelWriter.write(Mock.cityList(), cityInfoSheet);
            // 创建公司信息表的sheet,写入公司信息数据,3代表sheet的位置是第三个
            WriteSheet companyInfoSheet = EasyExcel.writerSheet(2, "公司信息表").head(CompanySheet.class).build();
            excelWriter.write(Mock.companyList(), companyInfoSheet);
        }
    }
}

Mock.java

java 复制代码
package com.example.springbooteasyexcel.data;


import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.data.RichTextStringData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.example.springbooteasyexcel.sheet.CitySheet;
import com.example.springbooteasyexcel.sheet.CompanySheet;
import com.example.springbooteasyexcel.sheet.UserSheet;
import org.apache.poi.ss.usermodel.IndexedColors;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
/**
 * 以下数据均来自于网络,如有侵权,请联系删除
 */
public class Mock {

    public static List<UserSheet> userList() {
        List<UserSheet> list = new ArrayList<>(10);
        list.add(UserSheet.builder().userId(001L).userName("张三").userPhone("11112223123").userEmail("zhansan@163.com").userAddress("北京朝阳区").gender(buildCellData("男")).registerTime(Calendar.getInstance().getTime()).build());
        list.add(UserSheet.builder().userId(002L).userName("李四").userPhone("11112223123").userEmail("lisi@qq.com").userAddress("南京玄武门").gender(buildCellData("女")).registerTime(Calendar.getInstance().getTime()).build());
        list.add(UserSheet.builder().userId(003L).userName("王五").userPhone("11112223123").userEmail("wangwu@google.com").userAddress("杭州未来科技城").gender(buildCellData("男")).registerTime(Calendar.getInstance().getTime()).build());
        list.add(UserSheet.builder().userId(004L).userName("赵六").userPhone("11112223123").userEmail("zhaoliu@baidu.com").userAddress("上海徐家汇").gender(buildCellData("女")).registerTime(Calendar.getInstance().getTime()).build());
        return list;
    }

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

    public static List<CitySheet> cityList() {
        List<CitySheet> list = new ArrayList<>(10);
        list.add(CitySheet.builder().cityName("杭州市").cityDesc("杭州市一般指杭州。 杭州,简称"杭",古称临安、钱塘,浙江省辖地级市、省会、副省级市、特大城市、国务院批复确定的浙江省经济、文化、科教中心,长江三角洲中心城市之一,环杭州湾大湾区核心城市、G60科创走廊中心城市。").build());
        list.add(CitySheet.builder().cityName("合肥市").cityDesc("合肥市一般指合肥。 合肥,简称"庐"或"合",古称庐州、庐阳、合淝,安徽省辖地级市、省会,是合肥都市圈中心城市,国务院批复确定的中国长三角城市群副中心城市,全国四大科教基地、现代制造业基地和综合交通枢纽。").build());
        list.add(CitySheet.builder().cityName("武汉市").cityDesc("武汉市一般指武汉。 武汉,简称"汉",别称江城,是湖北省省会,中部六省唯一的副省级市,超大城市,中国中部地区的中心城市,全国重要的工业基地、科教基地和综合交通枢纽,联勤保障部队机关驻地。").build());
        list.add(CitySheet.builder().cityName("深圳市").cityDesc("深圳市一般指深圳。 深圳,简称"深",别称鹏城,广东省辖地级市,是广东省副省级市,国家计划单列市,超大城市,国务院批复确定的中国经济特区、全国性经济中心城市、国际化城市、科技创新中心、区域金融中心、商贸物流中心。").build());
        return list;
    }

    public static List<CompanySheet> companyList() {
        List<CompanySheet> list = new ArrayList<>(10);
        list.add(CompanySheet.builder().companyName("阿里巴巴").companyBoss("马云").companyBase("杭州市").companyDesc("阿里巴巴集团经营多项业务,另外也从关联公司的业务和服务中取得经营商业生态系统上的支援。业务和关联公司的业务包括:淘宝网、天猫、聚划算、全球速卖通、阿里巴巴国际交易市场、1688、阿里妈妈、阿里云、蚂蚁集团 [408]  、菜鸟网络等。").build());
        list.add(CompanySheet.builder().companyName("字节跳动").companyBoss("张一鸣").companyBase("北京市").companyDesc("字节跳动的全球化布局始于2015年 [3]  ,"技术出海"是字节跳动全球化发展的核心战略 [4]  ,其旗下产品有今日头条、西瓜视频、抖音、头条百科、皮皮虾、懂车帝、悟空问答等。").build());
        list.add(CompanySheet.builder().companyName("腾讯").companyBoss("马化腾").companyBase("深圳市").companyDesc("社交和通信服务QQ及微信/WeChat、社交网络平台QQ空间、腾讯游戏旗下QQ游戏平台、门户网站腾讯网、腾讯新闻客户端和网络视频服务腾讯视频等。").build());
        list.add(CompanySheet.builder().companyName("百度").companyBoss("李彦宏").companyBase("北京市").companyDesc("百度(Baidu)是拥有强大互联网基础的领先AI公司。百度愿景是:成为最懂用户,并能帮助人们成长的全球顶级高科技公司。").build());
        return list;
    }
}

CitySheet.java

java 复制代码
package com.example.springbooteasyexcel.sheet;

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class CitySheet {

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

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

}

CompanySheet.java

java 复制代码
package com.example.springbooteasyexcel.sheet;


import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class CompanySheet {

    @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;
}

UserSheet.java

java 复制代码
package com.example.springbooteasyexcel.sheet;

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

    @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;
}

(2)效果展示

单个sheet&表头合并

多个sheet导出

2. 表格导入

(1)导入逻辑简析

Excel导入到数据库,我用一张流程图展示一下

EasyExcel有一个AnalysisEventListener,可以自定义一个Listener继承AnalysisEventListener,里面有一个invoke方法,每条数据都会进入这个方法。我们可以在这里做校验、存储、抛异常等动作,EasyExcel将这些都流程化了,写起代码来非常舒服。当然也有一些点需要注意下,比如自定义Listener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去。

(2)测试类代码

UserData.java

java 复制代码
package com.example.springbooteasyexcel.read;

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;

}

UserExcelReadListener.java

java 复制代码
package com.example.springbooteasyexcel.read;

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

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;

/**
 * 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
 */
@Slf4j
public class UserExcelReadListener extends AnalysisEventListener<UserData> {

    /**
     * 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 100;

    /**
     * 创建一个Pattern对象,使用正则表达式校验手机号格式
     */
    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<>(BATCH_COUNT);

    @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了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
        if (cachedDataList.size() >= BATCH_COUNT) {
            // to saveData();
            // 存储完成清理 list
            cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
        }
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        log.info("所有数据解析完成!全部校验通过的数据有{}条", cachedDataList.size());
        // 这里也要保存数据,确保最后遗留的数据也存储到数据库saveData();
        // todo saveData();

    }

    @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条");
        }
    }

    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 Boolean.FALSE;
        }
        return Boolean.TRUE;
    }

    private Boolean phoneValid(int rowIndex, String phone) {
        if (StringUtils.isBlank(phone)) {
            errorMsgList.add("第" + rowIndex + "行,'手机号'不能为空");
            return Boolean.FALSE;
        }
        return Boolean.TRUE;
    }

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

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

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

    /**
     * 邮箱的校验
     *
     * @param rowIndex 行数
     * @param email    邮箱
     */
    private Boolean emailValid(int rowIndex, String email) {
        // 校验邮箱是否为空
        if (StringUtils.isBlank(email)) {
            errorMsgList.add("第 " + rowIndex + " 行'邮箱'不能为空");
            return Boolean.FALSE;
        }
        return Boolean.TRUE;
    }
}

ReadExcelTest.java

java 复制代码
package com.example.springbooteasyexcel.read;

import com.alibaba.excel.EasyExcel;

public class ReadExcelTest {

    public static void main(String[] args) {
        UserExcelReadListener userExcelReadListener = new UserExcelReadListener();
        EasyExcel.read("用户信息表.xlsx", UserData.class, userExcelReadListener).sheet().doRead();
        System.out.println(userExcelReadListener.getErrorMsgList());
    }
}

执行结果

四、总结一下

第一点

Alibaba EasyExcel不仅支持写Excel,还支持读Excel和填充Excel,有兴趣的话可以自己去研究,官网地址已经贴在上面了,我这里只做一个引路的。

第二点

常用注解有三个@ExcelProperty@ColumnWidth@ExcelIgnore

@ExcelProperty不仅确定表头,还可以合并行

@ColumnWidth主要是控制列宽

@ExcelIgnore忽略不需要输出的字段

用法如下:

java 复制代码
  @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;

效果如下:

第三点

写有两种形式

(1)写到文件

java 复制代码
  /**
     * 最简单的写
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link DemoData}
     * <p>
     * 2. 直接写即可
     */
    @Test
    public void simpleWrite() {
        // 注意 simpleWrite在数据量不大的情况下可以使用(5000以内,具体也要看实际情况),数据量大参照 重复多次写入

        // 写法1 JDK8+
        // since: 3.0.0-beta1
        String fileName = TestFileUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx";
        // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        // 如果这里想使用03 则 传入excelType参数即可
        EasyExcel.write(fileName, DemoData.class)
            .sheet("模板")
            .doWrite(() -> {
                // 分页查询数据
                return data();
            });

        // 写法2
        fileName = TestFileUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx";
        // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        // 如果这里想使用03 则 传入excelType参数即可
        EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());

        // 写法3
        fileName = TestFileUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx";
        // 这里 需要指定写用哪个class去写
        try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) {
            WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
            excelWriter.write(data(), writeSheet);
        }
    }

(2)写到Web流

这里的ContentType和CharacterEncoding不要乱码,否则很容易乱码或者文件损坏

java 复制代码
 /**
     * 文件下载(失败了会返回一个有部分数据的Excel)
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link DownloadData}
     * <p>
     * 2. 设置返回的 参数
     * <p>
     * 3. 直接写,这里注意,finish的时候会自动关闭OutputStream,当然你外面再关闭流问题不大
     */
    @GetMapping("download")
    public void download(HttpServletResponse response) throws IOException {
        // 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
        String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
        EasyExcel.write(response.getOutputStream(), DownloadData.class).sheet("模板").doWrite(data());
    }

写到最后,用好自定义Listener可以很好的优化代码,并且逻辑也可以变得更清晰。

相关推荐
中国胖子风清扬29 分钟前
Rust 日志库完全指南:从入门到精通
spring boot·后端·rust·学习方法·logback
玉衡子34 分钟前
MySQL基础架构全面解析
数据库·后端
快乐肚皮35 分钟前
fencing token机制
java·fencing token
程序新视界36 分钟前
在职场,尽量不要成为这样的“人才”
面试·求职
叶落阁主44 分钟前
Neovim 插件 i18n.nvim 介绍
java·vue.js·vim
渣哥1 小时前
让集合线程安全的几种靠谱方法
java
郭京京1 小时前
goweb内置的 net/http 包
后端·go
dylan_QAQ1 小时前
Java转Go全过程06-工程管理
java·后端·go
小奋斗1 小时前
以Chrome 为代表的浏览器架构详解
面试·程序员
用户4099322502121 小时前
如何用FastAPI玩转多模块测试与异步任务,让代码不再“闹脾气”?
后端·ai编程·trae