数据导出组件
组件编码
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
 
 
三、定制化开发
一般来说,预定义的导出样式可能不满足需求,可以自行开发导出的方式。预定义两种导出方式,单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,这样才能找到该填充器。
四、版本更新日志
0.3.0.RELEASE [2018-10-26]
- 发布 
0.3.0.RELEASE稳定版 - 新增根据 SQL 执行结果导出的方式
 
0.3.0-SNAPSHOT [2018-10-12]
- 版本跟随 starter 升级至 0.3.0-SNAPSHOT
 - 修复已知BUG
 
0.1.0-SNAPSHOT [2018-08-25]
- 支持大数据量导出
 - 支持头行结构导出、头行打平导出
 - 预定义两种导出方式,支持自定义导出方式
 
展望
- 如果数据量过大,又是分页查询,由于使用了事务,可能导致数据库连接占用久,如果同时多个导出,可能导致连接不够用,将在下个版本进行优化。
 - 如果导出数据量过大,等待时间会很长,下个版本考虑增加异步的方式,后台生成Excel后,再去下载Excel。
 - ….