数据导出组件
组件编码
hzero-starter-export
一、简介
1.1 概述
hzero-starter-export 导出组件基于 apache poi 4.0.0 开发,使用 SXSSF 支持大数据量导出,导出格式为*.xlsx。在项目中只需依赖该jar包,再结合三个注解即可完成数据的导出。对于导出Excel样式,提供了两种默认实现,同时支持自定义导出样式。
1.2 组件坐标:
<dependency>
    <groupId>org.hzero.starter</groupId>
    <artifactId>hzero-starter-export</artifactId>
    <version>${hzero.starter.version}</version>
</dependency>
1.3 特性
- 支持大数据量导出,不存在内存溢出
- 支持可选择列导出
- 支持头行结构导出、头行打平导出
- 支持自定义Excel导出样式
- 支持自定义单元格格式
- 支持一个DTO中的列分组
- 支持数据自定义渲染
- 使用及其方便简单
二、使用指南
使用该组件导出数据主要会用到三个注解:@ExcelSheet、@ExcelColumn、@ExcelExport。
2.1 @ExcelSheet
在导出的DTO类上,使用@ExcelSheet标注导出的Sheet,头行结构中,行上也需要使用该注解标注。在@ExcelSheet中,可配置导出Sheet的标题,分页查询大小等,基本不需配置,使用默认的即可。
/**
 * Sheet(Excel)标题 首先根据多语言取,如果多语言为空则取title,title为空则取类名
 */
@AliasFor("zh")
String title() default "";
/**
 * 中文标题
 */
@AliasFor("title")
String zh() default "";
/**
 * 英文标题
 */
String en() default "";
/**
 * 多语言KEY 根据 key & code 获取多语言
 * @see #promptCode
 * @see #title
 */
String promptKey() default "";
/**
 * 多语言CODE 根据 key & code 获取多语言
 * @see #promptKey
 * @see #title
 */
String promptCode() default "";
/**
 * 行偏移量 从第几行开始显示数据 大于等于0
 */
int rowOffset() default 0;
/**
 * 占位符,偏移的列可使用占位符显示
 */
String placeholder() default "*****";
/**
 * 列偏移量 从第几列开始显示数据 大于等于0
 */
int colOffset() default 0;
/**
 * 分页大小 每次查询的数量
 */
int pageSize() default 5000;
2.2 @ExcelColumn
在导出DTO类中,在需要作为导出列的字段上,使用@ExcelColumn标注,该注解可配置列标题、显示顺序等。
- 在头行结构多Sheet导出中,如果某列需要显示到子Sheet中,可配置 showInChildren=true;
- 如果某个字段是List子列表,需要配置 child=true。
- 对于一个DTO,想要分组导出不同字段的组合,可设置 groups属性,通过定义不同的接口来分组,同时需要在@ExcelExport配置对应的groups标识。
- 导出的Cell格式可通过 pattern设置,在BaseConstants.Pattern里定义了一些基础的日期、数字、金额的格式。
- 如果想要对某个字段自定义显示样式,可设置 renders属性,需实现ValueRenderer接口。
/**
 * 列标题 首先根据多语言取,如果多语言为空则取title,title为空则取类名
 */
@AliasFor("zh")
String title() default "";
/**
 * 中文标题
 */
@AliasFor("title")
String zh() default "";
/**
 * 英文标题
 */
String en() default "";
/**
 * 多语言KEY 根据 key & code 获取多语言
 * @see #promptCode
 * @see #title
 */
String promptKey() default "";
/**
 * 多语言CODE 根据 key & code 获取多语言
 * @see #promptKey
 * @see #title
 */
String promptCode() default "";
/**
 * 在子列表中显示该列
 */
boolean showInChildren() default false;
/**
 * 列顺序
 */
int order() default 1;
/**
 * Cell 格式,参考 {@link BaseConstants.Pattern}
 */
String pattern() default "";
/**
 * 是否子节点
 */
boolean child() default false;
/**
 * 列宽度
 */
String width() default "3000";
/**
 * 是否可编辑
 */
boolean editable() default true;
/**
 * 分组标识
 */
Class<?>[] groups() default {};
/**
 * 数据渲染器,根据需求自行实现渲染器,设置Cell数据时会通过该渲染器来渲染数据和类型。
 *
 * <pre> example:
 *  public static class ExampleRenderer implements ValueRenderer {
 *
 *      public Object render(Object value, Object data) {
 *          ExampleDTO dto = (ExampleDTO) data;
 *          return "template name = " + dto.name;
 *      }
 *  }
 * </pre>
 */
