@ExcelMerge
(自分で追加した):
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ExcelMerge {
}
MergeHandler
:
public class CustomAbstractMergeStrategy<T, R> implements WorkbookWriteHandler {
/**
* 結合する行数
*/
private List<Integer> mergeRowCountList;
/**
* 結合が必要な列のインデックス
*/
private List<Integer> mergeColumnIndexList;
/**
* オブジェクト
*/
private Class<T> clazz;
/**
* セルの結合ルール:
* 1. データにのみ適用、ヘッダーには適用しない
* 2. 結合が必要なフィールドは @ExcelMerge で注釈が必要
* 3. 結合条件は: 入力パラメータ Function<T, R> function が必要で、List<T> list 内の結合するセルが連続している必要あり(ソートが必要な場合もある)
*
* @param clazz
* @param list
* @param function
*/
private CustomAbstractMergeStrategy(Class<T> clazz, List<T> list, Function<T, R> function) {
if (CollUtil.isNotEmpty(list)) {
Map<R, Long> collect = list.stream().collect(Collectors.groupingBy(function, LinkedHashMap::new, Collectors.counting()));
this.mergeRowCountList = collect.values().stream().map(Long::intValue).collect(Collectors.toList());
this.clazz = clazz;
this.mergeColumnIndexList = new ArrayList<>();
markMergeIndex();
}
}
public static <T, R> CustomAbstractMergeStrategy<T, R> of(Class<T> clazz, List<T> list, Function<T, R> function) {
return new CustomAbstractMergeStrategy<>(clazz, list, function);
}
/**
* 結合する列のインデックスをマーク
*/
private void markMergeIndex() {
Field[] fields = clazz.getDeclaredFields();
int columnIndex = 0;
for (Field field : fields) {
if (field.isAnnotationPresent(ExcelProperty.class)) {
if (field.isAnnotationPresent(ExcelMerge.class)) {
mergeColumnIndexList.add(columnIndex);
}
columnIndex++;
}
}
}
/**
* ワークブック作成前に呼び出される
*/
@Override
public void beforeWorkbookCreate() {
}
/**
* ワークブック作成後に呼び出される
*
* @param writeWorkbookHolder
*/
@Override
public void afterWorkbookCreate(WriteWorkbookHolder writeWorkbookHolder) {
}
/**
* ワークブックの全操作後に呼び出される
*
* @param writeWorkbookHolder
*/
@Override
public void afterWorkbookDispose(WriteWorkbookHolder writeWorkbookHolder) {
if (CollUtil.isEmpty(mergeColumnIndexList)) {
return;
}
if (CollUtil.isEmpty(mergeRowCountList)) {
return;
}
Sheet sheet = writeWorkbookHolder.getWorkbook().getSheetAt(0);
// 0行目がヘッダー、1行目からデータ
int rowIndex = 1;
for (Integer mergeRowCount : mergeRowCountList) {
if (mergeRowCount > 1) {
for (Integer columnIndex : mergeColumnIndexList) {
CellRangeAddress cellRangeAddress = new CellRangeAddress(rowIndex, rowIndex + mergeRowCount - 1, columnIndex, columnIndex);
sheet.addMergedRegionUnsafe(cellRangeAddress);
}
rowIndex += mergeRowCount;
} else {
rowIndex += 1;
}
}
}
}
Data:
@Data
public class Data implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 計画ID
*/
@ExcelProperty(value = "計画ID")
@ExcelMerge
private Integer id;
/**
* 試験計画
*/
@ExcelProperty(value = "試験計画")
@ExcelMerge
private String examPlanName;
/**
* 種類
*/
@ExcelProperty(value = "種類")
private String subjectTypeName;
/**
* 科目名
*/
@ExcelProperty(value = "科目名")
private String examSubjectName;
/**
* 問題数
*/
@ExcelProperty(value = "問題数")
private Integer questionCount;
/**
* 受験人数
*/
@ExcelProperty(value = "受験人数")
private Integer examCount;
}
Util:
public class Util{
public static <T> void exportExcelWithMerge(List<T> data, Class<? extends T> cls, String fileName, Integer sheetNo, String sheetName, WriteHandler writeHandler) {
HttpServletResponse response = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse();
try (OutputStream out = response.getOutputStream()) {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
EasyExcel.write(out, cls).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).registerWriteHandler(writeHandler).sheet(sheetNo, sheetName).doWrite(data);
} catch (IOException e) {
log.error("error", e);
throw new BusinessException(BizErrorCodeEnum.FILE_DOWNLOAD_FAILED);
}
}
public static <T> void exportExcelWithMerge(List<T> data, Class<? extends T> cls, String fileName, WriteHandler writeHandler) {
exportExcelWithMerge(data, cls, fileName, 0, "Sheet1", writeHandler);
}
}
Test:
@PostMapping("/export")
@ApiOperation("export")
public void export(@RequestBody @Validated ExamPlanExportReq req) {
List<ExportExamPlanResp> data = getDataFromDb(req);
FileUtil.exportExcelWithMerge(data, ExportExamPlanResp.class, "list", CustomAbstractMergeStrategy.of(ExportExamPlanResp.class, data, ExportExamPlanResp::getId));
}
WorkbookWriteHandlerを使用する理由は、すべてのデータの解析が完了した後にafterWorkbookDisposeメソッドが呼び出されるためです。
セルの結合は、RowWriteHandlerやCellWriteHandlerでも実装可能ですが、CellWriteHandlerで行うのは非効率です。
CellWriteHandlerでは各行の各セルを反復処理する必要があるため、
M行 x N列のデータがある場合、結合処理にM x N回の反復が必要になります。
RowWriteHandlerの方が良いのですが、それでもM回の反復処理が必要です。
そのため、WorkbookWriteHandlerを使うのが最も効率的です。
すべてのデータの解析が完了した後、一度だけ結合処理を行えば良いので、とても高速に動作します。
以上の理由から、セルの結合にはWorkbookWriteHandlerを使うのがベストであると判断し、
この実装を選択しました。効率的な処理を行うための設計です。