1. 程式人生 > >Poiji:基於列名繫結方式將Excel單元行轉換為JavaBean的開源框架

Poiji:基於列名繫結方式將Excel單元行轉換為JavaBean的開源框架

公司的日常事務中經常需要使用excel進行資料彙總,匯入匯出進行歸類統計分析。

因為沒有廣泛流行的單元行到類轉換/屬性繫結工具,在功能開發之初或者很長一段時間內,

業務系統中我們處理普通excel資料的方法如下:


例如我們公司工程專案中需要到現場部署裝置,

1、從問題域出發我們大概可以建立具有以下屬性的類,用以描述每一行記錄的所具有的屬性以及性狀特徵

 

public class Device  {
    
    private String device_id; //裝置編號

    private String name;     //裝置名稱

    private
String model; //裝置型號 private String location; //存放位置 private String serial; //序列號 private String ipv4; //IP地址 private String project_number; //專案編號 private String project_name; //專案名稱 private String constructor_name;//建設人 private String customer_name; //
客戶單位名稱 private String project_manager_name; //負責人 private Date lastExamine; //最後一次檢查/巡查日期 }

通過對裝置的基本特性、特徵、用途分析,我們粗略地將他的屬性分為3大類
抽取了一些代表性的屬性,如下:

1、自有屬性
通常是出廠設定或者與生俱來的:名稱、型號、序列號等、
2、業務屬性
專案編號、專案名稱、建設人、客戶單位名稱
3、維保屬性
巡檢日期 等

當然比較合理的設計方法應該建立多個物件,用於描述業務物件(裝置)在系統

功能中參與的一系列行為、事件等類。本文為了配合框架的使用,將屬性糅合到一個類

當中,資料型別也只用了簡單的字串型別來闡述問題。

經過以上構造,一個嶄新鮮活的類,如新生兒般,輕裝上陣,便參與到OOP世界的業務系統中。


編寫程式碼操作

...
    public static Object getCellValue(Cell cell)
    {
       return getCellValue(cell,NULL);
    }

    public static  String getCellValue(Cell cell, Object defaultValue)
    {
        if(cell==null)
            return defaultValue;

        cell.setCellType(CellType.STRING);
        String value = cell.getStringCellValue();
        if(StringUtils.isNotEmpty(value)) {
            return value.trim().replaceAll("\\s+", "");
        }
        return defaultValue;
    }

...

List<Device> list = LinkedList<Device>();

for(int i = 1; i< iRowNums; i++)
{
    Device dev =  new Device();

     //裝置編號
     Cell celldevice_id = row.getCell(0);
     String device_id = getCellValue(celldevice_id);
     dev.setDevice_di(device_id);

    //裝置名稱
    ...


    //專案編號
    Cell cellproject_number = row.getCell(6);
    String project_number = getCellValue(cellproject_number);
    dev.setProject_number(project_number);

    list.add(dev);

}

當然偷懶的方法也是有的,通過工具類來幫助我少寫程式碼commons-beanutil.jar
中的屬性工具類

 public static String  getProperty(Object bean,String propertyName)
  {
        try {
            String value = (String)PropertyUtils.getSimpleProperty(bean,propertyName);
            if(StringUtils.isEmpty(value))
                return "";
            return value;
        }catch (Exception ex)
        {
            logger.error("getProperty failed:{}",ex);
            return "";
        }
 }



public Cell enumerateCell(Row row, Cell copyOfdev, int offet, int length,String []fields)
{
        for(int i = offet; i <length;i++){
            Cell cell = row.getCell(i);
            String value = getCellValue(cell);
            String propertyName = fields[i-(offet-1)];
            setProperty(copyOfMessage,propertyName,value);
        }
        return copyOfMessage;
}

 String fields[] = {
                "device_id", "name","model", "location","serial", "ipv4", "project_number",
        "project_name","constructor_name","customer_name","project_manager_name","lastExamine"
        };

我們給他列名對應的索引值讓他去跑,也能拿到他的屬性,這樣的缺陷是要維護屬性在String陣列中的索引。

電子計算機是一個數字電路系統,按照數值進行計算是他的強項,對於開發人員來說,每次寫類似的程式碼,都要錙銖必較的
計算每一個屬性對應的位置,生怕寫錯了索引帶來錯誤。

然而需求總是隨著時間、政策法令、政治、宗教等客觀因素,以及人的主管意願在變化著。

----以下需求屬於為了講解需求變化場景,可能與事實需求有出入

需求情形1:公司要求,在模板上新增申請人、稽核人
需求情形2:客戶(公安等)要求每個裝置的IP要記錄備案
需求情形3:xxx裝置需要符合國標(GBxxxx),需要新增屬性 AAA

 

很顯然,我的剛剛描述的類,他的業務屬性和維保屬性肯定會有需要變化的。


公司實際業務場景也可能遇到類似的需求變化,萬一哪一天新增到欄位對業務需求比較重要呢,需要提到前面幾列,
或者列的順序沒有維護好,錯亂了怎麼辦,偶然間閃過一個概念,能不能把Excel單元行繫結到JavaBean,偶然間就構思簡單的
搜尋關鍵字:how to binding excel row to javabean,Google一番找到了本文將要使用的框架Poiji

https://github.com/ozlerhakan/poiji
A tiny library converting excel rows to a list of Java objects based on Apache POI

一個基於Apache POI輕量級工具庫,將Excel行轉換成Java物件列表


按照官方文件,我們把程式碼重構了一下

public class Device  {

