Java之解析Excel设计详解

  • 2019 年 10 月 8 日
  • 筆記

“ 优雅的代码,会让你的工作事半功倍。 ” —— 23号老板

0

1

引入

原创:爱吃回锅肉的瘦子、凌空夜望

相关文章系列:

前面几篇我们简单介绍了解析excel,这回是来讲解如何设计一个关于Excel操作的简单工具类,并将每一行数据传化成javabean的形式来进行使用。

在写代码之前,我们必须得先仔细分析Excel的属性,因为代码毕竟是为功能实现服务,而功能实现是基于事物本身客观存在的属性。

Excel的文件格式有xls、xlsx、xlsm、xltx、xlsb、xltx、xlam。常用的是xls,xlsx。我们目前只在这两格式进行解析。

如何判断他是什么格式;简单的方式利用文件名的后缀名判断,需要使用到api为String.endwith()。这个方法简单,性能相对好些。那么后缀名不能获取怎么办?是否可以根据文件流的方式知道excel格式呢?

这时候我们需要去查阅poi官方文档,发现有两种方式:

POI官网:

http://poi.apache.org/components/spreadsheet/quick-guide.html

1、抽象工厂模式 :WorkbookFactory. Create(Fileinputstream)方法,这样无论什么格式都能基于接口形式进行解析;另外一种使用两个工具类来进行判断:

2、POIFSFileSystem.hasPOIFSHeader(),POIXMLDocument.hasOOXMLHeader();分别判断是xls,xlsx。

完成了excel格式问题现在目前已经解决,然后我们在看看poi文档发现xls、xlsx的类都是实现了wookbook,在观察里面的接口方法都有我们解析需要的方法。也就是说我们在解析时候,我们可以使用wookbook接口的方式进行,以此减少我们代码量,也就是设计模式原则中的里氏替换原则。

02

理解

在一个excel里有多个sheet,每个sheet都有row,每个row里有cell;这里都可以看成对象,然后对象中有对此子属性进行依赖;现在我们得进入我们实现功能(解析成Javabean)结合属性进行分析:我们要解析成javabean必须得有字段名与值,多个sheet意味着可能有两种情况,一种是每个sheet的表头都是一样的,这样只需要一个model就可以了。另外存在两种不同的sheet,这时候我们必须传多个model进去,进行赋值。但由于篇幅有限,我现在先讲解最简单的,每个sheet都一样表头且表头设置在第一行。接下来是row,Row里有cell,cell里有值,而值有不同格式,文本格式,数字格式等等。

现在我们总结了一下 excel属性 有多种文件格式,里面有sheet,sheet有row,row有cell,也就是说我们至少要内嵌3个循环体才能到达我们要的值,每个值根据格式不同,还得不断判断。而且对于表头我们还需进行翻译,以及使用键值对的方式,所以我们在三个循环体外围,得有一个一个循环体专门用来翻译表头成英文;

现在我们理清了整个流程,现在有个问题是针对不同excel上传上来,我们要如何翻译表头:我的想法是单独写一个翻译类。对于数据校验,我目前想到两种实现方式;一种是使用泛型,然后泛型限制成必须实现一个父类model,model规定了一个抽象方法,在解析的时候使用,也就是模板设计模式;另外一种就是存储在map中,利用beanutil进行转换成bean,然后bean写校验方法。这两种中,综合来看第一种就是时间复杂度会相对较低,缺点必须model实现接口或父类。第二种呢通用性较好,理解起来简单。但综合考虑,我更倾向于用泛型的方式进行,一对于以后的增加功能更好修改,二减少了客户端的代码量,维护容易些。

0

3

实现

假设我们有一个excel,表头有姓名,年龄,证件号码,证件类型。相应的我们的字段命名则应该为name,age,certificatetype, certificateno;

对于证件类型为了方便枚举或者数据库存储的时候减少储存,我们采取字段翻译,例如身份证用01表示,驾驶证有02表示;对于字段的翻译我们可以另外封装一个类进行翻译,我这边采取的是map形式。

那么字段翻译在什么时候做呢?显然表头翻译就是在遍历的时候,而对于model在set时候进行。

那么接下来代码就清晰了:

首先定义一个model接口:一个方法进行空校验(或者其他必填校验),第二个就是存储数据位置

