前言
在我们的项目需求中,经常会遇到导出的需求,其中excel的导出最为常见。生成Excel比较有名的框架有Apache poi,jxl等,但他们都存在一个严重的问题就是非常的耗内存,如果你的系统并发量不大的话可能还行,但是一旦并发上来后一定会OOM或者JVM频繁的full gc.
一、EasyExcel特点
EasyExcel是阿里巴巴开源的一个excel处理框架,以使用简单,节省内存著称,
64M内存1分钟内读取75M(46W行25列)的Excel(当然还有急速模式能更快,但是内存占用会在100M多一点)。
EasyExcel能大大减少占用内存的主要原因是在解析Excel时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析。
不支持的功能
1、单个文件的并发写入、
2、读取读取图片
3、宏
4、csv读取(这个后续可能会考虑)
三、常见问题
1、读取文件务必使用2.0.5+(现在项目中用的是2.2.10)
2、读写反射对象用到了Cglib动态代理,所以成员变量必须符合驼峰规范,而且使用@Data不能使用@Accessors(chain = true)。后续会考虑支持非驼峰。
3、出现 NoSuchMethodException, ClassNotFoundException, NoClassDefFoundError。极大概率是jar冲突,建议clean项目,或者统一poi 的版本,理论上来说easyexcel兼容poi的3.17,4.0.1,4.1.0所有较新版本
4、用String去接收数字,出现小数点等情况这个是BUG,但是很难修复,后续版本会修复这个问题。目前请使用@NumberFormat注解,里面的参数就是调用了java自带的NumberFormat.format方法,不知道怎么入参的可以自己网上查询。
easyExcel的官方文档地址:https://alibaba-easyexcel.github.io/index.html
四、常用注解
4-1、读
ExcelProperty 指定当前字段对应excel中的那一列。可以根据名字或者Index去匹配。当然也可以不写,默认第一个字段就是index=0,以此类推。千万注意,要么全部不写,要么全部用index,要么全部用名字去匹配。千万别三个混着用,除非你非常了解源代码中三个混着用怎么去排序的。
@Getter
@Setter
@EqualsAndHashCode
public class IndexOrNameData{
//强制读取第三个这里不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去山配*
@ExcelProperty(index = 2)
private Double doubleData
/*用名字去匹配,这里需要注意,如果名字重复会导致只有一个字殷读取到数据@ExcelProperty ("字符串标题”) */
private Stringstring;
@ExcelProperty ("日期标题")
private Date date;
}
ExcelIgnore 默认所有字段都会和excel去匹配,加了这个注解会忽略该字段
//强制读取第三个这里不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去山配*
@ExcelIgnore
private Double doubleData
DateTimeFormat 日期转换,用String去接收excel日期格式的数据会调用这个注解。里面的value参照java.text.SimpleDateFormat。
@DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
private String date;
NumberFormat 数字转换,用String去接收excel数字格式的数据会调用这个注解。里面的value参照java.text.DecimalFormat。
@NumberFormat("#.##%")
private String doubleData; //接收百比的数字
4-2、写
ExcelProperty index 指定写到第几列,默认根据成员变量排序。value指定写入的名称,默认成员变量的名字,多个value可以参照快速开始中的复杂头
ExcelIgnore 默认所有字段都会写入excel,这个注解会忽略这个字段
DateTimeFormat 日期转换,将Date写到excel会调用这个注解。里面的value参照java.text.SimpleDateFormat
NumberFormat 数字转换,用Number写excel会调用这个注解。里面的value参照java.text.DecimalFormat
ExcelIgnoreUnannotated 默认不加ExcelProperty 的注解的都会参与读写,加了不会参与
五、EasyExcel的使用
1、依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.10</version>
</dependency>
2、读excel
2.1最简单的
对象
@Data
public class DemoData {
private String string;
private Date date;
private Double doubleData;
}
controller类
@PostMapping("/outstoragcExce1")
@Apioperation("读取出库excel表")
public DeviceResponse storageservice(@RequestBody MultipartFile file) {
try{
storageservice.storageservice(file);
} catch (Exception e){
return new DeviceResponse(Constant.FAIL CODE,"出库导失败");
}
return new DeviceResponse(Constant.SUCCESS CODE,"出库导入成功");
}
service实现类
@Autowired
private StorageService storageServicel
@override
publil void slorageservice(MulliparlFile file) {
Tnnutstream is = null:
try{
is=file.getInputstream();
} catch (IDException e){
e.printstackTrace();
}
//1.进行读取数数据,slorageReLrieval是我的puju类,
//2.new Soragelistenpr(storagpServire)这个是监听器,主要用来i取数据的,别急后面会讲
//3.特别注意的是storageservice这个service,我上面有注入进去 @Autowired,切记不要new会报错
EasyExcel.read(is,StorageRetrieval.class, new Soragelisterer(storageService))sheet().doRead();
}
SorageListener监听器
@Component
public class SorageListener extends AnalysisEventListener<pojo类> {
private static final Logger LOGGER = LoggerFactory.getLogger(SorageListener.class);
//读取数据初始化值
private static final int BATCH_COUNT = 50;
List<pojo类> list = new ArrayList<pojo类>();
private StorageService storageService;
public SorageListener() {
storageService=new StorageServiceImpl();
}
public SorageListener(StorageService storageService) {
this.storageService = storageService;
}
/**
* 这个每一条数据解析都会来调用,数据是一条一条进行解析的
*
* @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}
* @param context
*/
@Override
public void invoke(StorageRetrieval data, AnalysisContext context) {
list.add(data);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (list.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
list.clear();
}
}
/**
* 所有excel表中数据解析完成了 都会来调用这个
* 解释为什么要保存数据?
*初始化读取数量为50,表中信息已经加载完毕,,假设excel表中最后只剩下30行遗留数据,所以为了防止存在遗留数据 尽量判断下集合是否为空,不为空在进行存储(这是我的逻辑需要判断,如果不需要也可进行不判断)
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
if(list.size()==0){
return;
}
saveData();
LOGGER.info("所有数据解析完成!");
}
/**
* 加上存储数据库
*/
public void saveData() {
storageService.save(list); //代码实现类层保存数据
LOGGER.info("存储数据库成功!");
}
}
2.2、指定列的下标或者列名
@Data
public class IndexOrNameData {
/**
* 强制读取第三个 这里不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配
*/
@ExcelProperty(index = 2)
private Double doubleData;
/**
* 用名字去匹配,这里需要注意,如果名字重复,会导致只有一个字段读取到数据
*/
@ExcelProperty("字符串标题")
private String string;
@ExcelProperty("日期标题")
private Date date;
}
/**
* 指定列的下标或者列名
*
* <p>1. 创建excel对应的实体对象,并使用{@link ExcelProperty}注解. 参照{@link IndexOrNameData}
* <p>2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link IndexOrNameDataListener}
* <p>3. 直接读即可
*/
@Test
public void indexOrNameRead() {
String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 这里默认读取第一个sheet
EasyExcel.read(fileName, IndexOrNameData.class, new IndexOrNameDataListener()).sheet().doRead();
}
3、写excel
3.1最简单的
实体对象
@Data
@piModel(value = "年龄统计实体类”
public class FToWAgeStatisticalVo implements Serializable {
private static final long serialVersionUID = -7891558029837989473L;
@ApiModelProperty("区间")
@ExcelProperty(value = "区间")
private String ageGap;
@ApiModeLProperty("病例数”)
@ExcelProperty(value ="病例数)
private Integer casesNumber ;
@ApiModeProperty("密接数”)
@ExceProperty(value = "密接数”)
private Integer closeNumber ;
}
service实现
@Override
public void avestatisticalExcel(Httpservlethesponse resonse,FlowReionStatisticalParam flowReionStatisticalParam) throws Exception{
//这里文件名如果涉及中文一定要使用URL编码,否则会乱码
String fileName = URLEncoder.encode( s: "floWAgeStatistical.xlsx" StandardCharsets.UTF_8.toString());
List<FloWAgeStatisticalVo> data = ageStatistical(flowRegionStatisticalParam);
response.setContentType("application/force-download");
response.setcharacterEncoding("utf-8");
response.setHeader( s: "Content-Disposition", s1: "attachment;filename=" + fileName);
EasyExcel.write(response.getoutputstream(),FLoWAgeStatisticalVo.class)
.autoclosestream(true)
.exceType(ExcelTypeEnum.XLSX)
.sheet( sheetName: "年龄统计表")
.doWrite(data) ;
}
3.2、列宽、行高
@Data
@ContentRowHeight(10)
@HeadRowHeight(20)
@ColumnWidth(25)
public class WidthAndHeightData {
@ExcelProperty("字符串标题")
private String string;
@ExcelProperty("日期标题")
private Date date;
/**
* 宽度为50
*/
@ColumnWidth(50)
@ExcelProperty("数字标题")
private Double doubleData;
}
3.3、合并单元格
@Getter
@Setter
@EqualsAndHashCode
// 将第6-7行的2-3列合并成一个单元格
// @OnceAbsoluteMerge(firstRowIndex = 5, lastRowIndex = 6, firstColumnIndex = 1, lastColumnIndex = 2)
public class DemoMergeData {
// 这一列 每隔2行 合并单元格
@ContentLoopMerge(eachRow = 2)
@ExcelProperty("字符串标题")
private String string;
@ExcelProperty("日期标题")
private Date date;
@ExcelProperty("数字标题")
private Double doubleData;
}
/**
* 合并单元格
* <p>1. 创建excel对应的实体对象 参照{@link DemoData}
* <p>2. 创建一个merge策略 并注册
* <p>3. 直接写即可
*/
@Test
public void mergeWrite() {
String fileName = TestFileUtil.getPath() + "mergeWrite" + System.currentTimeMillis() + ".xlsx";
// 每隔2行会合并 把eachColumn 设置成 3 也就是我们数据的长度,所以就第一列会合并。当然其他合并策略也可以自己写
LoopMergeStrategy loopMergeStrategy = new LoopMergeStrategy(2, 0);
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
EasyExcel.write(fileName, DemoData.class).registerWriteHandler(loopMergeStrategy).sheet("模板")
.doWrite(data());
}
3.4、复杂头写入
@Data
@ApiModel("学校学生缺勤信息")
public class SchoolAnalyseVo {
@ApiModelProperty("学校Id")
@ExcelIgnore()
private Long schoolId;
@ExcelProperty("学校")
private String schoolName;
@ExcelProperty("学校类型")
private String schoolType;
..........
@ExcelProperty({"症状", "发热"})
private String fever;
@ExcelProperty({"症状", "咳嗽"})
private String cough;
@ExcelProperty({"症状", "头痛"})
private String headache;
.........
@ExcelProperty({"疾病","普通感冒", "人数"})
private String commonColdNumber ;
@ExcelProperty({"疾病","普通感冒", "因病缺勤率"})
private String commonColdRate ;
@ExcelProperty({"疾病","流感", "人数"})
private String influenzaNumber;
.........
}
3.5、日期、数字或者自定义格式转换
@Data
public class ConverterData {
/**
* 我想所有的 字符串起前面加上"自定义:"三个字
*/
@ExcelProperty(value = "字符串标题", converter = CustomStringStringConverter.class)
private String string;
/**
* 我想写到excel 用年月日的格式
*/
@DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
@ExcelProperty("日期标题")
private Date date;
/**
* 我想写到excel 用百分比表示
*/
@NumberFormat("#.##%")
@ExcelProperty(value = "数字标题")
private Double doubleData;
}
自定义转换器
public class CustomStringStringConverter implements Converter<String> {
@Override
public Class supportJavaTypeKey() {
return String.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
/**
* 这里读的时候会调用
*
* @param cellData
* @param contentProperty
* @param globalConfiguration
* @return
*/
@Override
public String convertToJavaData(CellData cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
return "自定义:" + cellData.getStringValue();
}
/**
* 这里是写的时候会调用 不用管
*
* @param value
* @param contentProperty
* @param globalConfiguration
* @return
*/
@Override
public CellData convertToExcelData(String value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
return new CellData(value);
}
}
3.6、指定写入列
@Getter
@Setter
@EqualsAndHashCode
public class IndexData {
@ExcelProperty(value = "字符串标题", index = 0)
private String string;
@ExcelProperty(value = "日期标题", index = 1)
private Date date;
/**
* 这里设置3 会导致第二列空的
*/
@ExcelProperty(value = "数字标题", index = 3)
private Double doubleData;
}
3.7、其他读操作
https://www.yuque.com/easyexcel/doc/write
4、填充excel
4.1 最简单的填充
对象
@Getter
@Setter
@EqualsAndHashCode
public class FillData{
private string name;
private double number;
private Date date;
}
代码文章来源:https://www.toymoban.com/news/detail-618338.html
/*最简单的填充
* @since 2.1.1
*/
@Test
public void simpleFill() [
// 模板注 用]来表示你要用的变量 如果本来就有””,”]”特殊字符 用””]"代替
String templateFileName =TestFileUtil.getPath() + "demo" + File.separator + "fill" + File.separator + "simple.xlsx";
// 方案1 根据对象填充
String fileName = TestFileUtil,getPath() + "simpleFill" + System,currentTimeMillis() + ".xlsx",
// 这里 会填充到第一个sheet, 然后文件流会自动关闭
FillData fillData = new FillData();
fillData.setName("张一");
fillData.setNumber(5.2);
EasyExcel.write(fileName).withTemplate(templateFileName).sheet().doFill(fillData);
// 方案2 根据Map填充
fileName = TestFileUtil.getPath() + "simpleFill" + System.currentTimeMillis() + ".xlsx"
// 这里 会填充到第一个sheet, 然后文件流会自动关闭
Map<string, Object> map = new HashMap<string, Object>();
map.put("name”,"张二");
map .put("number", 5.2) :
EasyExcel.write(fileName).withTemplate(templateFileName).sheet().doFill(map);
}
4.2、其他填充
填充列表、复杂的填充、数据量大的复杂填充、横向的填充、多列表组合填充填充
https://www.yuque.com/easyexcel/doc/fill文章来源地址https://www.toymoban.com/news/detail-618338.html
到了这里,关于java实现excel的导出之使用easyExcel的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!