EasyExcel 범용 내보내기 도구(주석 없음, 다중 스레드 지원)

  1. 기본 클래스 설명 =======

  2. ExcelExportHandler(내보내기 유틸리티 클래스)

  3. DataQuery(일반 데이터 조회)

  4. DataProcessor(함수 매개변수)

  5. SpringContextUtil(빈 객체 가져오기)

  6. 코드 ====

ExcelExportHandler


import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.PageUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alibaba.excel.EasyExcelFactory;
import com.alibaba.excel.util.ListUtils;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.handler.WriteHandler;
import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
import com.alibaba.excel.write.handler.context.SheetWriteHandlerContext;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.WriteTable;
import com.alibaba.excel.write.style.column.AbstractColumnWidthStyleStrategy;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.itxl.common.utils.SpringContextUtil;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author kimMinJoon
 * @date 2024/3/27
 */
public class ExcelExportHandler<T>{

    private Map<String,WriteSheet> 시트맵 = null;
    private Map<String,WriteTable> 테이블맵 = null;

    private List<WriteHandler> 쓰기핸들러리스트 = null;

    private com.alibaba.excel.ExcelWriter 작성자 = null;

    private OutputStream 출력스트림 = null;

    private String 현재시트이름 = "sheet";

    private Integer 시트번호 = 1;

    private Integer 현재시트데이터크기 = 0;
    private static final Integer 시트최대갯수 = 1000000;

    private List<List<String>> 헤더리스트 = null;

    /**
     * 응답 파일 스트림 타입
     */
    private static final String 콘텐츠타입 = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
    /**
     * 응답 헤더 파일명 인코딩 형식
     */
    private static final String 인코딩 = "UTF-8";

    private Long 전체데이터수;

    private boolean 다중스레드사용 = false;

    public static <T> ExcelExportHandler<T> 생성(){
        return new ExcelExportHandler<>();
    }

    public ExcelExportHandler<T> 응답설정(HttpServletResponse 응답,String 파일이름) throws IOException {
        응답.setContentType(콘텐츠타입);
        응답.setCharacterEncoding(인코딩);
        // 여기서 URLEncoder.encode는 중국어 문자 깨짐 방지를 위함 (easyexcel과 무관)
        파일이름 = URLEncoder.encode(파일이름, 인코딩).replaceAll("\\+", "%20");
        응답.setHeader("fileName",파일이름+".xlsx");
        return 출력스트림설정(응답.getOutputStream());
    }

    public ExcelExportHandler<T> 출력스트림설정(OutputStream 출력스트림){
        this.출력스트림 = 출력스트림;
        return this;
    }

    private WriteSheet 시트가져오기(String 시트이름){
        if (CollUtil.isEmpty(시트맵)){
            시트맵 = new HashMap<>();
        }
        if (StrUtil.isEmpty(시트이름)){
            시트이름 = 현재시트이름;
        }
        if (시트번호 > 1){
            시트이름 = 시트이름 + 시트번호;
        }
        WriteSheet 시트 = 시트맵.get(시트이름);
        if (시트 == null){
            시트 = new WriteSheet();
            시트.setSheetName(시트이름);
            시트맵.put(시트이름,시트);
        }
        return 시트;
    }

    public ExcelExportHandler<T> 현재시트설정(String 시트이름){
        this.현재시트이름 = 시트이름;
        return this;
    }

    private WriteTable 테이블가져오기(String 테이블이름){
        if (CollUtil.isEmpty(테이블맵)){
            테이블맵 = new HashMap<>();
        }
        if (StrUtil.isEmpty(테이블이름)){
            테이블이름 = "table";
        }
        return 테이블맵.computeIfAbsent(테이블이름, k -> new WriteTable());
    }

    public ExcelExportHandler<T> 쓰기핸들러추가(WriteHandler 쓰기핸들러){
        if (CollUtil.isEmpty(쓰기핸들러리스트)){
            쓰기핸들러리스트 = new ArrayList<>();
        }
        쓰기핸들러리스트.add(쓰기핸들러);
        return this;
    }

