기본 클래스 설명
- DataExporter(내보내기 유틸리티 클래스)
- PageQuery(범용 페이지 조회)
- SupplierWrapper(함수 래퍼)
- BeanLocator(스프링 빈 조회)
코드
DataExporter
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.PageUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alibaba.excel.util.ListUtils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.sample.common.utils.BeanLocator;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Excel 내보내기 유틸리티 클래스
* @author developer
* @date 2024/03/30
*/
public class DataExporter <T> {
private HttpServletResponse httpResponse;
private ServletOutputStream outputStream;
private boolean useMemory = false;
private boolean parallelProcessing = false;
private String outputFileName = "export";
private Workbook workbook = new SXSSFWorkbook();
private String currentSheetName = "sheet";
private static final String CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
private static final String ENCODING = "UTF-8";
private AtomicInteger currentRowPosition;
private Integer totalRecords;
private Integer sheetIndex = 1;
private static final Integer MAX_SHEET_ROWS = 1000000;
private List<List<Integer>> columnWidths;
private List<List<Integer>> rowHeights;
private List<List<Integer>> styleCoordinates;
private List<CellStyle> cellStyles;
private Integer defaultColumnWidth = 20;
private Integer defaultRowHeight = 20;
private List<List<Integer>> valueCoordinates;
private List<String> cellContents;
private List<List<Integer>> mergeRegions;
private List<List<Integer>> dropdownCoordinates;
private List<List<String>> dropdownOptions;
private List<List<Integer>> chartCoordinates;
private List<byte[]> chartData;
public DataExporter(HttpServletResponse response) {
this.httpResponse = response;
}
/**
* 메모리 모드 활성화 (속도 향상)
* @return 현재 인스턴스
*/
public DataExporter<T> enableMemoryMode() {
workbook = new XSSFWorkbook();
return this;
}
/**
* 파일명 설정
* @param fileName 파일명
* @return 현재 인스턴스
*/
public DataExporter<T> withFileName(String fileName) {
this.outputFileName = fileName;
return this;
}
/**
* 응답 스트림 초기화
* @return 현재 인스턴스
* @throws IOException IO 예외
*/
public DataExporter<T> initializeResponse() throws IOException {
httpResponse.setContentType(CONTENT_TYPE);
httpResponse.setCharacterEncoding(ENCODING);
outputFileName = URLEncoder.encode(outputFileName, ENCODING).replaceAll("\\+", "%20");
httpResponse.setHeader("fileName", outputFileName + ".xlsx");
this.outputStream = httpResponse.getOutputStream();
return this;
}
/**
* 페이지 설정
* @param total 전체 레코드 수
* @return 현재 인스턴스
*/
public DataExporter<T> withTotalRecords(Integer total) {
this.totalRecords = total;
return this;
}
/**
* 병렬 처리 활성화
* @param total 전체 레코드 수
* @return 현재 인스턴스
*/
public DataExporter<T> enableParallelProcessing(Integer total) {
this.parallelProcessing = true;
return withTotalRecords(total);
}
/**
헤더 설정
* @param headers 헤더 목록
* @param rowIndex 행 인덱스
* @param colIndex 열 인덱스
* @return 현재 인스턴스
*/
public DataExporter<T> setHeader(List<String> headers, Integer rowIndex, Integer colIndex) {
for (int i = 0; i < headers.size(); i++) {
addCellContent(headers.get(i), rowIndex, colIndex + i).applyHeaderStyle(rowIndex, colIndex + i);
}
return this;
}
/**
* 셀 내용 설정
* @param value 내용
* @param rowIndex 행 인덱스
* @param colIndex 열 인덱스
* @return 현재 인스턴스
*/
public DataExporter<T> addCellContent(String value, Integer rowIndex, Integer colIndex) {
if (CollUtil.isEmpty(cellContents)) {
cellContents = new ArrayList<>();
}
if (CollUtil.isEmpty(valueCoordinates)) {
valueCoordinates = new ArrayList<>();
}
cellContents.add(value);
valueCoordinates.add(ListUtils.newArrayList(rowIndex, colIndex));
return this;
}
/**
* 셀 내용 설정 (내부 메서드)
* @param value 내용
* @param rowIndex 행 인덱스
* @param colIndex 열 인덱스
* @return 현재 인스턴스
*/
private DataExporter<T> writeCellValue(String value, Integer rowIndex, Integer colIndex) {
Sheet sheet = retrieveSheet();
Row row = getOrCreateRow(sheet, rowIndex);
Cell cell = getOrCreateCell(row, colIndex);
cell.setCellValue(value);
return this;
}
/**
* 시트 조회
* @return Sheet 객체
*/
public Sheet retrieveSheet() {
Sheet sheet = workbook.getSheet(currentSheetName + sheetIndex);
if (sheet == null) {
sheet = workbook.createSheet(currentSheetName + sheetIndex);
}
return sheet;
}
/**
* 시트명 설정
* @param sheetName 시트명
* @return 현재 인스턴스
*/
public DataExporter<T> withSheetName(String sheetName) {
this.currentSheetName = sheetName;
return this;
}
/**
* 행 조회 또는 생성
* @param sheet 시트
* @param rowIndex 행 인덱스
* @return Row 객체
*/
private Row getOrCreateRow(Sheet sheet, Integer rowIndex) {
Row row = sheet.getRow(rowIndex);
if (row == null) {
row = sheet.createRow(rowIndex);
}
return row;
}
/**
* 셀 조회 또는 생성
* @param row 행
* @param colIndex 열 인덱스
* @return Cell 객체
*/
private Cell getOrCreateCell(Row row, Integer colIndex) {
Cell cell = row.getCell(colIndex);
if (cell == null) {
cell = row.createCell(colIndex);
}
return cell;
}
/**
* 빈 셀 스타일 조회
* @return CellStyle 객체
*/
public CellStyle createCellStyle() {
return workbook.createCellStyle();
}
/**
* 셀 스타일 설정
* @param rowIndex 행 인덱스
* @param colIndex 열 인덱스
* @param style 셀 스타일
* @return 현재 인스턴스
*/
public DataExporter<T> applyStyle(Integer rowIndex, Integer colIndex, CellStyle style) {
if (CollUtil.isEmpty(styleCoordinates)) {
styleCoordinates = new ArrayList<>();
}
if (CollUtil.isEmpty(cellStyles)) {
cellStyles = new ArrayList<>();
}
styleCoordinates.add(ListUtils.newArrayList(rowIndex, colIndex));
cellStyles.add(style);
return this;
}
/**
* 헤더 스타일 적용
* @param rowIndex 행 인덱스
* @param colIndex 열 인덱스
* @return 현재 인스턴스
*/
private DataExporter<T> applyHeaderStyle(Integer rowIndex, Integer colIndex) {
CellStyle style = createCellStyle();
return applyStyle(rowIndex, colIndex, style);
}
/**
* 열 너비 설정
* @param column 열 인덱스
* @param width 너비
* @return 현재 인스턴스
*/
public DataExporter<T> setColumnWidth(Integer column, Integer width) {
if (CollUtil.isEmpty(columnWidths)) {
columnWidths = new ArrayList<>();
}
columnWidths.add(ListUtils.newArrayList(column, width));
return this;
}
/**
* 열 너비 일괄 설정
* @param columns 열 정보 목록
* @return 현재 인스턴스
*/
public DataExporter<T> setColumnWidths(List<List<Integer>> columns) {
if (CollUtil.isEmpty(columnWidths)) {
columnWidths = new ArrayList<>();
}
columnWidths.addAll(columns);
return this;
}
/**
* 행 높이 설정
* @param rows 행 정보 목록
* @return 현재 인스턴스
*/
public DataExporter<T> setRowHeights(List<List<Integer>> rows) {
if (CollUtil.isEmpty(rowHeights)) {
rowHeights = new ArrayList<>();
}
rowHeights.addAll(rows);
return this;
}
/**
* 행 높이 및 열 너비 설정
* @param dimensions 차원 정보
* @return 현재 인스턴스
*/
public DataExporter<T> configureDimensions(List<List<Integer>> dimensions) {
for (List<Integer> dimension : dimensions) {
configureDimension(dimension.get(0), dimension.get(1), dimension.get(2), dimension.get(3));
}
return this;
}
/**
* 행 높이 및 열 너비 개별 설정
* @param row 행 인덱스
* @param rowHeight 행 높이
* @param column 열 인덱스
* @param columnWidth 열 너비
* @return 현재 인스턴스
*/
public DataExporter configureDimension(Integer row, Integer rowHeight, Integer column, Integer columnWidth) {
return setRowHeight(row, rowHeight).setColumnWidth(column, columnWidth);
}
/**
* 기본 행 높이 설정
* @param height 높이
* @return 현재 인스턴스
*/
public DataExporter<T> withDefaultRowHeight(Integer height) {
this.defaultRowHeight = height;
return this;
}
/**
* 기본 열 너비 설정
* @param width 너비
* @return 현재 인스턴스
*/
public DataExporter<T> withDefaultColumnWidth(Integer width) {
this.defaultColumnWidth = width;
return this;
}
/**
* 기본 행 높이 및 열 너비 설정
* @param height 높이
* @param width 너비
* @return 현재 인스턴스
*/
public DataExporter<T> withDefaultDimensions(Integer height, Integer width) {
return withDefaultColumnWidth(width).withDefaultRowHeight(height);
}
/**
* 개별 행 높이 설정
* @param rowIndex 행 인덱스
* @param height 높이
* @return 현재 인스턴스
*/
public DataExporter<T> setRowHeight(Integer rowIndex, Integer height) {
rowHeights.add(ListUtils.newArrayList(rowIndex, height));
return this;
}
/**
* 데이터 쓰기
* @param query 페이지 조회 객체
* @param row 시작 행
* @param col 시작 열
* @return 현재 인스턴스
* @throws IOException IO 예외
* @throws ExecutionException 실행 예외
* @throws InterruptedException 중단 예외
*/
public DataExporter<T> writeData(PageQuery<List<List<String>>> query, Integer row, Integer col)
throws IOException, ExecutionException, InterruptedException {
if (currentRowPosition == null) {
currentRowPosition = new AtomicInteger(row);
}
if (totalRecords == null) {
writeData(query.getResult(), currentRowPosition.get(), col);
return this;
}
int totalPages = PageUtil.totalPage(totalRecords, query.getPageSize());
AtomicInteger pageCounter = new AtomicInteger(0);
if (!parallelProcessing) {
processPages(query, currentRowPosition.get(), col, pageCounter, totalPages);
return this;
}
ThreadPoolExecutor executor = BeanLocator.getBean(ThreadPoolExecutor.class);
List<Future<?>> taskResults = new ArrayList<>();
for (int i = 0; i < 4; i++) {
Future<?> task = executor.submit(() -> {
processPages(query, currentRowPosition.get(), col, pageCounter, totalPages);
});
taskResults.add(task);
}
for (Future<?> task : taskResults) {
task.get();
}
return this;
}
/**
* 데이터 쓰기
* @param data 데이터 목록
* @param row 시작 행
* @param col 시작 열
* @throws IOException IO 예외
*/
public void writeData(List<List<String>> data, Integer row, Integer col) throws IOException {
if (currentRowPosition == null) {
currentRowPosition = new AtomicInteger(row);
}
if (currentRowPosition.get() + data.size() > MAX_SHEET_ROWS) {
List<List<String>> firstPart = ListUtil.sub(data, 0, MAX_SHEET_ROWS - currentRowPosition.get());
writeRows(firstPart, currentRowPosition.get(), col);
data = ListUtil.sub(data, MAX_SHEET_ROWS - currentRowPosition.get(), data.size());
currentRowPosition.set(0);
sheetIndex++;
}
writeRows(data, currentRowPosition.get(), col);
}
private void processPages(PageQuery<List<List<String>>> query, Integer row, Integer col,
AtomicInteger pageCounter, int totalPages) {
while (pageCounter.get() < totalPages) {
query.setPageNumber(pageCounter.incrementAndGet());
List<List<String>> result = query.getResult();
try {
writeData(result, row + (pageCounter.get() - 1) * query.getPageSize(), col);
} catch (IOException e) {
throw new RuntimeException(e);
}
query.clearPageInfo();
}
}
/**
* 데이터 쓰기 완료
* @return 현재 인스턴스
* @throws IOException IO 예외
*/
public DataExporter<T> finish() throws IOException {
workbook.write(outputStream);
return this;
}
/**
* 행 데이터 쓰기
* @param data 데이터
* @param row 시작 행
* @param col 시작 열
* @throws IOException IO 예외
*/
private void writeRows(List<List<String>> data, Integer row, Integer col) throws IOException {
synchronized (outputStream) {
for (int i = 0; i < data.size(); i++) {
for (int j = 0; j < data.get(i).size(); j++) {
writeCellValue(data.get(i).get(j), row + i, col + j);
}
}
currentRowPosition.set(row + data.size());
finish();
}
}
private List<List<String>> convertToGrid(List<T> items, List<String> fields) {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
List<List<String>> result = new ArrayList<>(items.size());
for (T item : items) {
JSONObject json = JSONUtil.parseObj(item);
List<String> rowData = new ArrayList<>(fields.size());
for (String field : fields) {
Object value = json.get(field);
if (value == null) {
value = "";
} else if (value instanceof Date) {
value = formatter.format(value);
} else {
value = value.toString();
}
rowData.add(value.toString());
}
result.add(rowData);
}
return result;
}
/**
* 빌더 패턴 실행
* @return 현재 인스턴스
*/
public DataExporter<T> build() {
Sheet sheet = retrieveSheet();
sheet.setDefaultColumnWidth(defaultColumnWidth * 256);
sheet.setDefaultRowHeight(defaultRowHeight.shortValue());
if (columnWidths != null) {
for (List<Integer> width : columnWidths) {
sheet.setColumnWidth(width.get(0), width.get(1));
}
}
if (rowHeights != null) {
for (List<Integer> height : rowHeights) {
Row row = getOrCreateRow(sheet, height.get(0));
row.setHeight(height.get(1).shortValue());
}
}
if (styleCoordinates != null) {
for (int i = 0; i < styleCoordinates.size(); i++) {
List<Integer> coord = styleCoordinates.get(i);
CellStyle style = cellStyles.get(i);
Cell cell = getOrCreateCell(getOrCreateRow(sheet, coord.get(0)), coord.get(1));
cell.setCellStyle(style);
}
}
if (valueCoordinates != null) {
for (int i = 0; i < valueCoordinates.size(); i++) {
List<Integer> coord = valueCoordinates.get(i);
String content = cellContents.get(i);
writeCellValue(content, coord.get(0), coord.get(1));
}
}
if (mergeRegions != null) {
for (List<Integer> region : mergeRegions) {
combineCells(region.get(0), region.get(1), region.get(2), region.get(3));
}
}
if (dropdownOptions != null) {
for (int i = 0; i < dropdownCoordinates.size(); i++) {
List<Integer> coord = dropdownCoordinates.get(i);
List<String> options = dropdownOptions.get(i);
createDropdown(options, coord.get(0), coord.get(1), coord.get(2), coord.get(3));
}
}
if (chartCoordinates != null) {
for (int i = 0; i < chartCoordinates.size(); i++) {
List<Integer> anchor = chartCoordinates.get(i);
byte[] imageBytes = chartData.get(i);
Drawing<?> drawing = sheet.createDrawingPatriarch();
ClientAnchor imageAnchor = drawing.createAnchor(
anchor.get(0), anchor.get(1), anchor.get(2), anchor.get(3),
anchor.get(4), anchor.get(5), anchor.get(6), anchor.get(7)
);
int imageIndex = workbook.addPicture(imageBytes, Workbook.PICTURE_TYPE_PNG);
drawing.createPicture(imageAnchor, imageIndex);
}
}
return this;
}
/**
* 차트 설정
* @param bytes 차트 이미지 데이터
* @param dx1 x offset 1
* @param dy1 y offset 1
* @param dx2 x offset 2
* @param dy2 y offset 2
* @param col1 시작 열
* @param row1 시작 행
* @param col2 종료 열
* @param row2 종료 행
* @return 현재 인스턴스
*/
public DataExporter<T> addChart(byte[] bytes, int dx1, int dy1, int dx2, int dy2,
int col1, int row1, int col2, int row2) {
if (chartCoordinates == null) {
chartCoordinates = new ArrayList<>();
}
if (chartData == null) {
chartData = new ArrayList<>();
}
chartData.add(bytes);
chartCoordinates.add(ListUtils.newArrayList(dx1, dy1, dx2, dy2, col1, row1, col2, row2));
return this;
}
/**
* 셀 병합
* @param firstRow 시작 행
* @param lastRow 종료 행
* @param firstCol 시작 열
* @param lastCol 종료 열
*/
private void combineCells(Integer firstRow, Integer lastRow, Integer firstCol, Integer lastCol) {
CellRangeAddress region = new CellRangeAddress(firstRow, lastRow, firstCol, lastCol);
retrieveSheet().addMergedRegion(region);
}
/**
* 셀 병합
* @param firstRow 시작 행
* @param lastRow 종료 행
* @param firstCol 시작 열
* @param lastCol 종료 열
* @return 현재 인스턴스
*/
public DataExporter<T> mergeCells(Integer firstRow, Integer lastRow, Integer firstCol, Integer lastCol) {
if (mergeRegions == null) {
mergeRegions = new ArrayList<>();
}
mergeRegions.add(ListUtils.newArrayList(firstRow, lastRow, firstCol, lastCol));
return this;
}
/**
* 셀 병합 (일괄)
* @param mergeInfo 병합 정보 목록
* @return 현재 인스턴스
*/
public DataExporter<T> mergeCells(List<List<Integer>> mergeInfo) {
if (this.mergeRegions == null) {
this.mergeRegions = new ArrayList<>();
}
this.mergeRegions.addAll(mergeInfo);
return this;
}
/**
* 드롭다운 목록 설정
* @param options 옵션 목록
* @param firstRow 시작 행
* @param lastRow 종료 행
* @param firstCol 시작 열
* @param lastCol 종료 열
* @return 현재 인스턴스
*/
public DataExporter<T> addDropdown(List<String> options, Integer firstRow, Integer lastRow,
Integer firstCol, Integer lastCol) {
if (dropdownCoordinates == null) {
dropdownCoordinates = new ArrayList<>();
}
if (dropdownOptions == null) {
dropdownOptions = new ArrayList<>();
}
dropdownCoordinates.add(ListUtils.newArrayList(firstRow, lastRow, firstCol, lastCol));
dropdownOptions.add(options);
return this;
}
/**
* 드롭다운 목록 생성
* @param options 옵션 목록
* @param firstRow 시작 행
* @param lastRow 종료 행
* @param firstCol 시작 열
* @param lastCol 종료 열
*/
public void createDropdown(List<String> options, Integer firstRow, Integer lastRow,
Integer firstCol, Integer lastCol) {
Sheet sheet = retrieveSheet();
DataValidationHelper validationHelper = sheet.getDataValidationHelper();
DataValidationConstraint constraint = validationHelper.createExplicitListConstraint(
options.toArray(new String[0])
);
CellRangeAddressList range = new CellRangeAddressList(firstRow, lastRow, firstCol, lastCol);
DataValidation validation = validationHelper.createValidation(constraint, range);
validation.setShowErrorBox(true);
validation.createErrorBox("오류", "목록에서 옵션을 선택해야 합니다.");
sheet.addValidationData(validation);
}
}
PageQuery
import org.sample.common.utils.SupplierWrapper;
/**
* 페이지 조회 추상 클래스
* @author developer
* @date 2024/03/25
*/
public abstract class PageQuery<T> implements SupplierWrapper<T> {
private final ThreadLocal<Integer> pageHolder = new ThreadLocal<>();
private Integer pageSize = 10000;
public void setPageNumber(Integer pageNumber) {
pageHolder.set(pageNumber);
}
public Integer getPageNumber() {
return pageHolder.get();
}
public void clearPageInfo() {
pageHolder.remove();
}
public Integer getPageSize() {
return pageSize;
}
public void setPageSize(Integer pageSize) {
if (pageSize == null) {
return;
}
this.pageSize = pageSize;
}
@Override
public T getResult() {
return execute();
}
}
SupplierWrapper
/**
* 함수형 래퍼 인터페이스
* @author developer
* @param <T> 반환 타입
*/
public interface SupplierWrapper<T> {
/**
* 결과 반환
* @return T
*/
T execute();
/**
* 실행 메서드
* @return T
*/
T getResult();
}
BeanLocator
package org.sample.common.utils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* 스프링 빈 조회 유틸리티
* @author developer
* @date 2024/02/02
*/
@Component
public class BeanLocator implements ApplicationContextAware {
private static ApplicationContext context;
public static ApplicationContext getContext() {
return context;
}
public static Object getBean(String beanName) {
return context.getBean(beanName);
}
public static <T> T getBean(Class<T> type) {
return context.getBean(type);
}
public static <T> T getBean(String beanName, Class<T> type) {
return context.getBean(beanName, type);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (BeanLocator.context == null) {
BeanLocator.context = applicationContext;
}
}
}
차트 내보내기 기능 사용법
- JFreeChart를 사용하여 차트를 이미지로 그린 후 byte 배열로 변환하여 메서드 호출
1.1 JFreeChart 사용법은 Java 차트 라이브러리 JFreeChart 활용 가이드(입문)를 참조