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;
/**
* 用于标记实体类字段与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;
/**
* 测试实体类
*
* @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;
/**
* 通用 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 是阿里巴巴开源的一款基于 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 com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.alibaba.excel.write.style.column.AbstractColumnWidthStyleStrategy;
import org.apache.poi.ss.usermodel.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
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;
/**
* Excel 导入导出工具类
*
* @author zjx
* @since 2026/1/27
*/
public class EasyExcelUtils {
/**
* 默认 Sheet 名称
*/
private static final String DEFAULT_SHEET_NAME = "Sheet1";
/**
* 默认最小列宽(字符数)
*/
private static final int DEFAULT_MIN_COLUMN_WIDTH = 18;
/**
* 表头背景色
*/
private static final short HEADER_BACKGROUND_COLOR = IndexedColors.GREY_25_PERCENT.getIndex();
/**
* 表头字体是否加粗
*/
private static final boolean HEADER_FONT_BOLD = true;
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)
// 自定义样式
.registerWriteHandler(createCellStyleStrategy())
// 列宽设置策略(默认18个字宽度,且内容宽度大于18个中文时,保持原宽)
.registerWriteHandler(new AbstractColumnWidthStyleStrategy() {
private static final int MIN_WIDTH = DEFAULT_MIN_COLUMN_WIDTH * 256;
@Override
protected void setColumnWidth(CellWriteHandlerContext context) {
Cell cell = context.getCell();
if (cell == null) {
return;
}
Sheet sheet = context.getWriteSheetHolder().getSheet();
int columnIndex = cell.getColumnIndex();
int currentWidth = sheet.getColumnWidth(columnIndex);
if (currentWidth < MIN_WIDTH) {
sheet.setColumnWidth(columnIndex, MIN_WIDTH);
}
}
})
.sheet(DEFAULT_SHEET_NAME)
.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);
}
/**
* 创建单元格样式
*
* @return 单元格样式策略
*/
private static HorizontalCellStyleStrategy createCellStyleStrategy() {
// 1. 创建表头样式
WriteCellStyle headWriteCellStyle = createHeaderCellStyle();
// 2. 创建内容样式
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
// 3. 将两种样式组合成策略(表头和内容使用不同样式)
return new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
}
/**
* 获取表头样式
*/
private static WriteCellStyle createHeaderCellStyle() {
// 1. 设置背景色为浅灰色
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
headWriteCellStyle.setFillForegroundColor(HEADER_BACKGROUND_COLOR);
headWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
// 2. 设置字体加粗
WriteFont headWriteFont = new WriteFont();
headWriteFont.setBold(HEADER_FONT_BOLD);
headWriteCellStyle.setWriteFont(headWriteFont);
// 3. 设置文本居中
headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
headWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
// 禁止表头换行
headWriteCellStyle.setWrapped(false);
return headWriteCellStyle;
}
}/**
* EasyExcel 测试类
* @since 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.ExcelIgnore;
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;
/**
* 测试实体类
* @since 2025/11/5 4:47:47
* @author zjx
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UserVO {
@ExcelIgnore
private Long id;
@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;
}/**
* 模拟操作数据库
* @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 那么方便,但是它和Apache POI都比较适合定制化场景(如动态表头),这里提供一个动态表头导出示例,其他用法可参考官网。
<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>/**
* 通用的 Excel【动态表头】对象
*
* @author zjx
* @since 2025/12/25
*/
@Data
public class ExcelHeader {
/**
* 表头字段名
*/
private String field;
/**
* 表头显示名称(中文)
*/
private String label;
/**
* 列宽(预留可选)
*/
private Integer width;
/**
* 对齐方式(预留可选)
*/
private String align;
public ExcelHeader(String field, String label) {
this.field = field;
this.label = label;
}
public ExcelHeader(String field, String label, Integer width) {
this.field = field;
this.label = label;
this.width = width;
}
}/**
* 通用的 Excel 数据对象 DTO ( 表头 + 数据 + 文件名称 )
*
* @author zjx
* @since 2025/12/25
*/
@Data
public class ExportDataDTO {
/**
* 文件名称(可选)
*/
private String fileName;
/**
* 表头信息
*/
private List<ExcelHeader> headers;
/**
* 数据行
*/
private List<LinkedHashMap<String, Object>> rows;
public static ExportDataDTO create(String fileName, List<ExcelHeader> headers, List<LinkedHashMap<String, Object>> rows) {
ExportDataDTO dto = new ExportDataDTO();
dto.setFileName(fileName);
dto.setHeaders(headers);
dto.setRows(rows);
return dto;
}
}/**
* 通用 Excel 工具
*
* @author zjx
* @since 2026/1/30
*/
public class ExcelUtils {
/**
* 动态表头导出(使用 Hutool 工具实现) -- 支持自定义列宽
*
* @param dto Excel 数据对象 (表头 + 数据 + 文件名称)
* @param response HttpServletResponse 对象
*/
public static void exportExcelByHutool(ExportDataDTO dto, HttpServletResponse response) {
// true = xlsx
ExcelWriter writer = ExcelUtil.getWriter(true);
List<ExcelHeader> headers = dto.getHeaders();
// ================= 1. 表头别名 =================
for (ExcelHeader header : headers) {
writer.addHeaderAlias(header.getField(), header.getLabel());
}
// 只输出别名字段,避免 LinkedHashMap 中多余字段
writer.setOnlyAlias(true);
// ================= 2. 写数据 =================
writer.write(dto.getRows(), true);
// ================= 3. 列宽控制 =================
for (int i = 0; i < headers.size(); i++) {
ExcelHeader header = headers.get(i);
if (header.getWidth() != null && header.getWidth() > 0) {
// 自定义列宽(字符数)
writer.setColumnWidth(i, header.getWidth());
} else {
// 自动列宽
writer.autoSizeColumn(i);
}
}
// ================= 4. 输出 =================
try {
response.setContentType(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"
);
String fileName = dto.getFileName();
if (fileName == null || fileName.trim().isEmpty()) {
fileName = "export.xlsx";
} else if (!fileName.endsWith(".xlsx")) {
fileName = fileName + ".xlsx";
}
String encodedName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
response.setHeader(
"Content-Disposition",
"attachment;filename*=utf-8''" + encodedName
);
writer.flush(response.getOutputStream(), true);
} catch (Exception e) {
throw new ServiceException("导出Excel失败:" + e.getMessage());
} finally {
writer.close();
}
}
}@GetMapping("/export-excel")
public void exportDynamicHeaderExcelDemo(HttpServletResponse response) {
// 构建动态表头
List<ExcelHeader> headers = new ArrayList<>();
headers.add(new ExcelHeader("sex", "性别", 20));
headers.add(new ExcelHeader("name", "姓名", 20));
headers.add(new ExcelHeader("age", "年龄", 10));
// 构建表数据
List<LinkedHashMap<String, Object>> rows = new ArrayList<>();
for (int i = 0; i < 5; i++) {
LinkedHashMap<String, Object> row = new LinkedHashMap<>();
row.put("sex", "男");
row.put("name", "张三");
row.put("age", i);
rows.add(row);
}
ExportDataDTO dto = ExportDataDTO.create("测试导出动态表头.xlsx", headers, rows);
// 导出 Excel
ExcelUtils.exportExcelByHutool(dto, response);
}提示
动态表头对象 ExcelHeader 和 数据对象 ExportDataDTO 适用于任何地方,他们对表头和数据的封装非常巧妙,不会存在表头、数据乱序等情况,这里采用 Hutool 作为导出实现;如需其他实现(如 Apache POI),更改一下 ExcelUtils 即可。