    public ExcelExportHandler<T> 빌더(){
        ExcelWriterBuilder 작성빌더 = EasyExcelFactory.write(출력스트림);
        if (CollUtil.isNotEmpty(쓰기핸들러리스트)){
            for (WriteHandler 쓰기핸들러 : 쓰기핸들러리스트) {
                작성빌더.registerWriteHandler(쓰기핸들러);
            }
        }
        this.작성자 = 작성빌더.build();
        return this;
    }

    public ExcelExportHandler<T> 헤더설정(List<String> 헤더리스트){
        return 헤더목록설정(헤더변환(헤더리스트));
    }

    private List<List<String>> 헤더변환(List<String> 헤더리스트) {
        List<List<String>> 결과 = new ArrayList<>(헤더리스트.size());
        for (String 헤더 : 헤더리스트) {
            결과.add(Collections.singletonList(헤더));
        }
        return 결과;
    }

    private ExcelExportHandler<T> 헤더목록설정(List<List<String>> 헤더리스트){
        테이블가져오기(null).setHead(헤더리스트);
        return this;
    }


    public ExcelExportHandler<T> 데이터쓰기(List<List<Object>> 데이터리스트){
        if (CollUtil.isEmpty(데이터리스트)){
            return this;
        }
        while (현재시트데이터크기 + 데이터리스트.size() > 시트최대갯수){
            List<List<Object>> 왼쪽데이터 = ListUtil.sub(데이터리스트, 0, 시트최대갯수 - 현재시트데이터크기);
            작성자.write(왼쪽데이터,시트가져오기(null),테이블가져오기(null));
            데이터리스트 = ListUtil.sub(데이터리스트, 시트최대갯수 - 현재시트데이터크기, 데이터리스트.size());
            현재시트데이터크기 = 0;
            시트번호++;
        }
        현재시트데이터크기 += 데이터리스트.size();
        작성자.write(데이터리스트,시트가져오기(null),테이블가져오기(null));
        return this;
    }

    public ExcelExportHandler<T> 데이터쓰기(DataQuery<List<List<Object>>> 데이터조회) throws InterruptedException {
        return 데이터쓰기(데이터조회, 데이터조회.getPageSize());
    }

    public ExcelExportHandler<T> 데이터쓰기(DataQuery<List<List<Object>>> 데이터조회, Integer 페이지크기) throws InterruptedException {
        if (전체데이터수 == null){
            List<List<Object>> 결과 = 데이터조회.accept();
            return 데이터쓰기(결과);
        }
        데이터조회.setPageSize(페이지크기);
        int 전체페이지수 = PageUtil.totalPage(전체데이터수, 데이터조회.getPageSize());
        AtomicInteger 현재페이지번호 = new AtomicInteger(1);
        if (!다중스레드사용){
            데이터쓰기(현재페이지번호, 전체페이지수, 데이터조회);
            return this;
        }
        ThreadPoolTaskExecutor 스레드풀 = SpringContextUtil.getBean(ThreadPoolTaskExecutor.class);
        List<Future<?>> 퓨처리스트 = new ArrayList<>();
        int 스레드풀크기 = Math.min(전체페이지수, 4);
        for (int i = 0; i < 스레드풀크기; i++) {
            Future<?> 퓨처 = 스레드풀.submit(() -> {
                데이터쓰기(현재페이지번호, 전체페이지수, 데이터조회);
            });
            퓨처리스트.add(퓨처);
        }
        for (Future<?> 퓨처 : 퓨처리스트) {
            try {
                퓨처.get();
            } catch (ExecutionException e) {
                throw new RuntimeException(e);
            }
        }
        return this;
    }

