文章目录

1.Apache POI

Apache POI是Apache软件基金会的开源函式库,提供跨平台的Java API实现Microsoft Office格式档案读写。但是存在如下一些问题:

1.1 学习使用成本较高

对POI有过深入了解的才知道原来POI还有SAX模式(Dom解析模式)。但SAX模式相对比较复杂,excel有03和07两种版本,两个版本数据存储方式截然不同,sax解析方式也各不一样。

想要了解清楚这两种解析方式,才去写代码测试,估计两天时间是需要的。再加上即使解析完,要转换到自己业务模型还要很多繁琐的代码。总体下来感觉至少需要三天,由于代码复杂,后续维护成本巨大。

POI的SAX模式的API可以一定程度的解决一些内存溢出的问题,但是POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大,一个3M的Excel用POI的SAX解析,依然需要100M左右内存。

1.2 POI的内存消耗较大

大部分使用POI都是使用他的userModel模式。userModel的好处是上手容易使用简单,随便拷贝个代码跑一下,剩下就是写业务转换了,虽然转换也要写上百行代码,相对比较好理解。然而userModel模式最大的问题是在于非常大的内存消耗,一个几兆的文件解析要用掉上百兆的内存。现在很多应用采用这种模式,之所以还正常在跑一定是并发不大,并发上来后一定会OOM或者频繁的full gc。

总体上来说,简单写法重度依赖内存,复杂写法学习成本高。

1.3 特点

  1. 功能强大

  2. 代码书写冗余繁杂

  3. 读写大文件耗费内存较大,容易OOM

2. 初识EasyExcel

2.1 重写了POI对07版Excel的解析

  • EasyExcel重写了POI对07版Excel的解析,可以把内存消耗从100M左右降低到10M以内,并且再大的Excel不会出现内存溢出,03版仍依赖POI的SAX模式。

  • 下图为64M内存1分钟内读取75M(46W行25列)的Excel(当然还有急速模式能更快,但是内存占用会在100M多一点)

    img

  • 在上层做了模型转换的封装,让使用者更加简单方便

2.2 特点

  1. 在数据模型层面进行了封装,使用简单
  2. 重写了07版本的Excel的解析代码,降低内存消耗,能有效避免OOM
  3. 只能操作Excel
  4. 不能读取图片

3.快速入门

3.1 导入依赖坐标

<!-- EasyExcel -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>2.1.6</version>
</dependency>
<!-- lombok 优雅编程 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.10</version>
</dependency>
<!-- junit -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>

3.2 最简单的读

3.2.1 需求、准备工作

/**
 * 需求:单实体导入
 * 导入Excel学员信息到系统。
 * 包含如下列:姓名、出生日期、性别
 * 模板:逐浪教育学员信息表.xls文件
 */

3.2.2 编写导出数据的实体

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * 学生实体类
 * lombok:通过一个插件 + 一个依赖 ,就可以在编译的时候自动帮助生成实体类常用方法
 *
 * @author 狐狸半面添
 * @create 2023-02-26 14:56
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
    /**
     * 学生姓名
     */
    private String name;

    /**
     * 学生出生日期
     */
    private Date birthday;

    /**
     * 学生性别
     */
    private String gender;

    /**
     * id
     */
    private String id;
}

3.2.3 读取Excel的监听器,用于处理读取产生的数据

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.fox.easyexcel.domain.Student;

/**
 * 读取文档的监听器类
 *
 * @author 狐狸半面添
 * @create 2023-02-26 15:10
 */
public class StudentListener extends AnalysisEventListener<Student> {
    /**
     * 读监听器,每读一行内容,都会调用一次invoke,在invoke可以操作使用读取到的数据
     *
     * @param student 每次读取到的数据封装的对象
     * @param analysisContext
     */
    public void invoke(Student student, AnalysisContext analysisContext) {
        System.out.println(student);
    }

    /**
     * 读取完整个文档之后,调用的方法
     *
     * @param analysisContext
     */
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        // todo
    }
}

3.2.4 读取Excel文件

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.read.builder.ExcelReaderBuilder;
import com.alibaba.excel.read.builder.ExcelReaderSheetBuilder;
import com.fox.easyexcel.domain.Student;
import com.fox.easyexcel.listener.StudentListener;
import org.junit.Test;

/**
 * @author 狐狸半面添
 * @create 2023-02-26 15:03
 */
public class ExcelTest {
    /**
     * 工作簿:一个excel文件就是一个工作簿
     * 工作表:一个工作簿可以有多个工作表(sheet)
     */
    @Test
    public void test01() {
        /*
            1.获得一个工作簿对象
            构建一个读的工作簿对象,参数说明:
                - pathName:要读的文件的路径
                - head:文件中每一行数据要存储到的实体的类型的class
                - readListener:读监听器,每读一行内容,都会调用一次该对象的invoke,在invoke可以操作使用读取到的数据
         */
        ExcelReaderBuilder readWorkBook = EasyExcel.read("逐浪教育学员信息表.xlsx", Student.class, new StudentListener());

        // 2.获得一个工作表对象,默认读取第一个工作表
        ExcelReaderSheetBuilder sheet = readWorkBook.sheet();

        // 3.读取工作表中的内容
        sheet.doRead();
    }
}

