Excel处理 (excel)
Excel处理模块基于FastExcel(原EasyExcel)框架,提供了完整的Excel导入导出解决方案,支持数据校验、格式转换、单元格合并、下拉选项、动态数据源等高级功能。
模块概述
核心功能
- Excel导入:支持同步/异步导入,数据校验,错误收集,自定义监听器
- Excel导出:支持基础导出、模板导出、多Sheet导出、单元格合并
- 数据转换:字典转换、枚举转换、大数字处理、自定义转换器
- 界面增强:下拉选项、级联下拉、单元格合并、批注、必填标识
- 模板支持:单表模板、多表模板、多Sheet模板填充
- 动态下拉:支持从数据库动态获取下拉选项数据
技术特性
- 基于FastExcel(原EasyExcel)框架
- 支持大文件流式处理,内存占用低
- 注解驱动,配置简单
- 完善的异常处理和错误收集机制
- 灵活的扩展能力,支持自定义监听器和转换器
模块结构
ruoyi-common-excel
├── annotation/ # 注解定义
│ ├── CellMerge.java # 单元格合并注解
│ ├── ExcelDictFormat.java # 字典格式化注解
│ ├── ExcelEnumFormat.java # 枚举格式化注解
│ ├── ExcelNotation.java # 批注注解
│ ├── ExcelRequired.java # 必填标识注解
│ └── ExcelDynamicOptions.java # 动态下拉选项注解
├── convert/ # 转换器
│ ├── ExcelDictConvert.java # 字典转换器
│ ├── ExcelEnumConvert.java # 枚举转换器
│ └── ExcelBigNumberConvert.java # 大数字转换器
├── core/ # 核心类
│ ├── DefaultExcelListener.java # 默认导入监听器
│ ├── DefaultExcelResult.java # 导入结果封装
│ ├── DropDownOptions.java # 下拉选项配置
│ ├── CellMergeStrategy.java # 单元格合并策略
│ ├── CellMergeHandler.java # 单元格合并处理器
│ ├── ExcelDownHandler.java # 下拉选项处理器
│ ├── ExcelListener.java # 监听器接口
│ ├── ExcelResult.java # 结果接口
│ └── ExcelOptionsProvider.java # 动态选项提供者接口
├── handler/ # 处理器
│ └── DataWriteHandler.java # 数据写入处理器
└── utils/
└── ExcelUtil.java # 核心工具类快速开始
基础导入示例
// 1. 定义实体类
@Data
public class UserImportVo {
@ExcelProperty("姓名")
@NotBlank(message = "姓名不能为空")
private String name;
@ExcelProperty("年龄")
@Min(value = 0, message = "年龄不能为负数")
private Integer age;
@ExcelProperty("邮箱")
@Email(message = "邮箱格式不正确")
private String email;
@ExcelProperty("性别")
@ExcelDictFormat(dictType = "sys_user_gender")
private String gender;
}
// 2. 执行导入(带数据校验)
@PostMapping("/import")
public R<String> importUser(@RequestParam("file") MultipartFile file) throws IOException {
ExcelResult<UserImportVo> result = ExcelUtil.importExcel(
file.getInputStream(),
UserImportVo.class,
true // 启用数据校验
);
// 获取成功数据
List<UserImportVo> successList = result.getList();
// 获取错误信息
List<String> errorList = result.getErrorList();
if (!errorList.isEmpty()) {
return R.fail("导入失败:" + String.join("<br/>", errorList));
}
// 处理导入的数据
userService.batchInsert(successList);
return R.ok(result.getAnalysis()); // 如:读取完成!成功100条,失败0条
}基础导出示例
@GetMapping("/export")
public void exportUser(HttpServletResponse response) {
List<UserExportVo> users = userService.selectExportList();
ExcelUtil.exportExcel(users, "用户信息", UserExportVo.class, response);
}
// 导出实体类
@Data
public class UserExportVo {
@ExcelProperty("用户ID")
private Long userId;
@ExcelProperty("姓名")
private String name;
@ExcelProperty("性别")
@ExcelDictFormat(dictType = "sys_user_gender")
private String gender;
@ExcelProperty("状态")
@ExcelEnumFormat(enumClass = UserStatus.class, valueField = "value", labelField = "label")
private Integer status;
@ExcelProperty("创建时间")
private Date createTime;
}详细功能
1. Excel导入
1.1 同步导入
适用于小数据量(建议1000条以内)的简单导入场景,不进行数据校验。
// 导入为Map集合(不需要实体类)
List<Map<Integer, String>> mapData = ExcelUtil.importExcel(inputStream);
// 导入为实体对象(不校验)
List<User> users = ExcelUtil.importExcel(inputStream, User.class);1.2 异步导入(推荐)
支持数据校验,提供详细的错误信息,适用于生产环境。
// 启用数据校验的导入
ExcelResult<User> result = ExcelUtil.importExcel(inputStream, User.class, true);
// 获取成功数据
List<User> successList = result.getList();
// 获取错误信息列表
List<String> errors = result.getErrorList();
// 获取统计信息
String analysis = result.getAnalysis(); // 如:读取完成!成功100条,失败5条错误处理机制:
DefaultExcelListener提供了完善的错误处理:
// 数据类型转换异常处理
// 输出格式:第X行-第Y列-表头Z: 解析异常
if (exception instanceof ExcelDataConvertException excelDataConvertException) {
Integer rowIndex = excelDataConvertException.getRowIndex();
Integer columnIndex = excelDataConvertException.getColumnIndex();
errMsg = StrUtil.format("第{}行-第{}列-表头{}: 解析异常<br/>",
rowIndex + 1, columnIndex + 1, headMap.get(columnIndex));
}
// Bean Validation校验异常处理
// 输出格式:第X行数据校验异常: 字段1错误, 字段2错误
if (exception instanceof ConstraintViolationException constraintViolationException) {
Set<ConstraintViolation<?>> constraintViolations = constraintViolationException.getConstraintViolations();
String constraintViolationsMsg = StreamUtils.join(constraintViolations, ConstraintViolation::getMessage, ", ");
errMsg = StrUtil.format("第{}行数据校验异常: {}",
context.readRowHolder().getRowIndex() + 1, constraintViolationsMsg);
}1.3 自定义监听器
对于复杂的导入逻辑,可以继承DefaultExcelListener实现自定义监听器。
public class CustomUserListener extends DefaultExcelListener<User> {
private final UserService userService;
private final int BATCH_SIZE = 500;
private List<User> batchList = new ArrayList<>();
public CustomUserListener(UserService userService) {
this.userService = userService;
}
@Override
public void invoke(User user, AnalysisContext context) {
// 自定义数据预处理逻辑
if (StringUtils.isNotBlank(user.getPhone())) {
user.setPhone(formatPhone(user.getPhone()));
}
// 调用父类方法执行默认处理(包含校验)
super.invoke(user, context);
// 分批处理大数据量导入
batchList.add(user);
if (batchList.size() >= BATCH_SIZE) {
userService.batchSave(batchList);
batchList.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 处理剩余数据
if (!batchList.isEmpty()) {
userService.batchSave(batchList);
}
super.doAfterAllAnalysed(context);
}
@Override
public void onException(Exception exception, AnalysisContext context) {
// 自定义异常处理逻辑
log.error("数据处理异常,行号:{}", context.readRowHolder().getRowIndex(), exception);
super.onException(exception, context);
}
private String formatPhone(String phone) {
// 格式化手机号逻辑
return phone.replaceAll("[^0-9]", "");
}
}
// 使用自定义监听器
CustomUserListener listener = new CustomUserListener(userService);
ExcelResult<User> result = ExcelUtil.importExcel(inputStream, User.class, listener);2. Excel导出
2.1 基础导出
// 导出到HTTP响应
ExcelUtil.exportExcel(userList, "用户信息", User.class, response);
// 导出到输出流
ExcelUtil.exportExcel(userList, "用户信息", User.class, outputStream);导出时自动注册的处理器:
ExcelWriterSheetBuilder builder = FastExcel.write(os, clazz)
.autoCloseStream(false)
// 自动列宽
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
// 大数字转换器(防止精度丢失)
.registerConverter(new ExcelBigNumberConvert())
// 批注和必填标识处理器
.registerWriteHandler(new DataWriteHandler(clazz))
// 默认样式
.registerWriteHandler(initCellStyle())
.sheet(sheetName);2.2 单元格合并导出
适用于相同数据需要合并显示的场景,如部门统计报表。
// 实体类配置
@Data
public class DepartmentUserVo {
@CellMerge // 相同部门的单元格将被合并
@ExcelProperty("部门")
private String department;
@CellMerge(mergeBy = {"department"}) // 在同一部门内合并相同团队
@ExcelProperty("团队")
private String team;
@CellMerge(mergeBy = {"department", "team"}) // 在同一部门同一团队内合并
@ExcelProperty("职位")
private String position;
@ExcelProperty("姓名")
private String name;
@ExcelProperty("薪资")
private BigDecimal salary;
}
// 导出时启用合并
ExcelUtil.exportExcel(deptUsers, "部门用户", DepartmentUserVo.class, true, response);合并算法原理:
CellMergeHandler通过以下步骤计算合并区域:
- 扫描实体类中带有@CellMerge注解的字段
- 遍历数据列表,对比相邻行的字段值
- 如果指定了mergeBy属性,还需对比依赖字段的值
- 相同值的连续行将被合并为一个单元格
// 合并条件判断逻辑
private boolean isMerge(Object currentRow, Object preRow, CellMerge cellMerge) {
final String[] mergeBy = cellMerge.mergeBy();
if (StrUtil.isAllNotBlank(mergeBy)) {
// 比对当前行和上一行的依赖字段值
for (String fieldName : mergeBy) {
final Object valCurrent = ReflectUtil.getFieldValue(currentRow, fieldName);
final Object valPre = ReflectUtil.getFieldValue(preRow, fieldName);
if (!Objects.equals(valPre, valCurrent)) {
// 依赖字段如有任一不等值,则标记为不可合并
return false;
}
}
}
return true;
}2.3 下拉选项导出
为Excel添加下拉选择功能,提升数据录入体验。
// 创建下拉选项
List<DropDownOptions> options = Arrays.asList(
new DropDownOptions(2, Arrays.asList("男", "女")), // 第3列性别下拉
new DropDownOptions(3, Arrays.asList("在职", "离职", "试用")) // 第4列状态下拉
);
ExcelUtil.exportExcel(userList, "用户信息", User.class, response, options);下拉选项处理策略:
ExcelDownHandler根据选项数量自动选择最优策略:
- 选项数量 ≤ 10个:使用简单下拉框,直接嵌入数据校验
- 选项数量 > 10个:使用额外Sheet存储选项,通过名称管理器引用
- 级联下拉:使用INDIRECT函数实现二级联动
if (options.size() > 20) {
// 使用额外表形式,避免Excel打开缓慢
dropDownWithSheet(helper, workbook, sheet, index, options);
} else {
// 使用固定值形式
dropDownWithSimple(helper, sheet, index, options);
}2.4 级联下拉选项
支持二级联动下拉选择,如省市级联。
// 构建省市数据
List<Province> provinceList = Arrays.asList(
new Province(1L, "北京"),
new Province(2L, "上海"),
new Province(3L, "广东")
);
List<City> cityList = Arrays.asList(
new City(1L, 1L, "朝阳区"),
new City(2L, 1L, "海淀区"),
new City(3L, 2L, "浦东新区"),
new City(4L, 2L, "静安区"),
new City(5L, 3L, "广州市"),
new City(6L, 3L, "深圳市")
);
// 构建级联下拉选项
DropDownOptions cascadeOptions = DropDownOptions.buildLinkedOptions(
provinceList, // 省份列表
1, // 省份列索引(第2列)
cityList, // 城市列表
2, // 城市列索引(第3列)
Province::getId, // 省份ID获取方法
City::getProvinceId, // 城市获取省份ID方法
province -> DropDownOptions.createOptionValue(province.getId(), province.getName())
);
ExcelUtil.exportExcel(addressList, "地址信息", Address.class, response,
Arrays.asList(cascadeOptions));级联下拉实现原理:
- 创建隐藏的linkedOptions_X工作表存储级联数据
- 第一行存储一级选项(省份)
- 每一列存储对应省份下的二级选项(城市)
- 使用名称管理器定义数据区域
- 二级下拉使用INDIRECT函数动态引用一级选择的结果
// 二级下拉公式:=INDIRECT(A2),其中A列是一级选项所在列
String secondOptionsFunction = String.format("=INDIRECT(%s%d)",
mainSheetFirstOptionsColumnName, i + 1);
markLinkedOptionsToSheet(helper, sheet, i, options.getNextIndex(),
helper.createFormulaListConstraint(secondOptionsFunction));2.5 动态下拉选项
支持从数据库动态获取下拉选项数据。
// 1. 实现ExcelOptionsProvider接口
@Component
public class DeptOptionsProvider implements ExcelOptionsProvider {
@Resource
private ISysDeptService deptService;
@Override
public Set<String> getOptions() {
return deptService.list().stream()
.map(SysDept::getDeptName)
.collect(Collectors.toSet());
}
}
// 2. 在实体类中使用@ExcelDynamicOptions注解
@Data
public class UserExportVo {
@ExcelProperty("姓名")
private String name;
@ExcelProperty("部门")
@ExcelDynamicOptions(providerClass = DeptOptionsProvider.class)
private String deptName;
@ExcelProperty("职位")
@ExcelDynamicOptions(providerClass = PositionOptionsProvider.class)
private String position;
}
// 3. 导出时自动加载动态选项
ExcelUtil.exportExcel(userList, "用户信息", UserExportVo.class, response);3. 模板导出
适用于格式固定的报表导出,支持复杂的Excel布局。
3.1 单表模板导出
// 模板文件:resources/templates/user-report.xlsx
// 模板内容使用 {.属性名} 作为占位符
// 例如:姓名:{.name},年龄:{.age},部门:{.department}
List<User> users = userService.getReportData();
ExcelUtil.exportTemplate(users, "用户报表", "templates/user-report.xlsx", response);3.2 多表模板导出
在一个模板中填充多个不同类型的数据。
// 模板内容使用 {key.属性名} 作为占位符
// 报表标题:{header.title} {header.date}
// 用户信息:{userInfo.name} {userInfo.department}
// 订单列表:{orders.orderNo} {orders.amount}
// 汇总信息:{summary.total} {summary.average}
Map<String, Object> data = new HashMap<>();
data.put("header", headerObject); // 报表头信息
data.put("userInfo", userObject); // 单个对象
data.put("orders", orderList); // 列表数据
data.put("summary", summaryObject); // 汇总信息
ExcelUtil.exportTemplateMultiList(data, "综合报表", "templates/multi-report.xlsx", response);3.3 多Sheet模板导出
使用相同模板生成多个工作表。
List<Map<String, Object>> sheetDataList = new ArrayList<>();
// 第一个Sheet的数据(研发部)
Map<String, Object> sheet1Data = new HashMap<>();
sheet1Data.put("deptName", "研发部");
sheet1Data.put("users", dept1Users);
sheet1Data.put("summary", dept1Summary);
sheetDataList.add(sheet1Data);
// 第二个Sheet的数据(市场部)
Map<String, Object> sheet2Data = new HashMap<>();
sheet2Data.put("deptName", "市场部");
sheet2Data.put("users", dept2Users);
sheet2Data.put("summary", dept2Summary);
sheetDataList.add(sheet2Data);
// 第三个Sheet的数据(财务部)
Map<String, Object> sheet3Data = new HashMap<>();
sheet3Data.put("deptName", "财务部");
sheet3Data.put("users", dept3Users);
sheet3Data.put("summary", dept3Summary);
sheetDataList.add(sheet3Data);
ExcelUtil.exportTemplateMultiSheet(sheetDataList, "各部门报表", "templates/dept-report.xlsx", response);注解详解
1. @ExcelProperty
FastExcel原生注解,用于定义Excel列映射。
public class User {
// 基本用法
@ExcelProperty(value = "姓名", index = 0)
private String name;
// 多级表头
@ExcelProperty(value = {"基本信息", "年龄"}, index = 1)
private Integer age;
// 指定转换器
@ExcelProperty(value = "创建时间", converter = LocalDateTimeConverter.class)
private LocalDateTime createTime;
}2. @ExcelDictFormat
字典格式化注解,支持字典值与显示文本的双向转换。
public class User {
// 使用系统字典
@ExcelDictFormat(dictType = "sys_user_gender")
@ExcelProperty("性别")
private String gender;
// 使用自定义表达式
@ExcelDictFormat(readConverterExp = "0=正常,1=停用,2=删除")
@ExcelProperty("状态")
private String status;
// 多值字段(如角色,多个值用逗号分隔)
@ExcelDictFormat(dictType = "sys_role", separator = ",")
@ExcelProperty("角色")
private String roles;
}转换逻辑:
// 导出时:数据库code值 → Excel显示文本
// 例如:gender = "0" → Excel显示"男"
@Override
public WriteCellData<String> convertToExcelData(Object object, ...) {
ExcelDictFormat anno = getAnnotation(contentProperty.getField());
String type = anno.dictType();
String value = Convert.toStr(object);
String label;
if (StringUtils.isBlank(type)) {
// 使用表达式转换
label = ExcelUtil.convertByExp(value, anno.readConverterExp(), anno.separator());
} else {
// 使用字典服务转换
label = SpringUtils.getBean(DictService.class).getDictLabel(type, value, anno.separator());
}
return new WriteCellData<>(label);
}
// 导入时:Excel显示文本 → 数据库code值
// 例如:Excel的"男" → gender = "0"
@Override
public Object convertToJavaData(ReadCellData<?> cellData, ...) {
ExcelDictFormat anno = getAnnotation(contentProperty.getField());
String type = anno.dictType();
String label = cellData.getStringValue();
String value;
if (StringUtils.isBlank(type)) {
value = ExcelUtil.reverseByExp(label, anno.readConverterExp(), anno.separator());
} else {
value = SpringUtils.getBean(DictService.class).getDictValue(type, label, anno.separator());
}
return Convert.convert(contentProperty.getField().getType(), value);
}3. @ExcelEnumFormat
枚举格式化注解,支持枚举类型的双向转换。
// 枚举定义
public enum UserStatus {
ACTIVE(1, "激活"),
INACTIVE(0, "禁用"),
LOCKED(2, "锁定");
private final Integer value;
private final String label;
UserStatus(Integer value, String label) {
this.value = value;
this.label = label;
}
// getter方法...
}
// 实体类使用
public class User {
@ExcelProperty("状态")
@ExcelEnumFormat(
enumClass = UserStatus.class,
valueField = "value", // 枚举中存储值的字段名
labelField = "label" // 枚举中显示文本的字段名
)
private Integer status; // 存储枚举的value值(0/1/2)
}
// 导出时:status=1 → Excel显示"激活"
// 导入时:Excel的"激活" → status=1转换实现:
// 构建枚举值到标签的映射
private Map<Object, String> beforeConvert(ExcelContentProperty contentProperty) {
ExcelEnumFormat anno = getAnnotation(contentProperty.getField());
Map<Object, String> enumValueMap = new HashMap<>();
// 获取枚举类的所有常量
Enum<?>[] enumConstants = anno.enumClass().getEnumConstants();
for (Enum<?> enumConstant : enumConstants) {
// 通过反射获取枚举的value字段值
Object value = ReflectUtils.invokeGetter(enumConstant, anno.valueField());
// 通过反射获取枚举的label字段值
String label = ReflectUtils.invokeGetter(enumConstant, anno.labelField());
enumValueMap.put(value, label);
}
return enumValueMap;
}4. @CellMerge
单元格合并注解,用于合并相同值的相邻单元格。
public class SalesReport {
@CellMerge // 基本用法:相同值自动合并
@ExcelProperty("区域")
private String region;
@CellMerge(index = 1) // 指定列索引
@ExcelProperty("省份")
private String province;
@CellMerge(mergeBy = {"region"}) // 依赖区域字段
@ExcelProperty("城市")
private String city;
@CellMerge(mergeBy = {"region", "province", "city"}) // 多级依赖
@ExcelProperty("门店")
private String store;
@ExcelProperty("销售额")
private BigDecimal sales;
}注解属性:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| index | int | -1 | 列索引,-1表示自动推断 |
| mergeBy | String[] | {} | 依赖的字段名列表 |
5. @ExcelRequired
必填字段标识注解,用于标记必填列,在表头显示特殊颜色提醒。
public class User {
@ExcelRequired // 使用默认红色字体
@ExcelProperty("姓名")
private String name;
@ExcelRequired(fontColor = IndexedColors.BLUE) // 自定义蓝色字体
@ExcelProperty("邮箱")
private String email;
@ExcelRequired(fontColor = IndexedColors.ORANGE)
@ExcelProperty("手机号")
private String phone;
}处理逻辑:
// DataWriteHandler中处理必填标识
if (CollUtil.isNotEmpty(headColumnMap) && headColumnMap.containsKey(cell.getStringCellValue())) {
WriteFont headWriteFont = new WriteFont();
headWriteFont.setBold(true);
// 设置必填字段的字体颜色
headWriteFont.setColor(headColumnMap.get(cell.getStringCellValue()));
writeCellStyle.setWriteFont(headWriteFont);
}6. @ExcelNotation
批注注解,为Excel单元格添加批注说明。
public class User {
@ExcelNotation("请填写真实姓名,2-20个字符")
@ExcelProperty("姓名")
private String name;
@ExcelNotation("格式:yyyy-MM-dd,例如:2024-01-15")
@ExcelProperty("出生日期")
private Date birthDate;
@ExcelNotation("请选择正确的性别")
@ExcelProperty("性别")
@ExcelDictFormat(dictType = "sys_user_gender")
private String gender;
}批注创建逻辑:
if (CollUtil.isNotEmpty(notationMap) && notationMap.containsKey(cell.getStringCellValue())) {
String notationContext = notationMap.get(cell.getStringCellValue());
// 创建绘图对象
Comment comment = drawing.createCellComment(
new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), 0, (short) 5, 5)
);
comment.setString(new XSSFRichTextString(notationContext));
cell.setCellComment(comment);
}7. @ExcelDynamicOptions
动态下拉选项注解,支持从数据库动态获取下拉选项。
// 1. 定义选项提供者
@Component
public class RoleOptionsProvider implements ExcelOptionsProvider {
@Resource
private ISysRoleService roleService;
@Override
public Set<String> getOptions() {
return roleService.selectRoleAll().stream()
.map(SysRole::getRoleName)
.collect(Collectors.toSet());
}
}
// 2. 在实体类中使用
public class UserExportVo {
@ExcelProperty("角色")
@ExcelDynamicOptions(providerClass = RoleOptionsProvider.class)
private String roleName;
}高级特性
1. 大数字处理
ExcelBigNumberConvert自动处理长整型数据,防止Excel显示科学计数法或精度丢失。
public class Order {
@ExcelProperty("订单ID")
private Long orderId; // 如:1234567890123456789L
// 自动转为字符串格式导出,保证显示完整
@ExcelProperty("手机号")
private Long phone; // 13812345678L
// 自动处理,保证显示完整
}转换阈值:
// 超过15位数字时自动转为字符串
// JavaScript安全整数范围:-9007199254740991 ~ 9007199254740991
public class ExcelBigNumberConvert implements Converter<Long> {
@Override
public WriteCellData<Object> convertToExcelData(Long value, ...) {
if (value != null && String.valueOf(value).length() > 15) {
return new WriteCellData<>(String.valueOf(value));
}
return new WriteCellData<>(value);
}
}2. 数据校验
支持Bean Validation校验注解,导入时自动校验数据有效性。
public class UserImportVo {
@NotBlank(message = "姓名不能为空")
@Length(min = 2, max = 50, message = "姓名长度必须在2-50之间")
@ExcelProperty("姓名")
private String name;
@NotNull(message = "年龄不能为空")
@Min(value = 0, message = "年龄不能为负数")
@Max(value = 150, message = "年龄不能超过150")
@ExcelProperty("年龄")
private Integer age;
@Email(message = "邮箱格式不正确")
@ExcelProperty("邮箱")
private String email;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
@ExcelProperty("手机号")
private String phone;
@NotBlank(message = "身份证号不能为空")
@Size(min = 18, max = 18, message = "身份证号必须为18位")
@ExcelProperty("身份证号")
private String idCard;
}
// 导入时校验
ExcelResult<UserImportVo> result = ExcelUtil.importExcel(inputStream, UserImportVo.class, true);
// 校验失败的数据会被记录到errorList中
List<String> errors = result.getErrorList();3. 自定义样式
// 获取默认样式策略
HorizontalCellStyleStrategy styleStrategy = ExcelUtil.initCellStyle();
// 自定义表头样式
WriteCellStyle headStyle = new WriteCellStyle();
headStyle.setFillForegroundColor(IndexedColors.ROYAL_BLUE.getIndex());
headStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
headStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
headStyle.setVerticalAlignment(VerticalAlignment.CENTER);
WriteFont headFont = new WriteFont();
headFont.setColor(IndexedColors.WHITE.getIndex());
headFont.setBold(true);
headFont.setFontHeightInPoints((short) 12);
headStyle.setWriteFont(headFont);
// 自定义内容样式
WriteCellStyle contentStyle = new WriteCellStyle();
contentStyle.setHorizontalAlignment(HorizontalAlignment.LEFT);
contentStyle.setVerticalAlignment(VerticalAlignment.CENTER);
WriteFont contentFont = new WriteFont();
contentFont.setFontHeightInPoints((short) 11);
contentStyle.setWriteFont(contentFont);
// 应用自定义样式
HorizontalCellStyleStrategy customStyle = new HorizontalCellStyleStrategy(headStyle, contentStyle);4. 错误处理最佳实践
@PostMapping("/import")
public R<ImportResult> importUsers(@RequestParam("file") MultipartFile file) {
try {
// 文件校验
if (file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String filename = file.getOriginalFilename();
if (!StringUtils.endsWith(filename, ".xlsx") && !StringUtils.endsWith(filename, ".xls")) {
return R.fail("仅支持Excel文件格式");
}
// 执行导入
ExcelResult<UserImportVo> result = ExcelUtil.importExcel(
file.getInputStream(),
UserImportVo.class,
true
);
List<UserImportVo> successList = result.getList();
List<String> errorList = result.getErrorList();
// 构建返回结果
ImportResult importResult = new ImportResult();
importResult.setSuccessCount(successList.size());
importResult.setErrorCount(errorList.size());
if (!errorList.isEmpty()) {
// 有错误数据
importResult.setErrors(errorList);
if (successList.isEmpty()) {
// 全部失败
return R.fail("导入失败", importResult);
}
// 部分成功
userService.batchSave(successList);
return R.ok("部分数据导入成功", importResult);
}
// 全部成功
userService.batchSave(successList);
return R.ok(result.getAnalysis(), importResult);
} catch (IOException e) {
log.error("导入用户失败", e);
return R.fail("文件读取失败:" + e.getMessage());
} catch (Exception e) {
log.error("导入用户异常", e);
return R.fail("导入失败:" + e.getMessage());
}
}性能优化建议
1. 大文件分批导入
public class BatchUserListener extends DefaultExcelListener<User> {
private final int BATCH_SIZE = 1000;
private final UserService userService;
private List<User> batchList = new ArrayList<>();
private int totalSuccess = 0;
public BatchUserListener(UserService userService) {
this.userService = userService;
}
@Override
public void invoke(User user, AnalysisContext context) {
// 添加到批次列表
batchList.add(user);
if (batchList.size() >= BATCH_SIZE) {
// 分批保存
saveBatch();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 处理剩余数据
if (!batchList.isEmpty()) {
saveBatch();
}
log.info("所有数据导入完成,成功:{}条", totalSuccess);
}
private void saveBatch() {
try {
userService.batchSave(batchList);
totalSuccess += batchList.size();
log.info("批量保存成功,本批次:{}条,累计:{}条", batchList.size(), totalSuccess);
} catch (Exception e) {
log.error("批量保存失败", e);
} finally {
batchList.clear();
}
}
}2. 大数据量导出优化
@GetMapping("/export-large")
public void exportLargeData(HttpServletResponse response) {
// 设置响应头
ExcelUtil.resetResponse("大数据导出", response);
try (ServletOutputStream os = response.getOutputStream()) {
ExcelWriter excelWriter = FastExcel.write(os, User.class)
.autoCloseStream(false)
.build();
WriteSheet writeSheet = FastExcel.writerSheet("用户数据").build();
int pageSize = 10000;
int pageNum = 1;
List<User> users;
do {
// 分页查询数据
users = userService.getUsers(pageNum, pageSize);
if (!users.isEmpty()) {
// 分批写入
excelWriter.write(users, writeSheet);
}
pageNum++;
// 清理引用,帮助GC
users = null;
} while (users == null || users.size() == pageSize);
excelWriter.finish();
} catch (IOException e) {
log.error("导出失败", e);
}
}3. 内存优化技巧
// 1. 使用SXSSF模式(流式写入)
// FastExcel默认支持,无需额外配置
// 2. 限制下拉选项行数
// ExcelDownHandler中只校验前1000行
CellRangeAddressList addressList = new CellRangeAddressList(1, 1000, celIndex, celIndex);
// 3. 避免一次性加载全部数据
// 使用分页查询 + 流式写入
// 4. 及时关闭流
try (InputStream is = file.getInputStream()) {
ExcelResult<User> result = ExcelUtil.importExcel(is, User.class, true);
// ...
}
// 5. 使用对象池或复用实例
// FastExcel内部已优化,无需手动处理常见问题
Q1: 日期格式问题
public class User {
// 使用FastExcel的DateTimeFormat注解
@DateTimeFormat("yyyy-MM-dd")
@ExcelProperty("入职日期")
private LocalDate joinDate;
// 或使用Java自带的注解配合转换器
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ExcelProperty("创建时间")
private LocalDateTime createTime;
}Q2: 数字精度问题
长整型数据自动使用ExcelBigNumberConvert转换器处理,无需额外配置。
public class Order {
@ExcelProperty("订单号")
private Long orderNo; // 超过15位自动转字符串
}Q3: 中文乱码问题
确保响应头设置正确:
// ExcelUtil.resetResponse方法已处理
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("UTF-8");
String fileName = URLEncoder.encode(filename, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");Q4: 模板路径问题
模板文件放在src/main/resources目录下:
// 正确的路径写法
ExcelUtil.exportTemplate(data, "报表", "templates/report.xlsx", response);
// 模板文件位置
// src/main/resources/templates/report.xlsxQ5: 字典转换不生效
确保:
- DictService已正确配置且Spring容器中存在对应的Bean
- 字典类型(dictType)在系统中存在
- 字典数据已正确配置
// 检查字典服务是否正常
DictService dictService = SpringUtils.getBean(DictService.class);
String label = dictService.getDictLabel("sys_user_gender", "0", ",");Q6: 合并单元格不生效
检查:
- 数据是否按合并字段排序
- mergeBy属性是否正确配置
- 导出时是否启用了合并参数
// 数据必须按合并字段排序
List<DepartmentUser> sortedList = deptUsers.stream()
.sorted(Comparator.comparing(DepartmentUser::getDepartment)
.thenComparing(DepartmentUser::getTeam))
.collect(Collectors.toList());
// 导出时启用合并
ExcelUtil.exportExcel(sortedList, "部门用户", DepartmentUser.class, true, response);Q7: 下拉选项过多导致Excel打开慢
ExcelDownHandler已自动处理:
- 选项数量 ≤ 20个:使用简单下拉
- 选项数量 > 20个:使用额外Sheet存储
如果仍然很慢,可以限制选项数量或使用数据校验提示替代下拉。
最佳实践
1. 实体类设计原则
@Data
public class UserExcelVo {
// 1. 使用明确的类型,避免Object
@ExcelProperty("用户ID")
private Long userId;
// 2. 字符串类型字段添加长度校验
@Length(max = 50, message = "姓名长度不能超过50")
@ExcelProperty("姓名")
private String name;
// 3. 字典字段使用@ExcelDictFormat
@ExcelDictFormat(dictType = "sys_user_gender")
@ExcelProperty("性别")
private String gender;
// 4. 枚举字段使用@ExcelEnumFormat
@ExcelEnumFormat(enumClass = UserStatus.class)
@ExcelProperty("状态")
private Integer status;
// 5. 必填字段添加@ExcelRequired和校验注解
@ExcelRequired
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
@ExcelProperty("邮箱")
private String email;
// 6. 添加批注说明复杂字段
@ExcelNotation("格式:yyyy-MM-dd")
@ExcelProperty("出生日期")
private LocalDate birthDate;
}2. 异常处理规范
// 1. 使用统一的结果封装
@Data
public class ImportResult {
private int successCount;
private int errorCount;
private List<String> errors;
}
// 2. 分级处理错误
public R<ImportResult> importUsers(MultipartFile file) {
// 文件级校验
if (file.isEmpty()) {
return R.fail("文件不能为空");
}
// 数据级校验(自动)
ExcelResult<UserImportVo> result = ExcelUtil.importExcel(...);
// 业务级校验
List<UserImportVo> validList = result.getList().stream()
.filter(this::validateBusiness)
.collect(Collectors.toList());
// 返回详细结果
return R.ok(buildImportResult(result, validList));
}3. 性能优化策略
// 1. 大数据量分批处理
// 导入:使用自定义监听器分批保存
// 导出:使用分页查询+流式写入
// 2. 合理设置批次大小
private static final int IMPORT_BATCH_SIZE = 1000; // 导入批次
private static final int EXPORT_PAGE_SIZE = 10000; // 导出分页
// 3. 避免复杂计算
// 将复杂的格式转换放在数据库查询层面
// 避免在导出时进行大量的数据转换
// 4. 使用缓存
// 字典数据使用Redis缓存
// 下拉选项数据预加载4. 模板设计规范
# 模板占位符规范
{.field} # 单条数据字段
{key.field} # 多数据源字段
{list.field} # 列表数据字段
# 模板文件组织
resources/
└── templates/
├── user-report.xlsx # 用户报表模板
├── order-export.xlsx # 订单导出模板
└── dept-summary.xlsx # 部门汇总模板总结
Excel处理模块提供了完整的Excel导入导出解决方案:
- 导入功能:支持同步/异步导入、数据校验、错误收集、自定义监听器
- 导出功能:支持基础导出、单元格合并、下拉选项、模板导出
- 数据转换:字典转换、枚举转换、大数字处理
- 界面增强:必填标识、批注说明、级联下拉
- 扩展能力:自定义监听器、转换器、选项提供者
通过合理使用各种注解和配置,可以满足绝大多数业务场景的需求。模块设计注重性能和易用性,支持大数据量处理,是企业级应用的理想选择。