    private void 데이터쓰기(AtomicInteger 현재페이지번호, Integer 전체페이지수, DataQuery<List<List<Object>>> 데이터조회){
        while (현재페이지번호.get() <= 전체페이지수) {
            데이터조회.setPageNum(현재페이지번호.getAndIncrement());
            List<List<Object>> 결과 = 데이터조회.accept();
            synchronized (작성자){
                데이터쓰기(결과);
            }
            데이터조회.removePage();
        }
    }

    public static  List<List<Object>> 데이터변환(List<Object> 데이터리스트,List<String> 필드리스트){
        SimpleDateFormat 날짜포맷 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        List<List<Object>> 결과 = new ArrayList<>(데이터리스트.size());
        for (Object 데이터 : 데이터리스트) {
            JSONObject json = JSONUtil.parseObj(데이터);
            List<Object> 객체리스트 = new ArrayList<>(필드리스트.size());
            for (String 필드 : 필드리스트) {
                Object 값 = json.get(필드);
                if (값 == null){
                    값 = "";
                }else if (값 instanceof Date){
                    값 = 날짜포맷.format(값);
                }else{
                    값 = 값.toString();
                }
                객체리스트.add(값);
            }
            결과.add(객체리스트);
        }
        return 결과;
    }

    public void 닫기() {
        if (작성자 == null){
           return;
        }
        작성자.close();
    }

    /**
     * 페이지네이션 쿼리 활성화
     * @param 전체데이터수
     * @return
     */
    public ExcelExportHandler<T> 페이지네이션(Long 전체데이터수){
        this.전체데이터수 = 전체데이터수;
        return this;
    }

    /**
     * 다중 스레딩 활성화
     * @param 전체데이터수
     * @return
     */
    public ExcelExportHandler<T> 다중스레드(Long 전체데이터수){
        this.다중스레드사용 = true;
        return 페이지네이션(전체데이터수);
    }

    /**
     * 셀 병합
     * @param 첫번째행
     * @param 마지막행
     * @param 첫번째열
     * @param 마지막열
     * @return
     */
    public ExcelExportHandler<T> 셀병합(Integer 첫번째행, Integer 마지막행, Integer 첫번째열, Integer 마지막열){
        List<List<Integer>> 병합정보 = new ArrayList<>();
        병합정보.add(ListUtils.newArrayList(첫번째행,마지막행,첫번째열,마지막열));
        return 셀병합(병합정보);
    }


    /**
     * 셀 병합
     * @param 병합정보
     * @return
     */
    public ExcelExportHandler<T> 셀병합(List<List<Integer>> 병합정보) {
        SheetWriteHandler 병합핸들러 = new SheetWriteHandler(){
            @Override
            public void afterSheetCreate(SheetWriteHandlerContext 컨텍스트) {
            for (List<Integer> 병합 : 병합정보) {
                CellRangeAddress 주소리스트 = new CellRangeAddress(병합.get(0), 병합.get(1), 병합.get(2), 병합.get(3));
                컨텍스트.getWriteSheetHolder().getSheet().addMergedRegionUnsafe(주소리스트);
            }
            }
        };
        return 쓰기핸들러추가(병합핸들러);
    }

    /**
     * 드롭다운 목록 설정
     * @param 축정보
     * @param 데이터
     * @return
     */
    public ExcelExportHandler<T> 드롭다운목록설정(List<Integer> 축정보,List<String> 데이터){
        SheetWriteHandler 드롭다운핸들러 = new SheetWriteHandler() {
            @Override
            public void afterSheetCreate(SheetWriteHandlerContext 컨텍스트) {
                CellRangeAddressList 셀범위주소리스트 = new CellRangeAddressList(축정보.get(0), 축정보.get(1), 축정보.get(2), 축정보.get(3));
                DataValidationHelper 헬퍼 = 컨텍스트.getWriteSheetHolder().getSheet().getDataValidationHelper();
                DataValidationConstraint 제약조건 = 헬퍼.createExplicitListConstraint(데이터.toArray(new String[0]));
                DataValidation 데이터검증 = 헬퍼.createValidation(제약조건, 셀범위주소리스트);
                컨텍스트.getWriteSheetHolder().getSheet().addValidationData(데이터검증);
            }
        };
        return 쓰기핸들러추가(드롭다운핸들러);
    }