3.3 最简单的写

3.3.1 需求、准备工作

/**
 * 需求:单实体导出
 * 导出多个学生对象到Excel表格
 * 包含如下列:姓名、出生日期、性别
 * 模板详见:逐浪教育学员信息表.xlsx
 */

3.3.2 编写导出数据的实体

import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * 学生实体类
 * lombok:通过一个插件 + 一个依赖 ,就可以在编译的时候自动帮助生成实体类常用方法
 * 注解 @ContentRowHeight():内容的行高
 * 注解 @HeadRowHeight:表头的行高
 *
 * @author 狐狸半面添
 * @create 2023-02-26 14:56
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
    /**
     * 学生姓名
     */
    @ExcelProperty("学生姓名")
    @ColumnWidth(20)
    private String name;

    /**
     * 学生出生日期
     */
    @ExcelProperty("出生日期")
    @ColumnWidth(20)
    private Date birthday;

    /**
     * index 从0开始
     * 学生性别
     */
    @ExcelProperty(value = "学生性别", index = 1)
    private String gender;

    /**
     * id
     */
    @ExcelIgnore
    private String id;
}

3.3.3 准备数据并写入到文件

    @Test
    public void test02() {
        /*
            1.构建一个写的工作簿对象
                - pathName:要写入的文件路径
                - head:封装写入的数据的实体类型
         */
        ExcelWriterBuilder writeWorkBook = EasyExcel.write("逐浪教育学员信息表.xlsx", Student.class);

        // 2.获取工作表对象,默认是第一个工作表
        ExcelWriterSheetBuilder sheet = writeWorkBook.sheet();

        // 3.生成十个测试对象
        ArrayList<Student> students = new ArrayList<Student>();
        for (int i = 1; i <= 10; i++) {
            students.add(new Student("逐浪者-" + i, new Date(), "男", null));
        }

        // 4.将数据写入工作表
        sheet.doWrite(students);
    }

3.4 文件上传和下载

基于SpringMVC的文件上传和下载

3.4.1 导入依赖

        <!-- EasyExcel -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>2.1.6</version>
        </dependency>
        <!-- lombok 优雅编程 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
        </dependency>
        <!-- junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.6.3</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

3.4.2 文件上传

🍀 编写excel中每一行对应的实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
    /**
     * 学生姓名
     */
    private String name;
    /**
     * 学生性别
     */
    private String gender;

    /**
     * 学生出生日期
     */
    private Date birthday;
    /**
     * id
     */
    private String id;
}
🍀 回调监听器StudentReadListener
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.fox.easyexcel.domain.Student;
import com.fox.easyexcel.service.StudentService;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.ArrayList;

/**
 * 读取文档的监听器类
 *
 * @author 狐狸半面添
 * @create 2023-02-26 15:10
 */
@Component
@Scope("prototype") //作者要求每次读取都要使用新的Listener
public class StudentListener extends AnalysisEventListener<Student> {
    @Resource
    private StudentService studentService;

    public static final ThreadLocal<ArrayList<Student>> threadLocal = new ThreadLocal<ArrayList<Student>>();

    /**
     * 读监听器,每读一行内容,都会调用一次invoke,在invoke可以操作使用读取到的数据
     *
     * @param student         每次读取到的数据封装的对象
     * @param analysisContext
     */
    public void invoke(Student student, AnalysisContext analysisContext) {
        ArrayList<Student> students = threadLocal.get();
        if (students == null) {
            threadLocal.set(new ArrayList<Student>());
            students = threadLocal.get();
        }
        students.add(student);
        if (students.size() == 5) {
            studentService.save(students);
            students.clear();
        }
    }

    /**
     * 读取完整个文档之后,调用的方法
     *
     * @param analysisContext
     */
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        // todo
    }
}
🍀 业务代码接口StudentService和实现类StudentServiceImpl
import com.fox.easyexcel.domain.Student;
import java.util.ArrayList;

/**
 * @author 狐狸半面添
 * @create 2023-02-26 16:27
 */
public interface StudentService {
    /**
     * 保存学生信息
     *
     * @param students 信息列表
     */
    void save(ArrayList<Student> students);
}
import com.fox.easyexcel.domain.Student;
import com.fox.easyexcel.service.StudentService;
import org.springframework.stereotype.Service;

import java.util.ArrayList;

/**
 * @author 狐狸半面添
 * @create 2023-02-26 16:27
 */
@Service
public class StudentServiceImpl implements StudentService {
    @Override
    public void save(ArrayList<Student> students) {
        System.out.println("save to database = " + students);
    }
}
🍀 读取上传的Excel文件
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.read.builder.ExcelReaderBuilder;
import com.alibaba.excel.read.builder.ExcelReaderSheetBuilder;
import com.fox.easyexcel.domain.Student;
import com.fox.easyexcel.listener.StudentListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;


/**
 * @author 狐狸半面添
 * @create 2023-02-26 16:29
 */
@RestController
@RequestMapping("/student")
@Slf4j
public class StudentController {
    @Resource
    private StudentListener studentListener;

