Excel导入在实际的开发中,经常会用到,可是我们往往需要研究一番这个Excel表格的结构才能够着手写Excel导入的代码,而这份代码有不能够复用,常常导致一表一码的情况,为了提高开发效率,因此写了些代码和解决思路
大前提
每一份Excel表格都有唯一标示某列或某行业务含义的标示,例如列名,某单元格特定字符。
这是一份教学进度表
很明显根据某些特定标示,我是能够知道哪个单元格是我想要的数据
解决思路
1.如何让程序根据某一字段标识与导入实体类字段存在对应关系,找出数据所在位置?
例如给出如下对应关系(参照上图教学进度的,给出如下配置)
List<ImportField> fields = new ArrayList<>();
//公用字段
fields.add(new ImportField("课程名称","name"));
fields.add(new ImportField("专业","major"));
fields.add(new ImportField("年级","grade"));
fields.add(new ImportField("班级","classes"));
//多值字段
fields.add(new ImportField("授课方式","type",true,1,0));
fields.add(new ImportField("授课教师","teacher",true,1,0));
fields.add(new ImportField("授课内容","content",true,1,0));
fields.add(new ImportField("上课地点","address",true,1,0));
fields.add(new ImportField("周次","week",true,1,0));
fields.add(new ImportField("星期","day",true,1,0));
fields.add(new ImportField("节次","scope",true,1,0));
2.如何通过给定的映射关系遍历Sheet和得到某一标识所在行和列?
首先我们需要定义一个导入配置,然后程序就是根据这一个导入配置去取数据
例如给出如下实体类
public class ImportField {
//别名
private String alias;
//数据库字段名称或者实体类名称
private String name;
//是否多个
private boolean isMulti = false;
//行偏移量
private int xOffset=0;
//列偏移量
private int yOffset=0;
//起始行
private int row;
//起始列
private int col;
//多值属性记录数
private int multiCount = 0;
//是否分析得到起始行列位置
private boolean isComplete = false;
public ImportField() {
}
public String getAlias() {
return alias;
}
public void setAlias(String alias) {
this.alias = alias;
}
public boolean isMulti() {
return isMulti;
}
public void setMulti(boolean multi) {
isMulti = multi;
}
public int getxOffset() {
return xOffset;
}
public void setxOffset(int xOffset) {
this.xOffset = xOffset;
}
public int getyOffset() {
return yOffset;
}
public void setyOffset(int yOffset) {
this.yOffset = yOffset;
}
//计算所在列是需要加上行偏移量
public int getRow() {
return row+xOffset;
}
public void setRow(int row) {
this.row = row;
}
//计算所在列是需要加上列偏移量
public int getCol() {
return col+yOffset;
}
public void setCol(int col) {
this.col = col;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public ImportField(String alias, String name, boolean isMulti, int xOffset, int yOffset) {
this.alias = alias;
this.name = name;
this.isMulti = isMulti;
this.xOffset = xOffset;
this.yOffset = yOffset;
}
public ImportField(String alias, String name) {
this.alias = alias;
this.name = name;
}
public void setPosition(int row, int col){
this.col = col;
this.row = row;
}
public boolean isComplete() {
return isComplete;
}
public void setComplete(boolean complete) {
isComplete = complete;
}
public void getNextRow(){
this.row ++;
}
public void addMultiCount(){
this.multiCount++;
}
public int getMultiCount() {
return multiCount;
}
遍历Excel得到标识位置所在行列
//分析即将导入的文件
/**
* HashMap中包含
*List<ImportField> fields
*HSSFSheet
*/
private static HashMap<String,Object> analysisExcel(
InputStream inputStream, List<ImportField> fields)
throws IOException,FormatException{
HashMap<String,Object> analysisResult = new HashMap<>();
int progress = 0;//扫描进度
HSSFWorkbook hssfWorkbook = new HSSFWorkbook(inputStream);
HSSFSheet hssfSheet = hssfWorkbook.getSheetAt(0);//默认获取第一个
if (hssfSheet == null) {
throw new FormatException("读取模板文件为空!");
}
for (int rowNum = 1; rowNum <= hssfSheet.getLastRowNum(); rowNum++) {
HSSFRow hssfRow = hssfSheet.getRow(rowNum);
if(hssfRow == null){
continue;
}
for(int colNum = 0; colNum<hssfRow.getPhysicalNumberOfCells(); colNum++){
HSSFCell cell = hssfRow.getCell(colNum);
if(cell != null && StringUtils.isNoneBlank(cell.getStringCellValue())){
String val = cell.getStringCellValue().trim();
for(ImportField field : fields){
if(val.indexOf(field.getAlias()) != -1){
field.setPosition(cell.getRowIndex(),cell.getColumnIndex());
field.setComplete(true);
progress++;
if(progress == fields.size()){
//如果所需字段的位置都找到了,提前结束循环
analysisResult.put(FIELDS,fields);
analysisResult.put(SHEET,hssfSheet);
return analysisResult;
}
}
}
}
}
}
//如果没有提前跳出遍历,那么就是证明某个标识在Excel中没找到
String errorMsg = "";
for (ImportField field : fields){
if(!field.isComplete()){
errorMsg = errorMsg+" "+field.getAlias();
}
}
throw new FormatException("导入课程表格式错误!不存在列"+errorMsg);
}
3.读取数据,返回指定实体类集合
通过以上的遍历,可以得到含有行列位置的导入配置和一个Sheet对象
//导入excel
private static List<Map<String,String>> importExcel(HashMap<String,Object> analysisResult)
throws FormatException{
List<ImportField> fields = (List<ImportField>) analysisResult.get(FIELDS);
HSSFSheet hssfSheet = (HSSFSheet) analysisResult.get(SHEET);
if(fields == null || hssfSheet == null){
throw new FormatException("分析导入文件异常");
}
//按行大小排序,因为是从第一行开始遍历的读取的,因此排序可以优先读取,不用再倒回来读取
fields.sort(new Comparator<ImportField>() {
@Override
public int compare(ImportField o1, ImportField o2) {
return o1.getRow()-o2.getRow();
}
});
//获取公共属性和多值属性
HashMap<String,String> singleFields = new HashMap<>();
HashMap<String,List<String>> multiFields = new HashMap<>();
for (int rowNum = 1; rowNum <= hssfSheet.getLastRowNum(); rowNum++) {
HSSFRow hssfRow = hssfSheet.getRow(rowNum);
if(hssfRow == null){
continue;
}
for(int colNum = 0; colNum<hssfRow.getPhysicalNumberOfCells(); colNum++){
HSSFCell cell = hssfRow.getCell(colNum);
if(cell == null){
continue;
}
String val = cell.getStringCellValue();
for(ImportField field : fields){
if (field.getRow() == rowNum && field.getCol() == colNum){
if(!field.isMulti()){
singleFields.put(field.getName(),val);
}else{
//excel表格在A4纸出现第二页的情况下处理如下
if(val.indexOf(field.getAlias()) == -1 && StringUtils.isNoneBlank(val)){
if(!multiFields.containsKey(field.getName())){
List<String> vals = new ArrayList<>();
vals.add(val);
multiFields.put(field.getName(),vals);
}else{
multiFields.get(field.getName()).add(val);
}
field.addMultiCount();
}
field.getNextRow();//跳出循环后读取下一行
}
break;
}
}
}
}
//至此我们得到了2个HashMap
//公用属性和多值属性
//那么每一个多值属性的数量应该是相等的,否则这份Excel表格的据就不合格的。
int multiCount = 0;
String errorMsg ="";
String currentCol="";//选取的某一个多记录属性字段
for(ImportField field : fields){
if (field.isMulti()){
if(multiCount == 0){
currentCol = field.getAlias();
multiCount = field.getMultiCount();
}else {
if(field.getMultiCount() != multiCount){
errorMsg = errorMsg +","+field.getAlias()+"记录数为:"+field.getMultiCount();
}
}
}
}
if(StringUtils.isNoneBlank(errorMsg)){
throw new FormatException("导入课程表列["+errorMsg.substring(1)+"]与["+currentCol+"]记录数:"+multiCount+"不一致");
}
//将公用属性放入多值属性
//例如
//公用字段是
//{“name”:"小明"}
//多种字段是
//{"phone":" [电话1,电话2,电话3]"}
//结合之后变成
//{[{“name”:"小明","phone":"[电话1]"},{“name”:"小明","phone":"[电话2]"},{“name”:"小明","phone":"[电话3]"}]}
for(ImportField field : fields){
if(!field.isMulti()){
List<String> vals = new ArrayList<>();
for(int i=0;i<multiCount;i++){
vals.add(singleFields.get(field.getName()));
}
multiFields.put(field.getName(),vals);
}
}
List<Map<String,String>> list = new ArrayList<>();
for(int i=0;i<multiCount;i++){
HashMap<String,String> data = new HashMap<>();
for(String key : multiFields.keySet()){
List<String> vals = multiFields.get(key);
data.put(key,vals.get(i));
}
list.add(data);
}
return list;
}
4.通过上面的读取可以得到List<HashMap<String,String>>,下面的问题就是HashMap转实体类对象的问题了,我推荐一下这种方式
<dependency>
<groupId>net.sf.dozer</groupId>
<artifactId>dozer</artifactId>
<version>5.5.1</version>
</dependency>
然后简单封装一下
public class BeanMapper {
private static DozerBeanMapper dozer = new DozerBeanMapper();
public BeanMapper() {
}
public static <T> T map(Object source, Class<T> destinationClass) {
return dozer.map(source, destinationClass);
}
public static <T> List<T> mapList(Collection sourceList, Class<T> destinationClass) {
ArrayList destinationList = Lists.newArrayList();
Iterator i$ = sourceList.iterator();
while(i$.hasNext()) {
Object sourceObject = i$.next();
Object destinationObject = dozer.map(sourceObject, destinationClass);
destinationList.add(destinationObject);
}
return destinationList;
}
public static void copy(Object source, Object destinationObject) {
dozer.map(source, destinationObject);
}
}
然后一句话转完
List<T> list = BeanMapper.mapList(data,target);
总结
第一次用markdown写文章,感觉倍爽,代码应该还有不完善的地方,其实最好的方式是自定义一个注解,然后加上校验,可是由于知识水平的限制,暂时没有什么进展。
完整的代码
https://github.com/Mygraduate/Supervisor_Java.git
代码所在位置