在本文中,我们将深入探讨一个Java实现的表格导出工具类,它使用了ExcelBuilder类和BaseExportSupportImpl类。我们将详细分析这两个类的实现,以及其中涉及到的设计模式,如建造者模式和模板方法模式。我们还将介绍如何使用这些类创建定制的表格导出功能。
我们将使用 Apache POI 库,这是一个广泛使用的 Java 库,用于处理 Microsoft Office 格式的文档。并且,我们实现了 ExcelBuilder
类和 BaseExportSupportImpl
类来简化表格的创建和操作。
1. 依赖
首先,我们需要将 Apache POI 库添加到项目的依赖中。对于 Maven 项目,请在 pom.xml
文件中添加以下依赖:
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
2. ExcelBuilder 类
ExcelBuilder 类是一个灵活的表格数据构建器,它实现了链式调用,方便我们创建和操作 Excel 表格。在这个类中,我们使用了建造者模式(Builder Pattern)来设计,使得对象的构建过程与表示分离,这样我们就可以在不改变对象表示的情况下,对对象的构建进行定制。
以下是 ExcelBuilder 类的代码实现:
import cn.hutool.core.util.ObjectUtil;
import com.bbx.openmerchantplatform.dto.ExcelFooterDTO;
import com.bbx.openmerchantplatform.utils.excel.annotation.ExcelTag;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.streaming.SXSSFCell;
import org.apache.poi.xssf.streaming.SXSSFRow;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Comparator;
import java.util.List;
/**
* @ClassName ExcelBuilder
* @Description 表格数据建造者
* @Author elong
* @Date 2023/4/13 21:01
* @Version 1.0
*/
public class ExcelBuilder {
private SXSSFSheet sheet;
private CellStyle style;
private Field[] fields;
private SXSSFWorkbook wb;
private boolean isAddHeader;
//行指针
private int rowPointer = -1;
//列指针
private int colPointer = 0;
public ExcelBuilder withSheet(SXSSFSheet sheet) {
this.sheet = sheet;
return this;
}
public ExcelBuilder withStyle(CellStyle style) {
this.style = style;
return this;
}
public ExcelBuilder withFields(Field[] fields) {
this.fields = fields;
return this;
}
public ExcelBuilder withWorkBook(SXSSFWorkbook workBook) {
this.wb = workBook;
return this;
}
/**
* 移动行指针和列指针
* @param rowOffset
* @param colOffset
* @return
*/
public ExcelBuilder movePointer(int rowOffset, int colOffset) {
this.rowPointer += rowOffset;
this.colPointer += colOffset;
return this;
}
/**
* 选中指定行和指定列
* @param rowIndex
* @param colIndex
* @return
*/
public ExcelBuilder select(int rowIndex, int colIndex) {
this.rowPointer = rowIndex;
this.colPointer = colIndex;
return this;
}
/**
* 选中新的一行
*
* @return
*/
public ExcelBuilder newRow() {
return movePointer(1, -colPointer);
}
/**
* 选中下一行
*
* @return
*/
public ExcelBuilder nextRow() {
return movePointer(1,0);
}
/**
* 选中下一列
*
* @return
*/
public ExcelBuilder nextCol() {
return movePointer(0,1);
}
/**
* 选中指定行
*
* @param rowIndex
* @return
*/
public ExcelBuilder selectRow(int rowIndex) {
return select(rowIndex, 0);
}
/**
* 选中指定列
*
* @param colIndex
* @return
*/
public ExcelBuilder selectCol(int colIndex) {
return select(0,colIndex);
}
public SXSSFCell checkCell() {
SXSSFRow rowObj = sheet.getRow(rowPointer);
if (rowObj == null) {
rowObj = sheet.createRow(rowPointer);
}
SXSSFCell cell = rowObj.getCell(colPointer);
if (ObjectUtil.isNull(cell)) {
cell = rowObj.createCell(colPointer);
}
return cell;
}
/**
* 添加数据
*
* @param value
* @return
*/
public ExcelBuilder addCell(String value) {
SXSSFCell cell = checkCell();
cell.setCellValue(value);
cell.setCellStyle(style);
return this;
}
/**
* 设置当前cell对齐方向
*
* @param alignment
* @return
*/
public ExcelBuilder setCellStyleAlignment(HorizontalAlignment alignment) {
style.setAlignment(alignment);
SXSSFCell cell = checkCell();
cell.setCellStyle(style);
return this;
}
/**
* 添加数据行
*
* @param datum
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws NoSuchMethodException
*/
public void addRow(Object datum) throws Exception {
newRow();
int type = 0;
for (Field field : fields) {
if (String.class.equals(field.getType())) {
type = Cell.CELL_TYPE_STRING;
}
Object property = PropertyUtils.getProperty(datum, field.getName());
this.addCell(property != null ? property.toString() : "");
this.nextCol();
}
}
/**
* 批量添加数据
*
* @param dataList
* @param <T>
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws NoSuchMethodException
*/
public <T> void addRows(List<T> dataList) throws Exception {
if (!isAddHeader) {
createExcelHeader();
isAddHeader = true;
}
for (T datum : dataList) {
addRow(datum);
}
}
/**
* 合并选中列
*
* @param rowSpan
* @param colSpan
* @return
*/
public ExcelBuilder merge(int rowSpan, int colSpan) {
sheet.addMergedRegion(new CellRangeAddress(rowPointer, rowPointer + rowSpan - 1, colPointer, colPointer + colSpan - 1));
colPointer += colSpan;
return this;
}
/**
* 向当前列继续添加数据
*
* @param value
* @return
*/
public ExcelBuilder addContentToCurrentCell(String value) {
SXSSFCell cell = checkCell();
String currentData = cell.getStringCellValue();
currentData += value;
cell.setCellValue(currentData);
cell.setCellStyle(style);
return this;
}
/**
* 新增属性名行
*/
private void createExcelHeader() {
//指针指向新一行
newRow();
//遍历属性名
for (Field field : fields) {
ExcelTag excelTag = field.getAnnotation(ExcelTag.class);
CellStyle style = wb.createCellStyle();
Font font = wb.createFont();
font.setColor(excelTag.fontColor().getIndex());
style.setFont(font);
setStyle(style);
addCell(excelTag.tag());
nextCol();
}
}
private ExcelBuilder setStyle(CellStyle style) {
SXSSFCell cell = checkCell();
cell.setCellStyle(style);
return this;
}
/**
* 添加表尾数据
*
* @param footerList
* @param footerTitle
*/
public void addExcelFooter(List<ExcelFooterDTO> footerList, String footerTitle) {
newRow();
setStyle(style);
addCell(footerTitle);
nextCol();
footerList.sort(Comparator.comparing(ExcelFooterDTO::getSort));
for (ExcelFooterDTO entry : footerList) {
if (ObjectUtil.isNotNull(entry.getValue())) {
addCell(entry.getTitle() + ": " + entry.getValue());
nextCol();
}
}
}
/**
* 建造者初始化
*
* @return
*/
public ExcelBuilder init() {
if (ObjectUtil.isNotNull(sheet)) {
//设置默认列宽
sheet.setDefaultColumnWidth(15);
}
return this;
}
}
2.1 类成员变量
ExcelBuilder 类的主要成员变量包括:
sheet
:SXSSFSheet 类型,表示当前操作的工作表。
style
:CellStyle 类型,表示单元格的样式。
fields
:Field 数组,表示表格中的字段。
wb
:SXSSFWorkbook 类型,表示工作簿对象。
rowPointer
和 colPointer
:分别表示行指针和列指针。
2.2 构造器和工具方法
ExcelBuilder 类提供了一些构造器和工具方法,用于设置类的成员变量。例如,withSheet、withStyle、withFields 和 withWorkBook 方法分别用于设置 sheet、style、fields 和 wb 成员变量。
此外,还提供了如 movePointer、select 等方法,用于移动行列指针。
2.3 行列操作方法
ExcelBuilder 类提供了一系列方法,用于行列的操作,例如:
newRow
:选中新的一行。
nextRow
:选中下一行。
nextCol
:选中下一列。
selectRow
:选中指定行。
selectCol
:选中指定列。
2.4 数据操作方法
数据操作方法主要包括:
addCell
:添加一个单元格。
addRow
:添加一行数据。
addRows
:批量添加数据行。
2.5 样式和格式化方法
ExcelBuilder 类还提供了一些方法,用于设置单元格样式和格式化,例如:
setCellStyleAlignment
:设置当前单元格对齐方向。
setStyle
:设置单元格样式。
2.6 表头和表尾方法
我们还需要为表格添加表头和表尾。以下是相关的方法:
createExcelHeader
:创建表头,根据字段的注解信息生成表头。
addExcelFooter
:添加表尾数据。
3. BaseExportSupportImpl 类
BaseExportSupportImpl 类是一个用于支持导出功能的基础实现类。它封装了一些通用的导出方法,使得我们可以方便地在具体的导出类中实现定制的导出功能。
它的作用是提供通用的表格导出功能,包括数据导出到 Excel 文件,并支持多种自定义选项。为了实现这些功能,我们采用了以下设计模式:
模板方法模式
:模板方法模式定义了一个操作中算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变算法的结构即可重定义该算法的某些特定步骤。在 BaseExportSupportImpl 类中,我们定义了一个名为 loadAndWrite的模板方法,它包含了导出操作的基本步骤。子类可以根据需要覆盖这些步骤,以实现特定的导出行为。
import cn.hutool.core.util.ObjectUtil;
import com.bbx.openmerchantplatform.dto.ExcelFooterDTO;
import com.bbx.openmerchantplatform.excel.builder.ExcelBuilder;
import com.bbx.openmerchantplatform.utils.excel.annotation.BaseExportSupport;
import com.bbx.openmerchantplatform.utils.excel.annotation.ExcelTag;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
/**
* 使用说明:
* 创建具体导出类
*
* @author: huangzhb
* @Date: 2019年09月17日 22:20:36
* @Description:
*/
@Slf4j
public abstract class BaseExportSupportImpl<T> implements BaseExportSupport<T> {
private Class<T> clazz;
private SXSSFWorkbook wb;
private Field[] fields;
private Consumer preOperate;
private Consumer postOperate;
private ExcelBuilder builder;
public int getFieldsLength() {
return fields.length;
}
public BaseExportSupportImpl() {
initialize();
}
/**
* 初始化
*/
private void initialize() {
//初始化(样式初始化,sheet初始化)
ParameterizedType pt = (ParameterizedType) this.getClass().getGenericSuperclass();
this.clazz = (Class<T>) pt.getActualTypeArguments()[0];
this.wb = new SXSSFWorkbook();
//获取sheet标题
ExcelTag annotation = Optional.ofNullable(getClazz()
.getAnnotation(ExcelTag.class))
.orElseThrow(() -> new RuntimeException("在该类找不到 ExcelTag annotation"));
String tag = annotation.tag();
//获取表格dto各属性
fields = FieldUtils.getFieldsWithAnnotation(getClazz(), ExcelTag.class);
//初始化建造者
builder = new ExcelBuilder()
.withWorkBook(wb)
.withSheet(wb.createSheet(tag)) //创建sheet
.withStyle(wb.createCellStyle()) //创建style
.withFields(fields)
.init();
}
/**
* 提供给子类
* 可自己实现前置和后置操作
*
* @return ExcelBuilder
*/
protected ExcelBuilder getBuilder() {
return this.builder;
}
/**
* 获取class
*
* @return
*/
public Class<T> getClazz() {
return clazz;
}
/**
* 提供前置操作
*
* @param preOperate
* @param <R>
*/
public <R> void setPreOperate(Consumer<R> preOperate) {
this.preOperate = preOperate;
}
/**
* 提供后置操作
*
* @param postOperate
* @param <R>
*/
public <R> void setPostOperate(Consumer<R> postOperate) {
this.postOperate = postOperate;
}
/**
* 追加数据到sheet
*
* @param dataList
* @throws Exception
*/
@Override
public <T> void add(List<T> dataList) throws Exception {
builder.addRows(dataList);
}
/**
* 添加表尾数据
*
* @param footerList
* @param footerTitle
*/
@Override
public void createExcelFooter(List<ExcelFooterDTO> footerList, String footerTitle) {
builder.addExcelFooter(footerList, footerTitle);
}
/**
* 模板方法:loadAndWriteToSheet交由子类实现
* 执行流程
* 1.前置操作
* 2.写数据
* 3.后置操作
*
* @param runnableParams
*/
@Override
public final void loadAndWrite(Map<String, Object> runnableParams) {
//前置操作
try {
if (ObjectUtil.isNotNull(preOperate)) {
preOperate.accept(this);
}
} catch (Exception e) {
log.error("表格生成错误,前置操作执行错误:", e);
onError(runnableParams);
throw new RuntimeException("表格生成错误,前置操作执行错误", e);
}
//添加数据到表中的操作
try {
loadAndWriteToSheet(runnableParams);
} catch (Exception e) {
log.error("表格生成错误,导入数据到表格中出错:", e);
onError(runnableParams);
throw new RuntimeException("表格生成错误,导入数据到表格中出错:", e);
}
//后置操作
try {
if (ObjectUtil.isNotNull(postOperate)) {
postOperate.accept(this);
}
} catch (Exception e) {
log.error("表格生成错误,后置操作执行错误:", e);
onError(runnableParams);
throw new RuntimeException("表格生成错误,后置操作执行错误:", e);
}
}
/**
* 可选操作:在第一行添加一行合并居中标题
* 行宽为属性总数的宽度
*
* @param params
*/
protected void setHeaderTitle(Map params) {
builder.selectRow(0)
.addCell(params.get("headerTitle").toString())
.merge(1, fields.length - 1);
}
@Override
public void write(OutputStream outputStream) throws Exception {
//生成文档
try {
//生成文档
wb.write(outputStream);
outputStream.flush();
} catch (Exception e) {
throw new RuntimeException("输出表格文件错误", e);
} finally {
if (outputStream != null) {
outputStream.close();
}
//删除sxssf类生成的临时文件
if (wb != null) {
wb.dispose();
}
}
}
@Override
public abstract void loadAndWriteToSheet(Map<String, Object> runnableParams) throws Exception;
@Override
public void onError(Map<String, Object> runnableParams) {
}
@Override
public void onSuccess(Map<String, Object> runnableParams) {
}
}
4.设计模式解析
建造者模式
ExcelBuilder类使用了建造者模式。这种模式可以将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。在这个例子中,ExcelBuilder类提供了一个灵活的方法来构建Excel表格,使得我们可以在不同的场景下创建不同风格和结构的表格。
模板方法模式
BaseExportSupportImpl类使用了模板方法模式。这种模式定义了一个操作中的算法骨架,而将一些步骤延迟到子类中。这使得子类可以在不改变算法结构的情况下重定义算法的某些特定步骤。在这个例子中,BaseExportSupportImpl类定义了表格导出的基本框架,包括构建表头、构建表格主体和构建表尾等步骤。子类可以根据具体需求实现这些步骤,以实现定制的表格导出功能。