    @PostMapping("/read")
    public String readExcel(MultipartFile uploadExcel) {
        try {
            // 1.获取工作簿
            ExcelReaderBuilder readWorkBook = EasyExcel.read(uploadExcel.getInputStream(), Student.class, studentListener);
            // 2.获取工作表
            ExcelReaderSheetBuilder sheet = readWorkBook.sheet();
            // 3.读取数据
            sheet.doRead();
            // 4.释放线程
            StudentListener.threadLocal.remove();
            return "success";
        } catch (Exception e) {
            log.error("读取文件失败:{}", e.getMessage());
            return "fail";
        }
    }
}
🍀 ApiFox测试

image-20230226172545893

3.4.3 文件下载

🍀 编写实体类并创建对象以便写入表格
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * 学生实体类
 * lombok:通过一个插件 + 一个依赖 ,就可以在编译的时候自动帮助生成实体类常用方法
 * 注解 @ContentRowHeight():内容的行高
 * 注解 @HeadRowHeight:表头的行高
 *
 * @author 狐狸半面添
 * @create 2023-02-26 14:56
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {

    /**
     * id
     */
    @ExcelIgnore
    private String id;
    /**
     * 学生姓名
     */
    @ExcelProperty("学生姓名")
    private String name;
    /**
     * 学生性别
     */
    @ExcelProperty("学生性别")
    private String gender;

    /**
     * 学生出生日期
     */
    @ExcelProperty("学生出生日期")
    private Date birthday;
}
🍀 将数据写入到响应体实现下载
    /**
     * 文件下载
     * 1. 编写实体类并创建对象以便写入表格
     * 2. 设置响应参数:文件的ContentType和文件名,同时设置编码避免乱码
     * 3. 直接写,内部会调用finish方法自动关闭OutputStream
     */
    @GetMapping("/download")
    public void download(HttpServletResponse response) throws IOException {
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("utf-8");
        // 防止中文乱码:import java.net.URLEncoder
        String fileName = URLEncoder.encode("测试", "UTF-8");
        response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName + ".xlsx");
        // 创建文件簿
        ExcelWriterBuilder workBook = EasyExcel.write(response.getOutputStream(), Student.class);
        // 创建一个文件表
        ExcelWriterSheetBuilder sheet = workBook.sheet("模板");
        // 生成测试数据
        ArrayList<Student> students = new ArrayList<Student>();
        for (int i = 1; i <= 10; i++) {
            students.add(new Student(null, "逐浪者-" + i, "男", new Date()));
        }
        // 写入,并且会关闭流
        sheet.doWrite(students);
    }
🍀 浏览器测试

💻 访问:http://localhost:8080/student/download

image-20230226172823971

image-20230226172838437

3.5 自定义单元格样式

EasyExcel支持调整行高、列宽、背景色、字体大小等内容,但是控制方式与使用原生POI无异,比较繁琐,不建议使用。

但是可以使用模板填充的方式,向预设样式的表格中直接写入数据,写入数据的时候会保持原有样式。

4.填充

4.1 填充一组数据

🍀 准备模板

​ Excel表格中用{} 来表示包裹要填充的变量,如果单元格文本中本来就有{}左右大括号,需要在括号前面使用斜杠转义\{\}

​ 代码中被填充数据的实体对象的成员变量名或被填充map集合的key需要和Excel中被{}包裹的变量名称一致。

🍀 封装数据

编写封装填充数据的类或选用Map

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 使用实体类封装填充数据
 * 实体中成员变量名称需要和Excel表各种{}包裹的变量名匹配
 *
 * @author 狐狸半面添
 * @create 2023-02-26 17:32
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FillData {
    /**
     * 姓名
     */
    private String name;
    /**
     * 年龄
     */
    private int age;
}

🍀 填充

准备数据并填充到文件

    /**
     * 单组数据填充:封装类的方式
     */
    @Test
    public void test03() {
        // 1.准备模板
        String templatePath = FillData.class.getResource("/fill_data_template1.xlsx").getPath();

        // 2.创建一个工作簿对象
        ExcelWriterBuilder writeWorkBook = EasyExcel.write("excel填充-单组数据.xlsx", FillData.class)
                .withTemplate(templatePath);

        // 3.创建工作表对象
        ExcelWriterSheetBuilder sheet1 = writeWorkBook.sheet("sheet1");

        // 4.准备数据
        FillData fillData = new FillData("逐浪者", 19);

        // 5.填充数据,并关闭流
        sheet1.doFill(fillData);

    }

    /**
     * 单组数据填充:map的方式
     */
    @Test
    public void test04() {
        // 1.准备模板
        String templatePath = FillData.class.getResource("/fill_data_template1.xlsx").getPath();

        // 2.创建一个工作簿对象
        ExcelWriterBuilder writeWorkBook = EasyExcel.write("excel填充-单组数据.xlsx", FillData.class)
                .withTemplate(templatePath);

        // 3.创建工作表对象
        ExcelWriterSheetBuilder sheet1 = writeWorkBook.sheet("sheet1");

        // 4.准备数据:使用Map数据填充
        HashMap<String, Object> mapFillData = new HashMap<>();
        mapFillData.put("name", "狐狸半面添");
        mapFillData.put("age", 18);

        // 5.填充数据,并关闭流
        sheet1.doFill(mapFillData);
    }