    @ExcelCellName("裝置編號")
    private String device_id; //裝置編號

    @ExcelCellName("裝置名稱")
    private String name;     //裝置名稱

    @ExcelCellName("裝置型號")
    private String model;   //裝置型號

    @ExcelCellName("存放位置")
    private String location;   //存放位置

    @ExcelCellName("序列號")
    private String serial;   //序列號

    @ExcelCellName("IP地址")
    private String ipv4;   //IP地址
            
    @ExcelCellName("專案編號")
    private String project_number;   //專案編號

    @ExcelCellName("專案名稱")
    private String project_name;   //專案名稱

    @ExcelCellName("建設人")
    private String constructor_name;//建設人

    @ExcelCellName("客戶單位名稱")
    private String customer_name;   //客戶單位名稱

    @ExcelCellName("負責人")
    private String project_manager_name;   //負責人

   @ExcelCellName("最後一次檢查/巡查日期")
    private Date lastExamine;        //最後一次檢查/巡查日期

}

在每個屬性上使用註釋,添加註解,把列名加上去,例如 裝置名稱列,寫成

@ExcelCellName("裝置名稱")
    private String name; 

然後一行程式碼,我將獲得批量的java物件

 List<Device> devs = Poiji.fromExcel(new File("g:\\device-import-template.xlsx"), Device.class);
 System.out.println(devs);

筆者在實際開發測試過程中遇到一種情況,excel表頭有空格的情況下無法繫結數值,日常工作過程中難免有人為
操作誤差/失誤,誤輸入空格,這種情況將有可能導致匯入的業務資料就是錯誤的,資料在特定業務場景下運轉起來,
因蝴蝶效應,有可能造成更深遠的影響。
筆者已經修改原始碼,預設去除行首行尾的空格。trimTagName方法

截至寫本文章的時候,官方更新到1.18.1版本,由於1.18.1版本需要POI 4.0版本,現實中生產環境中使用的是3.1.6
筆者修改了一下原始碼,目前相容3.1.6,可以放到生產環境使用。

package com.poiji.deserialize;


import com.poiji.bind.Poiji;
import com.poiji.deserialize.model.byid.Employee;
import com.poiji.deserialize.model.byname.EmployeeByName;
import com.poiji.option.PoijiOptions;
import com.poiji.option.PoijiOptions.PoijiOptionsBuilder;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.io.File;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;

import static com.poiji.util.Data.unmarshallingDeserialize;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;

/**
 * Created by [email protected] on 2018-11-15.
 */
@RunWith(Parameterized.class)
public class DeserializersByNameTagWithWhiteSpaceTest {

    private String path;
    private List<Employee> expectedEmployess;
    private Class<?> expectedException;

    public DeserializersByNameTagWithWhiteSpaceTest(String path,
                                   List<Employee> expectedEmployess,
                                   Class<?> expectedException) {
        this.path = path;
        this.expectedEmployess = expectedEmployess;
        this.expectedException = expectedException;
    }

    @Parameterized.Parameters(name = "{index}: ({0})={1}")
    public static Iterable<Object[]> queries() {
        return Arrays.asList(new Object[][]{               
                {"src/test/resources/employees-tagwithwhitespace.xlsx", unmarshallingDeserialize(), null},
        });
    }

    @Test
    public void shouldMapExcelToJava() {

        PoijiOptions options = PoijiOptionsBuilder.settings().datePattern("dd/MM/yyyy").dateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd")).trimCellValue(true).trimTagName(true).build();
        try {
            List<EmployeeByName> actualEmployees = Poiji.fromExcel(new File(path), EmployeeByName.class,options);

            assertThat(actualEmployees, notNullValue());
            assertThat(actualEmployees.size(), not(0));
            assertThat(actualEmployees.size(), is(expectedEmployess.size()));

            EmployeeByName actualEmployee1 = actualEmployees.get(0);
            EmployeeByName actualEmployee2 = actualEmployees.get(1);
            EmployeeByName actualEmployee3 = actualEmployees.get(2);

            Employee expectedEmployee1 = expectedEmployess.get(0);
            Employee expectedEmployee2 = expectedEmployess.get(1);
            Employee expectedEmployee3 = expectedEmployess.get(2);

            assertThat(actualEmployee1.toString(), is(expectedEmployee1.toString()));
            assertThat(actualEmployee2.toString(), is(expectedEmployee2.toString()));
            assertThat(actualEmployee3.toString(), is(expectedEmployee3.toString()));
        } catch (Exception e) {
            if (expectedException == null) {
                fail(e.getMessage());
            } else {
                assertThat(e, instanceOf(expectedException));
            }
        }
    }

}

以下奉送上修改後的poiji 1.18.0原始碼

平時空閒時間不多,如有疑問歡迎留言交流。

poi1.18.0修改版,相容POI 3.1.x,自動去除表頭輸入的空格