现状
在日常Java开发中,有时会遇到基于现有的查询结果来导出Excel文件的业务需求,一般会想到使用开源的POI库,大多数人应该会先从网上找几个例子,再复制粘贴到项目中,基于需求来set对应的单元格内容,代码中会存在许多定制化代码,如下图所示:
设计
- 抽象表头信息ExcelHeader类
一般导出的Excel第一行都是表头信息,从第二行开始是需要导出的数据。我们可以使用一个Model类来定义Excel表头及所处列的信息。
// 实现Comparable接口是为了根据order字段排序,确定这个表头的标题放在列的位置顺序
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExcelHeader implements Comparable<ExcelHeader> {
// 表头的标题名称
private String title;
// 表头的标题所在列的顺序
private int order;
// 单元格的宽度
private int width;
// 对应字段
private String fieldName;
@Override
public int compareTo(ExcelHeader o) {
return Integer.compare(order, o.order);
}
}
- 抽象数据模型注解
导出的数据在后端是基于某个Model类的List集合,我们可以定义一个注解来标识此Model的需要导出的字段,从而实现通用的导出。
// 用来在导出对象的field上加入的注解,通过该annotation说明某个属性所对应的标题
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@Documented
public @interface ExcelResources {
// 导出字段的标题名称,用于构造ExcelHeader对象的title
String title();
// 导出字段在excel的顺序,用于构造ExcelHeader对象的order
int order() default 9999;
// 导出字段列的宽度,用于构造ExcelHeader对象的width
int width() default 150;
}
- 实现
1、先定义工具类通用的静态导出方法
/**
* @param objs 导出的数据集合,一般为后端DB查询结果
* @param clz 导出的Excel内容Model类,其导出属性上需要加@ExcelResources
* @param sheetName 导出的Excel的Sheet页名称
* @param pageSize 导出的Excel的每个Sheet页的数据量,若objs.size()>pageSize,会写入到(objs.size()/pageSize+1)个Sheet页中
* @param fileName 导出的Excel文件名,会加上_yyyyMMddHHmmss避免重名
* @param response HttpServletResponse
*/
public static void exportExcel(List<?> objs, Class<?> clz, String sheetName, int pageSize, String fileName,
HttpServletResponse response)
throws IOException, InvocationTargetException, IllegalAccessException, NoSuchMethodException {
if (objs == null || clz == null) {
throw new NullPointerException("parameter must be not null");
}
OutputStream outputStream = null;
try {
Workbook wb = handleObj2Excel(objs, clz, sheetName, pageSize);
setResponseHeader(response, fileName);
// 定入到输出流
outputStream = response.getOutputStream();
wb.write(outputStream);
// 刷新流
outputStream.flush();
} finally {
IOUtils.closeQuietly(outputStream);
}
}
2、私有方法handleObj2Excel()会通过参数objs数据,clz上的自定义注解来反射获取数据并set到单元格中,并返回HSSFWorkbook对象。
private static HSSFWorkbook handleObj2Excel(List<?> objs, Class<?> clz, String sheetName, int pageSize)
throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
HSSFWorkbook wb = new HSSFWorkbook();
// 获取excel标题列表并排序,详见下面私有方法
List<ExcelHeader> headers = getHeaderList(clz);
Collections.sort(headers);
int size = objs.size();
if (size > 0) {
int sheetCount = size % pageSize == 0 ? size / pageSize : size / pageSize + 1;
HSSFSheet sheet;
// 设置第i个sheet页内的数据
for (int i = 1; i <= sheetCount; i++) {
// 创建sheet页
if (!StringUtils.isEmpty(sheetName)) {
sheet = wb.createSheet(sheetName + i);
} else {
sheet = wb.createSheet();
}
HSSFRow row = sheet.createRow(0);
// 设置head的数据格式,详见下面私有方法
CellStyle titleStyle = setCellStyle(wb, POSITION_TITLE);
for (int m = 0, len = headers.size(); m < len; m++) {
HSSFCell head = row.createCell(m);
head.setCellStyle(titleStyle);
head.setCellValue(headers.get(m).getTitle());
sheet.setColumnWidth(m, 40 * headers.get(m).getWidth());
}
// 设置body数据部分的格式
int begin = (i - 1) * pageSize;
int end = Math.min((begin + pageSize), objs.size());
int rowCount = 1;
CellStyle bodyStyle = setCellStyle(wb, POSITION_BODY);
for (int n = begin; n < end; n++) {
row = sheet.createRow(rowCount);
Object obj = objs.get(n);
for (int x = 0, len = headers.size(); x < len; x++) {
Cell body = row.createCell(x);
body.setCellStyle(bodyStyle);
// 反射获取值并set到对应的单元格中,
// objs中model的field与clz的field名称需要相同对应才能导出
body.setCellValue(BeanUtils.getProperty(obj, headers.get(x).getFieldName()));
}
rowCount++;
}
}
}
return wb;
}
// 由参数clz来获取@ExcelResources注解并生成表头信息ExcelHeader类的集合
private static List<ExcelHeader> getHeaderList(Class<?> clz) {
Field[] ms = clz.getDeclaredFields();
List<ExcelHeader> headers = new ArrayList<>(ms.length);
for (Field m : ms) {
String field = m.getName();
if (m.isAnnotationPresent(ExcelResources.class)) {
ExcelResources er = m.getAnnotation(ExcelResources.class);
// 标题,序号,宽度,其成员变量
headers.add(new ExcelHeader(er.title(), er.order(), er.width(), field));
}
}
return headers;
}
// 设置单元格样式
private static CellStyle setCellStyle(Workbook workBook, String position) {
CellStyle cellStyle = workBook.createCellStyle();
cellStyle.setAlignment(HorizontalAlignment.GENERAL);
cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
// 设置单元格字体,表头字段可以大些
Font headerFont = workBook.createFont();
if (POSITION_TITLE.equals(position)) {
headerFont.setFontHeightInPoints((short) 12);
headerFont.setBold(true);
} else {
headerFont.setFontHeightInPoints((short) 10);
}
cellStyle.setFont(headerFont);
return cellStyle;
}
3、私有方法setResponseHeader()为设置响应头及文件名。
// 设置返回的响应头
private static void setResponseHeader(HttpServletResponse response, String fileName) {
// 返回的文件名
String resName = new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1)
+ "_" + DateFormatUtils.format(System.currentTimeMillis(), "yyyyMMddHHmmss") + ".xls";
response.setContentType("application/vnd.ms-excel");
// excel的导出设置(文件名必须去掉空格,否则出现问题)
response.setHeader("Content-Disposition", "attachment;filename="
+ resName.replaceAll("\\s*", StringUtils.EMPTY));
}
例子
1、先定义一个Model类,并将要导出的字段上使用@ExcelResources注解,如下图:
2、通过DB查询出数据后,直接调用工具类导出Excel。
3、查看导出的文件内容
扩展性
由于自定义注解,具有可扩展性,比如定义单元格的格式(字体,颜色,边框,类型 等)、添加单元格的校验(单选,多选,范围等)。
本文暂时没有评论,来添加一个吧(●'◡'●)