|
|
package com.yoho.unions.common.utils;
|
|
|
|
|
|
import com.alibaba.fastjson.JSON;
|
|
|
import com.yoho.unions.common.annotation.BatchExportField;
|
|
|
import com.yoho.unions.common.annotation.BatchImportField;
|
|
|
import com.yoho.unions.common.annotation.ImportRegexp;
|
|
|
import org.apache.commons.collections.CollectionUtils;
|
|
|
import org.apache.commons.io.IOUtils;
|
|
|
import org.apache.commons.lang.StringUtils;
|
|
|
import org.apache.poi.hssf.usermodel.HSSFCell;
|
|
|
import org.apache.poi.hssf.usermodel.HSSFDateUtil;
|
|
|
import org.apache.poi.ss.usermodel.Cell;
|
|
|
import org.apache.poi.ss.usermodel.Row;
|
|
|
import org.apache.poi.ss.usermodel.Sheet;
|
|
|
import org.apache.poi.ss.usermodel.Workbook;
|
|
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
import org.springframework.beans.BeanUtils;
|
|
|
import org.springframework.beans.BeanWrapper;
|
|
|
import org.springframework.beans.BeanWrapperImpl;
|
|
|
|
|
|
import java.io.*;
|
|
|
import java.lang.reflect.Field;
|
|
|
import java.text.NumberFormat;
|
|
|
import java.text.SimpleDateFormat;
|
|
|
import java.util.*;
|
|
|
import java.util.regex.Pattern;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
import static com.yoho.unions.common.utils.YHPreconditions.checkArgument;
|
|
|
|
|
|
|
|
|
/**
|
|
|
* 解析上传的Excel xlsx文件,返回解析后的数据数组
|
|
|
* !!!重要:批量导入时,第一列数据必须为必填
|
|
|
* Created by xueyin on 2016/1/29.
|
|
|
* Modified by xuhongyun on 16/0308 批量导入时,需要判断表头数与VO字段是否匹配
|
|
|
*/
|
|
|
public class XlsxUtils {
|
|
|
private static final Logger logger = LoggerFactory.getLogger(XlsxUtils.class);
|
|
|
|
|
|
/**
|
|
|
* 从文件中解析出Excel数据并转换为指定的bean
|
|
|
* 表格字段与Bean的映射由BatchOperateField注解指明
|
|
|
*
|
|
|
* @param file excel文件
|
|
|
* @param beanClass 需要转换成的bean类
|
|
|
* @param <T>
|
|
|
* @return
|
|
|
* @throws IOException
|
|
|
*/
|
|
|
@SuppressWarnings("resource")
|
|
|
static public <T> List<T> parse(File file, Class<T> beanClass) throws IOException {
|
|
|
logger.info("parse excel sheet to the specified bean. fileName:{}, beanName:{}", file.exists()?file.getName():null,beanClass!=null?beanClass.getName():null);
|
|
|
List<T> beanList = new LinkedList<>();
|
|
|
Workbook wb = new XSSFWorkbook(new FileInputStream(file));
|
|
|
Sheet sheet = wb.getSheetAt(0);
|
|
|
if (sheet == null) {
|
|
|
logger.warn("parse excel file:{} fail, invalid file format.", file.getName());
|
|
|
throw new IOException("解析sheet失败,请检查文件格式.");
|
|
|
}
|
|
|
|
|
|
int size = sheet.getPhysicalNumberOfRows();
|
|
|
if (size <=1) {
|
|
|
// 至少有两行数据才认为文件是有效的,一个Title,一条数据
|
|
|
logger.warn("parse excel file:{} to bean:{} fail, empty record set.", file.getName(), beanClass.getName());
|
|
|
throw new IOException("导入文件内容为空");
|
|
|
}
|
|
|
|
|
|
// 1 从bean中取出关联的表头字段数
|
|
|
int annotCount = 0;
|
|
|
Field[] fields = beanClass.getDeclaredFields();
|
|
|
for (Field field : fields) {
|
|
|
BatchImportField bif = field.getAnnotation(BatchImportField.class);
|
|
|
if (bif == null) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
annotCount++;
|
|
|
}
|
|
|
logger.info("parse excel file:{} to {}, header field count:{}.", file.getName(), beanClass.getName(), annotCount);
|
|
|
|
|
|
// 2 xlsx第一行默认为表头, 检查表头是否匹配
|
|
|
List<String> head = getRowContent(sheet, 0, annotCount);
|
|
|
|
|
|
if(null == head) {
|
|
|
throw new IOException("第1行解析失败!");
|
|
|
}
|
|
|
|
|
|
logger.warn("batch getRowContent is {}", JSON.toJSONString(head));
|
|
|
if (head.size() != annotCount) {
|
|
|
logger.warn("parse excel file:{} to bean:{} fail, uncompi.", file.getName(), beanClass.getName());
|
|
|
throw new IOException("解析导入文件失败,表头字段数要求为:" + annotCount + ",实际为:" + head.size());
|
|
|
}
|
|
|
|
|
|
// 汇总所有行解析错误的信息
|
|
|
final StringBuilder error = new StringBuilder();
|
|
|
|
|
|
// 将每行数据转换为bean
|
|
|
for (int i = 1; i <size; i++) {
|
|
|
// 解析内容
|
|
|
List content = getRowContent(sheet, i, annotCount);
|
|
|
if (CollectionUtils.isEmpty(content)) {
|
|
|
// 空行需要跳过
|
|
|
logger.warn("batch import file:{} to bean:{}, line {} content is null", file.getName(), beanClass.getName(), i);
|
|
|
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
if (i >= 100000) { // 防止运营模板表格里面出现N行空行记录
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
if (content.size() != annotCount) {
|
|
|
throw new IOException(String.format("第%d行数据个数不匹配,表头:%d 当前行:%d",
|
|
|
i + 1, annotCount, content.size()));
|
|
|
}
|
|
|
|
|
|
//System.out.println(content.stream().collect(Collectors.joining(",")));
|
|
|
logger.debug("parse excel file:{} to bean:{}, line:{}, data[{}].", file.getName(), beanClass.getName(),
|
|
|
i + 1, content.stream().collect(Collectors.joining(",")));
|
|
|
|
|
|
// 将map值映射到vo
|
|
|
try {
|
|
|
beanList.add(batchData2Bean(content, beanClass));
|
|
|
} catch (Exception e) {
|
|
|
logger.warn("parse excel file:{} to bean:{} fail, uncompi.", file.getName(), beanClass.getName());
|
|
|
error.append("第" + (i + 1) + "行解析失败:" + e.getMessage());
|
|
|
error.append(IOUtils.LINE_SEPARATOR);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
final String parseErrMsg = error.toString();
|
|
|
if(StringUtils.isNotEmpty(parseErrMsg)) {
|
|
|
throw new IOException(parseErrMsg);
|
|
|
}
|
|
|
|
|
|
return beanList;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 将指定数据写入Excel表,表头数据从Bean的BatchOperateField注解中取出
|
|
|
*
|
|
|
* @param saveFile
|
|
|
* @param beanList
|
|
|
* @param beanClass
|
|
|
* @return
|
|
|
* @throws IOException
|
|
|
*/
|
|
|
|
|
|
// @SuppressWarnings({ "unchecked", "resource" })
|
|
|
static public <T> void write(File saveFile, List<T> beanList, Class<T> beanClass) throws IOException {
|
|
|
logger.info("write the specified data to excel sheet. fileName:{}, beanList size:{}, beanName:{}", saveFile.exists()?saveFile.getName():null,
|
|
|
CollectionUtils.isNotEmpty(beanList)?beanList.size():0,beanClass!=null?beanClass.getName():null);
|
|
|
Workbook wb = createWookbook(beanList, beanClass);
|
|
|
// 将excel写入到文件
|
|
|
OutputStream os = new FileOutputStream(saveFile);
|
|
|
wb.write(os);
|
|
|
os.close();
|
|
|
}
|
|
|
|
|
|
static public <T> Workbook createWookbook(List<T> beanList, Class<T> beanClass) throws IOException {
|
|
|
Workbook wb = new XSSFWorkbook();
|
|
|
Sheet sheet = wb.createSheet("ExportSheet1");
|
|
|
if (CollectionUtils.isEmpty(beanList)) {
|
|
|
return wb;
|
|
|
}
|
|
|
int rowIndex = 0;
|
|
|
for (T bean : beanList) {
|
|
|
// 将bean转换为带表头的表数据,根据BatchOperationField字段进行映射
|
|
|
Map<String, String> rowData = null;
|
|
|
try {
|
|
|
rowData = batchBean2Map(bean, beanClass);
|
|
|
} catch (Exception e) {
|
|
|
throw new IOException("第" + (rowIndex + 1) + "行:" + e.getMessage(), e);
|
|
|
}
|
|
|
Row row = sheet.createRow(rowIndex);
|
|
|
// 写第一行数据时必须先插入标题
|
|
|
if (rowIndex == 0) {
|
|
|
int cellIndex = 0;
|
|
|
for (String key : rowData.keySet()) {
|
|
|
Cell cell = row.createCell(cellIndex++);
|
|
|
cell.setCellValue(key);
|
|
|
}
|
|
|
// 标题头写完,新创建一行,用于后面写入数据
|
|
|
row = sheet.createRow(++rowIndex);
|
|
|
}
|
|
|
rowIndex++;
|
|
|
// 写入数据,每个bean取到的field顺序是一致的,这里不需要与标题去做匹配(使用 values 也不需要匹配)
|
|
|
int cellIndex = 0;
|
|
|
for (String value: rowData.values()) {
|
|
|
Cell cell = row.createCell(cellIndex++);
|
|
|
if(isNumber(value)){
|
|
|
cell.setCellType(HSSFCell.CELL_TYPE_NUMERIC);
|
|
|
cell.setCellValue(Double.parseDouble(value.toString()));
|
|
|
}else{
|
|
|
cell.setCellValue(value);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return wb;
|
|
|
}
|
|
|
|
|
|
public static boolean isNumber(String str) {
|
|
|
if(StringUtils.isEmpty(str)) return false;
|
|
|
return str.matches("\\d+\\.\\d+$");
|
|
|
}
|
|
|
|
|
|
|
|
|
/**
|
|
|
* 将上传数据的单条map数据转为bean
|
|
|
*
|
|
|
* @param data CSV或XLSX解析出来的单条数据,顺序与表格顺序一致
|
|
|
* @param beanClass 需要转换的bean类
|
|
|
* @return 实例化出来的Bean类
|
|
|
* @throws Exception 直接抛出公共异常,在消息中指明详细错误
|
|
|
*/
|
|
|
@SuppressWarnings("rawtypes")
|
|
|
public static <T> T batchData2Bean(List data, Class<T> beanClass) throws Exception {
|
|
|
logger.info("batchData2Bean parse the data to bean. data:{}, beanName:{}",
|
|
|
CollectionUtils.isNotEmpty(data)?data.size():0,beanClass!=null?beanClass.getName():null);
|
|
|
T bean = BeanUtils.instantiate(beanClass);
|
|
|
BeanWrapper beanWrapper = new BeanWrapperImpl(bean);
|
|
|
|
|
|
// 汇总每一行解析出错的信息
|
|
|
final StringBuilder sb = new StringBuilder();
|
|
|
|
|
|
for (Field field : beanClass.getDeclaredFields()) {
|
|
|
field.setAccessible(true);
|
|
|
// 所有需要映射的字段都必须有MappedImportField注解
|
|
|
BatchImportField mif = field.getAnnotation(BatchImportField.class);
|
|
|
if (mif == null) {
|
|
|
logger.info("import class" + beanClass.getName() + "have non-import-annotationed field:" + field.getName());
|
|
|
continue;
|
|
|
}
|
|
|
// 注解的列序号超出数据大小
|
|
|
if (mif.index() > data.size()) {
|
|
|
throw new Exception("第" + mif.index() + "列在导入文件中无法找到");
|
|
|
}
|
|
|
String value = (String)data.get(mif.index());
|
|
|
try {
|
|
|
ImportRegexp regexp = field.getAnnotation(ImportRegexp.class);
|
|
|
if (regexp != null && !StringUtils.isBlank(regexp.value())) {
|
|
|
// 判断满不满足正则表达式
|
|
|
checkArgument(Pattern.compile(regexp.value()).matcher(value).matches());
|
|
|
}
|
|
|
if (field.getType() == int.class || field.getType() == Integer.class) {
|
|
|
// 从Excel中取出的数值类型很大可能为0.0格式,向int转换时会异常
|
|
|
beanWrapper.setPropertyValue(field.getName(), Double.valueOf(value).intValue());
|
|
|
} else if (field.getType() == byte.class || field.getType() == Byte.class) {
|
|
|
// 从Excel中取出的数值类型很大可能为0.0格式,向int转换时会异常
|
|
|
beanWrapper.setPropertyValue(field.getName(), Double.valueOf(value).byteValue());
|
|
|
} else if (field.getType() == short.class || field.getType() == Short.class) {
|
|
|
// 从Excel中取出的数值类型很大可能为0.0格式,向int转换时会异常
|
|
|
beanWrapper.setPropertyValue(field.getName(), Double.valueOf(value).shortValue());
|
|
|
} else {
|
|
|
beanWrapper.setPropertyValue(field.getName(), value);
|
|
|
}
|
|
|
} catch (Exception e) {
|
|
|
logger.error("第" + (mif.index() + 1) + "列数据格式不匹配.", e);
|
|
|
sb.append("第" + (mif.index() + 1) + "列数据格式不匹配,请检查。");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
final String parseErrMsg = sb.toString();
|
|
|
if(StringUtils.isNotEmpty(parseErrMsg)) {
|
|
|
throw new IOException(parseErrMsg);
|
|
|
}
|
|
|
|
|
|
return bean;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 将上传数据对应的bean转换为MAP,便于Excel导出
|
|
|
*
|
|
|
* @param bean CSV或XLSX解析出来的bean
|
|
|
* @param beanClass bean类形
|
|
|
* @return 实例化出来的Bean类
|
|
|
* @throws Exception 直接抛出公共异常,在消息中指明详细错误
|
|
|
*/
|
|
|
@SuppressWarnings("rawtypes")
|
|
|
public static <T> Map batchBean2Map(Object bean, Class<T> beanClass) throws Exception {
|
|
|
logger.info("batchBean2Map parse the bean to map. bean:{}, beanName:{}",bean!=null?bean:null ,beanClass!=null?beanClass.getName():null);
|
|
|
Map<String, String> map = new LinkedHashMap<>();
|
|
|
BeanWrapper beanWrapper = new BeanWrapperImpl(bean);
|
|
|
for (Field field : beanClass.getDeclaredFields()) {
|
|
|
field.setAccessible(true);
|
|
|
// 所有需要映射的字段都必须有MappedImportField注解
|
|
|
BatchExportField mif = field.getAnnotation(BatchExportField.class);
|
|
|
if (mif == null) {
|
|
|
logger.info("export class" + beanClass.getName() + "have non-export-annotationed field:" + field.getName());
|
|
|
continue;
|
|
|
}
|
|
|
Object propertyValue = beanWrapper.getPropertyValue(field.getName());
|
|
|
if (null != propertyValue) {
|
|
|
map.put(mif.name(), propertyValue.toString());
|
|
|
} else {
|
|
|
map.put(mif.name(), null);
|
|
|
}
|
|
|
}
|
|
|
return map;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 读取Excel单行内容,包含表头也通过此方法读取
|
|
|
* @param sheet
|
|
|
* @param index 当前需要解析的行序号
|
|
|
* @param acceptCnt 单行内容最大可接入的数据个数, 与表头的个数相同
|
|
|
* @return List<String> 内容
|
|
|
*/
|
|
|
public static List<String> getRowContent(Sheet sheet, int index, int acceptCnt) throws IOException {
|
|
|
Row row = sheet.getRow(index);
|
|
|
// 此处改为不直接抛出异常,对于标题行,直接以异常处理;对于内容行,直接忽略,以解决excel复制导致的空行问题(问题单: YBW-8019)
|
|
|
if(null == row) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
List<String> content = new LinkedList<>();
|
|
|
int rowCnt = acceptCnt;
|
|
|
// 行数据少时, 单独判断
|
|
|
if (row.getLastCellNum() < acceptCnt){
|
|
|
// for (int n = 0; n < row.getLastCellNum(); n++) {
|
|
|
// // 存在非空数据,需要报错
|
|
|
// if (! StringUtils.isEmpty(getCellFormatValue(row.getCell(n)))) {
|
|
|
// logger.warn("export getRowContent field is {} row.getCell(n) is {}", index + 1, row.getCell(n));
|
|
|
// throw new IOException(String.format("第%d行数据解析失败, 数据个数与表头不匹配.", index + 1));
|
|
|
// }
|
|
|
// }
|
|
|
//
|
|
|
// // 走到这里说明有一个空行,需要兼容,直接返回空数据,调用的地方进行兼容
|
|
|
// return content;
|
|
|
// refactor: 对最末行的非必填字段做兼容
|
|
|
rowCnt = row.getLastCellNum();
|
|
|
}
|
|
|
|
|
|
// 行数据多时, 如果多出的是空数据则需要兼容,否则就要报错
|
|
|
// refactor: 不再作如此严格校验,只取需要的字段数即可
|
|
|
// if (row.getLastCellNum() > acceptCnt) {
|
|
|
// for (int n = acceptCnt - 1; n < row.getLastCellNum(); n++) {
|
|
|
// if (! StringUtils.isEmpty(getCellFormatValue(row.getCell(n)))) {
|
|
|
// logger.warn("export getRowContent getCellFormatValue(row.getCell(n):{} row.getCell(n){}" , getCellFormatValue(row.getCell(n)), row.getCell(n));
|
|
|
// throw new IOException(String.format("第%d行数据解析失败, 数据个数与表头不匹配.", index + 1));
|
|
|
// }
|
|
|
// }
|
|
|
// }
|
|
|
// 到这里,不管行数据个数到底是多少,只取表头个数的数据解析后返回
|
|
|
for (int i = 0; i < rowCnt; i++) {
|
|
|
String value = getCellFormatValue(row.getCell((short) i));
|
|
|
|
|
|
// fixbug: 仍然要进行判断空行处理
|
|
|
if (StringUtils.isEmpty(value.trim()) && content.isEmpty()) {
|
|
|
content.add("");
|
|
|
} else {
|
|
|
content.add(value);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 跳过空行
|
|
|
if (content.size() == 0) {
|
|
|
return content;
|
|
|
}
|
|
|
// 补齐数据不齐的情况
|
|
|
while (content.size() < acceptCnt) {
|
|
|
content.add("");
|
|
|
}
|
|
|
|
|
|
return content;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 根据Cell类型获取数据
|
|
|
* @param cell
|
|
|
* @return
|
|
|
*/
|
|
|
private static String getCellFormatValue(Cell cell) {
|
|
|
if (cell == null) {
|
|
|
return "";
|
|
|
}
|
|
|
|
|
|
String cellvalue = "";
|
|
|
// 判断当前Cell的Type
|
|
|
switch (cell.getCellType()) {
|
|
|
// 如果当前Cell的Type为NUMERIC
|
|
|
case HSSFCell.CELL_TYPE_NUMERIC:
|
|
|
case HSSFCell.CELL_TYPE_FORMULA: {
|
|
|
// 判断当前的cell是否为Date
|
|
|
if (HSSFDateUtil.isCellDateFormatted(cell)) {
|
|
|
// 如果是Date类型则,转化为Data格式
|
|
|
Date date = cell.getDateCellValue();
|
|
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
cellvalue = sdf.format(date);
|
|
|
}
|
|
|
// 如果是纯数字
|
|
|
else {
|
|
|
// 取得当前Cell的数值
|
|
|
double value = cell.getNumericCellValue();
|
|
|
NumberFormat nf = NumberFormat.getInstance();
|
|
|
nf.setGroupingUsed(false);
|
|
|
nf.setMaximumFractionDigits(5);//最大小数位
|
|
|
cellvalue = String.valueOf(nf.format(value));
|
|
|
}
|
|
|
break;
|
|
|
}
|
|
|
// 如果当前Cell的Type为STRIN
|
|
|
case HSSFCell.CELL_TYPE_STRING:
|
|
|
// 取得当前的Cell字符串
|
|
|
cellvalue = cell.getRichStringCellValue().getString();
|
|
|
break;
|
|
|
// 默认的Cell值
|
|
|
default:
|
|
|
cellvalue = " ";
|
|
|
}
|
|
|
|
|
|
return cellvalue.trim();
|
|
|
}
|
|
|
|
|
|
} |
|
|
\ No newline at end of file |
...
|
...
|
|