public interface NameToField extends Serializable {      //存储该数据在第几行,可以提示使用者哪行出现错误数据      public void  setRow(int row);      public boolean isEmpty();  }  

定义一个model, 实现接口NameToField

public class peopleInfo  implements NameToField{      private Integer row;      private String name;      private Integer age;      private String certificateno; //setter、getter      private String certificatetype;        public String getCertificatetype() {          return certificatetype;      }      //这边我使用的是org.apache.commons.beanutils.BeanUtils  map传bean,他的赋值是使用set方式,所以在此处进行字段翻译      public void setCertificatetype(String certificatetype) {          this.certificatetype = TranslationField.getCertificatetypes().get(certificatetype);      }        @Override      public void setRow(int row) {          this.row=row;      }      //此处只是简单的校验      @Override      public boolean isEmpty() {          return StringUtils.isBlank(this.name)                  &&StringUtils.isBlank(this.certificateno)                  &&StringUtils.isBlank(this.certificatetype);      }  }  

字段翻译类

public final class TranslationField {     //数据里的字段翻译      private static final Map<String, String> certificatetypes;      //表头字段翻译      private static final Map<String, String> fieldMap;      static {          //此处有线程危险问题,所以这些map是不对外开放操作的          certificatetypes=new HashMap<String, String>();          fieldMap=new HashMap<>();          fieldMap.put("姓名","name");          fieldMap.put("证件类型","certificatetype");          fieldMap.put("证件号码","certificateno");          fieldMap.put("年龄","age");          certificatetypes.put("身份证","01");          certificatetypes.put("驾驶证","02");      }      public static Map<String, String> getCertificatetypes() {          return certificatetypes;      }      public static Map<String, String> getfieldMap() {          return fieldMap;      }  }  

定义工具类:

/暂时只支持转一个model  public class ExcelUtil<T extends NameToField> {        private String filePath;      private static final String EXCEL_2003 = ".xls";      private static final String EXCEL_2007 = ".xlsx";      private Workbook workbook;      private List<Sheet> sheets;      private List<Row> rows;      private List<Cell> cells;      private List<T> models;      private Class<T> t;      /**       * @param filePath    文件名称       * @param inputStream 文件流       * @param t           excel转Model类型       * @throws IOException       */      public ExcelUtil(String filePath, InputStream inputStream, Class<T> t) throws Exception {            this.filePath = filePath;          workbook = initWorkBook(filePath, inputStream);          models = new ArrayList<T>();          this.t=t;          init();      }        public Workbook initWorkBook(String filePath, InputStream inputStream) throws IOException, InvalidFormatException {          Workbook workbook = null;          if (filePath.endsWith(EXCEL_2003)) {              workbook = new HSSFWorkbook(inputStream);          } else if (filePath.endsWith(EXCEL_2007)) {              workbook = new XSSFWorkbook(inputStream);          }else{              //此处只需要这个方法就可以了   workbook= WorkbookFactory.create(inputStream);          }          return workbook;      }          public void init() throws Exception {            sheets = new ArrayList<Sheet>();          rows = new ArrayList<Row>();          cells = new ArrayList<Cell>();            for (int i = 0, length = workbook.getNumberOfSheets(); i < length; ++i) {                Sheet sheet = workbook.getSheetAt(i);              sheets.add(sheet);                Row firstRow = sheet.getRow(sheet.getFirstRowNum());                //获取首行标题,并翻译成字段;              String name ;              for (int j = sheet.getFirstRowNum() + 1; j <= sheet.getLastRowNum(); j++) {                  Row row = sheet.getRow(j);                  rows.add(row);                    //存储每一行的信息                  Map<String, Object> map = new HashMap<String, Object>();                    for (int k = firstRow.getFirstCellNum(); k < firstRow.getLastCellNum(); k++) {                      if (row==null)continue;                      Cell cell = row.getCell(k);                      cells.add(cell);                       name = TranslationField.getfieldMap().get(firstRow.getCell(k).getStringCellValue());                      if (name != null) map.put(name, getCellValue(cell));                  }                    T ta = (T) t.newInstance();                  //用来存储行列信息                  ta.setRow(j+1);                  BeanUtils.populate(ta, map);                  //对于excel可能出现""类似于需要对他进行非空校验,如果有其他特殊比如邮箱正则                  if (ta!=null&&!ta.isEmpty()){                      models.add(ta);                  }                }          }        }        public Workbook getWorkbook() {          return workbook;      }        public List<Sheet> getSheets() {          return sheets;      }        public List<Row> getRows() {          return rows;      }        public List<Cell> getCells() {          return cells;      }        public List<T> getModels() {          return models;      }        //由于使用yyyyMMdd时间格式是方便转成int类型进行大小判断      private static final DateFormat df = new SimpleDateFormat("yyyyMMdd");      private static final DecimalFormat decimalFormat = new DecimalFormat("#.#");        /**       * 根据Cell源码及官方文档可知每个单元格的值都是根据Cell里的枚举       * @param cell       * @return       */      private static Object getCellValue(Cell cell) {          if (cell == null                  || (cell.getCellType() == Cell.CELL_TYPE_STRING && StringUtils.isBlank(cell                  .getStringCellValue()))) {                  return null;          }          int cellType = cell.getCellType();          switch (cellType) {              case Cell.CELL_TYPE_BLANK:                  return null;              case Cell.CELL_TYPE_BOOLEAN:                  return cell.getBooleanCellValue();                case Cell.CELL_TYPE_ERROR:                  return cell.getErrorCellValue();                case Cell.CELL_TYPE_FORMULA:                    return cell.getCellFormula();                case Cell.CELL_TYPE_NUMERIC:                  if (DateUtil.isCellDateFormatted(cell)) {                      Date date = cell.getDateCellValue();                      return df.format(date);                  } else {                      return decimalFormat.format(cell.getNumericCellValue());                  }              case Cell.CELL_TYPE_STRING:                  return cell.getStringCellValue();              default:                  return null;          }      }    }  

0

4

测试

客户端校验:

public static void main(String[] args) throws Exception {      Gson gson=new Gson();      FileInputStream in=new FileInputStream("C:\bboyHan\a.xlsx");      ExcelUtil excelUtil=new ExcelUtil<>("a.xlsx",in, PeopleInfo.class);      System.out.println(gson.toJson(excelUtil.getModels()));  }  

结果:

0

5

小结

文章重点讲解如何进行一个工具类的设计以及他的一些思维方式;纯属个人的一些浅见,还望有朋友一起交流学习。

对于poi解析excel是非常吃内存的,所以读者有时间的话可以了解poi中的sax,或者基于poi的开源工具easyExcel。