EasyExcel应对简单需求的demo设计

前言

Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。easyexcel重写了poi对07版Excel的解析,能够原本一个3M的excel用POI sax依然需要100M左右内存降低到几M,并且再大的excel不会出现内存溢出,03版依赖POI的sax模式。在上层做了模型转换的封装,让使用者更加简单方便。

——easyexcel

起步

  • maven or gradle
  • springboot
  • api or blog

快速上手

EasyExcelApi

EasyExcelGitHubUrl

简单需求demo

  • demo地址

喜欢直接看项目的可以直接 >> demo-easy-excel

  • 引入easyexcel

引入easyexcel (maven为例),引入easyexcel

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>easyexcel</artifactId>
  <version>2.1.4</version>
</dependency>
  • 创建对应Excel的Dto

业务中有各种类型,这里基于java8常用的类型进行测试。

/**
 * 父类 可能业务需要继承
 * @author quaint
 * @date 2020-01-14 11:23
 */
@Data
public class DemoParentDto {

    @ExcelProperty(index = 0,value = {"序号"})
    private Integer num;

}

/**
 * 子类 一般业务一个子类即可
 * @author quaint
 * @date 2020-01-14 11:20
 */
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
@NoArgsConstructor
@Data
public class DemoUserDto extends DemoParentDto{

    @ExcelProperty(value = {"姓名"})
    private String name;

    @ExcelProperty(value = {"性别"})
    private String sex;

    /**
     * @see LocalDateConverter (时间格式转换器)LocalDateTime同理,代码也会贴出来
     */
    @ExcelProperty(value = "生日",converter = LocalDateConverter.class)
    @DateTimeFormat("yyyy-MM-dd")
    private LocalDate birthday;

    @ExcelProperty(value = {"存款"})
    private BigDecimal money;

    /**
     * 获取6个测试数据
     * @return 6个
     */
    public static List<DemoUserDto> getUserDtoTest6(String search){
        List<DemoUserDto> list = new ArrayList<>();
        list.add(new DemoUserDto("quaint","男",LocalDate.of(2011,11,11),BigDecimal.ONE));
        list.add(new DemoUserDto("quaint2","女",LocalDate.of(2001,11,1),BigDecimal.TEN));
        list.add(new DemoUserDto("quaint3","男",LocalDate.of(2010,2,7),new BigDecimal(11.11)));
        list.add(new DemoUserDto("quaint4","男",LocalDate.of(2011,1,11),new BigDecimal(10.24)));
        list.add(new DemoUserDto("quaint5","女",LocalDate.of(2021,5,12),BigDecimal.ZERO));
        list.add(new DemoUserDto(search,"男",LocalDate.of(2010,7,11),BigDecimal.TEN));
        return list;
    }
}
  • 创建converter(导入导出时自定义转换对应字段)
/**
 * LocalDate and string converter
 * @author quait
 */
public class LocalDateConverter implements Converter<LocalDate> {

    @Override
    public Class supportJavaTypeKey() {
        return LocalDate.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }

    @Override
    public LocalDate convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration){
        // 将excel 中的 数据 转换为 LocalDate
        if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) {
            return LocalDate.parse(cellData.getStringValue(), DateTimeFormatter.ISO_LOCAL_DATE);
        } else {
            // 获取注解的 format  注意,注解需要导入这个 excel.annotation.format.DateTimeFormat;
            return LocalDate.parse(cellData.getStringValue(),
                DateTimeFormatter.ofPattern(contentProperty.getDateTimeFormatProperty().getFormat()));
        }
    }

    @Override
    public CellData convertToExcelData(LocalDate value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
        // 将 LocalDateTime 转换为 String
        if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) {
            return new CellData(value.toString());
        } else {
            return new CellData(value.format(DateTimeFormatter.ofPattern(contentProperty.getDateTimeFormatProperty().getFormat())));
        }
    }
}

/**
 * LocalDateTime and string converter
 *
 * @author quait
 */
public class LocalDateTimeConverter implements Converter<LocalDateTime> {

    @Override
    public Class supportJavaTypeKey() {
        return LocalDateTime.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }

    @Override
    public LocalDateTime convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration){
        // 将excel 中的 数据 转换为 LocalDateTime
        if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) {
            return LocalDateTime.parse(cellData.getStringValue(), DateTimeFormatter.ISO_LOCAL_DATE_TIME);
        } else {
            // 获取注解的 format  注意,注解需要导入这个 excel.annotation.format.DateTimeFormat;
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern(contentProperty.getDateTimeFormatProperty().getFormat());
            return LocalDateTime.parse(cellData.getStringValue(), formatter);
        }
    }

    @Override
    public CellData convertToExcelData(LocalDateTime value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
        // 将 LocalDateTime 转换为 String
        if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) {
            return new CellData(value.toString());
        } else {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern(contentProperty.getDateTimeFormatProperty().getFormat());
            return new CellData(value.format(formatter));
        }
    }
}
  • 创建Listener(监听Excel导入)