🍀 测试效果

image-20230226180655449

4.2 填充多组数据

🍀 准备模板

​ Excel表格中用{.} 来表示包裹要填充的变量,如果单元格文本中本来就有{}左右大括号,需要在括号前面使用斜杠转义\{\}

​ 代码中被填充数据的实体对象的成员变量名或被填充map集合的key需要和Excel中被{}包裹的变量名称一致。

🍀 封装数据

编写封装填充数据的类或选用Map

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 使用实体类封装填充数据
 * 实体中成员变量名称需要和Excel表各种{}包裹的变量名匹配
 *
 * @author 狐狸半面添
 * @create 2023-02-26 17:32
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FillData {
    /**
     * 姓名
     */
    private String name;
    /**
     * 年龄
     */
    private int age;
}

🍀 填充

准备数据并填充到文件

    /**
     * 多组数据填充:封装类的方式
     */
    @Test
    public void test05() {
        // 1.准备模板
        String templatePath = FillData.class.getResource("/fill_data_template2.xlsx").getPath();

        // 2.创建一个工作簿对象
        ExcelWriterBuilder writeWorkBook = EasyExcel.write("excel填充-多组数据.xlsx", FillData.class)
                .withTemplate(templatePath);

        // 3.创建工作表对象
        ExcelWriterSheetBuilder sheet1 = writeWorkBook.sheet("sheet1");

        // 4.准备数据
        ArrayList<FillData> data = new ArrayList<>();
        for (int i = 15; i <= 30; i++) {
            data.add(new FillData("逐浪者-" + i, i));
        }

        // 5.填充数据,并关闭流
        sheet1.doFill(data);
    }

🍀 测试效果

image-20230226181652636

4.3 组合填充

🍀 准备模板

即有多组数据填充,又有单一数据填充,为了避免两者数据出现冲突覆盖的情况,在多组填充时需要通过FillConfig对象设置换行。

1584207785924

🍀 封装数据

编写封装填充数据的类或选用Map

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 使用实体类封装填充数据
 * 实体中成员变量名称需要和Excel表各种{}包裹的变量名匹配
 *
 * @author 狐狸半面添
 * @create 2023-02-26 17:32
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FillData {
    /**
     * 姓名
     */
    private String name;
    /**
     * 年龄
     */
    private int age;
}

🍀 填充

准备数据并填充到文件

    /**
     * 组合数据填充
     */
    @Test
    public void test06() {
        // 1.准备模板
        String templatePath = FillData.class.getResource("/fill_data_template3.xlsx").getPath();

        // 2.创建一个工作簿对象
        ExcelWriter workBook = EasyExcel.write("excel填充-组合数据.xlsx", FillData.class)
                .withTemplate(templatePath).build();

        // 3.创建工作表对象
        WriteSheet sheet = EasyExcel.writerSheet().build();

        // 组合填充时,因为多组填充的数据量不确定,需要在多组填充完之后另起一行
        FillConfig fillConfig = FillConfig.builder().forceNewRow(true).build();


        // 4.准备数据
        ArrayList<FillData> data = new ArrayList<>();
        for (int i = 15; i <= 30; i++) {
            data.add(new FillData("逐浪者-" + i, i));
        }
        HashMap<String, String> dateAndTotal = new HashMap<>();
        dateAndTotal.put("date", "2023-02-26");
        dateAndTotal.put("total", "10000");

        // 5.填充数据,会一并关闭流
//        sheet1.doFill(data);

        // 多组填充
        workBook.fill(data, fillConfig, sheet);
        // 单组填充
        workBook.fill(dateAndTotal, sheet);

        // 关闭流
        workBook.finish();
    }

🍀 测试效果

image-20230226183019237

4.4 水平填充

🍀 准备模板

水平填充和多组填充模板一样,不一样的地方在于,填充时需要通过FillConfig对象设置水平填充。

🍀 封装数据

编写封装填充数据的类或选用Map

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 使用实体类封装填充数据
 * 实体中成员变量名称需要和Excel表各种{}包裹的变量名匹配
 *
 * @author 狐狸半面添
 * @create 2023-02-26 17:32
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FillData {
    /**
     * 姓名
     */
    private String name;
    /**
     * 年龄
     */
    private int age;
}

🍀 填充

准备数据并填充到文件

    /**
     * 水平填充
     */
    @Test
    public void test07() {
        // 1.准备模板
        String templatePath = FillData.class.getResource("/fill_data_template4.xlsx").getPath();

        // 2.创建一个工作簿对象
        ExcelWriter workBook = EasyExcel.write("excel填充-水平填充.xlsx", FillData.class)
                .withTemplate(templatePath).build();

        // 3.创建工作表对象
        WriteSheet sheet = EasyExcel.writerSheet().build();



        // 4.数据准备
        ArrayList<FillData> data = new ArrayList<>();
        for (int i = 15; i <= 30; i++) {
            data.add(new FillData("逐浪者-" + i, i));
        }

        // 换行
        FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
        // 多组填充
        workBook.fill(data, fillConfig, sheet);
        // 关闭流
        workBook.finish();
    }

