Java工具库
办公类工具库
需要注意的是,在使用办公类工具库时,实体类一定要有无参构造器,因为工具库会通过反射创建实体对象,如果没有无参构造器,会抛出类似 java.lang.NoSuchMethodException: UserVO.<init>() 的错误!
在项目中,推荐使用 EasyExcel ,如果介意它停更,可以尝试 Apache Fesod 。
Apache POI
Apache POI 是 Java 领域最主流、功能最全面的办公文档处理工具库,支持处理 Word、Excel、PowerPoint 等文档,由于它太过底层,并不直接提供实体类注解功能自动识别表头(需自行实现),适用于有深度定制化需求的操作。
以下是使用 Apache POI 导入导出 Excel 的示例代码,它支持通过自定义注解,映射实体类字段到表头:
<!-- 版本管理 -->
<properties>
<apache.poi.version>5.4.1</apache.poi.version>
</properties>
<!-- 引入 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>${apache.poi.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${apache.poi.version}</version>
</dependency>import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @ Description: 用于标记实体类字段与Excel列的对应关系
* @ Author: ZJX
**/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelColumn {
/**
* 表头显示名称
*/
String name();
/**
* 列顺序(值越小越靠前)
*/
int order() default Integer.MAX_VALUE;
/**
* 日期或数字格式(如:yyyy-MM-dd)
*/
String format() default "";
}import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* @ Description: 测试实体类
* @ Author: ZJX
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EmployeeForApache {
@ExcelColumn(name = "姓名", order = 1)
private String name;
@ExcelColumn(name = "年龄", order = 2)
private Integer age;
@ExcelColumn(name = "职位", order = 3)
private String position;
@ExcelColumn(name = "入职日期", order = 4, format = "yyyy-MM-dd")
private LocalDateTime hireDate;
@ExcelColumn(name = "是否在职", order = 5)
private Boolean active;
}import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* ============================================================
* Excel 实体类导入导出工具(基于 Apache POI),大概瞄一眼即可,无需关注细节,亲测可用
* ============================================================
*
* @ Author ZJX
*/
public class ApachePoiExcelUtil {
private static final String DATE_FORMAT_DEFAULT = "yyyy-MM-dd HH:mm:ss";
// ============================================================
// 一、Excel 导出功能(实体 → Excel)
// ============================================================
/**
* 导出实体列表为 Excel 文件
*
* @param dataList 实体对象列表
* @param clazz 实体类类型,用于读取 @ExcelColumn 注解
* @param sheetName sheet页名称
* @param fileName 导出的文件名(不含扩展名)
* @return ResponseEntity<byte [ ]> 可直接返回给前端下载
*/
public static <T> ResponseEntity<byte[]> exportExcel(List<T> dataList, Class<T> clazz, String sheetName, String fileName) {
try (
Workbook workbook = new XSSFWorkbook(); // 创建 Excel 工作簿,使用 .xlsx 格式 【小数据量使用 XSSFWorkbook, 超过10万行的大数据量使用 SXSSFWorkbook 】
ByteArrayOutputStream out = new ByteArrayOutputStream()
) {
// 1️⃣ 创建工作表
Sheet sheet = workbook.createSheet(sheetName == null ? "Sheet1" : sheetName);
// 2️⃣ 获取所有带 @ExcelColumn 的字段,并按 order 排序
List<Field> fields = getSortedExcelFields(clazz);
// 3️⃣ 创建表头与正文样式
CellStyle headerStyle = createHeaderStyle(workbook);
CellStyle bodyStyle = createCellStyle(workbook);
// 4️⃣ 构建表头行
Row headerRow = sheet.createRow(0);
for (int i = 0; i < fields.size(); i++) {
ExcelColumn col = fields.get(i).getAnnotation(ExcelColumn.class);
Cell cell = headerRow.createCell(i);
cell.setCellValue(col.name()); // 设置表头
cell.setCellStyle(headerStyle);
}
// 5️⃣ 填充数据
int rowIndex = 1;
for (T obj : dataList) {
Row row = sheet.createRow(rowIndex++);
for (int i = 0; i < fields.size(); i++) {
Field field = fields.get(i);
field.setAccessible(true);
Object value = field.get(obj);
Cell cell = row.createCell(i);
setCellValue(cell, value, field.getAnnotation(ExcelColumn.class).format());
cell.setCellStyle(bodyStyle);
}
}
// 6️⃣ 自动列宽
for (int i = 0; i < fields.size(); i++) {
sheet.autoSizeColumn(i);
sheet.setColumnWidth(i, Math.min(sheet.getColumnWidth(i) + 512, 255 * 256));
}
// 7️⃣ 写入输出流
workbook.write(out);
// 8️⃣ 文件名编码(兼容中英文)
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
String disposition = "attachment; filename*=UTF-8''" + encodedFileName + ".xlsx";
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, disposition);
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
// ✅ 返回 ResponseEntity,可直接作为接口响应
return ResponseEntity.ok().headers(headers).body(out.toByteArray());
} catch (Exception e) {
throw new ExcelException("导出Excel失败", e);
}
}
// ============================================================
// 二、Excel 导入功能(Excel → 实体)
// ============================================================
/**
* 从 Excel 文件中读取数据并映射为实体类对象列表
*
* @param file 前端上传的 Excel 文件
* @param clazz 要映射的实体类型
* @param startRow 起始行号(从 0 开始,一般 1 表示跳过表头)
* @return 映射好的实体对象列表
*/
public static <T> List<T> importExcel(MultipartFile file, Class<T> clazz, int startRow) {
List<T> resultList = new ArrayList<>();
try (InputStream in = file.getInputStream(); Workbook workbook = WorkbookFactory.create(in)) {
Sheet sheet = workbook.getSheetAt(0);
List<Field> fields = getSortedExcelFields(clazz);
// 遍历行
for (int i = startRow; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if (row == null) continue;
T instance = clazz.getDeclaredConstructor().newInstance();
boolean hasData = false;
// 遍历列
for (int j = 0; j < fields.size(); j++) {
Field field = fields.get(j);
field.setAccessible(true);
Cell cell = row.getCell(j);
Object cellValue = getSafeCellValue(cell);
// 检查是否为有效数据行
if (cellValue != null && !"".equals(cellValue.toString().trim())) {
hasData = true;
}
// 类型转换并赋值
Object convertedValue = convertValue(cellValue, field.getType());
field.set(instance, convertedValue);
}
if (hasData) resultList.add(instance);
}
} catch (Exception e) {
throw new ExcelException("导入Excel失败", e);
}
return resultList;
}
// ============================================================
// 三、内部通用工具方法
// ============================================================
/**
* 获取带有 @ExcelColumn 注解的字段并按顺序排序
*/
private static List<Field> getSortedExcelFields(Class<?> clazz) {
List<Field> fields = new ArrayList<>();
for (Field f : clazz.getDeclaredFields()) {
if (f.isAnnotationPresent(ExcelColumn.class)) {
fields.add(f);
}
}
fields.sort(Comparator.comparingInt(f -> f.getAnnotation(ExcelColumn.class).order()));
return fields;
}
/**
* 设置单元格值,并根据格式参数进行日期格式化
*/
private static void setCellValue(Cell cell, Object value, String format) {
if (value == null) {
cell.setCellValue("");
return;
}
if (value instanceof Number num) {
cell.setCellValue(num.doubleValue());
} else if (value instanceof Boolean bool) {
cell.setCellValue(bool);
} else if (value instanceof Date date) {
String pattern = format.isEmpty() ? DATE_FORMAT_DEFAULT : format;
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
cell.setCellValue(sdf.format(date));
} else {
cell.setCellValue(value.toString());
}
}
/**
* 安全地获取单元格值,自动识别类型(字符串、数字、日期、布尔)
*/
private static Object getSafeCellValue(Cell cell) {
if (cell == null) return null;
return switch (cell.getCellType()) {
case STRING -> cell.getStringCellValue();
case NUMERIC -> DateUtil.isCellDateFormatted(cell)
? cell.getDateCellValue()
: cell.getNumericCellValue();
case BOOLEAN -> cell.getBooleanCellValue();
default -> null;
};
}
/**
* 将字符串或数值转换为目标类型(支持常见类型自动转换)
*/
private static Object convertValue(Object value, Class<?> targetType) {
if (value == null) return null;
String str = value.toString().trim();
if (str.isEmpty()) return null;
try {
if (targetType == String.class) return str;
if (targetType == Integer.class || targetType == int.class) return (int) Double.parseDouble(str);
if (targetType == Long.class || targetType == long.class) return (long) Double.parseDouble(str);
if (targetType == Double.class || targetType == double.class) return Double.parseDouble(str);
if (targetType == Boolean.class || targetType == boolean.class) return Boolean.parseBoolean(str);
if (targetType == Date.class) {
List<String> fmts = List.of("yyyy-MM-dd HH:mm:ss", "yyyy/MM/dd", "yyyy-MM-dd");
for (String fmt : fmts) {
try {
return new SimpleDateFormat(fmt).parse(str);
} catch (Exception ignored) {
}
}
}
} catch (Exception ignored) {
}
return str;
}
/**
* 创建表头样式(蓝底白字 + 居中 + 加粗)
*/
private static CellStyle createHeaderStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
Font font = workbook.createFont();
font.setBold(true);
font.setColor(IndexedColors.WHITE.getIndex());
style.setFont(font);
style.setFillForegroundColor(IndexedColors.DARK_BLUE.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
return style;
}
/**
* 创建正文单元格样式(细边框 + 居中)
*/
private static CellStyle createCellStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderTop(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
return style;
}
/**
* 自定义 Excel 异常类,用于在导入/导出出错时抛出更明确的错误信息
*/
public static class ExcelException extends RuntimeException {
public ExcelException(String message, Throwable cause) {
super(message, cause);
}
}
}@RestController
@RequestMapping("/api/apache/excel")
public class PoiController {
/**
* 导出 Excel
* 测试: localhost:8082/api/apache/excel/export
*/
@GetMapping("/export")
public ResponseEntity<byte[]> exportExcel() {
// 模拟数据
List<EmployeeForApache> list = Arrays.asList(
new EmployeeForApache("张三", 25, "工程师", LocalDateTime.now(), true),
new EmployeeForApache("李四", 30, "设计师", LocalDateTime.now(), false),
new EmployeeForApache("王五", 28, "产品经理", LocalDateTime.now(), true)
);
return ApachePoiExcelUtil.exportExcel(list, EmployeeForApache.class, "员工信息", "员工信息表");
}
/**
* 导入 Excel
* 测试: localhost:8082/api/apache/excel/import
*/
@PostMapping("/import")
public ResponseEntity<?> importExcel(@RequestParam("file") MultipartFile file) {
// 从第1行开始读取(跳过表头)
List<EmployeeForApache> list = ApachePoiExcelUtil.importExcel(file, EmployeeForApache.class, 1);
return ResponseEntity.ok(list);
}
}Apache PDFBox
PDFBox 是一个用于处理 PDF 文档的开源 Java 工具,允许创建新的 PDF 文档、编辑现有文档以及从文档中提取内容。以下是一个简单示例,用于导出(编写内容并输出)和导入(读取内容并打印) PDF 文件:
<!-- 版本管理 -->
<properties>
<apache.pdfbox.version>3.0.6</apache.pdfbox.version>
</properties>
<!-- Apache PDFBox 主库 -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>${apache.pdfbox.version}</version>
</dependency>
<!-- 字体支持(写中文时引入) -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>fontbox</artifactId>
<version>${apache.pdfbox.version}</version>
</dependency>import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.text.PDFTextStripper;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* @ Description: 通用 PDF 导入导出工具类(基于 Apache PDFBox)
* @ Author: ZJX
**/
public class PdfBoxUtil {
/**
* 导出 PDF(支持中文)
*
* @param title PDF 标题
* @param content 主体内容
* @param fileName 导出文件名(不含扩展名)
* @return ResponseEntity<byte [ ]> 可直接作为 Controller 返回
*/
public static ResponseEntity<byte[]> exportPdf(String title, List<String> content, String fileName) {
try (PDDocument document = new PDDocument();
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
// 页面设置:A4 纵向
PDPage page = new PDPage(PDRectangle.A4);
document.addPage(page);
PDPageContentStream stream = new PDPageContentStream(document, page);
// 中文字体(加载系统字体或项目内字体,文件地址为 C:\Windows\Fonts\simkai.ttf,即【楷体常规】字体,需要把它复制到类路径 resources/fonts 下 )
PDType0Font font = PDType0Font.load(document, new ClassPathResource("fonts/simkai.ttf").getInputStream());
// 写标题
stream.beginText();
stream.setFont(font, 18);
stream.newLineAtOffset(70, 750);
stream.showText(title);
stream.endText();
// 写内容
float yPosition = 720;
for (String line : content) {
stream.beginText();
stream.setFont(font, 12);
stream.newLineAtOffset(70, yPosition);
stream.showText(line);
stream.endText();
yPosition -= 20;
}
stream.close();
// 输出流
document.save(out);
// 文件名编码(支持中文)
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
String contentDisposition = "attachment; filename*=UTF-8''" + encodedFileName + ".pdf";
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, contentDisposition);
headers.setContentType(MediaType.APPLICATION_PDF);
return ResponseEntity.ok()
.headers(headers)
.body(out.toByteArray());
} catch (Exception e) {
throw new RuntimeException("导出 PDF 失败", e);
}
}
/**
* 从 PDF 文件中提取文本内容
*
* @return 提取出的纯文本内容
*/
public static String importPdf(MultipartFile file) {
try {
// ✅ 读取整个 PDF 文件为字节数组
byte[] pdfBytes = file.getBytes();
// ✅ 使用 Loader.loadPDF(byte[]) 载入文档
try (PDDocument document = Loader.loadPDF(pdfBytes)) {
PDFTextStripper stripper = new PDFTextStripper();
return stripper.getText(document);
}
} catch (IOException e) {
throw new RuntimeException("读取 PDF 文件失败: " + e.getMessage(), e);
}
}
}@RestController
@RequestMapping("/api/apache/pdf")
public class PdfController {
/**
* 导出 PDF 示例
* 测试:localhost:8082/api/apache/pdf/export
*/
@GetMapping("/export")
public ResponseEntity<byte[]> exportPdf() {
List<String> content = Arrays.asList(
"姓名:张三",
"职位:数据分析师",
"部门:医疗审计部",
"签发日期:2025-11-05"
);
return PdfBoxUtil.exportPdf("员工档案信息", content, "员工档案");
}
/**
* 导入(解析)PDF 示例
* 测试: localhost:8082/api/apache/pdf/import
*/
@PostMapping("/import")
public String importPdf(@RequestParam("file") MultipartFile file) {
String text = PdfBoxUtil.importPdf(file);
return "解析结果:\n" + text;
}
}提示
PDFBox 默认是不支持中文字体的,需要在项目中引入字体文件,示例中使用的是【楷体常规】字体,文件地址为 C:\Windows\Fonts\simkai.ttf,需要把它复制到类路径 resources/fonts 下 。
EasyExcel
注意
虽然 EasyExcel 十分优秀,但他却停更了,Apache Fesod 可作为预选方案,其功能和 EasyExcel 十分类似,它非常新,支持流式读取,官方文档也特别友好,是一个非常有潜力的替代方案!
EasyExcel EasyExcel 是阿里巴巴开源的一款基于 Java 的高性能 Excel 读写框架,相较于 Apache POI,它更加高性能且简单易用。
以下示例使用 EasyExcel 实现 Excel 导入导出,支持中文,非常优雅:
<!-- 版本管理 -->
<properties>
<alibaba.easyexcel.version>4.0.3</alibaba.easyexcel.version>
</properties>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>${alibaba.easyexcel.version}</version>
</dependency>import com.alibaba.excel.EasyExcelFactory;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* @ Description: Excel 导入导出工具类
* @ Author: ZJX
**/
public class EasyExcelUtils {
private EasyExcelUtils() {
}
/**
* 导出 excel 工具类
*
* @param response Controller 层接收的 response
* @param excelName 导出的文件名
* @param clazz excel 对应的实体类
* @param s 数据源(业务抽象,如从数据库获取数据)
*/
public static <T> void exportExcel(HttpServletResponse response, String excelName, Class<T> clazz,
Supplier<? extends Collection<T>> s) throws IOException {
// 1. 设置响应内容类型和头信息
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
response.setCharacterEncoding("utf-8");
// 2. 设置文件名(防止中文乱码)
String fileName = URLEncoder.encode(excelName, "UTF-8").replace("+", "%20"); // 解决文件名空格变加号的问题
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
// 3. 获取数据
Collection<T> dataList = s.get();
// 4. 使用 EasyExcel 写入并下载
EasyExcelFactory.write(response.getOutputStream(), clazz)
.charset(StandardCharsets.UTF_8) // 显式设置编码
.sheet("Sheet1")
.doWrite(dataList);
}
/**
* 导入 excel 工具类 -- 适用于数据量不大的场景,若数据量巨大,需采用分批读写入库的方式
*
* @param file 上传的 excel 文件(如通过 @RequestParam MultipartFile file 接收)
* @param clazz excel 对应的实体类
* @param consumer 处理读取到的数据(例如 xxxMapper::saveList)
*/
public static <T> void importExcel(MultipartFile file, Class<T> clazz, Consumer<List<T>> consumer) throws IOException {
// 1. 读取 Excel 数据
List<T> list = EasyExcelFactory.read(file.getInputStream())
.head(clazz)
.ignoreEmptyRow(true)
.sheet()
.doReadSync();
// 2. 判空检查
if (list == null || list.isEmpty()) {
throw new IllegalArgumentException("Excel 数据为空,请检查文件内容!");
}
// 3. 调用外部业务处理方法(如 xxxMapper::saveList)
consumer.accept(list);
}
}/**
* @ Description: EasyExcel 测试类
* @ Date: 2025/11/5 4:41:41
* @ Author: ZJX
**/
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/alibaba/excel")
public class EasyExcelController {
private final UserMapper userMapper;
/**
* 导出 Excel
* 测试: localhost:8082/api/alibaba/excel/export
*/
@GetMapping("/export")
public void exportExcel(HttpServletResponse response) throws IOException {
EasyExcelUtils.exportExcel(response, "用户数据", UserVO.class, userMapper::getList);
}
/**
* 导入 Excel
* 测试: localhost:8082/api/alibaba/excel/import
*/
@PostMapping("/import")
public void importExcel(@RequestParam("file") MultipartFile file) throws IOException {
EasyExcelUtils.importExcel(file, UserVO.class, userMapper::saveList);
}
}import com.alibaba.excel.annotation.ExcelProperty;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* @ Description: 测试实体类
* @ Date: 2025/11/5 4:47:47
* @ Author: ZJX
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UserVO {
@ExcelProperty(value = "姓名", index =0)
private String name;
@ExcelProperty(value = "年龄", index = 1)
private Integer age;
@ExcelProperty(value = "职位", index = 2)
private String position;
@ExcelProperty(value = "入职时间", index = 4)
private LocalDateTime hireDate;
@ExcelProperty(value = "是否在职", index = 3)
private Boolean active;
}/**
* @ Description: 模拟操作数据库
* @ Author: ZJX
**/
@Repository
@Slf4j
public class UserMapper {
/**
* 模拟查询用户列表
*/
public List<UserVO> getList() {
return List.of(
new UserVO("张三", 25, "工程师", LocalDateTime.now(), true),
new UserVO("李四", 30, "设计师", LocalDateTime.now(), false),
new UserVO("王五", 28, "产品经理", LocalDateTime.now(), true));
}
/**
* 模拟保存用户列表
*/
public void saveList(List<UserVO> userList) {
// 打印一下即可
userList.forEach(item -> log.info(item.toString()));
}
}Hutool Excel 工具
Hutool-poi 是针对 Apache POI 的封装,因此需要用户自行引入 POI 库, Hutool 默认不引入。和 Apache POI 类似,Hutool 也不支持实体类注解,没有 EasyExcel 那么方便,这里就不使用示例了,去看官网写得非常棒。
<properties>
<apache.poi.version>5.4.1</apache.poi.version>
<hutool.version>5.8.41</hutool.version>
</properties>
<!-- 在父模块中加入 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-bom</artifactId>
<version>${hutool.version}</version>
<type>pom</type>
<!-- 注意这里是import -->
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 在子模块中加入 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-poi</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${apache.poi.version}</version>
</dependency>