/**
 * 官方提示:有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
 *
 * 如果想被spring 管理的话, 改为原型模式, Controller 以 getBean 形式获取 本博客展示被spring
 * @author quaint
 */
@EqualsAndHashCode(callSuper = true)
@Slf4j
@Data
@Scope(SCOPE_PROTOTYPE)
@Component
public class DemoUserListener extends AnalysisEventListener<DemoUserDto> {

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

    private List<DemoUserDto> list = new ArrayList<>();

   /**
     * 方式一
     *  可以换成 @Autowired 注入 service 或者mapper
     *  不被spring管理的话  使用构造函数 接收外面被spring管理的mapper -->constructor
     * @Autowired
     * DemoUserMapper demoUserMapper;
     */
    private List<DemoUserDto> virtualDataBase = new ArrayList<>();

    /**
     * 方式二
     * 假设 virtualDataBase 是 mapper, 这里就在外面new该类的时候传进来  调用方注入过得mapper
     * 并且 把Scope、Component注解去掉
     */
//    public DemoUserListener(List<DemoUserDto> virtualDataBase) {
//        this.virtualDataBase = virtualDataBase;
//    }

    /**
     * 这个每一条数据解析都会来调用
     */
    @Override
    public void invoke(DemoUserDto data, AnalysisContext context) {
        log.info("解析到一条数据:{}", JSONObject.toJSONString(data));
        list.add(data);
        // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
        if (list.size() >= BATCH_COUNT) {
            saveData();
            // 存储完成清理 list
            list.clear();
        }
    }

    /**
     * 所有数据解析完成了 会来调用
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 这里也要保存数据,确保最后遗留的数据也存储到数据库
        saveData();
        log.info("所有数据解析完成!");
    }

    /**
     * 在转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行。
     * @param exception exception
     * @param context context
     * @throws Exception e
     */
    @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());
        }
    }

    /**
     * 加上存储数据库
     */
    private void saveData() {
        log.info("{}条数据,开始存储数据库!", list.size());
        virtualDataBase.addAll(list);
        log.info("存储数据库成功!");
    }
}
  • 创建Handler
/**
 * 自定义拦截器。对第一行第一列的头超链接到:https://github.com/alibaba/easyexcel
 * 这里没有采用 spring 管理
 * @author Jiaju Zhuang
 */
@Slf4j
public class CustomCellWriteHandler implements CellWriteHandler {

    @Override
    public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row,
                                 Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {
        log.info("cell 创建之前");

    }

    @Override
    public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell,
                                Head head, Integer relativeRowIndex, Boolean isHead) {
        log.info("cell 创建后");
    }

    @Override
    public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
                                 List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
        // 这里可以对cell进行任何操作
        log.info("第{}行,第{}列写入完成。", cell.getRowIndex(), cell.getColumnIndex());

    }

}
  • 控制层Controller
/**
 * @author quaint
 * @date 2020-01-14 11:13
 */
@Controller
@Slf4j
public class DemoEasyExcelSpi implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @PostMapping("/in/excel")
    public String inExcel(@RequestParam("inExcel") MultipartFile inExcel, Model model){

        DemoUserListener demoUserListener = applicationContext.getBean(DemoUserListener.class);

        log.info("demoUserListener 在 spi 调用之前 hashCode为 [{}]", demoUserListener.hashCode());

        if (inExcel.isEmpty()){
            // 读取 local 指定文件
            List<DemoUserDto> demoUserList;
            String filePath = System.getProperty("user.dir")+"/demo-easy-excel/src/main/resources/ExcelTest.xlsx";
            try {
                // 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
                EasyExcel.read(filePath, DemoUserDto.class, demoUserListener).sheet().doRead();
                demoUserList = demoUserListener.getVirtualDataBase();

            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }

            model.addAttribute("users", demoUserList);

        } else {
            // 读取 web 上传的文件
            List<DemoUserDto> demoUserList;
            try {
                EasyExcel.read(inExcel.getInputStream(), DemoUserDto.class, demoUserListener).sheet().doRead();
                demoUserList = demoUserListener.getVirtualDataBase();
            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
            model.addAttribute("users", demoUserList);
        }
        log.info("demoUserListener 在 spi 调用之后 hashCode为 [{}]", demoUserListener.hashCode());
        return "index";
    }

    @PostMapping("/out/excel")
    public void export(HttpServletResponse response){

        String search = "@RequestBody Object search";
        // 根据前端传入的查询条件 去库里查到要导出的dto
        List<DemoUserDto> userDto = DemoUserDto.getUserDtoTest6(search);
        // 要忽略的 字段
        List<String> ignoreIndices = Collections.singletonList("性别");

        // 根据类型获取要反射的对象
        Class clazz = DemoUserDto.class;

        // 遍历所有字段, 找到忽略的字段
        Set<String> excludeFiledNames = new HashSet<>();
        while (clazz != Object.class){
            Arrays.stream(clazz.getDeclaredFields()).forEach(field -> {
                ExcelProperty ann = field.getAnnotation(ExcelProperty.class);
                if (ann!=null && ignoreIndices.contains(ann.value()[0])){
                    // 忽略 该字段
                    excludeFiledNames.add(field.getName());
                }
            });
            clazz = clazz.getSuperclass();
        }

        // 设置序号
        AtomicInteger i = new AtomicInteger(1);
        userDto.forEach(u-> u.setNum(i.getAndIncrement()));

        // 创建本地文件
       EasyExcelUtils.exportLocalExcel(userDto,DemoUserDto.class,"ExcelTest",excludeFiledNames);
        // 创建web文件
        EasyExcelUtils.exportWebExcel(response,userDto,DemoUserDto.class,"ExcelTest",null);
    }
}
  • 导出工具类
