Authored by LiQZ

Excel 导入导出功能提交

... ... @@ -54,6 +54,25 @@
<artifactId>yoho-core-rest-client-simple</artifactId>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.13</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.13</version>
</dependency>
<dependency>
<groupId>com.yoho.service.model</groupId>
<artifactId>promotion-service-model</artifactId>
</dependency>
... ...
package com.yoho.unions.common.annotation;
import java.lang.annotation.*;
/**
* 用于导出Excel时表头字段名称
* Created by xueyin on 2016/2/3.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface BatchExportField {
/**
* 标明导出数据时,数据表头名称
* @return
*/
String name();
}
... ...
package com.yoho.unions.common.annotation;
import java.lang.annotation.*;
/**
* 用于标注VO字段,标识该Field映射批量导入Excel中的哪个字段
* Created by xueyin on 2016/1/31.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface BatchImportField {
/**
* 标明导入数据时,映射到第几列
* @return
*/
int index();
}
... ...
package com.yoho.unions.common.annotation;
import java.lang.annotation.*;
/**
* 对 String 类型的,导入做正则表达式验证
* Created by LiQZ on 2016/12/15.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface ImportRegexp {
String value() default "";
}
... ...
... ... @@ -45,4 +45,8 @@ public class Constant {
//当天已经领取过
public final static int ORDER_SHARE_ALREADYDRAW_ERROR = 8;
// 批量导入导出文件后缀名定义
public static final String BATCH_FILE_POSTFIX = ".xlsx";
}
... ...
package com.yoho.unions.common.model;
/**
* 批量导出的条件
* @author mali
*
*/
public class ExportParam {
/**
* 搜索条件
*/
private String queryConf;
/**
* 类型
*/
private String type;
public String getQueryConf() {
return queryConf;
}
public void setQueryConf(String queryConf) {
this.queryConf = queryConf;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
@Override
public String toString() {
return "ExportParam [queryConf=" + queryConf + ", type=" + type + "]";
}
}
... ...
package com.yoho.unions.common.restapi.batch;
import com.yoho.error.exception.ServiceException;
import com.yoho.unions.common.model.ExportParam;
import com.yoho.unions.common.service.BatchService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* 批量导出服务接口
* Created by xueyin on 2016/2/2.
*/
@Controller
@RequestMapping("/batch")
public class BatchExportController {
private static final Logger LOGGER = LoggerFactory.getLogger(BatchExportController.class);
@Autowired
private BatchService batchService;
/**
* 下载批量操作结果
*
* @param path 批量导出的业务类型
* @return
*/
@RequestMapping(value = "/download")
public String download(HttpServletResponse response,@RequestParam(name = "path",required = true) String path) {
if (path.startsWith("http://")) { // 如果是绝对路径,则需要通过远程下载文件流
try {
return writeFile(response, "cert.pdf", batchService.downNetFile(path), "multipart/form-data");
} catch (Exception e) {
LOGGER.warn("writeFile find wrong.", e);
}
return writeErrorInfo(response, "文件不存在");
} else { // 本地下载
File file = batchService.localDownload(path);
try {
return writeFile(response, file.getName(), new FileInputStream(file), "multipart/form-data");
} catch (Exception e) {
LOGGER.warn("writeFile find wrong.", e);
return writeErrorInfo(response, "文件不存在");
}
}
}
/**
* 导出表格的excel表格
*
* @param param 批量导出的业务类型
* @return
*/
@RequestMapping(value = "/export")
public String export(HttpServletResponse response, @RequestBody ExportParam param) {
File file;
try {
file = batchService.batchExport(param);
} catch (ServiceException e) {
LOGGER.warn("batchExport find wrong.", e);
return writeErrorInfo(response, e.getMessage());
} catch (Exception e) {
LOGGER.warn("batchExport find wrong.", e);
return writeErrorInfo(response, "<p>导出记录条数太多</p>");
}
try {
return writeFile(response, file.getName(), new FileInputStream(file), "application/vnd.ms-excel");
} catch (FileNotFoundException e) {
LOGGER.warn("writeFile find wrong.", e);;
return writeErrorInfo(response, "文件不存在");
}
}
@RequestMapping(value = "/export.do")
public String exportForGet(HttpServletResponse response, ExportParam param) {
File file;
try {
file = batchService.batchExport(param);
return writeFile(response, file.getName(), new FileInputStream(file), "application/vnd.ms-excel");
} catch (ServiceException e) {
LOGGER.warn("exportForGet find wrong.", e);
return writeErrorInfo(response, e.getMessage());
} catch (Exception e) {
LOGGER.warn("exportForGet find wrong.", e);
return writeErrorInfo(response, "<p>导出记录条数太多</p>");
}
}
private String writeErrorInfo(HttpServletResponse response, String string) {
PrintWriter writer = null;
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-type", "text/html;charset=UTF-8");
try {
writer = response.getWriter();
writer.print(new String(string.getBytes("UTF-8"), "UTF-8"));
writer.flush();
} catch (IOException e) {
LOGGER.warn("os.write find wrong.", e);
} finally {
if (null != writer) {
try {
writer.close();
} catch (Exception e) {
LOGGER.warn("os.close find wrong.", e);
}
}
}
return null;
}
/**
* 向 response 中,写入文件
*/
private String writeFile(HttpServletResponse response, File file, String contentType) {
checkNotNull(file, "file is null!");
String msg = null;
response.setCharacterEncoding("utf-8");
response.setContentType(contentType);
response.setHeader("Content-Disposition", "attachment;fileName=" + file.getName());
try {
InputStream inputStream = new FileInputStream(file);
OutputStream os = response.getOutputStream();
byte[] b = new byte[2048];
int length;
while ((length = inputStream.read(b)) > 0) {
os.write(b, 0, length);
}
os.close();
inputStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
msg = "File not found";
} catch (IOException e) {
e.printStackTrace();
msg = "Read file error";
}
return msg;
}
/**
* 向 response 中,写入文件
*/
private String writeFile(HttpServletResponse response, String fileName, InputStream inputStream, String contentType) {
checkNotNull(inputStream, "inputStream is null!");
String msg = null;
response.setCharacterEncoding("utf-8");
response.setContentType(contentType);
response.setHeader("Content-Disposition", "attachment;fileName=" + fileName);
try {
OutputStream os = response.getOutputStream();
byte[] b = new byte[2048];
int length;
while ((length = inputStream.read(b)) > 0) {
os.write(b, 0, length);
}
os.close();
inputStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
msg = "File not found";
} catch (IOException e) {
e.printStackTrace();
msg = "Read file error";
}
return msg;
}
}
... ...
package com.yoho.unions.common.restapi.batch;
import com.yoho.error.exception.ServiceException;
import com.yoho.unions.common.ApiResponse;
import com.yoho.unions.common.service.BatchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
/**
* Created by xueyin on 2016/1/25.
*/
@Controller
@RequestMapping("/batch")
public class BatchImportController {
@Autowired
private BatchService batchService;
/**
* 批量导入接口
*
* @param file spring 默认的上传文件对象
* @param type 批量导入的业务类型
* @return
*/
@RequestMapping(value = "/import")
@ResponseBody
public ApiResponse upload(@RequestParam("file") MultipartFile file,
@RequestParam(name = "type",required = true) String type,
@RequestParam(name = "args", required = false) String args){
ApiResponse responseBean = new ApiResponse();
Object retMsg = null;
try {
retMsg = batchService.batchImport(file, type, args);
} catch (ServiceException e) {
responseBean.setCode(400);
responseBean.setMessage(e.getMessage());
return responseBean;
} catch (Exception e) {
responseBean.setCode(500);
responseBean.setMessage(e.getMessage());
return responseBean;
}
responseBean.setCode(200);
responseBean.setMessage("处理成功");
responseBean.setData(retMsg);
return responseBean;
}
}
... ...
package com.yoho.unions.common.service;
import com.yoho.error.exception.ServiceException;
import com.yoho.unions.common.constant.Constant;
import com.yoho.unions.common.model.ExportParam;
import com.yoho.unions.common.utils.XlsxUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.poi.POIXMLException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 批量操作服务
* Created by xueyin on 2016/1/28.
*/
@Service
public class BatchService {
private Logger logger = LoggerFactory.getLogger(BatchService.class);
@Value("${file.saveDir}")
private String saveDir;
@Resource(name = "batchImportBusiness")
Map<String, IBusinessImportService> importServiceMap;
@Resource(name = "batchExportBusiness")
Map<String, IBusinessExportService> exportServiceMap;
/**
* 批量上传
* @param file 待上传的文件
* @param type 批量上传类型,与Spring配置文件中batchImportBusiness中Key保持一致
* @return
* @throws Exception
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public Object batchImport(MultipartFile file, String type, String args) throws Exception {
if (!importServiceMap.containsKey(type)) {
logger.error("invalid import type:" + type);
throw new Exception("无效的导入类型:" + type);
}
IBusinessImportService service = (IBusinessImportService)importServiceMap.get(type);
// 先保存到临时目录
File localFile = createLocalFile();
Object retStr = null;
try {
file.transferTo(localFile);
logger.info("batch import " + file.getOriginalFilename() + "to local:" + localFile.getAbsolutePath());
// 将Xlsx数据解析为指定类
Class cls = service.getDataClass();
List<Object> dataList = XlsxUtils.parse(localFile, cls);
// 调用注册的业务服务处理导入数据
retStr = service.batchImport(dataList, args);
localFile.delete(); // 处理完成后,删除本地临时文件
} catch (ServiceException e) {
logger.warn("batchImport find wrong." ,e);
localFile.delete(); // 处理完成后,删除本地临时文件
throw new ServiceException( e.getCode(), e.getMessage());
} catch (POIXMLException e){
if (e.getMessage() != null && e.getMessage().contains("Package should contain a content type part [M1.13]")) {
throw new ServiceException(400, "解析出错,请上传 Excel 2007 以上版本");
}
logger.warn("batchImport find wrong." ,e);
localFile.delete(); // 处理完成后,删除本地临时文件
throw e;
} catch(Exception e) {
logger.warn("batchImport find wrong." ,e);
throw new Exception( e.getMessage());
}
return retStr;
}
/**
* 将批量数据导出为Excel文件
* @param <T>
* @param beanList 待导出的数据
* @param beanClass 数据类型
* @return Excel保存路径
* @throws IOException
*/
public <T> String batchExport(List<T> beanList, Class<T> beanClass) throws IOException {
// 生成需要保存的临时文件
File localFile = createLocalFile();
logger.info("batch export " + localFile.getAbsolutePath());
// 生成Excel表格
XlsxUtils.write(localFile, beanList, beanClass);
return localFile.getName();
}
/**
* 将批量数据导出为Excel文件(不校验类型)
* @param <T>
* @param beanList 待导出的数据
* @param beanClass 数据类型
* @return Excel保存路径
* @throws IOException
*/
public <T> File batchExportWithoutCheckType(List<T> beanList, Class<T> beanClass) throws IOException {
// 生成需要保存的临时文件
File localFile = createLocalFile();
logger.info("batch export " + localFile.getAbsolutePath());
// 生成Excel表格
XlsxUtils.write(localFile, beanList, beanClass);
return localFile;
}
/**
* 下载批量操作结果
* @param path
* @return
*/
public File localDownload(String path) {
return new File(saveDir + File.separator + path);
}
/**
* 生成本地暂存的文件
* @return
*/
private File createLocalFile() {
String fileName = String.valueOf(new Date().getTime()) + Constant.BATCH_FILE_POSTFIX;
String filePath = saveDir + File.separator + fileName;
File localFile = new File(filePath);
return localFile;
}
@SuppressWarnings("unchecked")
public File batchExport(ExportParam param) throws Exception {
logger.info("batchExport req is {}", param);
if (!exportServiceMap.containsKey(param.getType())) {
logger.error("invalid export type:" + param.getType());
throw new ServiceException( 400, "无效的导出类型:" + param.getType());
}
IBusinessExportService service = (IBusinessExportService)exportServiceMap.get(param.getType());
// 生成需要保存的临时文件
File localFile = createLocalFile();
logger.info("batch export " + localFile.getAbsolutePath());
// 生成Excel表格
XlsxUtils.write(localFile, service.batchExport(param.getQueryConf()), service.getDataClass());
return localFile;
}
public InputStream downNetFile(String path) throws Exception{
HttpGet httpget = new HttpGet(path);
HttpClient httpclient = new DefaultHttpClient();
HttpResponse netResponse;
try{
netResponse = httpclient.execute(httpget);
HttpEntity entity = netResponse.getEntity();
if (entity != null) {
return entity.getContent();
}
} finally {
//httpclient.getConnectionManager().shutdown();
}
return null;
}
}
... ...
package com.yoho.unions.common.service;
import java.util.List;
/**
* Created by xueyin on 2016/2/2.
*/
public interface IBusinessExportService {
/**
* 获取批量操作中数据对象的类,一般为VO的Class
* @return 数据集的class类
*/
@SuppressWarnings("rawtypes")
Class getDataClass();
/**
* 批量导出数据
* @param confStr 条件字符串,json格式
* @return 导出的数据集
*/
List<? extends Object> batchExport(String confStr);
}
... ...
package com.yoho.unions.common.service;
import java.util.List;
import java.util.concurrent.ExecutionException;
/**
* 业务导入服务接口类,详细实现由需要导入服务的业务实现
* Created by xueyin on 2016/2/1.
*/
public interface IBusinessImportService {
String SUCCES_LIST = "successList";
String FAIL_FILE_REASON = "failFileReason";
/**
* 获取批量操作中数据对象的类,一般为VO的Class
* @return 数据集的class类
*/
Class getDataClass();
/**
* 批量导入数据
* @param dataList 待导入的数据集
* @return 操作结果集,如果没有则返回null
*/
Object batchImport(List<Object> dataList) throws ExecutionException, InterruptedException;
/**
* 增加一个带参数的导入方法
*/
default Object batchImport(List<Object> dataList, String args) throws ExecutionException, InterruptedException {
return batchImport(dataList);
}
}
... ...
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
... ...
package com.yoho.unions.common.utils;
import com.google.common.base.Preconditions;
import com.yoho.error.ServiceError;
import com.yoho.error.exception.ServiceException;
/**
* 统一抛出 <code>com.yoho.error.exception.ServiceException</code> 异常
* 有新增到参考:
* @see Preconditions
* @author LiQZ on 2016/3/15.
*/
public final class YHPreconditions {
private YHPreconditions() {}
/**
* @see Preconditions#checkNotNull(Object, Object)
*/
public static <T> T checkNotNull(T reference, ServiceError errorMessage) {
if (reference == null) {
throw new ServiceException(errorMessage);
}
return reference;
}
public static <T> T checkNotNull(T reference, String errorMessage) {
if (reference == null) {
throw new ServiceException(400, errorMessage);
}
return reference;
}
public static <T> T checkNotNull(T reference) {
return Preconditions.checkNotNull(reference);
}
/**
* @see Preconditions#checkArgument(boolean, Object)
*/
public static void checkArgument(boolean expression, ServiceError errorMessage) {
if (!expression) {
throw new ServiceException(errorMessage);
}
}
public static void checkArgument(boolean expression, String errorMessage) {
if (!expression) {
throw new ServiceException(400, errorMessage);
}
}
public static void checkArgument(boolean expression) {
Preconditions.checkArgument(expression);
}
public static void checkState(boolean expression, ServiceError errorMessage) {
if (!expression) {
throw new ServiceException(errorMessage);
}
}
public static void checkState(boolean expression, String errorMessage) {
if (!expression) {
throw new ServiceException(500, errorMessage);
}
}
}
... ...
... ... @@ -67,6 +67,14 @@
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</dependency>
<dependency>
<groupId>com.yoho.core</groupId>
<artifactId>yoho-core-dal</artifactId>
</dependency>
... ...