    /**
     * 드롭다운 목록 설정
     * @param 열
     * @param 데이터
     * @return
     */
    public ExcelExportHandler<T> 드롭다운목록설정(Integer 열,List<String> 데이터){
        //드롭다운 목록을 열 열 아래 1000행으로 설정
        List<Integer> 축정보 = ListUtils.newArrayList(1,1000,열,열);
        return 드롭다운목록설정(축정보,데이터);
    }

    /**
     * 열 너비 설정
     * @return
     */
    public ExcelExportHandler<T> 열너비설정(List<List<Integer>> 열너비정보){
        AbstractColumnWidthStyleStrategy 전략 = new AbstractColumnWidthStyleStrategy(){
            @Override
            protected void setColumnWidth(CellWriteHandlerContext 컨텍스트) {
                Sheet 시트 = 컨텍스트.getWriteSheetHolder().getSheet();
                for (List<Integer> 열너비 : 열너비정보) {
                    시트.setColumnWidth(열너비.get(0),열너비.get(1) * 256);
                }
            }
        };
        return 쓰기핸들러추가(전략);
    }

    /**
     * 열 너비 설정
     * @return
     */
    public ExcelExportHandler<T> 열너비설정(Integer 열,Integer 너비){
        List<List<Integer>> 열너비정보 = new ArrayList<>();
        열너비정보.add(ListUtils.newArrayList(열,너비));
        return 열너비설정(열너비정보);
    }

    /**
     * 행 높이 설정
     * @param 행
     * @param 높이
     * @return
     */
    public ExcelExportHandler<T> 행높이설정(Integer 행,Integer 높이){
        List<List<Integer>> 행높이정보 = new ArrayList<>();
        행높이정보.add(ListUtils.newArrayList(행,높이));
        return 행높이설정(행높이정보);
    }

    /**
     * 행 높이 설정
     * @param 행높이정보
     * @return
     */
    private ExcelExportHandler<T> 행높이설정(List<List<Integer>> 행높이정보) {
        SheetWriteHandler 전략 = new SheetWriteHandler(){
            @Override
            public void afterSheetCreate(SheetWriteHandlerContext 컨텍스트) {
                Sheet 시트 = 컨텍스트.getWriteSheetHolder().getSheet();
                for (List<Integer> 행높이 : 행높이정보) {
                    Row 행 = 시트.getRow(행높이.get(0));
                    행.setHeightInPoints(행높이.get(1));
                }
            }
        };
        return 쓰기핸들러추가(전략);
    }

    /**
     * 셀 스타일 설정
     * @param 첫번째행
     * @param 마지막행
     * @param 첫번째열
     * @param 마지막열
     * @return
     */
    public ExcelExportHandler<T> 셀스타일설정(Integer 첫번째행, Integer 마지막행, Integer 첫번째열, Integer 마지막열,CellStyle 셀스타일){
        List<List<Integer>> 축정보 = new ArrayList<>();
        축정보.add(ListUtils.newArrayList(첫번째행,마지막행,첫번째열,마지막열));
        return 셀스타일설정(축정보,셀스타일);
    }

    /**
     * 셀 스타일 설정
     * @param 축정보
     * @return
     */
    public ExcelExportHandler<T> 셀스타일설정(List<List<Integer>> 축정보,CellStyle 셀스타일){
        SheetWriteHandler 핸들러 = new SheetWriteHandler(){
            @Override
            public void afterSheetCreate(SheetWriteHandlerContext 컨텍스트) {
                Sheet 시트 = 컨텍스트.getWriteSheetHolder().getSheet();
                for (List<Integer> 축 : 축정보) {
                    for (int i = 축.get(0); i < 축.get(1); i++) {
                        Row 행 = 시트.getRow(i);
                        for (int j = 축.get(2); j < 축.get(3); j++) {
                            //스타일 설정
                            Cell 셀 = 행.getCell(j);
                            셀.setCellStyle(셀스타일);
                        }
                    }
                }
                SheetWriteHandler.super.afterSheetCreate(컨텍스트);
            }
        };
        return 쓰기핸들러추가(핸들러);
    }


