EasyExcel的应用
一、简单使用
引入依赖:
这里我们可以使用最新的4.0.2
版本,也可以选择之前的稳定版本,3.1.x
以后的版本API
大致相同,新的版本也会向前兼容(3.1.x
之前的版本,部分API
可能在高版本被废弃),关于POI、JDK
版本适配问题,具体可参考官网-版本说明。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>4.0.2</version>
</dependency>
下载excel文件:
@GetMapping("/download")
public void excelDownload(HttpServletResponse response) throws IOException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
EasyExcel.write(response.getOutputStream(), Data.class).sheet("模板").doWrite(datas);
String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
}
读取excel文件:
@PostMapping("/read")
public void read(MultipartFile file) throws IOException {
1、这只是简单演示,一般不使用 doReadSync 方法,
此方法同步执行的,即它会阻塞当前线程,直到读取完整个Excel文件并返回所有数据。
读取大型文件时,可能会导致程序响应变慢或阻塞。
2、使用head映射字段时,该实体类上不能加 @Accessors 注解,加上此注解
会字段映射不成功。
3、一般会使用监听器 + doRead 方法实现excel文件的读取
List<Data> datas = EasyExcel.read(file.getInputStream()).sheet().head(Data.class).doReadSync();
System.out.println(datas);
}
二、常用注解
1、@ExcelProperty注解
这个注解应该是最常用的注解,通常用来映射字段跟excel的列名,有以下几个属性:
名称 | 默认值 | 描述 |
---|---|---|
value | 空 | 用于匹配excel中的头,必须全匹配,如果有多行头,会匹配最后一行头 |
order | Integer.MAX_VALUE | 优先级高于value ,会根据order 的顺序来匹配实体和excel中数据的顺序 |
index | -1 | 优先级高于value 和order ,会根据index 直接指定到excel中具体的哪一列 |
converter | 自动选择 | 指定当前字段用什么转换器,默认会自动选择。写的情况下只要实现com.alibaba.excel.converters.Converter#convertToExcelData(com.alibaba.excel.converters.WriteConverterContext<T>) 方法即可 |
注意:
1、如果没有特殊的调整一般,使用value属性就够了,在读取或者导出时都能匹配或者映射为对应的列名。
2、value 跟 index 可以在导出数据的时候配合使用,value指定列名,index指定该列的顺序,例如:@ExcelProperty(value = "性别",index = 3) 代表列名为 性别,导出到第三列的位置。 但是在导入时,如果设置了order属性,表示会根据指定列来匹配字段,例如上面就会将第三列匹配为性别字段,如果该列字段为空,或者字段类型不匹配就会报错,一般在读取数据时不会这么使用这个属性。3、order 属性代表按顺序匹配,比如说导出数据时,会按照字段上该属性的顺序,依次为列设置对应字段的值,比如order最小的,就匹配第一列的值,依次往后,在导出时也是一样,order最小的值,导出到第一列依次往后。
4、converter:自定义的类型转换器,该属性可以实现自定义处理类,这个功能通常用来在 Excel 数据与 Java 对象之间进行特定格式的转换,例如日期、布尔值、自定义对象等。
实现
Converter
接口
要自定义一个转换器,需要实现 EasyExcel 提供的Converter
接口。重写必要的方法
supportJavaTypeKey()
: 指定支持的 Java 数据类型。convertToExcelData()
: 将 Java 数据类型转换为 Excel 单元格数据。convertToJavaData()
: 将 Excel 单元格数据转换为 Java 数据类型EasyExcel 自带了一些常用的转换器(例如
LocalDateConverter
、IntegerConverter
等),可以直接使用而无需自定义。
例如:在姓名上加上该属性:
@ExcelProperty(value = "姓名",converter = ExcelStringConverter.class)
private String name;
实现自定义处理类:
public class ExcelStringConverter implements Converter<String> {
@Override
public Class<?> supportJavaTypeKey() {
return String.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
return cellData.getStringValue() + "导入数据进行处理!";
}
@Override
public WriteCellData<?> convertToExcelData(String value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
return new WriteCellData<>(value + "导出数据进行处理");
}
}
例如实现在 Excel 中用 "是"
和 "否"
表示布尔值,而不是默认的 true/false
。
public class BooleanStringConverter implements Converter<Boolean> {
@Override
public Class<?> supportJavaTypeKey() {
return Boolean.class; // 支持的 Java 类型
}
@Override
public WriteCellData<?> convertToExcelData(Boolean value, ExcelContentProperty contentProperty) {
return new WriteCellData<>(value ? "是" : "否"); // 将布尔值转为字符串
}
@Override
public Boolean convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty) {
String stringValue = cellData.getStringValue();
return "是".equals(stringValue); // 将字符串 "是"/"否" 转为布尔值
}
}
2、@ExcelIgnore注解
- 作用范围:数据实体类的字段上;
- 注解释义:当前字段不参与excel列的匹配,即处理时忽略该字段;
在默认情况下,数据模型类中的所有字段都会参与匹配,可如果你定义的Java类中,有些字段在读写时并不需要参与进来,这时就可以给对应字段加上@ExcelIgnore
注解,具备该注解的字段会被忽略。
3、@ExcelIgnoreUnannotated注解
- 作用范围:数据模型类上;
- 注解释义:匹配列时忽略所有未使用
@ExcelProperty
注解的字段;
如果类中许多字段都不想参与excel
读写,而你又嫌挨个加@ExcelIgnore
注解麻烦,这时就可以直接在类上加一个@ExcelIgnoreUnannotated
注解,以此来忽略所有未添加@ExcelProperty
注解的字段。
4、@DateTimeFormat注解
- 作用范围:数据实体类的字段上;
- 注解释义:用
String
接收日期数据时,会根据指定格式转换日期; - 可选参数:
value
:日期数据转换为字符串的目标格式;use1904windowing
:excel日期数据默认从1900
年开始,但有些会从1904
开始;
在解析excel
文件时,如果使用String
字段接收日期数据,就会根据指定的格式转换数据,格式可以参考java.text.SimpleDateFormat
的写法,例如yyyy-MM-dd HH:mm:ss
。而在往excel
写数据时,如果Java中的字段类型为Date、LocalDate、LocalDateTime
等日期类型,则会将日期数据转换为指定的格式写入对应列。
例如:
@DateTimeFormat(value = "yyyy年MM月dd日 HH时mm分ss秒")
5、@NumberFormat注解
- 作用范围:数据实体类的字段上;
- 注解释义:用
String
接收数值数据时,会根据指定格式转换数值; - 可选参数:
value
:数值转换为字符串的目标格式;roundingMode
:数值格式化时的舍入模式,如四舍五入、向上取整等;
这个注解和前一个注解类似,只不过是用于将非整数类型的数值数据转换成给定格式,格式可以参考java.text.DecimalFormat
的写法,如#.##
。除了可以指定格式外,还可以指定舍入模式,枚举可参考java.math.RoundingMode
类。
使用方法:
-
指定数字格式
使用@NumberFormat
注解的value
属性指定数字格式。例如:#
: 表示一个数字字符(整数部分)。0
: 表示一个数字字符(小数部分,不足补零)。,
: 表示千分位分隔符。.
: 表示小数点。
-
与
@ExcelProperty
搭配使用
在数值类型字段上添加@NumberFormat
,并用@ExcelProperty
指定列名。
@ExcelProperty("销售金额")
@NumberFormat("#,##0.00") // 指定数字格式,保留两位小数,带千分位
private BigDecimal salesAmount;
三、常用生成注解
1、@ColumnWidth注解
- 作用范围:数据模型类上、字段上;
- 注解释义:设置列的宽度;
这个注解如果加在类上,则会对所有字段生效;如果单独加在某个字段上,则只对特定的列有效,单位是px
。
例如:
@ExcelProperty("销售金额")
@ColumnWidth(200)
private BigDecimal salesAmount;
2、@ContentFontStyle注解
- 作用范围:数据模型类上、字段上;
- 注解释义:用于设置单元格内容字体格式的注解;
- 可选参数:
fontName
:字体名称,如“黑体、宋体、Arial”等;fontHeightInPoints
:字体高度,以磅为单位;italic
:是否设置斜体(字体倾斜);strikeout
:是否设置删除线;color
:字体的颜色,通过RGB
值来设置;typeOffset
:偏移量,用于调整字体的位置;underline
:是否添加下划线;bold
:是否对字体加粗;charset
:设置编码格式,只能对全局生效(字段上设置无效)。
这个注解用于设置主体内容的字体样式(不包含表头),与上个注解同理,加在类上对整个excel
文件生效,加在字段上只对单列有效,可以通过该注解来设置字体风格、高度、是否斜体等属性。
@ExcelProperty(value ="日期")
@DateTimeFormat(value = "yyyy年MM月dd日 HH时mm分ss秒")
@ContentFontStyle(
fontName = "黑体",/* 字体类型 */
fontHeightInPoints = 50, /* 字体高度,以磅为单位; */
italic = BooleanEnum.TRUE,/* 是否设置斜体(字体倾斜); */
strikeout = BooleanEnum.TRUE,/* 是否设置删除线; */
color = 14,/* 字体的颜色,通过RGB值来设置; 0 黑色 (默认)
9 红色
10 绿色
12 蓝色
13 黄色
14 粉色
15 青色
16 白色 */
typeOffset = 1,/*偏移量,用于调整字体的位置; */
underline = 1,/* 是否添加下划线; */
bold = BooleanEnum.TRUE/* 是否对字体加粗; */
)
private LocalDateTime date;
3、@ContentRowHeight注解
- 作用范围:数据模型类上;
- 注解释义:用于设置行高。
这个注解只能加在类上面,作用就是设置单元格的高度,但这里不能像Excel
那样精准设置不同行的高度,只能设置所有单元格统一的高度。
@ContentRowHeight(80)
4、@ContentStyle注解
- 作用范围:数据模型类上、字段上;
- 注解释义:用于设置内容格式;
属性名 | 类型 | 功能描述 |
horizontalAlignment | HorizontalAlignment | 设置单元格内容的水平对齐方式(如左对齐、居中、右对齐)。 |
verticalAlignment | VerticalAlignment | 设置单元格内容的垂直对齐方式(如顶部对齐、中间对齐、底部对齐)。 |
wrapped | boolean | 是否自动换行(true 开启自动换行)。 |
dataFormat | short | 设置单元格的数据格式(例如日期格式、数字格式等)。 |
fillPatternType | FillPatternType | 设置单元格的填充模式(如纯色填充、斜线填充等)。 |
fillForegroundColor | short | 设置单元格的前景色(通过颜色索引表示)。 |
fillBackgroundColor | short | 设置单元格的背景色(通过颜色索引表示)。 |
borderLeft | BorderStyle | 设置单元格左边框样式(如实线、虚线等)。 |
borderRight | BorderStyle | 设置单元格右边框样式。 |
borderTop | BorderStyle | 设置单元格顶部边框样式。 |
borderBottom | BorderStyle | 设置单元格底部边框样式。 |
leftBorderColor | short | 设置单元格左边框颜色。 |
rightBorderColor | short | 设置单元格右边框颜色。 |
topBorderColor | short | 设置单元格顶部边框颜色。 |
bottomBorderColor | short | 设置单元格底部边框颜色。 |
@ContentStyle(
horizontalAlignment = HorizontalAlignmentEnum.CENTER,/* 水平对齐方式,如居中、左对齐等; */
verticalAlignment = VerticalAlignmentEnum.CENTER,/*垂直对齐方式,如上对齐、下对齐等;*/
wrapped = BooleanEnum.TRUE, /* 设置文本是否应该换行(自动根据内容长度换行); */
dataFormat = 0, /*数据格式,对应excel的内置数据格式; */
fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND,/*设置单元格的填充模式(如纯色填充、斜线填充等)*/
fillForegroundColor = 9,/*设置单元格的前景色(通过颜色索引表示)。*/
fillBackgroundColor = 12,/*设置单元格的背景色(通过颜色索引表示)。*/
borderLeft = BorderStyleEnum.THICK,/*设置单元格左边框样式(如实线、虚线等)。*/
borderRight = BorderStyleEnum.THICK,/*设置单元格右边框样式。*/
borderTop = BorderStyleEnum.THICK,/*设置单元格顶部边框样式。*/
borderBottom = BorderStyleEnum.THICK,/*设置单元格底部边框样式。*/
leftBorderColor = 14,/*设置单元格左边框颜色。*/
rightBorderColor = 14,/*设置单元格右边框颜色。*/
topBorderColor = 14,/*设置单元格顶部边框颜色。*/
bottomBorderColor = 14/*设置单元格底部边框颜色。*/
)
这个注解的属性还有很多,需要的话可以自行再查阅。
5、@HeadFontStyle注解
- 作用范围:数据模型类上、字段上;
- 注解释义:用于定制标题字体格式。
这个注解的作用和可选参数,与@ContentFontStyle
注解类似,不过这个注解是针对列头(表头)有效罢了。
- 可选参数:
fontName:
字体名称,例如Arial
、宋体
等。- fontHeightInPoints:字体大小,以磅为单位.
- bold:是否加粗。
- color:字体颜色,使用 Excel 的内置颜色索引值。
6、@HeadRowHeight注解
- 作用范围:数据模型类上;
- 注解释义:用于设置标题行的行高。
此注解的作用参考@ContentRowHeight
注解,当前注解只对表头生效。
@HeadRowHeight(30) // 设置表头行高为 30pt
7、@HeadStyle注解
- 作用范围:数据模型类上
- 注解释义:用于设置标题样式。
该注解的作用和可选参数参考@
ContentStyle注解,但是当前注解只对表头生效。
8、@OnceAbsoluteMerge注解
- 作用范围:数据模型类上;
- 注解释义:用于合并指定的单元格;
- 可选参数:
firstRowIndex
:从哪行开始合并;lastRowIndex
:到哪行结束合并;firstColumnIndex
:从哪列开始合并;lastColumnIndex
:到哪列结束合并。
从这个注解提供的可选参数就能看出具体作用,这是通过单元格行、列索引的方式,指定生成excel
文件时要合并的区域。不过要注意,使用该注解只能合并一次(对应OnceAbsoluteMerge
这个合并策略类)。
@OnceAbsoluteMerge(firstRowIndex = 0, lastRowIndex = 1, firstColumnIndex = 0, lastColumnIndex = 1)
9、@ContentLoopMerge注解
- 作用范围:数据模型类的字段上;
- 注解释义:用于合并单元格;
- 可选参数:
eachRow
:指定每x
行合并为一行;columnExtend
:指定每x
列合并为一列。
该注解也是用于合并单元格的,但是可以合并多次,不过只能实现每隔n
个单元格合并,使用起来限制很大,通常也不会选择通过这种注解的形式来合并单元格,这里了解即可。
四、常用读取Excel方法:
1、同步读取所有数据后返回
同步的返回,不推荐使用,如果数据量大会把数据放到内存里面,会影响性能。这里只做简单得举例:
@PostMapping("/import")
public void importExcel(MultipartFile file) throws IOException {
// head: 指定读用哪个class去读
// headRowNumber: 指定行头,如果行头指定错误可能会读取不到数据。如果多行头,可以设置其他值。没有指定头,也就是默认是第1行。
// sheet:指定读哪个sheet页,从0开始,第一个sheet是0,第二个是1,默认就是读第一个
// doReadSync: 同步读,读取完所有数据返回
List<ExcelDemo> list = EasyExcel.read(file.getInputStream()).headRowNumber(1).head(ExcelDemo.class).sheet().doReadSync();
for (ExcelDemo data : list) {
log.info("读取到数据:{}", JSON.toJSONString(data));
}
}
2、使用监听器读取所有数据
监听器是EasyExcel常用的一个方法,监听器的好处就是可以一行一行获取数据,不用全部读完在进行处理。
监听器示例:
@Slf4j
public class ExcelDemoListener extends AnalysisEventListener<ExcelDemo> {
private final List<ExcelDemo> data = new ArrayList<>();
/**
* 每解析一条数据都会触发一次invoke()方法
*/
@Override
public void invoke(ExcelDemo excelDemo, AnalysisContext analysisContext) {
log.info("成功解析到一条数据:{}", excelDemo);
data.add(excelDemo);
}
/**
* 当一个excel文件所有数据解析完成后,会触发此方法
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
log.info("所有数据都已解析完毕!");
}
public List<ExcelDemo> getData() {
return data;
}
}
使用监听器读取excel文件:
@PostMapping("/import")
public void importExcel(MultipartFile file) throws IOException {
// 监听器需要手动new出来
ExcelDemoListener excelDemoListener = new ExcelDemoListener();
// 将监听器放入read方法中,会走监听器内部的方法来读取数据 EasyExcel.read(file.getInputStream(),ExcelDemo.class,excelDemoListener).sheet().doRead();
List<ExcelDemo> data = excelDemoListener.getData();
log.info("总共解析到到" + data.size() + "条数据!");
}
监听器内部逻辑可以自行定义,一般会设置数据上限,当数据读取到上限时,就自动批量存储到数据库中。
3、读多个sheet
一次读取全部sheet,需要使用 doReadAll 方法,这个方法一次读取全部sheet页,并传给监听器处理。这中适用于全部sheet页全部都是同一个实体类接收。
ExcelDemoListener excelDemoListener = new ExcelDemoListener();
EasyExcel.read(file.getInputStream(),ExcelDemo.class,excelDemoListener).doReadAll();
List<ExcelDemo> data = excelDemoListener.getData();
log.info("总共解析到到" + data.size() + "条数据!");
读取指定的sheet页,并指定不同的实体类接收。
try (ExcelReader excelReader = EasyExcel.read(file.getInputStream()).build()) {
// 这里为了简单 所以注册了 同样的head 和Listener 自己使用功能必须不同的Listener
ExcelDemoListener excelDemoListener = new ExcelDemoListener();
ReadSheet readSheet1 =
EasyExcel.readSheet(0).head(ExcelDemo.class).registerReadListener(excelDemoListener).build();
ReadSheet readSheet2 =
EasyExcel.readSheet(1).head(ExcelDemo.class).registerReadListener(excelDemoListener).build();
// 这里注意 一定要把sheet1 sheet2 一起传进去,不然有个问题就是03版的excel 会读取多次,浪费性能
excelReader.read(readSheet1, readSheet2);
}
4、日期、数字或者自定义格式转换
需要用到上面的注解,以及自定义转换器实现类。
5、多行头
当读取excel时,如果行头并不是第一行,就需要配合 headRowNumber 方法指定行头是哪一行,但是如果指定的不对,会导致数据读取失败。
ExcelDemoListener excelDemoListener = new ExcelDemoListener();
EasyExcel.read(file.getInputStream(),ExcelDemo.class,excelDemoListener).sheet()
// 默认读取第一行为表头,如果第一行不是,需要单独设置headRowNumber,从0开始
.headRowNumber(1).doRead();
List<ExcelDemo> data = excelDemoListener.getData();
log.info("总共解析到到" + data.size() + "条数据!");
6、读取表头数据
只需要在监听器中实现一个方法,只要重写invokeHeadMap方法即可
@Override
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
log.info("解析到一条头数据:{}", JSON.toJSONString(headMap));
}
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
log.info("解析到一条头数据:{}", JSON.toJSONString(headMap));
// 如果想转成成 Map<Integer,String>
// 方案1: 不要implements ReadListener 而是 extends AnalysisEventListener
// 方案2: 调用 ConverterUtils.convertToStringMap(headMap, context) 自动会转换
}
解析到一条头数据:{0:{"columnIndex":0,"dataFormatData":{"format":"General","index":0},"rowIndex":0,"stringValue":"姓名","type":"STRING"},1:{"columnIndex":1,"dataFormatData":{"$ref":"$[0].dataFormatData"},"rowIndex":0,"stringValue":"日期","type":"STRING"},2:{"columnIndex":2,"dataFormatData":{"$ref":"$[0].dataFormatData"},"rowIndex":0,"stringValue":"年龄","type":"STRING"},3:{"columnIndex":3,"dataFormatData":{"$ref":"$[0].dataFormatData"},"rowIndex":0,"stringValue":"薪水","type":"STRING"},4:{"columnIndex":4,"dataFormatData":{"$ref":"$[0].dataFormatData"},"rowIndex":0,"stringValue":"地址","type":"STRING"}}
成功解析到一条数据:ExcelDemo(name=张三, age=18, salary=11111.11, address=北京, dateTime=2024-12-21T20:20:20)
7、额外信息(批注、超链接、合并单元格信息读取)
一般很少用,需要可以再了解一下,这里只简单做示例:
需要 在监听器里面多实现一个 extra
方法:
@Override
public void extra(CellExtra extra, AnalysisContext context) {
log.info("读取到了一条额外信息:{}", JSON.toJSONString(extra));
switch (extra.getType()) {
case COMMENT:
log.info("额外信息是批注,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(), extra.getColumnIndex(),
extra.getText());
break;
case HYPERLINK:
if ("Sheet1!A1".equals(extra.getText())) {
log.info("额外信息是超链接,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(),
extra.getColumnIndex(), extra.getText());
} else if ("Sheet2!A1".equals(extra.getText())) {
log.info(
"额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{},lastColumnIndex:{},"
+ "内容是:{}",
extra.getFirstRowIndex(), extra.getFirstColumnIndex(), extra.getLastRowIndex(),
extra.getLastColumnIndex(), extra.getText());
} else {
Assert.fail("Unknown hyperlink!");
}
break;
case MERGE:
log.info(
"额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{},lastColumnIndex:{}",
extra.getFirstRowIndex(), extra.getFirstColumnIndex(), extra.getLastRowIndex(),
extra.getLastColumnIndex());
break;
default:
}
}
// 这里 需要指定读用哪个class去读,然后读取第一个sheet
EasyExcel.read(fileName, DemoExtraData.class, new DemoExtraListener())
// 需要读取批注 默认不读取
.extraRead(CellExtraTypeEnum.COMMENT)
// 需要读取超链接 默认不读取
.extraRead(CellExtraTypeEnum.HYPERLINK)
// 需要读取合并单元格信息 默认不读取
.extraRead(CellExtraTypeEnum.MERGE).sheet().doRead();
8、读取公式和单元格类型
也比较少用,需要自行钻研,只简单举例:
@Getter
@Setter
@EqualsAndHashCode
public class CellDataReadDemoData {
private CellData<String> string;
// 这里注意 虽然是日期 但是 类型 存储的是number 因为excel 存储的就是number
private CellData<Date> date;
private CellData<Double> doubleData;
// 这里并不一定能完美的获取 有些公式是依赖性的 可能会读不到 这个问题后续会修复
private CellData<String> formulaValue;
}
@Test
public void cellDataRead() {
String fileName = TestFileUtil.getPath() + "demo" + File.separator + "cellDataDemo.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet
EasyExcel.read(fileName, CellDataReadDemoData.class, new CellDataDemoHeadDataListener()).sheet().doRead();
}
9、数据转换等异常处理
需要在监听器里面实现重写onException方法即可:
@Override
public void onException(Exception exception, AnalysisContext context) {
log.error("解析失败,但是继续解析下一行:{}", exception.getMessage());
// 如果是某一个单元格的转换异常 能获取到具体行号
// 如果要获取头的信息 配合invokeHeadMap使用
if (exception instanceof ExcelDataConvertException) {
ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException)exception;
log.error("第{}行,第{}列解析异常,数据为:{}", excelDataConvertException.getRowIndex(),
excelDataConvertException.getColumnIndex(), excelDataConvertException.getCellData());
}
}
10、不创建对象的读
不创建对象的读,可以接收一个map集合,然后在监听器中自行进行转换。
@Slf4j
public class DataListener extends AnalysisEventListener<Map<Integer, String>> {
/**
* 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 5;
private List<Map<Integer, String>> dataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
@Override
public void invoke(Map<Integer, String> data, AnalysisContext context) {
log.info("解析到一条数据:{}", JSON.toJSONString(data));
dataList.add(data);
if (dataList.size() >= BATCH_COUNT) {
saveData();
dataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
saveData();
log.info("所有数据解析完成!");
}
/**
* 加上存储数据库
*/
private void saveData() {
log.info("{}条数据,开始存储数据库!", dataList.size());
log.info("存储数据库成功!");
}
}
// 这里 只要,然后读取第一个sheet 同步读取会自动finish
EasyExcel.read(fileName, new DataListener()).sheet().doRead();
五、常用导出Excel方法
1、简单导出
注意:简单导出 在数据量不大的情况下可以使用(5000以内,具体也要看实际情况),数据量大参照 重复多次写入
@PostMapping("/export")
public void exportExcel(HttpServletResponse response) throws IOException {
// write 指定输出流,跟模板对象
// sheet 指定导出sheet页名称
// dowrite 指定数据源
EasyExcel.write(response.getOutputStream(), ExcelDemo.class)
.sheet("excel模板")
.doWrite(this::getDatas);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("测试导出excel", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8" + fileName + ".xlsx");
}
2、根据参数只导出指定列
根据参数名列表,导出指定字段,需要使用 includeColumnFieldNames 方法:
//导出指定字段
Set<String> exportFields = new HashSet<String>();
exportFields.add("name");
exportFields.add("age");
// write 指定输出流,跟模板对象
// sheet 指定导出sheet页名称
// dowrite 指定数据源
EasyExcel.write(response.getOutputStream(), ExcelDemo.class)
.includeColumnFieldNames(exportFields)
.sheet("excel模板")
.doWrite(getDatas());
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("测试导出excel", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8" + fileName + ".xlsx");
根据参数名列表,忽略指定字段,需要使用 excludeColumnFiledNames 方法:
//导出指定字段
Set<String> noExportFields = new HashSet<String>();
noExportFields.add("name");
noExportFields.add("age");
// write 指定输出流,跟模板对象
// sheet 指定导出sheet页名称
// dowrite 指定数据源
EasyExcel.write(response.getOutputStream(), ExcelDemo.class)
.excludeColumnFieldNames(noExportFields)
.sheet("excel模板")
.doWrite(getDatas());
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("测试导出excel", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8" + fileName + ".xlsx");
导出指定列的数据还可以结合 @ExcelIgnore注解 或 @ExcelIgnoreUnannotated注解 使用,来导出指定列,或者忽略某些列。
3、复杂头写入
使用@ExcelProperty注解即可设置:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ExcelDemo {
@ExcelProperty({"表头1","姓名"})
private String name;
@ExcelProperty({"表头1","年龄"})
private int age;
@ExcelProperty({"薪水"})
private BigDecimal salary;
@ExcelProperty({"表头2","地址"})
private String address;
@ExcelProperty({"表头2","日期"})
private LocalDateTime dateTime;
}
相同表头会合并。
4、重复多次写入(写到单个或者多个Sheet)
重复写入同一个sheet页:
// 方法1: 如果写到同一个sheet
// 这里 需要指定写用哪个class去写
try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), ExcelDemo.class).build()) {
// 这里注意 如果同一个sheet只要创建一次
WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
// 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来
for (int i = 0; i < 5; i++) {
// 分页去数据库查询数据 这里可以去数据库查询每一页的数据
List<ExcelDemo> data = getDatas();
excelWriter.write(data, writeSheet);
}
}
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("测试导出excel", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8" + fileName + ".xlsx");
写入不同得sheet页,同一个对象:
// 方法2: 如果写到不同的sheet 同一个对象
// 这里 指定文件
try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), ExcelDemo.class).build()) {
// 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面
for (int i = 0; i < 5; i++) {
// 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样
WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).build();
// 分页去数据库查询数据 这里可以去数据库查询每一页的数据
List<ExcelDemo> data = getDatas();
excelWriter.write(data, writeSheet);
}
}
写入不同的sheet页,不同的对象:
// 方法3 如果写到不同的sheet 不同的对象
// 这里 指定文件
try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build()) {
// 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面
for (int i = 0; i < 5; i++) {
// 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样。这里注意DemoData.class 可以每次都变,我这里为了方便 所以用的同一个class
// 实际上可以一直变
WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).head(ExcelDemo.class).build();
// 分页去数据库查询数据 这里可以去数据库查询每一页的数据
List<ExcelDemo> data = getDatas();
excelWriter.write(data, writeSheet);
}
}
5、日期、数字或者自定义格式转换
日期、数字、自定义格式转行,可以结合 注解:
@DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
@NumberFormat("#.##%")
Converter 自定义转换器
上面写了,不再举例
6、图片导出
7、超链接、备注、公式、指定单个单元格的样式、单个单元格多种样式
8、根据模板写入
我得理解就是,在已经提供了一份excel文件中,继续导入数据,例如我们现在有一份excel文件如下所示:
现在我们要以这个文件为模板,在这个文件得基础上继续导出数据,会得到如下所示:
导出代码:
String templateFileName = "C:\\Users\\Administrator\\Desktop\\ces\\" + "response111.xlsx";
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
// 这里要注意 withTemplate 的模板文件会全量存储在内存里面,所以尽量不要用于追加文件,如果文件模板文件过大会OOM
// 如果要再文件中追加(无法在一个线程里面处理,可以在一个线程的建议参照多次写入的demo) 建议临时存储到数据库 或者 磁盘缓存(ehcache) 然后再一次性写入
EasyExcel.write(response.getOutputStream(), ExcelDemo.class).withTemplate(templateFileName).sheet().doWrite(getDatas());
9、列宽、行高
需要结合注解:
@ContentRowHeight(10) :设置行高为10px
@HeadRowHeight(20):设置标题行的行高为20px
@ColumnWidth(25):列的宽度为25px
@Data
@AllArgsConstructor
@NoArgsConstructor
@ContentRowHeight(10)
@HeadRowHeight(20)
@ColumnWidth(25)
public class ExcelDemo {
@ExcelProperty({"表头1","姓名"})
private String name;
@ExcelProperty({"表头1","年龄"})
private int age;
@ExcelProperty({"薪水"})
private BigDecimal salary;
@ExcelProperty({"表头2","地址"})
private String address;
@ExcelProperty({"表头2","日期"})
private LocalDateTime dateTime;
}
10、注解形式自定义样式
需要结合上面讲述得注解:
@Data
@AllArgsConstructor
@NoArgsConstructor
// 头背景设置成红色 IndexedColors.RED.getIndex()
@HeadStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 10)
// 头字体设置成20
@HeadFontStyle(fontHeightInPoints = 20)
// 内容的背景设置成绿色 IndexedColors.GREEN.getIndex()
@ContentStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 17)
// 内容字体设置成20
@ContentFontStyle(fontHeightInPoints = 20)
public class ExcelDemo {
// 字符串的头背景设置成粉红 IndexedColors.PINK.getIndex()
@HeadStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 14)
// 字符串的头字体设置成20
@HeadFontStyle(fontHeightInPoints = 30)
// 字符串的内容的背景设置成天蓝 IndexedColors.SKY_BLUE.getIndex()
@ContentStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 40)
// 字符串的内容字体设置成20
@ContentFontStyle(fontHeightInPoints = 30)
@ExcelProperty({"表头1","姓名"})
private String name;
@ExcelProperty({"表头1","年龄"})
private int age;
@ExcelProperty({"薪水"})
private BigDecimal salary;
@ExcelProperty({"表头2","地址"})
private String address;
@ExcelProperty({"表头2","日期"})
private LocalDateTime dateTime;
}
一般只会设置一些简单样式,具体需要可以自行查询相应注解。
11、使用内置策略模式设置自定义样式
EasyExcel中提供了两个样式策略,用来是设置导出文件得样式,只需要简单配置即可使用:
HorizontalCellStyleStrategy:允许设置每一行的样式一致,或者隔行样式一致。
AbstractVerticalCellStyleStrategy:使用这个策略时,可以为每一列单独设置样式,需要通过回调函数来定义不同列的样式。
设置每一行得样式:
HorizontalCellStyleStrategy 方法,一般接收两个 WriteCellStyle 类型得参数,第一个是设置表头格式得,第二个是设置内容格式的。
WriteCellStyle 是 EasyExcel 中用于设置 Excel 单元格样式的类。每个属性用于定义单元格的不同样式,包括字体、边框、填充颜色、对齐方式等。下面是对每个参数详细得简单的讲解:
WriteCellStyle cellStyle = new WriteCellStyle();
// 1. DataFormatData 设置数据格式
// 这里假设你设置为数字格式, 示例代码中未使用具体的 DataFormatData
// cellStyle.setDataFormatData(new DataFormatData("0.00"));
// 2. WriteFont 设置字体
WriteFont font = new WriteFont();
font.setFontHeightInPoints((short) 12); // 字体大小为 12
font.setBold(true); // 设置加粗
font.setFontName("Arial"); // 字体设置为 Arial
cellStyle.setWriteFont(font);
// 3. hidden 设置隐藏
cellStyle.setHidden(false); // 设置为可见
// 4. locked 设置是否锁定
cellStyle.setLocked(true); // 设置单元格为锁定,不能编辑
// 5. quotePrefix 设置前缀引号
cellStyle.setQuotePrefix(true); // 设置文本前加引号
// 6. HorizontalAlignment 设置水平对齐
cellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER); // 设置为居中对齐
// 7. wrapped 设置是否换行
cellStyle.setWrapped(true); // 设置文本自动换行
// 8. VerticalAlignment 设置垂直对齐
cellStyle.setVerticalAlignment(VerticalAlignment.CENTER); // 设置为垂直居中
// 9. rotation 设置文本旋转
cellStyle.setRotation((short) 90); // 设置文本旋转 90 度
// 10. indent 设置缩进
cellStyle.setIndent((short) 2); // 设置缩进为 2 个字符
// 11-14. BorderStyle 设置边框样式
cellStyle.setBorderLeft(BorderStyle.THIN); // 设置左边框为细边框
cellStyle.setBorderRight(BorderStyle.MEDIUM); // 设置右边框为中等边框
cellStyle.setBorderTop(BorderStyle.THICK); // 设置顶部边框为粗边框
cellStyle.setBorderBottom(BorderStyle.DOTTED); // 设置底部边框为虚线
// 15-18. 边框颜色设置
cellStyle.setLeftBorderColor(IndexedColors.BLACK.getIndex()); // 左边框颜色为黑色
cellStyle.setRightBorderColor(IndexedColors.BLUE.getIndex()); // 右边框颜色为蓝色
cellStyle.setTopBorderColor(IndexedColors.GREEN.getIndex()); // 顶部边框颜色为绿色
cellStyle.setBottomBorderColor(IndexedColors.RED.getIndex()); // 底部边框颜色为红色
// 19. FillPatternType 设置填充模式
cellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND); // 设置为实心填充模式
// 20-21. 背景颜色和前景颜色设置
cellStyle.setFillBackgroundColor(IndexedColors.YELLOW.getIndex()); // 设置背景色为黄色
cellStyle.setFillForegroundColor(IndexedColors.ORANGE.getIndex()); // 设置前景色为橙色
// 22. shrinkToFit 设置是否自适应缩小
cellStyle.setShrinkToFit(true); // 设置文本自适应缩小
简单使用:
//行头字体
WriteFont headFont = new WriteFont();
headFont.setFontName("华文楷体");
headFont.setFontHeightInPoints((short) 18);
headFont.setBold(true);
//内容字体
WriteCellStyle cellStyle = createCellStyle();
WriteFont contentFont = new WriteFont();
contentFont.setFontName("宋体");
contentFont.setFontHeightInPoints((short) 10);
contentFont.setBold(false);
//行头设置
WriteCellStyle headCellStyle = new WriteCellStyle();
headCellStyle.setWriteFont(headFont);
headCellStyle.setFillForegroundColor(IndexedColors.WHITE1.getIndex());
headCellStyle.setBorderTop(BorderStyle.THIN);
headCellStyle.setBorderBottom(BorderStyle.THIN);
headCellStyle.setBorderLeft(BorderStyle.THIN);
headCellStyle.setBorderRight(BorderStyle.THIN);
headCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
//内容设置
WriteCellStyle contentCellStyle = new WriteCellStyle();
contentCellStyle.setWriteFont(contentFont);
contentCellStyle.setBorderTop(BorderStyle.THIN);
contentCellStyle.setBorderBottom(BorderStyle.THIN);
contentCellStyle.setBorderLeft(BorderStyle.THIN);
contentCellStyle.setBorderRight(BorderStyle.THIN);
contentCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(headCellStyle, cellStyle);
EasyExcel.write(response.getOutputStream(), ExcelDemo.class)
.registerWriteHandler(horizontalCellStyleStrategy)
.sheet("模板")
.doWrite(getDatas());
设置列的样式:
AbstractVerticalCellStyleStrategy
是一个抽象类,必须通过继承并重写方法来实现自定义的列样式。主要是重写下面两个方法:
headCellStyle(Head head) ----设置标题栏的列样式
contentCellStyle(Head head) ----设置表格内容的列样式
具体实现代码:
先实现重写AbstractVerticalCellStyleStrategy
:
public class CustomVerticalCellStyleStrategy extends AbstractVerticalCellStyleStrategy {
// 重写定义表头样式的方法
@Override
protected WriteCellStyle headCellStyle(Head head) {
WriteCellStyle writeCellStyle = new WriteCellStyle();
if(head.getColumnIndex() == 0) {
//设置行头的第一列 单元格水平居中
writeCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
}else if(head.getColumnIndex() == 1){
//设置行头的第二列 单元格水平居左
writeCellStyle.setHorizontalAlignment(HorizontalAlignment.LEFT);
}else if(head.getColumnIndex() == 2){
//设置行头的第三列 单元格水平居右
writeCellStyle.setHorizontalAlignment(HorizontalAlignment.RIGHT);
}else if(head.getColumnIndex() == 3){
//设置行头大于第三列 单元格水平居右
writeCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
}
return writeCellStyle;
}
// 重写定义内容部分样式的方法
@Override
protected WriteCellStyle contentCellStyle(Head head) {
WriteCellStyle writeCellStyle = new WriteCellStyle();
writeCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
writeCellStyle.setFillBackgroundColor(IndexedColors.GREEN.getIndex());
writeCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
return writeCellStyle;
}
}
再将自定义样式策略注册到Excel中:
//数据表格的自定义列样式
CustomVerticalCellStyleStrategy customVerticalCellStyleStrategy
= new CustomVerticalCellStyleStrategy();
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
EasyExcel.write(response.getOutputStream(), ExcelDemo.class)
.registerWriteHandler(customVerticalCellStyleStrategy)
.sheet("模板")
.doWrite(getDatas());
上面是简单的示例,具体每一列的样式,可以根据自己的需要定义。
12、自定义单元格
通过实现 CellWriteHandler
接口,可以完全自定义单元格的写入行为和样式。
CellWriteHandler 接口,继承自 WriteHandler,用于在 Excel 写入过程中处理单元格的不同生命周期阶段。每个方法都处理特定的操作,通常用于 Excel 文件的写入时,针对单元格的创建、数据转换、处理和销毁等环节进行自定义处理。
这个接口的作用是提供钩子方法(默认方法)来让用户在写入数据到 Excel 时,能够在每个关键阶段插入自己的逻辑。以下是对每个方法的详细解释:
- 1. beforeCellCreate(CellWriteHandlerContext context)
- 作用:在单元格创建之前调用,用于在写入单元格之前进行一些准备工作。
- 参数:方法内部通过 context 参数提取了多个信息,包括 WriteSheetHolder(写入的工作表)、WriteTableHolder(写入的表格)、Row(当前行)、Head(表头信息)、columnIndex(列索引)、relativeRowIndex(相对行索引)、isHead(是否是表头)等。
- 默认实现:这个方法默认调用了另一个重载版本 beforeCellCreate(WriteSheetHolder, WriteTableHolder, Row, Head, Integer, Integer, Boolean),该重载方法的默认实现为空,表示没有默认操作,用户可以在实现接口时进行自定义。
- 2. beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead)
- 作用:实际执行创建单元格之前的处理逻辑的方法。可以根据需要对 Excel 单元格的创建过程进行自定义操作,如设置格式、填充数据等。
- 参数:接受了各个具体的参数,允许用户在不同的上下文中访问这些信息。
- 3. afterCellCreate(CellWriteHandlerContext context)
- 作用:在单元格创建之后调用。这个方法在单元格已经创建后执行,可以用来处理与单元格创建相关的操作,例如设置样式或处理其他后续步骤。
- 参数:同样是通过 context 提供相关的信息,包括工作表、表格、当前单元格、表头信息、相对行索引等。
- 默认实现:默认调用了另一个重载版本 afterCellCreate(WriteSheetHolder, WriteTableHolder, Cell, Head, Integer, Boolean),该重载方法默认没有实现任何逻辑,允许用户进行自定义。
- 4. afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead)
- 作用:执行单元格创建后的一些自定义处理逻辑。与 beforeCellCreate 方法类似,但在单元格已创建之后进行操作。
- 参数:该方法接受详细的参数,允许用户访问到当前工作表、单元格、表头等信息。
- 5. afterCellDataConverted(CellWriteHandlerContext context)
- 作用:在单元格的数据被转换(例如格式化、数据类型转换)之后调用。这个方法允许用户在数据转换后进行一些额外的处理,比如修改转换后的数据或进一步调整单元格的内容。
- 参数:context.getCellDataList() 提供了当前单元格的数据,方法会根据需要选择第一个数据进行处理(假设该列表不为空)。其他参数和前面的钩子方法类似。
- 6. afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, WriteCellData<?> cellData, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead)
- 作用:执行数据转换后的处理逻辑。用户可以在此时进一步调整数据内容、修改样式或执行其他操作。
- 参数:cellData 是转换后的数据,允许用户访问和操作。
- 7. afterCellDispose(CellWriteHandlerContext context)
- 作用:在单元格被销毁之前调用。这个方法是在单元格生命周期的最后阶段,用于清理或执行一些最终的操作。通常会在单元格数据处理和格式化完成后进行一些额外的操作,如记录日志或执行清理工作。
- 参数:context.getCellDataList() 提供了当前单元格的所有数据列表,用户可以在此进行处理。
- 8. afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead)
- 作用:处理单元格销毁之后的操作。此方法接收了与前面方法相同的详细参数,允许用户在最后一步进行操作。
- 参数:cellDataList 包含了所有需要处理的单元格数据,cell 是当前单元格,head 是表头信息,isHead 是一个布尔值,指示是否为表头行
总结:
这个接口主要用于对 Excel 写入过程中的每个关键步骤提供自定义的处理机制。通过实现CellWriteHandler 接口,用户可以在:
- 单元格创建之前 (beforeCellCreate)
- 单元格创建之后 (afterCellCreate)
- 数据转换之后 (afterCellDataConverted)
- 单元格销毁之前 (afterCellDispose)
等多个阶段进行干预和扩展,实现自定义的单元格处理逻辑。例如,可以用于设置单元格样式、数据格式化、数据验证、日志记录等。
实际上上述的方法都可以进行单元格的样式设置,但是每个方法代表不同的时期,不同的时期能获取到的信息不同,所以可以选择合适的方法来进行自定义格式,但是其实如果不是有复杂的需求,其实不推荐使用该方法进行自定义单元格样式。
简单示例:
EasyExcel.write(response.getOutputStream(), ExcelDemo.class)
.registerWriteHandler(new CellWriteHandler() {
@Override
public void afterCellDispose(CellWriteHandlerContext context) {
// 当前事件会在 数据设置到poi的cell里面才会回调
// 判断不是头的情况 如果是fill 的情况 这里会==null 所以用not true
if (BooleanUtils.isNotTrue(context.getHead())) {
// 第一个单元格
// 只要不是头 一定会有数据 当然fill的情况 可能要context.getCellDataList() ,这个需要看模板,因为一个单元格会有多个 WriteCellData
WriteCellData<?> cellData = context.getFirstCellData();
// 这里需要去cellData 获取样式
// 很重要的一个原因是 WriteCellStyle 和 dataFormatData绑定的 简单的说 比如你加了 DateTimeFormat
// ,已经将writeCellStyle里面的dataFormatData 改了 如果你自己new了一个WriteCellStyle,可能注解的样式就失效了
// 然后 getOrCreateStyle 用于返回一个样式,如果为空,则创建一个后返回
WriteCellStyle writeCellStyle = cellData.getOrCreateStyle();
writeCellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
// 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND
writeCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
// 这样样式就设置好了 后面有个FillStyleCellWriteHandler 默认会将 WriteCellStyle 设置到 cell里面去 所以可以不用管了
}
}
}).sheet("模板")
.doWrite(getDatas());
使用poi的样式设置,用的很少也不推荐使用,只做简单使用:
// 方法3: 使用poi的样式完全自己写 不推荐
// @since 3.0.0-beta2
// 坑1:style里面有dataformat 用来格式化数据的 所以自己设置可能导致格式化注解不生效
// 坑2:不要一直去创建style 记得缓存起来 最多创建6W个就挂了
fileName = TestFileUtil.getPath() + "handlerStyleWrite" + System.currentTimeMillis() + ".xlsx";
EasyExcel.write(fileName, DemoData.class)
.registerWriteHandler(new CellWriteHandler() {
@Override
public void afterCellDispose(CellWriteHandlerContext context) {
// 当前事件会在 数据设置到poi的cell里面才会回调
// 判断不是头的情况 如果是fill 的情况 这里会==null 所以用not true
if (BooleanUtils.isNotTrue(context.getHead())) {
Cell cell = context.getCell();
// 拿到poi的workbook
Workbook workbook = context.getWriteWorkbookHolder().getWorkbook();
// 这里千万记住 想办法能复用的地方把他缓存起来 一个表格最多创建6W个样式
// 不同单元格尽量传同一个 cellStyle
CellStyle cellStyle = workbook.createCellStyle();
cellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
// 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND
cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
cell.setCellStyle(cellStyle);
// 由于这里没有指定dataformat 最后展示的数据 格式可能会不太正确
// 这里要把 WriteCellData的样式清空, 不然后面还有一个拦截器 FillStyleCellWriteHandler 默认会将 WriteCellStyle 设置到
// cell里面去 会导致自己设置的不一样
context.getFirstCellData().setWriteCellStyle(null);
}
}
}).sheet("模板")
.doWrite(data());
13、合并单元格
可以配合注解:
@ContentLoopMerge
@OnceAbsoluteMerge
来进行单元格的合并。
// 将第6-7行的2-3列合并成一个单元格
@OnceAbsoluteMerge(firstRowIndex = 5, lastRowIndex = 6, firstColumnIndex = 1, lastColumnIndex = 2)
public class ExcelDemo {
// 这一列 每隔2行 合并单元格
@ContentLoopMerge(eachRow = 2)
@ExcelProperty({"姓名"})
private String name;
@ExcelProperty({"年龄"})
private int age;
@ExcelProperty({"薪水"})
private BigDecimal salary;
@ExcelProperty({"地址"})
private String address;
@ExcelProperty({"日期"})
private LocalDateTime dateTime;
}