ruoyi-vue-plus-表格操作
# 表格操作
官方文档 : https://easyexcel.opensource.alibaba.com (opens new window)
涉及类
类 | 说明 |
---|---|
ExcelUtil | 通用工具类 |
CellMerge -> CellMergeStrategy | 单元格合并注解 |
ExcelDictFormat -> ExcelDictConvert | 字典类格式化注解 |
ExcelEnumFormat -> ExcelEnumConvert | 枚举类格式化注解 |
ExcelBigNumberConvert | 解决单元格整数转化 (防失真) |
ExcelListener | 表格监听 |
ExcelResult | 表格响应对象 |
# 应用流程
- 获取数据集合
- 集合对象类 含有
@ExcelIgnoreUnannotated
和@ExcelProperty
标识列属性名称 - 获取导出的文件输出流
- 导入数据至输输出流
- 导出...
提示
- 如果指定数据集合的类没有采用上方两个注解默认采用方法名称
- 文件输出流导出一定要关闭 , 否则无法访问
简单应用
@Test
void test() {
List<TestUser> userlist = new ArrayList<TestUser>() {{
add(new TestUser("张三", 18, "0", new Date()));
add(new TestUser("李四", 20, "1", new Date()));
add(new TestUser("王五", 21, "0", new Date()));
}};
// 获取文件流 输出
File file = new File("E:\\DOTO\\users.xlsx");
// 一定要关闭流 (以下使用方式会自动关闭)
try(BufferedOutputStream outputStream = FileUtil.getOutputStream(file)) {
ExcelUtil.exportExcel(userlist,"users", TestUser.class, outputStream);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
TestUser
类
@Data
@AllArgsConstructor
@NoArgsConstructor
@ExcelIgnoreUnannotated
public class TestUser {
@ExcelProperty(value = "姓名")
private String name;
@ExcelProperty(value = "年龄")
private Integer age;
@ExcelProperty(value = "性别")
private String sex;
@ExcelProperty(value = "生日")
private Date birthday;
}
# 字典枚举
字典枚举可以理解成表格中单元格的下拉框 , 指定单元格仅能选择下拉框中的内容 , 通过下拉框指定的常量数据进行锁定用户输入的值 , 避免输入下拉框以外的值 . 对于这些问题 , ruoyi 框架有以下两种解决方案
- 字典转换
- 枚举转换
主要原理是 根据类实现 Converter接口的 convertToJavaData()/convertToExcelData() 方法转化 , 以下采用若依是实现方案 , 以SysUser使用为例
# 字典转换
Bean读取到字典数据
@ExcelProperty(value = "用户性别", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "sys_user_sex")
private String sex;
@ExcelDictFormat注解的意图 , 标识指定字典的key , 通过key拿到字典响应的字典数据
字符串枚举读取字典数据
@ExcelProperty(value = "用户性别", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "0=男,1=女,2=未知", separator = ",")
private String sex;
字典格式化注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ExcelDictFormat {
/**
* 如果是字典类型,请设置字典的type值 (如: sys_user_sex)
*/
String dictType() default "";
/**
* 读取内容转表达式 (如: 0=男,1=女,2=未知)
*/
String readConverterExp() default "";
/**
* 分隔符,读取字符串组内容
*/
String separator() default StringUtils.SEPARATOR;
}
**ExcelDictConvert
通用字典格式转换实现类 **
点击展开
@Slf4j
public class ExcelDictConvert implements Converter<Object> {
@Override
public Class<Object> supportJavaTypeKey() {
return Object.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return null;
}
/**
* Excel 转 Java
* @param cellData Excel 单元格数据
* @param contentProperty 当前属性信息
* @param globalConfiguration 全局配置
* @return 转化结果对象
*/
@Override
public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
// 获取注解中的key
ExcelDictFormat anno = getAnnotation(contentProperty.getField());
String type = anno.dictType();
String label = cellData.getStringValue();
String value;
// 是否写有标识的key
if (StringUtils.isBlank(type)) {
// 没有key , 采用 readConverterExp , 文本形式解析枚举 , 以 "," 分割符 , 例如 "(如: 0=男,1=女,2=未知)"
value = ExcelUtil.reverseByExp(label, anno.readConverterExp(), anno.separator());
} else {
// 有key , 通过Bean获取
value = SpringUtils.getBean(DictService.class).getDictValue(type, label, anno.separator());
}
// 转化对应的属性类型
return Convert.convert(contentProperty.getField().getType(), value);
}
/**
* Java 转 Excel
* @param object 属性值
* @param contentProperty 当前属性信息
* @param globalConfiguration 全局配置
* @return 转化结果 , WriteCellData类封装
*/
@Override
public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
if (ObjectUtil.isNull(object)) {
return new WriteCellData<>("");
}
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);
}
/**
* 获取注解信息
*/
private ExcelDictFormat getAnnotation(Field field) {
return AnnotationUtil.getAnnotation(field, ExcelDictFormat.class);
}
}
# 枚举转换
采用status属性做示例
@ExcelProperty(value = "用户类型", index = 1, converter = ExcelEnumConvert.class)
@ExcelEnumFormat(enumClass = UserStatus.class, textField = "info")
private String userStatus;
UserStatus 枚举 只有两个属性 code 枚举常量值 , info 可查阅的值
枚举转化注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ExcelEnumFormat {
/**
* 字典枚举类型
*/
Class<? extends Enum<?>> enumClass();
/**
* 字典枚举类中对应的code属性名称,默认为code
*/
String codeField() default "code";
/**
* 字典枚举类中对应的text属性名称,默认为text
*/
String textField() default "text";
}
ExcelEnumConvert
通用枚举格式转换实现类
点击展开
@Slf4j
public class ExcelEnumConvert implements Converter<Object> {
@Override
public Class<Object> supportJavaTypeKey() {
return Object.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return null;
}
/**
* Excel 转 Java
* @param cellData Excel 单元格数据
* @param contentProperty 当前属性信息
* @param globalConfiguration 全局配置
* @return 转化结果对象
*/
@Override
public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
cellData.checkEmpty();
// Excel中填入的是枚举中指定的描述
Object textValue = null;
// 判断单元格类型
switch (cellData.getType()) {
case STRING:
case DIRECT_STRING:
case RICH_TEXT_STRING:
textValue = cellData.getStringValue();
break;
case NUMBER:
textValue = cellData.getNumberValue();
break;
case BOOLEAN:
textValue = cellData.getBooleanValue();
break;
default:
throw new IllegalArgumentException("单元格类型异常!");
}
// 如果是空值
if (ObjectUtil.isNull(textValue)) {
return null;
}
// 通过反射读取枚举信息
Map<Object, String> enumCodeToTextMap = beforeConvert(contentProperty);
// 从Java输出至Excel是code转text
// 因此从Excel转Java应该将text与code对调
Map<Object, Object> enumTextToCodeMap = new HashMap<>();
enumCodeToTextMap.forEach((key, value) -> enumTextToCodeMap.put(value, key));
// 应该从text值 -> code中查找
Object codeValue = enumTextToCodeMap.get(textValue);
return Convert.convert(contentProperty.getField().getType(), codeValue);
}
/**
* Java 转 Excel
* @param object 属性值
* @param contentProperty 当前属性信息
* @param globalConfiguration 全局配置
* @return 转化结果 , WriteCellData类封装
*/
@Override
public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
if (ObjectUtil.isNull(object)) {
return new WriteCellData<>("");
}
// 通过反射读取枚举信息
Map<Object, String> enumValueMap = beforeConvert(contentProperty);
String value = Convert.toStr(enumValueMap.get(object), "");
return new WriteCellData<>(value);
}
/**
* 获取枚举信息
*/
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) {
Object codeValue = ReflectUtils.invokeGetter(enumConstant, anno.codeField());
String textValue = ReflectUtils.invokeGetter(enumConstant, anno.textField());
enumValueMap.put(codeValue, textValue);
}
return enumValueMap;
}
private ExcelEnumFormat getAnnotation(Field field) {
return AnnotationUtil.getAnnotation(field, ExcelEnumFormat.class);
}
}
# 单元格合并
意图 : 导出对象时将列中的相同字段值进行合并
应用
- 对导出的对象指定字段属性加上
@CellMerge
注解 - 在工具类上的方法含有 布尔值
merge
参数 设为true实现
提示
CellMerge注解中 有 index 属性 , 该属性用于控制对第几列进行合并 (一般情况不用管)
# 表格监听
实现表监听 , 继承 AnalysisEventListener
类 和 直接实现 ExcelListener
接口 , 监听表格的目的
- 创建监听类 , 并且继承
AnalysisEventListener
类 和 直接实现ExcelListener
接口 - 根据以下解析器执行时段的方法进行处理逻辑
- 执行流读取表格 , ExcelUtil类的 *importExcel()*方法实现监听解析
默认解析监听器
点击展开
@Slf4j
@NoArgsConstructor
public class DefaultExcelListener<T> extends AnalysisEventListener<T> implements ExcelListener<T> {
/**
* 直接实现 ExcelListener , 虽然 AnalysisEventListener 也实现了 AnalysisEventListener
*/
/**
* 是否Validator检验,默认为是
*/
private Boolean isValidate = Boolean.TRUE;
/**
* excel 表头数据
*/
private Map<Integer, String> headMap;
/**
* 导入回执
*/
private ExcelResult<T> excelResult;
public DefaultExcelListener(boolean isValidate) {
this.excelResult = new DefaultExcelResult<>();
this.isValidate = isValidate;
}
/**
* 处理异常
*
* @param exception ExcelDataConvertException
* @param context Excel 上下文
*/
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
String errMsg = null;
// 转化解析异常
if (exception instanceof ExcelDataConvertException) {
// 如果是某一个单元格的转换异常 能获取到具体行号
ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException) exception;
Integer rowIndex = excelDataConvertException.getRowIndex();
Integer columnIndex = excelDataConvertException.getColumnIndex();
errMsg = StrUtil.format("第{}行-第{}列-表头{}: 解析异常<br/>",
rowIndex + 1, columnIndex + 1, headMap.get(columnIndex));
if (log.isDebugEnabled()) {
log.error(errMsg);
}
}
// 约束异常
if (exception instanceof ConstraintViolationException) {
ConstraintViolationException constraintViolationException = (ConstraintViolationException) exception;
Set<ConstraintViolation<?>> constraintViolations = constraintViolationException.getConstraintViolations();
String constraintViolationsMsg = StreamUtils.join(constraintViolations, ConstraintViolation::getMessage, ", ");
errMsg = StrUtil.format("第{}行数据校验异常: {}", context.readRowHolder().getRowIndex() + 1, constraintViolationsMsg);
if (log.isDebugEnabled()) {
log.error(errMsg);
}
}
excelResult.getErrorList().add(errMsg);
throw new ExcelAnalysisException(errMsg);
}
/**
* 解析表头数据
*/
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
this.headMap = headMap;
log.debug("解析到一条表头数据: {}", JsonUtils.toJsonString(headMap));
}
/**
* 解析行数据
*/
@Override
public void invoke(T data, AnalysisContext context) {
// 对象校验 , 注解校验
if (isValidate) {
ValidatorUtils.validate(data);
}
// 添加至结果集
excelResult.getList().add(data);
}
/**
* 解析完执行
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
log.debug("所有数据解析完成!");
}
/**
* 读取监听到的数据
*/
@Override
public ExcelResult<T> getExcelResult() {
return excelResult;
}
}