/**
 * EasyExcelUtils  导出工具类,导入感觉没必要封装。。
 * @author quaint
 * @date 2020-01-14 14:26
 */
public abstract class EasyExcelUtils {

    /**
     * 导出excel
     * @param response http下载
     * @param dataList 导出的数据
     * @param clazz 导出的模板类
     * @param fileName 导出的文件名
     * @param excludeFiledNames 要排除的filed
     * @param <T> 模板
     */
    public static <T> void exportWebExcel(HttpServletResponse response, List<T> dataList, Class<T> clazz,
                                 String fileName, Set<String> excludeFiledNames) {

        // 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("utf-8");
        response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");

        try {
            // 导出excel
            EasyExcel.write(response.getOutputStream(), clazz)
                    .excludeColumnFiledNames(excludeFiledNames)
                    .sheet("fileName")
                    .doWrite(dataList);
        } catch (IOException e) {
            System.err.println("创建文件异常!");
        }

    }

    /**
     * 导出excel
     * @param dataList 导出的数据
     * @param clazz 导出的模板类
     * @param fileName 导出的文件名
     * @param excludeFiledNames 要排除的filed
     * @param <T> 模板
     */
    public static <T> void exportLocalExcel(List<T> dataList, Class<T> clazz, String fileName,
                                            Set<String> excludeFiledNames){
        //创建本地文件 test 使用
        String filePath = System.getProperty("user.dir")+"/demo-easy-excel/src/main/resources/"+fileName+".xlsx";

        File dbfFile = new File(filePath);
        if (!dbfFile.exists() || dbfFile.isDirectory()) {
            try {
                dbfFile.createNewFile();
            } catch (IOException e) {
                System.err.println("创建文件异常!");
                return;
            }
        }

        // 导出excel
        EasyExcel.write(filePath, clazz)
                .registerWriteHandler(new CustomCellWriteHandler())
                .excludeColumnFiledNames(excludeFiledNames)
                .sheet("SheetName").doWrite(dataList);

    }

}
  • 前端代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<style>
    .data-local{
        border: 1px black;
    }

</style>
<body>

    <form th:action="@{/in/excel}" method="post" enctype="multipart/form-data">
        <input name="inExcel" type="file" value="上传文件"/>
        <input type="submit" value="导入excel"/>
    </form>
    <h2>导入的数据展示位置:</h2>
    <div class="data-local" th:each="user : ${users}">
        <span th:text="${user}"></span>
    </div>

    <form th:action="@{/out/excel}" method="post">
        <input type="submit" value="导出下载文件"/>
    </form>

</body>
</html>
  • 效果图

总结

Listener和handler的自定义写法可以满足绝大多数需求,大佬设计的代码用起来就是舒服。就是@ExcelProperty注解的index属性的排序混合使用,还需要看源码是如何排序的。这里知识匮乏,望以后可以补充。

原文地址:https://www.cnblogs.com/quaint/p/12305541.html

时间: 2024-10-31 05:16:17

EasyExcel应对简单需求的demo设计的相关文章

简单Unity时间架构设计(克洛诺斯之匙)

简单Unity时间架构设计(克洛诺斯之匙) 好吧,这次的题目有点标题党之嫌,提出这个设计,是因为最近玩了鬼泣,其中有一个关卡叫做“为了自己的主人”,任务中,需要利用克洛诺斯之匙将时间变慢,便于通过激光镇. 使用克洛诺斯之匙之后,主角的行动是正常的,运走,攻击等等.而其他的如怪物,死亡特效等对象的更新都变慢了.当时我想,如何让不同的对象能够按不同频率更新呢? 在unity中,脚本按时更新的是Time.FixedUpdate,改变其速率只需要修改Time.timeScale就行了.然而这么做非常“鲁