🍀 测试效果

image-20230226212851557

4.5 注意事项

​ 为了节省内存,所以没有采用把整个文档在内存中组织好之后再整体写入到文件的做法,而是采用的是一行一行写入的方式,不能实现删除和移动行,也不支持备注写入。多组数据写入的时候,如果需要新增行,只能在最后一行增加,不能在中间位置添加。

4.6 填充综合练习

image-20230226213147101

    /**
     * 报表导出
     */
    @Test
    public void test08() {
        // 1.准备模板
        String templatePath = FillData.class.getResource("/report_template.xlsx").getPath();

        // 2.创建一个工作簿对象
        ExcelWriter workBook = EasyExcel.write("报表.xlsx", FillData.class)
                .withTemplate(templatePath).build();

        // 3.创建工作表对象
        WriteSheet sheet = EasyExcel.writerSheet().build();

        // ****** 准备数据 *******
        // 日期
        HashMap<String, String> dateMap = new HashMap<>();
        dateMap.put("date", "2020-03-16");

        // 总会员数
        HashMap<String, String> totalCountMap = new HashMap<>();
        dateMap.put("totalCount", "1000");

        // 新增员数
        HashMap<String, String> increaseCountMap = new HashMap<>();
        dateMap.put("increaseCount", "100");

        // 本周新增会员数
        HashMap<String, String> increaseCountWeekMap = new HashMap<>();
        dateMap.put("increaseCountWeek", "50");

        // 本月新增会员数
        HashMap<String, String> increaseCountMonthMap = new HashMap<>();
        dateMap.put("increaseCountMonth", "100");


        // 新增会员数据
        List<Student> students = new ArrayList<>();
        for (int i = 1; i <= 10; i++) {
            students.add(new Student(null, "逐浪者-" + i, "男", new Date()));
        }
        // **** 准备数据结束****

        // 写入统计数据
        workBook.fill(dateMap, sheet);
        workBook.fill(totalCountMap, sheet);
        workBook.fill(increaseCountMap, sheet);
        workBook.fill(increaseCountWeekMap, sheet);
        workBook.fill(increaseCountMonthMap, sheet);
        // 写入新增会员
        workBook.fill(students, sheet);
        // 关闭流
        workBook.finish();
    }

image-20230226213743792

5.常用API及注解

5.1 常用类

  • EasyExcel:入口类,用于构建开始各种操作;
  • ExcelReaderBuilder:构建出一个ReadWorkbook对象,即一个工作簿对象,对应的是一个Excel文件;
  • ExcelWriterBuilder:构建出一个WriteWorkbook对象,即一个工作簿对象,对应的是一个Excel文件;
  • ExcelReaderSheetBuilder:构建出一个ReadSheet对象,即一个工作表的对象,对应的Excel中的每个sheet,一个工作簿可以有多个工作表;
  • ExcelWriterSheetBuilder:构建出一WriteSheet对象,即一个工作表的对象,对应的Excel中的每个sheet,一个工作簿可以有多个工作表;
  • ReadListener:在每一行读取完毕后都会调用ReadListener来处理数据,我们可以把调用service的代码可以写在其invoke方法内部;
  • WriteHandler:在每一个操作包括创建单元格、创建表格等都会调用WriteHandler来处理数据,对使用者透明不可见;

所有配置都是继承的。Workbook的配置会被Sheet继承。所以在用EasyExcel设置参数的时候,在EasyExcel…sheet()方法之前作用域是整个sheet,之后针对单个sheet。

5.2 读取时的注解

🍀 @ExcelProperty

使用位置:标准作用在成员变量上,吧实体类中属性和excel表中列关联起来。

可选属性:

属性名含义说明
index对应Excel表中的列数默认-1,建议指定时从0开始
value对应Excel表中的列头
converter成员变量转换器自定义转换器需要实Converter接口

使用效果:index属性可以指定当前字段对应excel中的哪一列,可以根据列名value去匹配,也可以不写。

如果不使用@ExcelProperty注解,成员变量从上到下的顺序,对应表格中从左到右的顺序;

**使用建议:**要么全部不写,要么全部用index,要么全部用名字去匹配,尽量不要三个混着用。

🍀 @ExcelIgnore

标注在成员变量上,默认所有字段都会和excel去匹配,加了这个注解会忽略该字段

🍀 @DateTimeFormat

标注在成员变量上,日期转换,代码中用String类型的成员变量去接收excel中日期格式的数据会调用这个注解。里面的value参照java.text.SimpleDateFormat

    /**
     * 学生出生日期
     */
    @ExcelProperty("出生日期")
    @ColumnWidth(20)
    @DateTimeFormat("yyyy-MM-dd")
    private Date birthday;

🍀 @NumberFormat

标注在成员变量上,数字转换,代码中用String类型的成员变量去接收excel数字格式的数据会调用这个注解。里面的value参照java.text.DecimalFormat