    public static void 오류처리(HttpServletResponse 응답,String 메시지) throws IOException {
        응답.reset();
        응답.setContentType("application/json");
        응답.sendError(HttpServletResponse.SC_NOT_FOUND,메시지);
    }



}

DataQuery


import org.itxl.common.utils.DataProcessor;

/**
 * @author kimMinJoon
 * @date 2024/3/25
 */
public abstract class DataQuery<T> implements DataProcessor<T> {

    private final ThreadLocal<Integer> 로컬 = new ThreadLocal<>();

    private Integer 페이지크기 = 10000;

    public void 페이지번호설정(Integer 페이지번호){
        로컬.set(페이지번호);
    }

    public Integer 페이지번호가져오기(){
       return 로컬.get();
    }

    public void 페이지정보제거(){
        로컬.remove();
    }

    public Integer getPageSize() {
        return 페이지크기;
    }

    public void setPageSize(Integer 페이지크기) {
        if (페이지크기 == null){
            return;
        }
        this.페이지크기 = 페이지크기;
    }

    @Override
    public T accept() {
        return andThen();
    }
}


DataProcessor


/**
 * @author  kimMinJoon
 * 일반 함수 매개변수
 * @param <T>
 */
public interface DataProcessor<T>{
    /**
     * 메소드 구현
     * @return T
     */
    T andThen();

    /**
     * 메소드 실행
     * @return T
     */
    T accept();
}


SpringContextUtil


import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * @author kimMinJoon
 * @date 2024/2/2
 */
@Component
public class SpringContextUtil  implements ApplicationContextAware {
    private static ApplicationContext 애플리케이션컨텍스트;

    public static ApplicationContext getApplicationContext() {
        return 애플리케이션컨텍스트;
    }

    public static Object getBean(String 빈이름) {
        return 애플리케이션컨텍스트.getBean(빈이름);
    }

    public static <T> T getBean(Class<T> 클래스) {
        return 애플리케이션컨텍스트.getBean(클래스);
    }

    public static <T> T getBean(String 빈이름, Class<T> 클래스) {
        return 애플리케이션컨텍스트.getBean(빈이름, 클래스);
    }

    @Override
    public void setApplicationContext(ApplicationContext 애플리케이션컨텍스트) throws BeansException {
        if (SpringContextUtil.애플리케이션컨텍스트 == null){
            SpringContextUtil.애플리케이션컨텍스트 = 애플리케이션컨텍스트;
        }
    }
}


  1. 사용 방법 ====
ExcelExportHandler<Object> 내보내기도구 = ExcelExportHandler.생성();
try {
    내보내기도구.응답설정(response,"테스트")
            .헤더설정(ListUtils.newArrayList("테스트"))
            .빌더()
            .다중스레드(436L)
            .데이터쓰기(new DataQuery<List<List<Object>>>() {
                @Override
                public List<List<Object>> andThen() {
                    List<Object> 데이터=new ArrayList<>();
                    Date 날짜 = new Date();
                    데이터.add(날짜);
                    List<List<Object>> 결과 = new ArrayList<>();
                    결과.add(데이터);
                    return 결과;
                }
            });
} catch (Exception e) {
    try {
        ExcelExportHandler.오류처리(response,e.getMessage());
    } catch (IOException ex) {
        throw new RuntimeException(ex);
    }
    throw new RuntimeException(e);
}finally {
    내보내기도구.닫기();
}

태그: EasyExcel ExcelExport java Multi-threading

8월 5일 13:53에 게시됨