Java:自定義注解+反射實(shí)現(xiàn)導(dǎo)入導(dǎo)出Excel文檔

問題背景

最近遇到的需求:用戶填寫平臺提供的模板文件(sample.xlsx)倍靡,導(dǎo)入到平臺中辈双,代替填寫表單/表格的動作挽绩。用戶也可以將填好的表單/表格導(dǎo)出成Excel文件伞广,便于以文檔的形式存檔或傳輸汁果。

問題分析

從上述需求得出涡拘,這就是一個(gè)Excel文檔與Java Bean對象互相轉(zhuǎn)換的問題。

Excel文件的讀寫据德、操作鳄乏,可以使用Apache Poi或其他開源庫,在此不多說棘利。主要問題是橱野,當(dāng)模板文件內(nèi)容較為復(fù)雜,或者需要處理多個(gè)模板時(shí)善玫,怎樣能快速解析出文檔的內(nèi)容仲吏,與Java Bean的字段對應(yīng)上呢?
應(yīng)用Java反射的原理蝌焚,使用自定義注解 + 泛型裹唆,很容易實(shí)現(xiàn)靈活適配多個(gè)模板,并能快速支持模板的修改只洒,便于擴(kuò)展和維護(hù)许帐。

模板文件分析

分析模板文件,我發(fā)現(xiàn)可以將模板分為兩種毕谴。

  1. 表單式


    表單式

內(nèi)容行標(biāo)成畦、列標(biāo)均確定,例如該表格B1固定為姓名的值涝开,將內(nèi)容解析成單個(gè)JavaBean對象循帐。

  1. 表格式


    表格式

內(nèi)容列固定,例如A列均為學(xué)號舀武,應(yīng)將除去表頭外的每一行解析成一個(gè)JavaBean 對象拄养,返回Java Bean對象的列表。

分析完畢银舱,發(fā)現(xiàn)Java Bean 對象的某個(gè)字段的值與Excel文檔單元格內(nèi)容的對應(yīng)關(guān)系就是行標(biāo) + 列標(biāo)瘪匿,那么我們只需要記錄這個(gè)坐標(biāo),就能實(shí)現(xiàn)轉(zhuǎn)換寻馏。
使用在字段上加注解的方式棋弥,簡明易懂,并且很容易維護(hù)诚欠。下面給出實(shí)現(xiàn)代碼顽染。

實(shí)現(xiàn)

1漾岳、定義注解類

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelAnnotation {
    /**
     * 中文名稱 label  
     */
    String cnName() default "";
    /**
     * 英文名稱 對應(yīng)到JavaClass上的類名
    
     */
    String enName() default "";
    /**
     *
     文件中的行標(biāo) - 從0開始計(jì)數(shù)
     */
    int rowIndex() default -1;
 
    int sheetIndex() default -1;
    /**
     *
     * 文件中的列標(biāo) - 從0開始計(jì)數(shù)
     */
    int columnIndex() default  -1;
}

注解用來說明字段對應(yīng)的單元格工作簿序號、行標(biāo)粉寞、列標(biāo)等信息尼荆,以及

2、定義Java Bean

  • 表單式對應(yīng)的對象
@Data
class Person{
  @ExcelAnnotation(rowIndex = 0,columnIndex = 1,cnName = "姓名")
  private String name;

  @ExcelAnnotation(rowIndex = 2,columnIndex = 1,cnName = "電話號碼")
  private String phoneNum;
 ...
}
  • 表格式對應(yīng)的對象
    只需要定義列的中文名(cnName),不需要定義行標(biāo)
@Data
class Student{
  @ExcelAnnotation(cnName = "學(xué)號")
  private String number;

  @ExcelAnnotation(cnName = "姓名")
  private String name;

  @ExcelAnnotation(cnName = "電話號碼")
  private String phoneNum;
}

3仁锯、工具類實(shí)現(xiàn)寫入和寫出

定義Excel操作的工具類
ExcelUtils.java

@Log4j2
public class ExcelUtils {