🍀 @ExcelIgnoreUnannotated

标注在类上。

不标注该注解时,默认类中所有成员变量都会参与读写,无论是否在成员变量上加了@ExcelProperty 的注解。

标注该注解后,类中的成员变量如果没有标注 @ExcelProperty 注解将不会参与读写。

5.3 读取时通用参数

ReadWorkbook,ReadSheet 都会有的参数,如果为空,默认使用上级。

  • converter 转换器,默认加载了很多转换器。也可以自定义。

  • readListener 监听器,在读取数据的过程中会不断的调用监听器。

  • headRowNumber 指定需要读表格的 列头行数。默认有一行头,也就是认为第二行开始起为数据。

  • headclazz 二选一。读取文件头对应的列表,会根据列表匹配数据。建议使用class,就是文件中每一行数据对应的代码中的实体类型。

  • clazzhead 二选一。读取文件的头对应的class,也可以使用注解。如果两个都不指定,则会读取全部数据。

  • autoTrim 字符串、表头等数据自动trim

  • password 读的时候是否需要使用密码

5.4 ReadWorkbook(工作簿对象)参数

  • excelType 当前excel的类型,读取时会自动判断,无需设置。
  • inputStreamfile二选一。建议使用file。
  • fileinputStream二选一。读取文件的文件。
  • autoCloseStream 自动关闭流。
  • readCache 默认小于5M用 内存,超过5M会使用 EhCache,不建议使用这个参数。
  • useDefaultListener @since 2.1.4 默认会加入ModelBuildEventListener 来帮忙转换成传入class的对象,设置成false后将不会协助转换对象,自定义的监听器会接收到Map<Integer,CellData>对象,如果还想继续接听到class对象,请调用readListener方法,加入自定义的beforeListenerModelBuildEventListener、 自定义的afterListener即可。

5.5 ReadSheet(工作表对象)参数

  • sheetNo 需要读取Sheet的编号,建议使用这个来指定读取哪个Sheet
  • sheetName 根据名字去匹配Sheet,excel 2003不支持根据名字去匹配

5.6 写入时的注解

5.6.1 @ExcelProperty

使用位置:标准作用在成员变量上

可选属性:

属性名含义说明
index对应Excel表中的列数默认-1,指定时建议从0开始
value对应Excel表中的列头
converter成员变量转换器自定义转换器需要实Converter接口

使用效果

  • index 指定写到第几列,如果不指定则根据成员变量位置排序;

  • value指定写入的列头,如果不指定则使用成员变量的名字作为列头;

    如果要设置复杂的头,可以为value指定多个值。

import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * 学生实体类
 * lombok:通过一个插件 + 一个依赖 ,就可以在编译的时候自动帮助生成实体类常用方法
 * 注解 @ContentRowHeight():内容的行高
 * 注解 @HeadRowHeight:表头的行高
 *
 * @author 狐狸半面添
 * @create 2023-02-26 14:56
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
    /**
     * 学生姓名
     */
    @ExcelProperty(value = {"学员信息表", "学生姓名"})
    @ColumnWidth(20)
    private String name;

    /**
     * 学生出生日期
     */
    @ExcelProperty(value = {"学员信息表", "出生日期"})
    @ColumnWidth(20)
    @DateTimeFormat("yyyy-MM-dd")
    private Date birthday;

    /**
     * index 从0开始
     * 学生性别
     */
    @ExcelProperty(value = {"学员信息表", "学员性别"})
    @ColumnWidth(20)
    private String gender;

    /**
     * id
     */
    @ExcelIgnore
    private String id;
}

image-20230226162052388

5.6.2 其他注解

基本和读取时一致

  • @ContentRowHeight():标注在类上或属性上,指定内容行高

  • @HeadRowHeight():标注在类上或属性上,指定列头行高

  • @ColumnWidth():标注在类上或属性上,指定列宽

  • @ExcelIgnore:默认所有字段都会写入excel,这个注解会忽略这个字段

  • DateTimeFormat:日期转换,将Date写到excel会调用这个注解。里面的value参照java.text.SimpleDateFormat

  • NumberFormat:数字转换,用Number写excel会调用这个注解。里面的value参照java.text.DecimalFormat

  • ExcelIgnoreUnannotated:默认不加 ExcelProperty 的注解的都会参与读写,加了不会参与

5.7 写入时通用参数

WriteWorkbookWriteSheet都会有的参数,如果为空,默认使用上级。

  • converter 转换器,默认加载了很多转换器。也可以自定义。

  • writeHandler 写的处理器。可以实现WorkbookWriteHandler,SheetWriteHandler,RowWriteHandler,CellWriteHandler,在写入excel的不同阶段会调用,对使用者透明不可见。

  • relativeHeadRowIndex 距离多少行后开始。也就是开头空几行

  • needHead 是否导出头

  • headclazz二选一。写入文件的头列表,建议使用class。

  • clazzhead二选一。写入文件的头对应的class,也可以使用注解。

  • autoTrim 字符串、表头等数据自动trim