Class<? extends ValueRenderer>[] renders() default {};
Example:
@ExcelSheet(zh = "收货记录", en = "Receiving record")
public class ReveRecodeDTO {
    @ExcelColumn(zh = "事务编号", en = "trxNum", showInChildren=true)
    private String trxNum;
    @ExcelColumn(zh = "客户", en = "companyName", groups = {Group2.class})
    private String companyName;
    @ExcelColumn(zh = "物品编码", en = "itemCode", order = 4, groups = {Group1.class})
    private String itemCode;
    @ExcelColumn(zh = "物品名称", en = "itemName", order = 3, groups = {Group1.class})
    private String itemName;
    @ExcelColumn(zh = "日期", en = "trxDate", pattern = BaseConstants.Pattern.DATE)
    private Date trxDate;
    @ExcelColumn(zh = "数量", en = "quantity", groups = {Group2.class})
    private BigDecimal quantity;
    @ExcelColumn(zh = "金额", en = "netAmount", pattern = BaseConstants.Pattern.TB_ONE_DECIMAL)
    private BigDecimal netAmount;
    @ExcelColumn(zh = "原因", en = "moveReason")
    private String moveReason;
    @ExcelColumn(zh = "接收人", en = "receiptPerson")
    private String receiptPerson;
    @ExcelColumn(zh = "备注", en = "remark", renders = RemarkValueRenderer.class)
    private String remark;
    @ExcelColumn(zh = "详情列表", en = "detailsList", child = true)
    List<RecordLineDTO> detailsList;
    public interface Group1 {}
    public interface Group2 {}
    public class RemarkValueRenderer implements ValueRenderer {
        @Override
        public Object render(Object value, Object data) {
            RecordLineDTO dto = (RecordLineDTO) data;
            return "显示备注:" + dto.remark;
        }
    }
    // getter/setter
}
2.3 @ExcelExport
在导出接口上,使用@ExcelExport标注,注解需配置导出的DTO。
- 对于只有行的数据,一般可以直接使用已有的List、Page方法即可。
- 接口方法必须有HttpServletResponse和ExportParam参数,HttpServletResponse用于输出Excel,ExportParam用于封装参数。
- 在ExportParam中,通过exportType=COLUMN请求导出列;通过exportType=DATA导出Excel;通过fillerType指定导出样式;
- 对于头行结构,选择了行后,会将行字段名称放入selection中,在查询头行数据时,即可按需查询行数据。
- 接口方法如果有PageRequest参数,将会分批次查询数据填充到Sheet中,如果没有,则默认一次性查询所有数据。
- 导出时,所有的查询已经控制在一个事务内,不会出现幻读的问题。
/**
 * 将该注解加在请求数据的接口上:<br/>
 *
 * 接口方法必须带有 {@link HttpServletResponse} 参数,将通过 {@link HttpServletResponse#getWriter()} 返回数据 <br/>
 * 接口方法必须带有 {@link ExportParam} 参数:
 *  <ul>
 *      <li>通过 {@link ExportParam#fillerType} 指定导出方式</li>
 *      <li>通过 {@link ExportParam#exportType} 指定导出类型</li>
 *      <ul>
 *          <li>{@link ExportType#COLUMN} 查询导出的列</li>
 *          <li>{@link ExportType#DATA} 导出数据</li>
 *          <li>{@link ExportType#TEMPLATE} 导出模板</li>
 *      </ul>
 *      <li>通过 {@link ExportParam#ids} 传入选择导出的列</li>
 *  </ul>
 * 接口方法最好带有分页参数 {@link PageRequest},支持分页查询数据,从而避免大数据量导致内存溢出 <br/>
 *
 * @author bojiangzhou 2018/07/25
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExcelExport {
    /**
     * 导出对象
     */
    Class<?> value();
    /**
     * 分组标识
     */
    Class<?>[] groups() default {};
}
Example:
@GetMapping("/export")
@ExcelExport(ReveRecodeDTO.class)
public ResponseEntity export(ReveRecodeDTO record, ExportParam exportParam, HttpServletResponse response, PageRequest pageRequest) {
    List<ReveRecodeDTO> list = repository.export(record, exportParam, pageRequest);
    return Results.success(list);
}
2.4 导出数据
导出数据需要发起两次请求:
- 第一次请求可导出的列:host/v1/events/export?exportType=COLUMN
- 第二次传入列ID导出数据:host/v1/events/export?exportType=DATA&fillerType=multi-sheet&ids=1&ids=2&ids=3&ids=4&ids=5&ids=6&ids=7……- 单 sheet 页导出:fillerType=single-sheet
- 多 sheet 页导出:fillerType=multi-sheet
 
三、异步导出
使用指南
- 在前端异步导出组件上选择【异步】,点击确认,如果导出任务成功执行则会返回异步导出任务的编号
- 复制返回的异步导出任务编号,到【文件管理】->【异步导出文件】页面查询该任务状态
- 任务状态有三种【正在进行】、【已取消】、【已结束】,【已结束】任务可能为异常结束,异常信息会在展示在【异常信息】一栏,也可能为成功执行完成,会在操作栏看到【下载】按钮,点击下载文件。
注意事项
- 目前异步导出支持默认使用hzero-file文件服务存储异步导出文件,文件大小受限,最大为40MB。
- 异步导出组件支持扩展,可自定义实现,将文件存储到自己部署的文件服务器。
- 支持中断异步导出任务,需要开启management端口并开放async-export-endpoint
management:
  server:
    port: 8080
  endpoints:
    web:
      exposure:
        include: async-export-endpoint #or '*'
- 默认关闭异步导出,开启需要增加配置。
hzero:
  export:
    enable-async: true
    #异步线程池默认配置如下,如无需修改,可省略以下配置
    core-pool-size: 3
    maximum-pool-size: 10
    keep-alive-time: 0
    queue-size: 2147483645
    async-thread-name: async-export-executor
四、定制化开发
一般来说,预定义的导出样式可能不满足需求,可以自行开发导出的方式。预定义两种导出方式,单Sheet页导出和多Sheet页导出。
单Sheet页: 头行打平的方式在一个Sheet页中显示

多Sheet页:头行分开Sheet页导出

如果需要自定义导出方式,可实现 ExcelFiller 抽象类(Excel填充器),该抽象类有两个抽象方法:
/**
 * @return 填充器类型名称
 */
public abstract String getFillerType();
/**
 * 填充数据
 *
 * @param workbook 工作簿
 * @param root 导出列
 * @param data 数据
 */
protected abstract void fillData(SXSSFWorkbook workbook, ExportColumn root, List<?> data);
开发完成后,需要将自定义的填充器注册为Spring的bean,这样才能找到该填充器。