    public static <T> List<T> analysisExcelSheetAsTable(Sheet sheet,Class<T> clazz,int headerIndex) throws IntrospectionException {
        ArrayList<Row> rowContent = new ArrayList<>();
        TreeMap<Integer, Method> writeMethodTreeMap = new TreeMap<>();
        //  記錄表頭內(nèi)容與
        HashMap<String,Integer> headerCnNameMap = new HashMap<>();
        // 默認(rèn)的表頭數(shù)據(jù)
        // 獲取表頭數(shù)據(jù)
        Row tableHeader = sheet.getRow(headerIndex);
        //
        int index = 0;
        for(Cell headerCell: tableHeader){
            String headerContent =   ExcelUtils.getCellFormatValue(headerCell).toString().trim();
            headerCnNameMap.put(headerContent,index);
            index++;
        }
        // 忽略第一行表頭數(shù)據(jù)
        for (int i = (headerIndex+1); i < sheet.getPhysicalNumberOfRows(); i++) {
            rowContent.add(sheet.getRow(i));
        }
        for (Field field : clazz.getDeclaredFields()) {
            // 獲取字段上的注解
            Annotation[] annotations = field.getAnnotations();
            if (annotations.length == 0) {
                continue;
            }
            for (Annotation an : annotations) {
                // 若掃描到ExcelAnnotation注解
                if (an.annotationType().getName().equals(ExcelAnnotation.class.getName())) {
                    // 獲取指定類型注解
                    ExcelAnnotation excelAnnotation = field.getAnnotation(ExcelAnnotation.class);
                    try {
                        // 獲取該字段的method方法
                        PropertyDescriptor pd = new PropertyDescriptor(field.getName(), clazz);
                        // 從頭部獲取cnName
                        if(headerCnNameMap.containsKey(excelAnnotation.cnName())){
                            writeMethodTreeMap.put(headerCnNameMap.get(excelAnnotation.cnName()), pd.getWriteMethod());
                        }

                    } catch (IntrospectionException e) {
                        throw e;
                    }
                }
            }
        }
        DataFormatter dataFormatter = new DataFormatter();
        List<T> resultList = new ArrayList<>();
        for (Row row : rowContent) {
            String rowValue = dataFormatter.formatCellValue(row.getCell(0));
            try {
                T model = clazz.newInstance();
                if (!StringUtils.isEmpty(rowValue)) {
                    for(int cellIndex: writeMethodTreeMap.keySet()){
                        if(row.getCell(cellIndex) != null){
                            Cell cell = row.getCell(cellIndex);
                            String value = ExcelUtils.getCellFormatValue(cell).toString();
                            writeMethodTreeMap.get(cellIndex).invoke(model, value);
                        }

                    }
                    resultList.add(model);
                }
            } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
                e.printStackTrace();
            }

        }
        return resultList;
    }




    /**
     * 解析Excel表格內(nèi)容 - 按照表格解析
     * @param sheet
     * @param ignoreRowNum 忽略的行數(shù)(表頭)

     * @param <T>
     * @return
     */
    public static <T> List<T> analysisExcelSheetAsTable(Sheet sheet, int ignoreRowNum, Class<T> clazz) {
        ArrayList<Row> rowContent = new ArrayList<>();
        TreeMap<Integer, Method> writeMethodTreeMap = new TreeMap<>();
        // 從忽略的表頭開始讀
        for (int i = ignoreRowNum; i < sheet.getPhysicalNumberOfRows(); i++) {
            rowContent.add(sheet.getRow(i));
        }
        for (Field field : clazz.getDeclaredFields()) {
            // 獲取字段上的注解
            Annotation[] annotations = field.getAnnotations();
            if (annotations.length == 0) {
                continue;
            }
            for (Annotation an : annotations) {
                // 若掃描到ExcelAnnotation注解
                if (an.annotationType().getName().equals(ExcelAnnotation.class.getName())) {
                    // 獲取指定類型注解
                    ExcelAnnotation excelAnnotation = field.getAnnotation(ExcelAnnotation.class);
                    try {
                        // 獲取該字段的method方法
                        PropertyDescriptor pd = new PropertyDescriptor(field.getName(), clazz);
                        writeMethodTreeMap.put(excelAnnotation.columnIndex(), pd.getWriteMethod());
                    } catch (IntrospectionException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        DataFormatter dataFormatter = new DataFormatter();
        List<T> resultList = new ArrayList<>();
        for (Row row : rowContent) {
            String rowValue = dataFormatter.formatCellValue(row.getCell(0));
            try {
                T model = clazz.newInstance();
                if (!StringUtils.isEmpty(rowValue)) {
                    // 遍歷格子
                    int i = 0;
                    for (Cell cell : row) {

                        if (!writeMethodTreeMap.containsKey(i)) {
                            i++;
                            continue;
                        }
                        String value = ExcelUtils.getCellFormatValue(cell).toString();
                        writeMethodTreeMap.get(i).invoke(model, value);
                        i++;
                    }
                    resultList.add(model);
                }
            } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
                e.printStackTrace();
            }

        }
        return resultList;
    }

    /**
     * 讀取Cell
     *
     * @param cell
     * @return
     */
    public static Object getCellFormatValue(Cell cell)
    {
        Object cellValue;

        //判斷cell類型
        switch (cell.getCellTypeEnum())
        {
            case NUMERIC:
            {
                cellValue = cell.getNumericCellValue();
                break;
            }
            case FORMULA:
            {
                //判斷cell是否為日期格式
                if (DateUtil.isCellDateFormatted(cell))
                {
                    //轉(zhuǎn)換為日期格式Y(jié)YYY-mm-dd
                    cellValue = cell.getDateCellValue();
                }
                else
                {
                    //數(shù)字
                    cellValue = cell.getNumericCellValue();
                }
                break;
            }
            case STRING:
            {
                cellValue = cell.getRichStringCellValue().getString();
                break;
            }
            default:
                cellValue = "";
        }
        return cellValue;
    }
}

4耀找、調(diào)用

在業(yè)務(wù)類(Service)中調(diào)用

  • 調(diào)用僅為示范
// 導(dǎo)入
 public void importExcelFile(InputStream inputStream){
        try (Workbook workbook = WorkbookFactory.create(inputStream)) {
           
            Person person= ExcelUtil.analysisExcelSheetAsForm(workbook.getSheetAt(0),Person.class);
            List<Student> students= ExcelUtil.analysisExcelSheetAsTable(workbook.getSheetAt(1),1,Student.class);
// 僅示范調(diào)用方式,可自行返回
        
        } catch (Exception ex) {
            log.error("Excel解析失斠笛隆野芒!",ex);
            throw new BusinessException("Excel解析失敗双炕!");
        }
    }

    //導(dǎo)出
    public void exportExcelFile(List<Student> students,Person person,HttpServletResponse httpServletResponse){
        //1狞悲、讀取excel模板文件,作為本次下載的模板
        try(InputStream inputStream = new FileInputStream(templatePath);
            Workbook workbook = WorkbookFactory.create(inputStream)){
            httpServletResponse.setContentType("application/octet-stream");
            httpServletResponse.setHeader("Content-Disposition", "attachment;filename=test.xlsx");
            // 2.根據(jù)查詢到的內(nèi)容妇斤,填充Excel表格內(nèi)容
            ExcelUtil.writeToWorkbookAsForm(person,workbook.getSheetAt(0));
            ExcelUtil.writeToWorkbookAsTable(students,workbook.getSheetAt(1,1,Student.class);
            workbook.write(httpServletResponse.getOutputStream());
        } catch (IOException | InvalidFormatException e) {
            e.printStackTrace();
        }

    }

這樣就完成啦摇锋。
對于寫入Excel和讀取Excel的一些格式、編碼/解碼方式站超,我們可以放在通用配置里荸恕,也可以在注解類中增加項(xiàng)目來應(yīng)對特殊需求。

總結(jié)

本篇涉及知識
1死相、泛型
2融求、自定義注解 + Java反射
3、Apache Poi的使用

  • 為什么使用泛型算撮?
    1生宛、將數(shù)據(jù)類型算法進(jìn)行剝離是一種很常用的設(shè)計(jì)思路,可以幫助我們更好地開發(fā)出通用方法肮柜。
    2陷舅、使用泛型(而不是包容萬物的Object類型)使代碼更為可讀,并能規(guī)避編譯錯(cuò)誤审洞,還可以對傳入類型的上界莱睁、下界進(jìn)行規(guī)定。(super/extends)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末预明,一起剝皮案震驚了整個(gè)濱河市缩赛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌撰糠,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辩昆,死亡現(xiàn)場離奇詭異阅酪,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門术辐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來砚尽,“玉大人,你說我怎么就攤上這事辉词”毓拢” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵瑞躺,是天一觀的道長敷搪。 經(jīng)常有香客問我,道長幢哨,這世上最難降的妖魔是什么赡勘? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮捞镰,結(jié)果婚禮上闸与,老公的妹妹穿的比我還像新娘。我一直安慰自己岸售,他們只是感情好践樱,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著凸丸,像睡著了一般拷邢。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上甲雅,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天解孙,我揣著相機(jī)與錄音,去河邊找鬼抛人。 笑死弛姜,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的妖枚。 我是一名探鬼主播廷臼,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼绝页!你這毒婦竟也來了荠商?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤续誉,失蹤者是張志新(化名)和其女友劉穎莱没,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體酷鸦,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡饰躲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年牙咏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嘹裂。...
    茶點(diǎn)故事閱讀 38,617評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡妄壶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出寄狼,到底是詐尸還是另有隱情丁寄,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布泊愧,位于F島的核電站伊磺,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏拼卵。R本人自食惡果不足惜奢浑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望腋腮。 院中可真熱鬧雀彼,春花似錦、人聲如沸即寡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽聪富。三九已至莺丑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間墩蔓,已是汗流浹背梢莽。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留奸披,地道東北人昏名。 一個(gè)月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像阵面,于是被迫代替她去往敵國和親轻局。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評論 2 348