5.8 WriteWorkbook(工作簿对象)参数

  • excelType 当前excel的类型,默认为xlsx

  • outputStreamfile二选一。写入文件的流

  • fileoutputStream二选一。写入的文件

  • templateInputStream 模板的文件流

  • templateFile 模板文件

  • autoCloseStream 自动关闭流。

  • password 写的时候是否需要使用密码

  • useDefaultStyle 写的时候是否是使用默认头

5.9 WriteSheet(工作表对象)参数

  • sheetNo 需要写入的编号。默认0

  • sheetName 需要些的Sheet名称,默认同sheetNo

6.easyexcel工具类

6.1 excel通用读取监听类

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

/**
 * excel通用读取监听类
 *
 * @author 狐狸半面添
 * @create 2023-02-26 15:10
 */
@Slf4j
@Getter
@NoArgsConstructor
public class ExcelListener<T> extends AnalysisEventListener<T> {

    /**
     * 自定义用于暂时存储data 可以通过实例获取该值
     */
    private final List<T> dataList = new ArrayList<>();

    /**
     * 每解析一行都会回调invoke()方法
     *
     * @param data 每一行的数据
     */
    @Override
    public void invoke(T data, AnalysisContext context) {
        dataList.add(data);
        log.info("读取的一条信息:{}", data);
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        log.info("{}条数据,解析完成", dataList.size());
    }
}

6.2 工具类

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.fox.easyexcel.listener.ExcelListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.List;
import java.util.Set;

/**
 * @author 狐狸半面添
 * @create 2023-02-26 21:50
 */
public class EasyExcelUtils {
    private static final Log log = LogFactory.getLog(EasyExcelUtils.class);

    /**
     * 单sheet版本Excel读取
     * 从Excel中读取文件,读取的文件是一个DTO类
     *
     * @param inputStream 文件流
     * @param clazz       行数据类型
     */
    public static <T> List<T> readExcelOneSheet(InputStream inputStream, final Class<?> clazz) {
        // 1.创建监听类
        ExcelListener<T> listener = new ExcelListener<>();
        // 2.构建工作簿对象的输入流
        ExcelReader excelReader = EasyExcel.read(inputStream, clazz, listener).build();
        // 3.构建工作表对象的输入流,默认是第一张工作表
        ReadSheet readSheet = EasyExcel.readSheet(0).build();
        // 4.读取信息,每读取一行都会调用监听类的 invoke 方法
        excelReader.read(readSheet);
        // 5.关闭流,如果不关闭,读的时候会创建临时文件,到时磁盘会崩的
        excelReader.finish();
        return listener.getDataList();
    }

    /**
     * 多sheet版本Excel读取
     *
     * @param <T>      行数据的类型
     * @param filePath 文件路径
     * @param clazz    行数据的类型
     * @return 所有信息
     */
    public static <T> List<T> readExcelAllSheet(String filePath, final Class<?> clazz) {
        ExcelListener<T> listener = new ExcelListener<>();
        // 读取全部sheet
        // 这里需要注意 ExcelListener的doAfterAllAnalysed 会在每个sheet读取完毕后调用一次。然后所有sheet都会往同一个DemoDataListener里面写
        EasyExcel.read(filePath, clazz, listener).doReadAll();
        return listener.getDataList();
    }


