├── src ├── main │ ├── resources │ │ ├── application.properties │ │ ├── mybatis │ │ │ ├── mybatis-config.xml │ │ │ └── mapper │ │ │ │ └── TtlProductInfoMapper.xml │ │ └── application.yml │ └── java │ │ └── com │ │ └── kundy │ │ └── excelutils │ │ ├── mapper │ │ └── TtlProductInfoMapper.java │ │ ├── ExcelUtilsApplication.java │ │ ├── constant │ │ └── ExcelFormat.java │ │ ├── service │ │ ├── TtlProductInfoService.java │ │ └── impl │ │ │ └── TtlProductInfoServiceImpl.java │ │ ├── entity │ │ ├── ExcelHeaderInfo.java │ │ └── po │ │ │ └── TtlProductInfoPo.java │ │ ├── controller │ │ └── ExportController.java │ │ └── utils │ │ └── ExcelUtils.java └── test │ └── java │ └── com │ └── kundy │ └── excelutils │ └── ExcelUtilsApplicationTests.java ├── images └── E598B49A-A762-4E4B-9E4D-0C8DA2A23420.png ├── .gitignore ├── pom.xml └── README.md /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /images/E598B49A-A762-4E4B-9E4D-0C8DA2A23420.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dearKundy/excel-utils/HEAD/images/E598B49A-A762-4E4B-9E4D-0C8DA2A23420.png -------------------------------------------------------------------------------- /src/main/resources/mybatis/mybatis-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | /nbproject/private/ 21 | /nbbuild/ 22 | /dist/ 23 | /nbdist/ 24 | /.nb-gradle/ 25 | /build/ 26 | -------------------------------------------------------------------------------- /src/main/java/com/kundy/excelutils/mapper/TtlProductInfoMapper.java: -------------------------------------------------------------------------------- 1 | package com.kundy.excelutils.mapper; 2 | 3 | 4 | import com.kundy.excelutils.entity.po.TtlProductInfoPo; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | /** 10 | * @author kundy 11 | * @create 2019/2/16 10:42 AM 12 | */ 13 | public interface TtlProductInfoMapper { 14 | 15 | List listProduct(Map map); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/com/kundy/excelutils/ExcelUtilsApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.kundy.excelutils; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class ExcelUtilsApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/resources/mybatis/mapper/TtlProductInfoMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/java/com/kundy/excelutils/ExcelUtilsApplication.java: -------------------------------------------------------------------------------- 1 | package com.kundy.excelutils; 2 | 3 | import org.mybatis.spring.annotation.MapperScan; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | @SpringBootApplication 8 | @MapperScan("com.kundy.excelutils.mapper") 9 | public class ExcelUtilsApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(ExcelUtilsApplication.class, args); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/kundy/excelutils/constant/ExcelFormat.java: -------------------------------------------------------------------------------- 1 | package com.kundy.excelutils.constant; 2 | 3 | /** 4 | * @author kundy 5 | * @create 2019/2/15 11:54 AM 6 | */ 7 | public enum ExcelFormat { 8 | 9 | FORMAT_INTEGER("INTEGER"), 10 | FORMAT_DOUBLE("DOUBLE"), 11 | FORMAT_PERCENT("PERCENT"), 12 | FORMAT_DATE("DATE"); 13 | 14 | private String value; 15 | 16 | ExcelFormat(String value) { 17 | this.value = value; 18 | } 19 | 20 | public String getValue() { 21 | return value; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | servlet: 3 | context-path: "/api" 4 | 5 | spring: 6 | datasource: 7 | driver-class-name: com.mysql.jdbc.Driver 8 | url: jdbc:mysql://127.0.0.1:3306/yyt?useUnicode=true&characterEncoding=utf-8 9 | username: root 10 | password: 123456 11 | output: 12 | ansi: 13 | enabled: detect 14 | 15 | mybatis: 16 | config-location: classpath:mybatis/mybatis-config.xml 17 | mapper-locations: classpath:mybatis/mapper/*.xml 18 | 19 | logging: 20 | level: 21 | com.kundy.excelutils: debug -------------------------------------------------------------------------------- /src/main/java/com/kundy/excelutils/service/TtlProductInfoService.java: -------------------------------------------------------------------------------- 1 | package com.kundy.excelutils.service; 2 | 3 | import com.kundy.excelutils.entity.po.TtlProductInfoPo; 4 | 5 | import javax.servlet.http.HttpServletResponse; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | /** 10 | * @author kundy 11 | * @create 2019/2/16 11:22 AM 12 | */ 13 | public interface TtlProductInfoService { 14 | 15 | List listProduct(Map map); 16 | 17 | void export(HttpServletResponse response, String fileName); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/kundy/excelutils/entity/ExcelHeaderInfo.java: -------------------------------------------------------------------------------- 1 | package com.kundy.excelutils.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.experimental.Accessors; 6 | 7 | /** 8 | * @author kundy 9 | * @create 2019/2/15 11:34 AM 10 | * excel表头信息 11 | */ 12 | 13 | @Data 14 | @AllArgsConstructor 15 | @Accessors(chain = true) 16 | public class ExcelHeaderInfo { 17 | 18 | //标题的首行坐标 19 | private int firstRow; 20 | //标题的末行坐标 21 | private int lastRow; 22 | //标题的首列坐标 23 | private int firstCol; 24 | //标题的首行坐标 25 | private int lastCol; 26 | // 标题 27 | private String title; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/kundy/excelutils/entity/po/TtlProductInfoPo.java: -------------------------------------------------------------------------------- 1 | package com.kundy.excelutils.entity.po; 2 | 3 | import lombok.Data; 4 | import lombok.experimental.Accessors; 5 | 6 | /** 7 | * @author kundy 8 | * @create 2019/2/15 11:22 PM 9 | * 10 | */ 11 | @Data 12 | @Accessors(chain = true) 13 | public class TtlProductInfoPo { 14 | 15 | private Long id; 16 | private String productName; 17 | private Long categoryId; 18 | private String categoryName; 19 | private Long branchId; 20 | private String branchName; 21 | private Long shopId; 22 | private String shopName; 23 | private Double price; 24 | private Integer stock; 25 | private Integer salesNum; 26 | private String createTime; 27 | private String updateTime; 28 | private Byte isDel; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/kundy/excelutils/controller/ExportController.java: -------------------------------------------------------------------------------- 1 | package com.kundy.excelutils.controller; 2 | 3 | import com.kundy.excelutils.service.TtlProductInfoService; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | import javax.servlet.http.HttpServletResponse; 10 | import java.util.Random; 11 | 12 | /** 13 | * @author kundy 14 | * @create 2019/2/13 9:06 PM 15 | */ 16 | 17 | @RestController 18 | @RequestMapping("/excelUtils") 19 | public class ExportController { 20 | 21 | @Autowired 22 | private TtlProductInfoService productInfoService; 23 | 24 | @GetMapping("/export") 25 | public void export(HttpServletResponse response) { 26 | this.productInfoService.export(response, "商品信息" + new Random().nextInt(1000)); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.1.3.RELEASE 10 | 11 | 12 | 13 | com.kundy 14 | excel-utils 15 | 0.0.1-SNAPSHOT 16 | excel-utils 17 | excel导出工具类 18 | 19 | 20 | 1.8 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-web 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-test 32 | test 33 | 34 | 35 | 36 | org.mybatis.spring.boot 37 | mybatis-spring-boot-starter 38 | 1.3.2 39 | 40 | 41 | 42 | mysql 43 | mysql-connector-java 44 | 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-devtools 50 | true 51 | 52 | 53 | 54 | 55 | org.projectlombok 56 | lombok 57 | 1.16.22 58 | provided 59 | 60 | 61 | 62 | 63 | org.apache.poi 64 | poi 65 | 4.0.1 66 | 67 | 68 | 69 | org.apache.poi 70 | poi-ooxml 71 | 4.0.1 72 | 73 | 74 | 75 | 76 | commons-beanutils 77 | commons-beanutils 78 | 1.9.3 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | org.springframework.boot 87 | spring-boot-maven-plugin 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/main/java/com/kundy/excelutils/service/impl/TtlProductInfoServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.kundy.excelutils.service.impl; 2 | 3 | import com.kundy.excelutils.constant.ExcelFormat; 4 | import com.kundy.excelutils.entity.ExcelHeaderInfo; 5 | import com.kundy.excelutils.entity.po.TtlProductInfoPo; 6 | import com.kundy.excelutils.mapper.TtlProductInfoMapper; 7 | import com.kundy.excelutils.service.TtlProductInfoService; 8 | import com.kundy.excelutils.utils.ExcelUtils; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Service; 12 | 13 | import javax.servlet.http.HttpServletResponse; 14 | import java.util.*; 15 | import java.util.concurrent.Callable; 16 | import java.util.concurrent.FutureTask; 17 | 18 | /** 19 | * @author kundy 20 | * @create 2019/2/16 11:22 AM 21 | */ 22 | @Service 23 | @Slf4j 24 | public class TtlProductInfoServiceImpl implements TtlProductInfoService { 25 | 26 | // 每个线程导出记录最大行数 27 | private static final int THREAD_MAX_ROW = 20000; 28 | 29 | @Autowired 30 | private TtlProductInfoMapper mapper; 31 | 32 | @Override 33 | public List listProduct(Map map) { 34 | return this.mapper.listProduct(map); 35 | } 36 | 37 | @Override 38 | public void export(HttpServletResponse response, String fileName) { 39 | // 待导出数据 40 | List productInfoPos = this.multiThreadListProduct(); 41 | ExcelUtils excelUtils = new ExcelUtils(productInfoPos, getHeaderInfo(), getFormatInfo()); 42 | excelUtils.sendHttpResponse(response, fileName, excelUtils.getWorkbook()); 43 | } 44 | 45 | // 获取表头信息 46 | private List getHeaderInfo() { 47 | return Arrays.asList( 48 | new ExcelHeaderInfo(1, 1, 0, 0, "id"), 49 | new ExcelHeaderInfo(1, 1, 1, 1, "商品名称"), 50 | 51 | new ExcelHeaderInfo(0, 0, 2, 3, "分类"), 52 | new ExcelHeaderInfo(1, 1, 2, 2, "类型ID"), 53 | new ExcelHeaderInfo(1, 1, 3, 3, "分类名称"), 54 | 55 | new ExcelHeaderInfo(0, 0, 4, 5, "品牌"), 56 | new ExcelHeaderInfo(1, 1, 4, 4, "品牌ID"), 57 | new ExcelHeaderInfo(1, 1, 5, 5, "品牌名称"), 58 | 59 | new ExcelHeaderInfo(0, 0, 6, 7, "商店"), 60 | new ExcelHeaderInfo(1, 1, 6, 6, "商店ID"), 61 | new ExcelHeaderInfo(1, 1, 7, 7, "商店名称"), 62 | 63 | new ExcelHeaderInfo(1, 1, 8, 8, "价格"), 64 | new ExcelHeaderInfo(1, 1, 9, 9, "库存"), 65 | new ExcelHeaderInfo(1, 1, 10, 10, "销量"), 66 | new ExcelHeaderInfo(1, 1, 11, 11, "插入时间"), 67 | new ExcelHeaderInfo(1, 1, 12, 12, "更新时间"), 68 | new ExcelHeaderInfo(1, 1, 13, 13, "记录是否已经删除") 69 | ); 70 | } 71 | 72 | // 获取格式化信息 73 | private Map getFormatInfo() { 74 | Map format = new HashMap<>(); 75 | format.put("id", ExcelFormat.FORMAT_INTEGER); 76 | format.put("categoryId", ExcelFormat.FORMAT_INTEGER); 77 | format.put("branchId", ExcelFormat.FORMAT_INTEGER); 78 | format.put("shopId", ExcelFormat.FORMAT_INTEGER); 79 | format.put("price", ExcelFormat.FORMAT_DOUBLE); 80 | format.put("stock", ExcelFormat.FORMAT_INTEGER); 81 | format.put("salesNum", ExcelFormat.FORMAT_INTEGER); 82 | format.put("isDel", ExcelFormat.FORMAT_INTEGER); 83 | return format; 84 | } 85 | 86 | // 多线程查询报表 87 | private List multiThreadListProduct() { 88 | List>> tasks = new ArrayList<>(); 89 | List productInfoPos = new ArrayList<>(); 90 | 91 | int totalNum = 500000; 92 | int loopNum = new Double(Math.ceil((double) totalNum / THREAD_MAX_ROW)).intValue(); 93 | log.info("多线程查询,总数:{},开启线程数:{}", totalNum, loopNum); 94 | long start = System.currentTimeMillis(); 95 | 96 | executeTask(tasks, loopNum, totalNum); 97 | 98 | for (FutureTask> task : tasks) { 99 | try { 100 | productInfoPos.addAll(task.get()); 101 | } catch (Exception e) { 102 | e.printStackTrace(); 103 | } 104 | } 105 | 106 | log.info("查询结束,耗时:{}", System.currentTimeMillis() - start); 107 | return productInfoPos; 108 | } 109 | 110 | // 执行查询任务 111 | private void executeTask(List>> tasks, int loopNum, int total) { 112 | for (int i = 0; i < loopNum; i++) { 113 | Map map = new HashMap<>(); 114 | map.put("offset", i * THREAD_MAX_ROW); 115 | if (i == loopNum - 1) { 116 | map.put("limit", total - THREAD_MAX_ROW * i); 117 | } else { 118 | map.put("limit", THREAD_MAX_ROW); 119 | } 120 | FutureTask> task = new FutureTask<>(new listThread(map)); 121 | log.info("开始查询第{}条开始的{}条记录", i * THREAD_MAX_ROW, THREAD_MAX_ROW); 122 | new Thread(task).start(); 123 | // 将任务添加到tasks列表中 124 | tasks.add(task); 125 | } 126 | } 127 | 128 | private class listThread implements Callable> { 129 | 130 | private Map map; 131 | 132 | private listThread(Map map) { 133 | this.map = map; 134 | } 135 | 136 | @Override 137 | public List call() { 138 | return listProduct(map); 139 | } 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/com/kundy/excelutils/utils/ExcelUtils.java: -------------------------------------------------------------------------------- 1 | package com.kundy.excelutils.utils; 2 | 3 | import com.kundy.excelutils.constant.ExcelFormat; 4 | import com.kundy.excelutils.entity.ExcelHeaderInfo; 5 | import org.apache.commons.beanutils.BeanUtils; 6 | import org.apache.poi.hssf.usermodel.HSSFDataFormat; 7 | import org.apache.poi.ss.usermodel.*; 8 | import org.apache.poi.ss.util.CellRangeAddress; 9 | import org.apache.poi.xssf.streaming.SXSSFWorkbook; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import javax.servlet.http.HttpServletResponse; 14 | import java.io.OutputStream; 15 | import java.lang.reflect.Field; 16 | import java.text.SimpleDateFormat; 17 | import java.util.ArrayList; 18 | import java.util.Date; 19 | import java.util.List; 20 | import java.util.Map; 21 | 22 | /** 23 | * @author kundy 24 | * @create 2019/2/15 10:28 AM 25 | *

26 | * excel报表导出工具类 27 | */ 28 | 29 | public class ExcelUtils { 30 | private static final Logger LOGGER = LoggerFactory.getLogger(ExcelUtils.class); 31 | 32 | public static final int ROW_ACCESS_WINDOW_SIZE = 100; 33 | public static final int SHEET_MAX_ROW = 100000; 34 | 35 | private List list; 36 | private List excelHeaderInfos; 37 | private Map formatInfo; 38 | 39 | public ExcelUtils(List list, List excelHeaderInfos) { 40 | this.list = list; 41 | this.excelHeaderInfos = excelHeaderInfos; 42 | } 43 | 44 | public ExcelUtils(List list, List excelHeaderInfos, Map formatInfo) { 45 | this.list = list; 46 | this.excelHeaderInfos = excelHeaderInfos; 47 | this.formatInfo = formatInfo; 48 | } 49 | 50 | public Workbook getWorkbook() { 51 | Workbook workbook = new SXSSFWorkbook(ROW_ACCESS_WINDOW_SIZE); 52 | String[][] datas = transformData(); 53 | Field[] fields = list.get(0).getClass().getDeclaredFields(); 54 | CellStyle style = setCellStyle(workbook); 55 | int pageRowNum = 0; 56 | Sheet sheet = null; 57 | 58 | long startTime = System.currentTimeMillis(); 59 | LOGGER.info("开始处理excel文件。。。"); 60 | 61 | for (int i = 0; i < datas.length; i++) { 62 | // 如果是每个sheet的首行 63 | if (i % SHEET_MAX_ROW == 0) { 64 | // 创建新的sheet 65 | sheet = createSheet(workbook, i); 66 | pageRowNum = 0; // 行号重置为0 67 | for (int j = 0; j < getHeaderRowNum(excelHeaderInfos); j++) { 68 | sheet.createRow(pageRowNum++); 69 | } 70 | createHeader(sheet, style); 71 | } 72 | // 创建内容 73 | Row row = sheet.createRow(pageRowNum++); 74 | createContent(row, style, datas, i, fields); 75 | } 76 | LOGGER.info("处理文本耗时" + (System.currentTimeMillis() - startTime) + "ms"); 77 | return workbook; 78 | } 79 | 80 | // 创建表头 81 | private void createHeader(Sheet sheet, CellStyle style) { 82 | for (ExcelHeaderInfo excelHeaderInfo : excelHeaderInfos) { 83 | Integer lastRow = excelHeaderInfo.getLastRow(); 84 | Integer firstRow = excelHeaderInfo.getFirstRow(); 85 | Integer lastCol = excelHeaderInfo.getLastCol(); 86 | Integer firstCol = excelHeaderInfo.getFirstCol(); 87 | 88 | // 行距或者列距大于0才进行单元格融合 89 | if ((lastRow - firstRow) != 0 || (lastCol - firstCol) != 0) { 90 | sheet.addMergedRegion(new CellRangeAddress(firstRow, lastRow, firstCol, lastCol)); 91 | } 92 | // 获取当前表头的首行位置 93 | Row row = sheet.getRow(firstRow); 94 | // 在表头的首行与首列位置创建一个新的单元格 95 | Cell cell = row.createCell(firstCol); 96 | // 赋值单元格 97 | cell.setCellValue(excelHeaderInfo.getTitle()); 98 | cell.setCellStyle(style); 99 | sheet.setColumnWidth(firstCol, sheet.getColumnWidth(firstCol) * 17 / 12); 100 | } 101 | } 102 | 103 | // 创建正文 104 | private void createContent(Row row, CellStyle style, String[][] content, int i, Field[] fields) { 105 | List columnNames = getBeanProperty(fields); 106 | for (int j = 0; j < columnNames.size(); j++) { 107 | // 如果格式化Map为空,默认为字符串格式 108 | if (formatInfo == null) { 109 | row.createCell(j).setCellValue(content[i][j]); 110 | continue; 111 | } 112 | if (formatInfo.containsKey(columnNames.get(j))) { 113 | switch (formatInfo.get(columnNames.get(j)).getValue()) { 114 | case "DOUBLE": 115 | row.createCell(j).setCellValue(Double.parseDouble(content[i][j])); 116 | break; 117 | case "INTEGER": 118 | row.createCell(j).setCellValue(Integer.parseInt(content[i][j])); 119 | break; 120 | case "PERCENT": 121 | style.setDataFormat(HSSFDataFormat.getBuiltinFormat("0.00%")); 122 | Cell cell = row.createCell(j); 123 | cell.setCellStyle(style); 124 | cell.setCellValue(Double.parseDouble(content[i][j])); 125 | break; 126 | case "DATE": 127 | style.setDataFormat(HSSFDataFormat.getBuiltinFormat("yyyy-MM-dd HH:mm:ss")); 128 | Cell cell1 = row.createCell(j); 129 | cell1.setCellStyle(style); 130 | row.createCell(j).setCellValue(this.parseDate(content[i][j])); 131 | } 132 | } else { 133 | row.createCell(j).setCellValue(content[i][j]); 134 | } 135 | } 136 | } 137 | 138 | // 将原始数据转成二维数组 139 | private String[][] transformData() { 140 | int dataSize = this.list.size(); 141 | String[][] datas = new String[dataSize][]; 142 | // 获取报表的列数 143 | Field[] fields = list.get(0).getClass().getDeclaredFields(); 144 | // 获取实体类的字段名称数组 145 | List columnNames = this.getBeanProperty(fields); 146 | for (int i = 0; i < dataSize; i++) { 147 | datas[i] = new String[fields.length]; 148 | for (int j = 0; j < fields.length; j++) { 149 | try { 150 | // 赋值 151 | datas[i][j] = BeanUtils.getProperty(list.get(i), columnNames.get(j)); 152 | } catch (Exception e) { 153 | LOGGER.error("获取对象属性值失败"); 154 | e.printStackTrace(); 155 | } 156 | } 157 | } 158 | return datas; 159 | } 160 | 161 | // 获取实体类的字段名称数组 162 | private List getBeanProperty(Field[] fields) { 163 | List columnNames = new ArrayList<>(); 164 | for (Field field : fields) { 165 | String[] strings = field.toString().split("\\."); 166 | String columnName = strings[strings.length - 1]; 167 | columnNames.add(columnName); 168 | } 169 | return columnNames; 170 | } 171 | 172 | // 新建表格 173 | private static Sheet createSheet(Workbook workbook, int i) { 174 | Integer sheetNum = i / SHEET_MAX_ROW; 175 | workbook.createSheet("sheet" + sheetNum); 176 | //动态指定当前的工作表 177 | return workbook.getSheetAt(sheetNum); 178 | } 179 | 180 | // 获取标题总行数 181 | private static Integer getHeaderRowNum(List headerInfos) { 182 | Integer maxRowNum = 0; 183 | for (ExcelHeaderInfo excelHeaderInfo : headerInfos) { 184 | Integer tmpRowNum = excelHeaderInfo.getLastRow(); 185 | if (tmpRowNum > maxRowNum) maxRowNum = tmpRowNum; 186 | } 187 | return maxRowNum + 1; 188 | } 189 | 190 | // 设置总体表格样式 191 | private static CellStyle setCellStyle(Workbook workbook) { 192 | CellStyle style = workbook.createCellStyle(); 193 | style.setAlignment(HorizontalAlignment.CENTER); 194 | style.setWrapText(true); 195 | return style; 196 | } 197 | 198 | // 字符串转日期 199 | private Date parseDate(String strDate) { 200 | SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 201 | Date date = null; 202 | try { 203 | date = dateFormat.parse(strDate); 204 | } catch (Exception e) { 205 | LOGGER.error("字符串转日期错误"); 206 | e.printStackTrace(); 207 | } 208 | return date; 209 | } 210 | 211 | // 发送响应结果 212 | public void sendHttpResponse(HttpServletResponse response, String fileName, Workbook workbook) { 213 | try { 214 | fileName += System.currentTimeMillis() + ".xlsx"; 215 | fileName = new String(fileName.getBytes(), "ISO8859-1"); 216 | response.setContentType("application/octet-stream;charset=ISO8859-1"); 217 | response.setHeader("Content-Disposition", "attachment;filename=" + fileName); 218 | response.addHeader("Pargam", "no-cache"); 219 | response.addHeader("Cache-Control", "no-cache"); 220 | OutputStream os = response.getOutputStream(); 221 | workbook.write(os); 222 | os.flush(); 223 | os.close(); 224 | } catch (Exception e) { 225 | e.printStackTrace(); 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 前言 2 | 公司项目最近有一个需要:报表导出。整个系统下来,起码超过一百张报表需要导出。这个时候如何优雅的实现报表导出,释放生产力就显得很重要了。下面主要给大家分享一下该工具类的使用方法与实现思路。 3 | 4 | ### 实现的功能点 5 | 对于每个报表都相同的操作,我们很自然的会抽离出来,这个很简单。而最重要的是:如何把那些每个报表不相同的操作进行良好的封装,尽可能的提高复用性;针对以上的原则,主要实现了一下关键功能点: 6 | - 导出任意类型的数据 7 | - 自由设置表头 8 | - 自由设置字段的导出格式 9 | 10 | ### 使用实例 11 | 上面说到了本工具类实现了三个功能点,自然在使用的时候设置好这三个要点即可: 12 | - 设置数据列表 13 | - 设置表头 14 | - 设置字段格式 15 | 16 | 下面的export函数可以直接向客户端返回一个excel数据,其中`productInfoPos`为待导出的数据列表,`ExcelHeaderInfo`用来保存表头信息,包括表头名称,表头的首列,尾列,首行,尾行。因为默认导出的数据格式都是字符串型,所以还需要一个Map参数用来指定某个字段的格式化类型(例如数字类型,小数类型、日期类型)。这里大家知道个大概怎么使用就好了,下面会对这些参数进行详细解释 17 | ```java 18 | @Override 19 | public void export(HttpServletResponse response, String fileName) { 20 | // 待导出数据 21 | List productInfoPos = this.multiThreadListProduct(); 22 | ExcelUtils excelUtils = new ExcelUtils(productInfoPos, getHeaderInfo(), getFormatInfo()); 23 | excelUtils.sendHttpResponse(response, fileName, excelUtils.getWorkbook()); 24 | } 25 | 26 | // 获取表头信息 27 | private List getHeaderInfo() { 28 | return Arrays.asList( 29 | new ExcelHeaderInfo(1, 1, 0, 0, "id"), 30 | new ExcelHeaderInfo(1, 1, 1, 1, "商品名称"), 31 | 32 | new ExcelHeaderInfo(0, 0, 2, 3, "分类"), 33 | new ExcelHeaderInfo(1, 1, 2, 2, "类型ID"), 34 | new ExcelHeaderInfo(1, 1, 3, 3, "分类名称"), 35 | 36 | new ExcelHeaderInfo(0, 0, 4, 5, "品牌"), 37 | new ExcelHeaderInfo(1, 1, 4, 4, "品牌ID"), 38 | new ExcelHeaderInfo(1, 1, 5, 5, "品牌名称"), 39 | 40 | new ExcelHeaderInfo(0, 0, 6, 7, "商店"), 41 | new ExcelHeaderInfo(1, 1, 6, 6, "商店ID"), 42 | new ExcelHeaderInfo(1, 1, 7, 7, "商店名称"), 43 | 44 | new ExcelHeaderInfo(1, 1, 8, 8, "价格"), 45 | new ExcelHeaderInfo(1, 1, 9, 9, "库存"), 46 | new ExcelHeaderInfo(1, 1, 10, 10, "销量"), 47 | new ExcelHeaderInfo(1, 1, 11, 11, "插入时间"), 48 | new ExcelHeaderInfo(1, 1, 12, 12, "更新时间"), 49 | new ExcelHeaderInfo(1, 1, 13, 13, "记录是否已经删除") 50 | ); 51 | } 52 | 53 | // 获取格式化信息 54 | private Map getFormatInfo() { 55 | Map format = new HashMap<>(); 56 | format.put("id", ExcelFormat.FORMAT_INTEGER); 57 | format.put("categoryId", ExcelFormat.FORMAT_INTEGER); 58 | format.put("branchId", ExcelFormat.FORMAT_INTEGER); 59 | format.put("shopId", ExcelFormat.FORMAT_INTEGER); 60 | format.put("price", ExcelFormat.FORMAT_DOUBLE); 61 | format.put("stock", ExcelFormat.FORMAT_INTEGER); 62 | format.put("salesNum", ExcelFormat.FORMAT_INTEGER); 63 | format.put("isDel", ExcelFormat.FORMAT_INTEGER); 64 | return format; 65 | } 66 | ``` 67 | ### 实现效果 68 | ![5ca2a611c97b7742794ea5a8e4a7ef86.png](https://github.com/dearKundy/excel-utils/blob/master/images/E598B49A-A762-4E4B-9E4D-0C8DA2A23420.png) 69 | 70 | ### 源码分析 71 | 哈哈,自己分析自己的代码,有点意思。由于不方便贴出太多的代码,大家可以先到github上clone源码,再回来阅读文章。[✨源码地址✨](https://github.com/dearKundy/excel-utils) 72 | LZ使用的`poi 4.0.1`版本的这个工具,想要实用海量数据的导出自然得使用`SXSSFWorkbook`这个组件。关于poi的具体用法在这里我就不多说了,这里主要是给大家讲解如何对poi进行封装使用。 73 | 74 | #### 成员变量 75 | 我们重点看`ExcelUtils`这个类,这个类是实现导出的核心,先来看一下三个成员变量 76 | ```java 77 | private List list; 78 | private List excelHeaderInfos; 79 | private Map formatInfo; 80 | ``` 81 | ##### list 82 | 该成员变量用来保存待导出的数据 83 | 84 | ##### ExcelHeaderInfo 85 | 该成员变量主要用来保存表头信息,因为我们需要定义多个表头信息,所以需要使用一个列表来保存,`ExcelHeaderInfo`构造函数如下 86 | `ExcelHeaderInfo(int firstRow, int lastRow, int firstCol, int lastCol, String title)` 87 | - `firstRow`:该表头所占位置的首行 88 | - `lastRow`:该表头所占位置的尾行 89 | - `firstCol`:该表头所占位置的首列 90 | - `lastCol`:该表头所占位置的尾行 91 | - `title`:该表头的名称 92 | 93 | ##### ExcelFormat 94 | 该参数主要用来格式化字段,我们需要预先约定好转换成那种格式,不能随用户自己定。所以我们定义了一个枚举类型的变量,该枚举类只有一个字符串类型成员变量,用来保存想要转换的格式,例如`FORMAT_INTEGER`就是转换成整型。因为我们需要接受多个字段的转换格式,所以定义了一个Map类型来接收,该参数可以省略(默认格式为字符串) 95 | ```java 96 | public enum ExcelFormat { 97 | 98 | FORMAT_INTEGER("INTEGER"), 99 | FORMAT_DOUBLE("DOUBLE"), 100 | FORMAT_PERCENT("PERCENT"), 101 | FORMAT_DATE("DATE"); 102 | 103 | private String value; 104 | 105 | ExcelFormat(String value) { 106 | this.value = value; 107 | } 108 | 109 | public String getValue() { 110 | return value; 111 | } 112 | } 113 | ``` 114 | #### 核心方法 115 | ##### 1. 创建表头 116 | > 该方法用来初始化表头,而创建表头最关键的就是poi中Sheet类的`addMergedRegion(CellRangeAddress var1)`方法,该方法用于`单元格融合`。我们会遍历ExcelHeaderInfo列表,按照每个ExcelHeaderInfo的坐标信息进行单元格融合,然后在融合之后的每个单元`首行`和`首列`的位置创建单元格,然后为单元格赋值即可,通过上面的步骤就完成了任意类型的表头设置。 117 | ```java 118 | // 创建表头 119 | private void createHeader(Sheet sheet, CellStyle style) { 120 | for (ExcelHeaderInfo excelHeaderInfo : excelHeaderInfos) { 121 | Integer lastRow = excelHeaderInfo.getLastRow(); 122 | Integer firstRow = excelHeaderInfo.getFirstRow(); 123 | Integer lastCol = excelHeaderInfo.getLastCol(); 124 | Integer firstCol = excelHeaderInfo.getFirstCol(); 125 | 126 | // 行距或者列距大于0才进行单元格融合 127 | if ((lastRow - firstRow) != 0 || (lastCol - firstCol) != 0) { 128 | sheet.addMergedRegion(new CellRangeAddress(firstRow, lastRow, firstCol, lastCol)); 129 | } 130 | // 获取当前表头的首行位置 131 | Row row = sheet.getRow(firstRow); 132 | // 在表头的首行与首列位置创建一个新的单元格 133 | Cell cell = row.createCell(firstCol); 134 | // 赋值单元格 135 | cell.setCellValue(excelHeaderInfo.getTitle()); 136 | cell.setCellStyle(style); 137 | sheet.setColumnWidth(firstCol, sheet.getColumnWidth(firstCol) * 17 / 12); 138 | } 139 | } 140 | ``` 141 | 142 | ##### 2. 转换数据 143 | > 在进行正文赋值之前,我们先要对原始数据列表转换成字符串的二维数组,之所以转成字符串格式是因为可以统一的处理各种类型,之后有需要我们再转换回来即可。 144 | ```java 145 | // 将原始数据转成二维数组 146 | private String[][] transformData() { 147 | int dataSize = this.list.size(); 148 | String[][] datas = new String[dataSize][]; 149 | // 获取报表的列数 150 | Field[] fields = list.get(0).getClass().getDeclaredFields(); 151 | // 获取实体类的字段名称数组 152 | List columnNames = this.getBeanProperty(fields); 153 | for (int i = 0; i < dataSize; i++) { 154 | datas[i] = new String[fields.length]; 155 | for (int j = 0; j < fields.length; j++) { 156 | try { 157 | // 赋值 158 | datas[i][j] = BeanUtils.getProperty(list.get(i), columnNames.get(j)); 159 | } catch (Exception e) { 160 | LOGGER.error("获取对象属性值失败"); 161 | e.printStackTrace(); 162 | } 163 | } 164 | } 165 | return datas; 166 | } 167 | ``` 168 | 这个方法中我们通过使用反射技术,很巧妙的实现了任意类型的数据导出(这里的任意类型指的是任意的报表类型,不同的报表,导出的数据肯定是不一样的,那么在Java实现中的实体类肯定也是不一样的)。要想将一个List转换成相应的二维数组,我们得知道如下的信息; 169 | - 二维数组的列数 170 | - 二维数组的行数 171 | - 二维数组每个元素的值 172 | 173 | 如果获取以上三个信息呢? 174 | - 通过反射中的`Field[] getDeclaredFields()`这个方法获取实体类的所有字段,从而间接知道一共有多少列 175 | - List的大小不就是二维数组的行数了嘛 176 | - 虽然每个实体类的字段名不一样,那么我们就真的无法获取到实体类某个字段的值了吗?不是的,你要知道,你拥有了`反射`,你就相当于拥有了全世界,那还有什么做不到的呢。这里我们没有直接使用反射,而是使用了一个叫做`BeanUtils`的工具,该工具可以很方便的帮助我们对一个实体类进行字段的赋值与字段值的获取。很简单,通过`BeanUtils.getProperty(list.get(i), columnNames.get(j))`这一行代码,我们就获取了实体`list.get(i)`中名称为`columnNames.get(j)`这个字段的值。`list.get(i)`当然是我们遍历原始数据的实体类,而`columnNames`列表则是一个实体类所有字段名的数组,也是通过反射的方法获取到的,具体实现可以参考LZ的源代码。 177 | 178 | ##### 3. 赋值正文 179 | > 这里的正文指定是正式的表格数据内容,其实这一些没有太多的奇淫技巧,主要的功能在上面已经实现了,这里主要是进行单元格的赋值与导出格式的处理(主要是为了导出excel后可以进行方便的运算) 180 | ```java 181 | // 创建正文 182 | private void createContent(Row row, CellStyle style, String[][] content, int i, Field[] fields) { 183 | List columnNames = getBeanProperty(fields); 184 | for (int j = 0; j < columnNames.size(); j++) { 185 | if (formatInfo == null) { 186 | row.createCell(j).setCellValue(content[i][j]); 187 | continue; 188 | } 189 | if (formatInfo.containsKey(columnNames.get(j))) { 190 | switch (formatInfo.get(columnNames.get(j)).getValue()) { 191 | case "DOUBLE": 192 | row.createCell(j).setCellValue(Double.parseDouble(content[i][j])); 193 | break; 194 | case "INTEGER": 195 | row.createCell(j).setCellValue(Integer.parseInt(content[i][j])); 196 | break; 197 | case "PERCENT": 198 | style.setDataFormat(HSSFDataFormat.getBuiltinFormat("0.00%")); 199 | Cell cell = row.createCell(j); 200 | cell.setCellStyle(style); 201 | cell.setCellValue(Double.parseDouble(content[i][j])); 202 | break; 203 | case "DATE": 204 | row.createCell(j).setCellValue(this.parseDate(content[i][j])); 205 | } 206 | } else { 207 | row.createCell(j).setCellValue(content[i][j]); 208 | } 209 | } 210 | } 211 | ``` 212 | 213 | 导出工具类的核心方法就差不多说完了,下面说一下关于多线程查询的问题 214 | 215 | ### 多扯两点 216 | #### 1. 多线程查询数据 217 | > 理想很丰满,现实虽然不是很残酷,但是也跟想象的不一样。LZ虽然对50w的数据分别创建20个线程去查询,但是总体的效率并不是50w/20,而是仅仅快了几秒钟,知道原因的小伙伴可以给我留个言一起探讨一下。 218 | 219 | 下面先说说具体思路:因为多个线程之间是同时执行的,你不能够保证哪个线程先执行完毕,但是我们却得保证数据顺序的一致性。在这里我们使用了`Callable`接口,通过实现`Callable`接口的线程可以拥有返回值,我们获取到所有子线程的查询结果,然后合并到一个结果集中即可。那么如何保证合并的顺序呢?我们先创建了一个`FutureTask`类型的List,该`FutureTask`的类型就是返回的结果集。 220 | ```java 221 | List>> tasks = new ArrayList<>(); 222 | ``` 223 | 224 | 当我们每启动一个线程的时候,就将该线程的`FutureTask`添加到`tasks`列表中,这样tasks列表中的元素顺序就是我们启动线程的顺序。 225 | ```java 226 | FutureTask> task = new FutureTask<>(new listThread(map)); 227 | log.info("开始查询第{}条开始的{}条记录", i * THREAD_MAX_ROW, THREAD_MAX_ROW); 228 | new Thread(task).start(); 229 | // 将任务添加到tasks列表中 230 | tasks.add(task); 231 | ``` 232 | 233 | 接下来,就是顺序塞值了,我们按顺序从`tasks`列表中取出`FutureTask`,然后执行`FutureTask`的`get()`方法,该方法会阻塞调用它的线程,知道拿到返回结果。这样一套循环下来,就完成了所有数据的按顺序存储。 234 | ``` 235 | for (FutureTask> task : tasks) { 236 | try { 237 | productInfoPos.addAll(task.get()); 238 | } catch (Exception e) { 239 | e.printStackTrace(); 240 | } 241 | } 242 | ``` 243 | 244 | #### 2. 如何解决接口超时 245 | 如果需要导出海量数据,可能会存在一个问题:`接口超时`,主要原因就是整个导出过程的时间太长了。其实也很好解决,接口的响应时间太长,我们缩短响应时间不就可以了嘛。我们使用`异步编程`解决方案,异步编程的实现方式有很多,这里我们使用最简单的spring中的`Async`注解,加上了这个注解的方法可以立马返回响应结果。关于注解的使用方式,大家可以自己查阅一下,下面讲一下关键的实现步骤: 246 | 1. 编写异步接口,该接口负责接收客户端的导出请求,然后开始执行导出(注意:这里的导出不是直接向客户端返回,而是下载到服务器本地),只要下达了导出指令,就可以马上给客户端返回一个该excel文件的唯一标志(用于以后查找该文件),接口结束。 247 | 2. 编写excel状态接口,客户端拿到excel文件的唯一标志之后,开始每秒轮询调用该接口检查excel文件的导出状态 248 | 3. 编写从服务器本地返回excel文件接口,如果客户端检查到excel已经成功下载到`到服务器本地`,这个时候就可以请求该接口直接下载文件了。 249 | 250 | 这样就可以解决接口超时的问题了。 251 | 252 | ### 源码地址 253 | [https://github.com/dearKundy/excel-utils](https://github.com/dearKundy/excel-utils) 254 | 255 | ### 源码服用姿势 256 | 1. 建表(数据自己插入哦) 257 | ```sql 258 | CREATE TABLE `ttl_product_info` ( 259 | `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '记录唯一标识', 260 | `product_name` varchar(50) NOT NULL COMMENT '商品名称', 261 | `category_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '类型ID', 262 | `category_name` varchar(50) NOT NULL COMMENT '冗余分类名称-避免跨表join', 263 | `branch_id` bigint(20) NOT NULL COMMENT '品牌ID', 264 | `branch_name` varchar(50) NOT NULL COMMENT '冗余品牌名称-避免跨表join', 265 | `shop_id` bigint(20) NOT NULL COMMENT '商品ID', 266 | `shop_name` varchar(50) NOT NULL COMMENT '冗余商店名称-避免跨表join', 267 | `price` decimal(10,2) NOT NULL COMMENT '商品当前价格-属于热点数据,而且价格变化需要记录,需要价格详情表', 268 | `stock` int(11) NOT NULL COMMENT '库存-热点数据', 269 | `sales_num` int(11) NOT NULL COMMENT '销量', 270 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '插入时间', 271 | `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 272 | `is_del` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '记录是否已经删除', 273 | PRIMARY KEY (`id`), 274 | KEY `idx_shop_category_salesnum` (`shop_id`,`category_id`,`sales_num`), 275 | KEY `idx_category_branch_price` (`category_id`,`branch_id`,`price`), 276 | KEY `idx_productname` (`product_name`) 277 | ) ENGINE=InnoDB AUTO_INCREMENT=15000001 DEFAULT CHARSET=utf8 COMMENT='商品信息表'; 278 | ``` 279 | 280 | 2. 运行程序 281 | 3. 在浏览器的地址栏输入:[http://localhost:8080/api/excelUtils/export](http://localhost:8080/api/excelUtils/export)即可完成下载 282 | --------------------------------------------------------------------------------