设计模式-简单工厂(Demo)

简介 工厂都是创建型模式.用来NEW类的.本文章用一个故事讲述了简单工厂模式 故事 在遥远的未来.chenchen我生活条件不错.但是每天都要做饭.chenchen很懒.只想吃.不想干.所以就请了个女仆给chenchen做饭吃.女仆很牛.chenchen只要说.我要吃牛排.不一会.牛排就做好了放在桌子上.我又说还要红酒.女仆开好红酒.倒好放在桌子上.美滋滋的哈!小肉吃着.小酒喝着.美女陪着此等逍遥快活! 是什么? 代替客户端NEW类.客户端与多个类解耦 为什么要用? 上述故事.因为我做很多饭.要

简单应用程序的设计 -重复前缀

重复前缀 由于串操作指令只能每次对字符串的一个字符进行处理,所以使用了一个循环,以便完成对整个字符串的处理,为了进一步提高效率,8086和8088还提供了重复指令前缀,重复指令前缀可以加载串操作指令指令之前,已达到重复执行其后串操作指令的目的 重复前缀REP REP用作为一个串操作指令的前缀,它重复其后面的串操作指令动作,每一次重复都先判断CX是否为0,如果为0 就结束重复,否则CX的值就减1,重复其后面的串操作指令,所以CX等于0 时,就不执行其后面的字符操作指令 重复前缀REP主要用在传送指

简单应用程序的设计字符串处理

字符串是字符的一个序列,对字符串的操作包括复制检索,插入删除和替换等,为了便于对字符串进行有效的处理,8086和8088提供专门的用户处理字符串的指令,我们称之为字符串操作指令,简称串操作指令 在字符串操作指令中,一般由变址寄存器SI指向源操作数(串),由变址寄存器DI指向目的操作数,规定源串存放在当前数据段中,目的串存放在当前附加段中,也就是说 在涉及源操作数是,引用数据段寄存器DS,在涉及目的操作数时,引用附加段寄存器ES,换句话说,DS:SI指向源操作数,ES:DI指向目的操作数 串操作指

十个简单好用的设计技巧

本文作者Mark Praschan是一位具有将近十年经验的网页设计师,Web开发师,Web项目经理人. 文中强调复杂的高级效果能为设计增色不少,但如果用得不对,只会影响用户对重点内容的关注.高级效果可能正好是一项好的设计的冲击力所在,但即便如此,也还是需要一些更简单的效果与其配合. 简单的效果和技巧是建造当今设计的基石.比方说,如果你都不知道如何正确选择颜色和文字效果,灿烂的 星光效果又能有什么用? 本着“少就是多”的理念,通过十个简单好用的设计技巧 ,就足以大大提升你设计的专业性和感染力.基础

刚做完几个简单的响应式设计的网站项目下来,杂七杂八 (一)

之前没接触过responsive design这玩意,突然最近客户的项目都要求要有响应式设计的要求: 1,当浏览器缩放时,页面要根据浏览器大小,而自动适应. 2,当用手机或者移动设备打开页面时,页面会根据屏幕浏览器的大小自动适应. 3,移动设备有横屏和竖屏之分,页面也要相应适应 首页设计 UI设计师只设计出了2种mockup,一种是full site一种是mobile site然后扔过来,一看那mockup就知道是当前比较流行的设计风格. 1,页头head,左logo右菜单:在full site

Xamarin.Android再体验之简单的登录Demo

一.前言 在空闲之余,学学新东西 二.服务端的代码编写与部署 这里采取的方式是MVC+EF返回Json数据,(本来是想用Nancy来实现的,想想电脑太卡就不开多个虚拟机了,用用IIS部署也好) 主要是接受客户端的登陆请求,服务器端返回请求的结果 这里的内容比较简单不在啰嗦,直接上代码了: 1 using System.Linq; 2 using System.Web.Mvc; 3 namespace Catcher.AndroidDemo.EasyLogOn.Service.Controller

莪的拽、像省田各号①样没尽頭队——需求改进&amp;原型设计

需求改进&原型设计 Ⅰ. 需求&原型改进 Ⅱ. 系统设计 Ⅲ. Alpha任务分配计划 Ⅳ. 测试计划 原文地址:https://www.cnblogs.com/m870100/p/9859127.html

php 简单实现webSocket(demo)

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议. WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据. 在 WebSocket API 中,浏览器和服务器只需要完成一次握手的动作,浏览器和服务器之间就形成了一条快速通道创建持久性的连接,两者之间就直接可以数据互相传送.(长连接,循环连接的不算) 现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询.轮询是在特定的的时间间隔(如每1秒),由浏览器对