    /**
     * 网页上的下载导出,只有一个工作表
     *
     * @param fileName  文件名
     * @param clazz     类的字节码文件,行数据的类型
     * @param dataList  导出的数据
     * @param sheetName 工作表名
     * @param response  响应体
     * @throws IOException 异常对象
     */
    public static void writeWeb(String fileName, final Class<?> clazz, List<?> dataList, String sheetName, HttpServletResponse response) throws IOException {
        // 1.指定响应体内容类型
        response.setContentType("application/vnd.ms-excel");
        // 2.指定编码方式
        response.setCharacterEncoding("utf-8");
        // 3.URLEncoder.encode可以防止中文乱码:import java.net.URLEncoder
        fileName = URLEncoder.encode(fileName, "UTF-8");
        // 4.指定响应标头
        response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName + ".xlsx");
        // 5.获取工作簿对象的输出流
        ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build();
        // 6.设置工作表的名称
        if (!StringUtils.hasText(sheetName)) {
            sheetName = "sheet1";
        }
        // 7.指定写用哪个class去写
        WriteSheet writeSheet = EasyExcel.writerSheet(0, sheetName).head(clazz).build();
        // 8.将 dataList 中的数据逐行写入工作表中
        excelWriter.write(dataList, writeSheet);
        // 9.finish关闭流
        excelWriter.finish();
        // 10.关闭流
        response.getOutputStream().close();
    }

    public static <T> void writeExcelList(HttpServletResponse response, List<List<T>> data, String fileName, Class<?> clazz, String sheetName) throws Exception {
        OutputStream out = getOutputStream(fileName, response);
        ExcelWriterBuilder excelWriterBuilder = EasyExcel.write(out, clazz).excelType(ExcelTypeEnum.XLSX).registerWriteHandler(getDefaultHorizontalCellStyleStrategy());
        ExcelWriter excelWriter = excelWriterBuilder.build();
        ExcelWriterSheetBuilder excelWriterSheetBuilder;
        WriteSheet writeSheet;
        for (int i = 1; i <= data.size(); i++) {
            excelWriterSheetBuilder = new ExcelWriterSheetBuilder(excelWriter);
            excelWriterSheetBuilder.sheetNo(i);
            excelWriterSheetBuilder.sheetName(sheetName + i);
            writeSheet = excelWriterSheetBuilder.build();
            excelWriter.write(data.get(i - 1), writeSheet);
        }
        excelWriter.finish();
        out.close();
    }

    private static OutputStream getOutputStream(String fileName, HttpServletResponse response) throws Exception {
        fileName = URLEncoder.encode(fileName, "UTF-8");
        //  response.setContentType("application/vnd.ms-excel"); // .xls

        // .xlsx
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");

        response.setCharacterEncoding("utf8");

        response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName + ".xlsx");
        return response.getOutputStream();
    }

    /**
     * 获取默认表头内容的样式
     *
     * @return
     */
    private static HorizontalCellStyleStrategy getDefaultHorizontalCellStyleStrategy() {
        /** 表头样式 **/
        WriteCellStyle headWriteCellStyle = new WriteCellStyle();
        // 背景色(浅灰色)
        // 可以参考:https://www.cnblogs.com/vofill/p/11230387.html
        headWriteCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
        // 字体大小
        WriteFont headWriteFont = new WriteFont();
        headWriteFont.setFontHeightInPoints((short) 10);
        headWriteCellStyle.setWriteFont(headWriteFont);
        //设置表头居中对齐
        headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
        /** 内容样式 **/
        WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
        // 内容字体样式(名称、大小)
        WriteFont contentWriteFont = new WriteFont();
        contentWriteFont.setFontName("宋体");
        contentWriteFont.setFontHeightInPoints((short) 10);
        contentWriteCellStyle.setWriteFont(contentWriteFont);
//		//设置内容垂直居中对齐
//		contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
//		//设置内容水平居中对齐
//		contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
        // 设置边框样式
        contentWriteCellStyle.setBorderLeft(BorderStyle.THIN);
        contentWriteCellStyle.setBorderTop(BorderStyle.THIN);
        contentWriteCellStyle.setBorderRight(BorderStyle.THIN);
        contentWriteCellStyle.setBorderBottom(BorderStyle.THIN);
        // 头样式与内容样式合并
        return new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
    }

    /**
     * 导出 Excel到指定目录 :单个 sheet,带表头,
     *
     * @param tableData
     * @param fileName  导出的路径+文件名  例如:   file/test.xlsx
     * @param sheetName 导入文件的 sheet 名
     * @throws Exception
     */
    public static void writeExcelAutoColumnWidth(String fileName, List<?> tableData, String sheetName, Class<?> clazz) throws Exception {
        // 根据用户传入字段 假设我们要忽略 date
        EasyExcel.write(fileName, clazz)
                .sheet(sheetName)
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                .doWrite(tableData);
    }


    /**
     * 导出 Excel到指定目录 :单个 sheet,带表头,
     *
     * @param fileName  导出的路径+文件名  例如:   file/test.xlsx
     * @param tableData
     */
    public static void writeExcelWithOneSheet1(String fileName, List<?> tableData, String sheetName, Class<?> clazz, Set<String> excludeColumnFiledNames) {
        // 根据用户传入字段 假设我们要忽略 date
        EasyExcel.write(fileName, clazz)
                .excludeColumnFiledNames(excludeColumnFiledNames)
                .sheet(sheetName)
                .registerWriteHandler(styleWrite(false))
                .doWrite(tableData);
    }

    public static HorizontalCellStyleStrategy styleWrite(boolean isWrapped) {
        // 头的策略
        WriteCellStyle headWriteCellStyle = new WriteCellStyle();
        // 背景设置为红色
        // headWriteCellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
        WriteFont headWriteFont = new WriteFont();
        headWriteFont.setFontHeightInPoints((short) 18);
        headWriteCellStyle.setWriteFont(headWriteFont);
        // 内容的策略
        WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
        // 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.头默认了 FillPatternType所以可以不指定
        //contentWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
        // 背景绿色
        //contentWriteCellStyle.setFillForegroundColor(IndexedColors.GREEN.getIndex());
        WriteFont contentWriteFont = new WriteFont();
        // 字体大小
        contentWriteFont.setFontHeightInPoints((short) 11);
        //设置 自动换行
        contentWriteCellStyle.setWrapped(isWrapped);
        //设置 垂直居中
        contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        contentWriteCellStyle.setWriteFont(contentWriteFont);
        // 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现
        return new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);

        // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        //EasyExcel.write(fileName, DemoData.class).registerWriteHandler(horizontalCellStyleStrategy).sheet("模板")
        //    .doWrite(data());
    }
}
GitHub 加速计划 / ea / easyexcel
31.64 K
7.47 K
下载
快速、简洁、解决大文件内存溢出的java处理Excel工具
最近提交(Master分支:3 个月前 )
c42183df Bugfix 3 个月前
efa7dff6 * 重新加回 `commons-io` 3 个月前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