1) Pom文件引入Easy Excel
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>
2)实现AnalysisEventListener
public class EasyExcelListener extends AnalysisEventListener<Object> {
// 创建list集合封装最终的数据
private List<Object> list = new ArrayList<>();
// sheet页索引
private int sheetNo = 0;
@Override
public void invoke(Object t, AnalysisContext context) {
// 读取excle内容
int currentSheetNo = context.readSheetHolder().getSheetNo();
if (currentSheetNo != sheetNo) {
// 如果不根据sheet页索引更新状态重新创建list,list会反复添加前面的sheet页对象值
list = new ArrayList<>();
sheetNo = currentSheetNo;
}
list.add(t);
}
// 读取excel表头信息
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
}
// 读取完成后执行
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
/**
* 获取表格内容(简单excel读取可用该方法)
*
* @param obj 需要转化的实体
* @param
* @return
*/
public <T> List<T> getList(Class<T> obj) {
String jsonObj = JSONUtil.toJsonStr(list);
return JSONUtil.toList(jsonObj, obj);
}
/**
* 将表格转化为map集合(复杂excel读取用此方法)
*
* @return map集合
*/
public List<LinkedHashMap> getListMap() {
String jsonObj = JSONUtil.toJsonStr(list);
return JSONUtil.toList(jsonObj, LinkedHashMap.class);
}
}
ps: 为什么用LinkHashMap接收excel表格内容?
读取文件内容时,在不确定接收实体的情况下,对象都是以LinkedHashMap进行存储的。图中可以看到,每个map的key,value是以列索引以及对应的行值进行存储。
3)Excel工具类
@Slf4j
public class EasyExcelUtil {
private EasyExcelUtil() {
}
/**
* 根据easyexcel注解给指定实体赋值
*
* @param objects 读取的表格内容
* @param clazz 需转化的实体
* @param 实体
* @return 需转化的提示集合
*/
public static <T> List<T> convertList(List<LinkedHashMap> objects, Class<T> clazz) {
List<T> results = new ArrayList<>(objects.size());
try {
Map<String, Field> objIndex = new HashMap<>();
// 获取转化实体字段信息集合
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
// 根据实体上Easy Excel的ExcelProperty注解中的索引值对应excel读取数据的值
int index = field.getAnnotation(ExcelProperty.class).index();
// 设置字段可编辑
field.setAccessible(true);
objIndex.put(String.valueOf(index), field);
}
T obj = null;
for (LinkedHashMap o : objects) {
obj = clazz.newInstance();
for (Object key : o.keySet()) {
// 如果表格索引与字段注解指定索引一样则赋值
if (objIndex.containsKey(key)) {
Object object = o.get(key);
Object value = null;
Field field = objIndex.get(key);
if (ObjectUtil.isEmpty(object)) {
continue;
}
Class<?> type = field.getType();
String replace = object.toString();
// 有特殊需要处理的字段类型则在此进行处理
if (type == BigDecimal.class) {
value = "--".equals(replace) ? null : new BigDecimal(replace.replace(",", ""));
} else if (type == Integer.class) {
// String强转Integer会报错,所以需要单独进行转化
value = "--".equals(replace) ? null : Integer.valueOf(replace.replace(",", ""));
} else {
value = object;
}
field.set(obj, value);
}
}
results.add(obj);
}
} catch (Exception e) {
log.error("字段解析失败", e);
Asserts.fail("字段解析失败:" + e.getMessage());
}
return results;
}
}
参考实体:
@ExcelProperty注解必须有,且对应到字段值对应的列数。如果不确定列数可以先读取linkedlist 看看对应的索引值
@Data
@ApiModel("其他资金明细excel实体")
public class OtherFundDetailsExcelDTO {
@ApiModelProperty(value = "发生日期")
@ExcelProperty(value = "发生日期", index = 0)
private String happenDate;
@ApiModelProperty(value = "交易所")
@ExcelProperty(value = "交易所", index = 2)
private String exchange;
@ApiModelProperty(value = "类型")
@ExcelProperty(value = "类型", index = 4)
private String type;
@ApiModelProperty(value = "金额")
@ExcelProperty(value = "金额", index = 6)
private BigDecimal money;
@ApiModelProperty(value = "备注")
@ExcelProperty(value = "备注", index = 8)
private String remark;
}
1)Controller获取文件内容
@ApiOperation("导入")
@PostMapping(value = "/excel/upload")
public void upload(@ApiParam(name = "file", value = "file", required = true) @RequestParam(value = "file") MultipartFile file, HttpServletResponse response) throws IOException {
// step1. 读取excel内容
EasyExcelListener easyExcelListener = new EasyExcelListener();
ExcelReaderBuilder read = EasyExcelFactory.read(file.getInputStream(), easyExcelListener);
ExcelReader excelReader = read.build();
// step2. 获取各个sheet页信息
List<ReadSheet> sheets = excelReader.excelExecutor().sheetList();
// step3. 获取各个Shhet页表格内容存于map
Map<Integer, List<LinkedHashMap>> sheetInfos = new HashMap<>(sheets.size());
for (ReadSheet sheet : sheets) {
Integer sheetNo = sheet.getSheetNo();
excelReader.read(sheet);
sheetInfos.put(sheetNo, easyExcelListener.getListMap());
}
iSettlementMothReportService.saveExcelInfo(sheetInfos);
}
2) Service将读取内容转化为实体类(此代码仅提供两种excel表格读取方式,横向数据读取以及纵向集合读取)
public void saveExcelInfo(Map<Integer, List<LinkedHashMap>> sheetInfos) {
SettlementMothReportSaveDTO settlementMothReportSaveDTO = new SettlementMothReportSaveDTO();
for (Integer sheetNo : sheetInfos.keySet()) {
List<LinkedHashMap> maps = sheetInfos.get(sheetNo);
// 不同sheet页数据处理方式不同
switch (sheetNo) {
case 0:
settlementMonthlyReport(maps, settlementMothReportSaveDTO);
break;
case 1:
varietyCollects(maps, settlementMothReportSaveDTO);
break;
case 2:
holdingsSubsidiary(maps, settlementMothReportSaveDTO);
break;
case 3:
dealDetail(maps, settlementMothReportSaveDTO);
break;
default:
break;
}
}
iBzjBaseInfoService.saveSettlementMothReport(settlementMothReportSaveDTO);
}
private void settlementMonthlyReport(List<LinkedHashMap> maps, SettlementMothReportSaveDTO settlementMothReportSaveDTO) {
// 基本资料
BzjBaseInfo baseInfo = new BzjBaseInfo();
// 期货期权账户出入金明细
List<LinkedHashMap> optionAccountsDetails = new ArrayList<>();
// 是否基本资料
boolean whetherBaseInfo = true;
// 是否期货期权账户出入金明细
boolean whetherOptionAccountsDetails = false;
for (LinkedHashMap map : maps) {
if (whetherBaseInfo) {
// 基本资料
whetherBaseInfo = dealBaseInfo(map, baseInfo);
if (!whetherBaseInfo) {
continue;
}
}
if (map.containsValue(OptionAccountsDetailsConstant.OPTION_ACCOUNTS_DETAILS)) {
whetherOptionAccountsDetails = true;
}
if (!whetherBaseInfo && whetherOptionAccountsDetails) {
// 期货期权账户出入金明细
whetherOptionAccountsDetails = dealOptionAccountsDetails(map, optionAccountsDetails);
if (!whetherOptionsFuturesFunds) {
continue;
}
}
}
if (!optionAccountsDetails.isEmpty()) {
List<FuturesOptionAccountsDetailsExcelDTO> dtos = EasyExcelUtil.convertList(optionAccountsDetails, FuturesOptionAccountsDetailsExcelDTO.class);
settlementMothReportSaveDTO.setOptionAccountsDetails(dtos);
}
settlementMothReportSaveDTO.setBzjBaseInfo(baseInfo);
}
ps:为什么要用模块标题作为标识而不是索引?
因为以模块标题作为标识,如果上一个模块出现增删数据时,不影响本模块的读取。如果直接用表格行索引,那么表格内容出现变更时就无法灵活读取数据。
/**
* 处理基础资料
*
* @param map excel表格信息
* @param baseInfo 基础资料信息
* @return 是否基本资料
*/
private boolean dealBaseInfo(LinkedHashMap map, BzjBaseInfo baseInfo) {
// 是否基础信息内容
if (map.containsValue(BaseInfoConstant.BASE_INFO)) {
return true;
}
// 根据表格内容分析,对象值在2,7列。合并单元格在读取之后会将值存在合并的第一列索引下,如合并A-C列,那A列会存储字段值,B、C列值为空。具体内容可见上面读取excel截图
Object obj = map.get("2");
String str = null;
if (ObjectUtil.isNotEmpty(obj)) {
str = obj.toString();
}
Object object = map.get("7");
String value = null;
if (ObjectUtil.isNotEmpty(object)) {
value = object.toString();
}
// 通过本行的第一个字段名,获取本行的所有字段信息(后面的判断均是如此,常量类对应了各个字段名)
if (map.containsValue("客户期货期权内部资金账户")) {
baseInfo.setCusFuturesInnerFundAccount(str);
baseInfo.setTransactionMonth(value);
}
if (map.containsValue(BaseInfoConstant.CUSTOMER_NAME)) {
baseInfo.setCustomerName(str);
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
baseInfo.setQueryTime(LocalDateTime.parse(value, fmt));
}
if (map.containsValue(BaseInfoConstant.FUTURES_COMPANY_NAME)) {
baseInfo.setFuturesCompanyName(str);
baseInfo.setCusSecuritiesSpotInnnerFundAccount(value);
return false;
}
return true;
}
/**
* 处理期货期权账户出入金明细
*
* @param map excel表格信息
* @param optionAccountsDetails 账户出入金明细Excel集合
* @return 是否期货期权账户出入金明细
*/
private Boolean dealOptionAccountsDetails(LinkedHashMap map, List<LinkedHashMap> optionAccountsDetails) {
// 确定模块结束标识
if (map.containsValue("合计")) {
return false;
}
// 模块开始标识为模块标题以及模块第一行字段名,所以读取数据时要排除这两行
if (!map.containsValue("发生日期") && !map.containsValue("期货期权账户出入金明细")) {
optionAccountsDetails.add(map);
}
return true;
}
PS:为什么确定模块开始标识和结束标识?
确定了开始和结束标识相当于直接将读取数据锁死,只读取开始和结束中间的数据,避免数据误读!
以上是Easy Excel读取复杂表格的代码分析,请多指教!
晚安,玛卡巴卡~