dom4j解析XML配置,Java反射與POI API實現Excel文件導出導入模板工具類(上)

前言

最近在公司裏做基金公司的財務報表開發一類的工作,很多頁面都要實現Excel文件的導出功能。雖然網絡上已經有了很多使用POI API實現導出導入Excel文件的博客,但是感覺還是無法做到通用而避免很多繁瑣的代碼。爲了避免一些重複的開發工作,於是筆者想到了寫一個通用的Excel導出導入工具類,只需要通過一些XML配置,調這個工具類,通過將自己實現的Dao獲取的數據列表dataList作爲參數傳遞進去即可。寫這個Excel文件導出導入工具類也是當初在華爲外包工作時收到了一些啓發,廢話不多說,下面開始上乾貨!

開發工具及配置

操作系統:Win10
開發工具:IntellJ 2018.02版本,
JAVA環境:JDK1.8,
框架:SpringBoot 2.05,
數據庫:Mysql,
持久層框架:Mybatis

1. 使用XML配置

     XML 指可擴展標記語言,被設計用來傳輸和存儲數據;它與HTML很類似,但又有本質的區別,HTML用來展示數據,而 XML 主要用來傳輸和存儲數據;早幾年的Java SSM框架,Spring的配置文件中就大量地用到了XML文件用於存儲元數據,後來由於Spring 4.0之後開始支持註解的配置,到SpringBoot流行之後更是幾乎用註解取代了XML配置,但是在Mybatis中實現一些複雜的動態查詢用XML寫實現SQL功能還是要比使用註解方便得多。
      談到XML配置文件,就不得不提它的解析器,因爲最終程序都要將XML標籤中存儲的值解析爲實體類及其屬性值才能實現對應的功能。XML文件的解析器主要有Dom,Sax,Jdom和Dom4j 4種方式,詳細使用API請參考這篇以下即便文章:

鑑於JDK自帶Dom和Sax的API Jar包,Dom的API使用起來比較繁瑣,而Dom4j在公認爲性能最後,並在Hibernate和Mybatis持久層框架中得到了廣泛應用。於是本人採用Sax API和Dom4j的Api作了作了一次解析XML配置文件的比較,結果發現Dom4j解析速度要明顯快於Sax。

爲了方便開發,筆者博客依賴我之前的項目:springboot整合Mybatis

感興趣的讀者可使用git 克隆下來或者下載zip文件解壓後導入自己的IDE工具閱讀完整的代碼

1.1 配置元表數據xml文件

      在類路徑resources目錄下新建example.xml文件,代碼如下:

<?xml version="1.0" encoding="UTF-8" ?>
<workbook name="userInfoAndCities">
    <sheet name="userInfo" voClass="com.example.mybatis.model.UserTO" >
        <column name="id" displayName="ID" type="java.lang.Integer"  width="50"/>
        <column name="userAccount" displayName="用戶賬號" type="String" width="80"/>
        <column name="password" displayName="密碼" type="String" width="80"/>
        <column name="nickName" displayName="暱稱" type="String" width="80"/>
        <column name="deptNo" displayName="部門編號" type="java.lang.Integer" width="80"/>
        <column name="deptName" displayName="部門名稱" type="String" width="150"/>
        <column name="phoneNum" displayName="手機號碼" type="String" width="150"/>
        <column name="emailAddress" displayName="郵箱地址" type="String" width="150"/>
        <column name="birthDay" displayName="出生日期" type="java.util.Date" width="150"/>
    </sheet>
    <sheet name="provinceCities" voClass="com.example.mybatis.model.CityTO">
        <column name="cityCode" displayName="城市編碼" type="String" width="80"/>
        <column name="parentCode" displayName="父城市編碼" type="String" width="100"/>
        <column name="cityName" displayName="城市名稱" type="String" width="120"/>
    </sheet>
</workbook>

使用dom4j解析xml 文件時需要在pom.xml文件中映入響應的jar包依賴:

	<dependency>
			<groupId>dom4j</groupId>
			<artifactId>dom4j</artifactId>
			<version>1.6.1</version>
	</dependency>

1.2 自定義SaxDemoHandler並繼承SAXParserHandler

      使用Sax解析器需要自己定義一個Handler處理解析標籤邏輯,我們可以繼承
com.sun.org.apache.xml.internal.resolver.readers包下的SAXParserHandler類,然後重寫其中的startDocument,startElement,endElement和endDocument方法,將從xml配置文件中解析出來的元數據保存在自定義類的全局變量中,下滿爲筆者自定義的xml文件解析處理器SaxDemoHandler,代碼及註釋如下:

package com.example.mybatis.utils;

import com.example.mybatis.model.ColumnInfo;
import com.example.mybatis.model.SheetInfo;
import com.sun.org.apache.xml.internal.resolver.readers.SAXParserHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;

import java.util.HashMap;
import java.util.Map;

/**
 * SaxReader解析XML文檔
 */
public class SaxDemoHandler extends SAXParserHandler {

    private String workBookName;

    private Map<String, SheetInfo> sheetInfoMap;

    private String sheetName;

    private static Logger logger = LoggerFactory.getLogger(SaxDemoHandler.class);

    @Override
    public void startDocument() throws SAXException {
        //開始解析文檔
        super.startDocument();
        logger.info("----開始解析文檔----");
        sheetInfoMap = new HashMap<>();

    }

    @Override
    public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
        super.startElement(namespaceURI, localName, qName, atts);
        logger.info("namespaceURI={},localName={},qName={}",namespaceURI,localName,qName);
        if(qName.equals("workbook")){
            String workBookName = atts.getValue("name");
            this.workBookName = workBookName;
        }
        if(qName.equals("sheet")){
            String sheetName = atts.getValue("name");
            this.sheetName = sheetName;
            String voClass = atts.getValue("voClass");
            SheetInfo sheetInfo = new SheetInfo(sheetName,voClass);
            sheetInfoMap.put(sheetName,sheetInfo);
        }
        if(qName.equals("column")){
            String columnName = atts.getValue("name");
            String displayName = atts.getValue("displayName");
            String type = atts.getValue("type");
            Integer width = Integer.valueOf(atts.getValue("width"));
            ColumnInfo columnInfo = new ColumnInfo(columnName,displayName,type,width);
            sheetInfoMap.get(this.sheetName).getColumns().add(columnInfo);
        }

    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        super.characters(ch, start, length);
        String characters = new String(ch,start,length);
        logger.info("characters={}",characters);
    }

    @Override
    public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
        super.endElement(namespaceURI, localName, qName);
        logger.info("---解析{}元素結束",qName);
    }

    @Override
    public void endDocument() throws SAXException {
        super.endDocument();
        logger.info("---結束解析文檔---");
    }

    public String getWorkBookName() {
        return workBookName;
    }

    public Map<String, SheetInfo> getSheetInfoMap() {
        return sheetInfoMap;
    }


}

1.3 XML文件解析器測試類

      在Test目錄下寫一個測試類,分別測試Sax和Dom4j解析和讀取example.xml文件文件的用時,完整代碼如下

package com.example.mybatis;

import com.example.mybatis.model.SheetInfo;
import com.example.mybatis.utils.SaxDemoHandler;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.junit.Test;
import org.xml.sax.SAXException;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;

public class XmlResolverTest {
    @Test
    public void saxReaderTest(){
        //使用工廠方法獲取一個SAXParserFactory實例
        long startTime = System.currentTimeMillis();
        SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
        //獲取SaxParse實例
        try {
            SAXParser saxParser = saxParserFactory.newSAXParser();
            SaxDemoHandler handler = new SaxDemoHandler();
            saxParser.parse("src/main/resources/excelConfig/example.xml",handler);
            long endTime = System.currentTimeMillis();
            System.out.println("sax resolve example.xml expense time:"+(endTime-startTime)+"ms");
            Map<String, SheetInfo> sheetInfoMap = handler.getSheetInfoMap();

        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }

    }

    @Test
    public void dom4jReaderTest(){
        //1.創建saxReader對象
        long startTime = System.currentTimeMillis();
        SAXReader saxReader = new SAXReader();
        File file = new File("src/main/resources/excelConfig/example.xml");
        try {
            //2.加載xml文件
            Document document = saxReader.read(file);
            //3.獲取根節點
            Element rootElement = document.getRootElement();
            //4.獲取workbook的name屬性值
            String workbookName = rootElement.attributeValue("name");
            System.out.println("workbookName:"+workbookName);
            //4.獲取元素迭代器
            Iterator<Element> sheetIterator = rootElement.elementIterator("sheet");
            while(sheetIterator.hasNext()){
                Element sheetElement = sheetIterator.next();
                String sheetName = sheetElement.attributeValue("name");
                System.out.println("sheetName:"+sheetName);
                Iterator<Element> columnIterator = sheetElement.elementIterator("column");
                while(columnIterator.hasNext()){
                    Element columnElement = columnIterator.next();
                    String columnName = columnElement.attributeValue("name");
                    String displayName = columnElement.attributeValue("displayName");
                    String type = columnElement.attributeValue("type");
                    Integer width = Integer.valueOf(columnElement.attributeValue("width"));
                    System.out.println("columnName:"+columnName+"; displayName:"+displayName+"; type:"+type+"; width:"+width);
                }
            }
            long endTime = System.currentTimeMillis();
            System.out.println("dom4j resolve example.xml expense time:"+(endTime-startTime)+"ms");

        } catch (DocumentException e) {
            e.printStackTrace();
        }

    }
    
}

     分別運行測試類中的saxReaderTes測試方法3次,控制檯打印出的三次耗時分別爲:220ms,149ms,110ms,平均耗時159.67ms;然後在運行測試類中的dom4jReaderTest測試方法3次,控制檯打印出的三次耗時分別爲:91ms,109ms,78ms,平均耗時92.67ms。由此可見dom4j的解析效率要顯著好於Sax,所以筆者在工具類中使用了dom4j解析器

2 Excel導出代碼開發

Apache POI 是Apache軟件基金會開發使用Java分佈式設計或修改Microsoft Office文件的開源庫。它提供 API 給Java程序對Microsoft Office格式檔案讀和寫的功能;Apache POI 是創建和維護操作各種符合Office Open XML(OOXML)標準和微軟的 OLE 2 複合文檔格式(OLE2)的 Java API;HSSFWorkbook類與XSSFWorkbook類分別用於操作Microsoft Excel 2003及以下(.xls格式)和 Microsoft Excel 2007及以上(.xlsx)文件

2.1 Excel文件導出導入工具類

Excel文件的導出過程,其實質就是將數據寫入到excel文件中去的過程,在它的導出Excel方法中傳遞三個參數,分別是一個SheetInfo對象,一個泛型的列表,和一個輸入流對象,代碼如下:

package com.example.mybatis.utils;

import com.example.mybatis.model.ColumnInfo;
import com.example.mybatis.model.SheetInfo;
import org.apache.poi.ss.usermodel.CellType;
//import org.apache.poi.ss.usermodel.FontFamily;
//import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.xssf.usermodel.*;
//import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTColor;

import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;;
import java.util.List;


public class ExcelReadWriteUtil {

    /**
     * poi導出excel工具類
     * @param sheetInfo SheetInfo 對象
     * @param data 泛型數據
     * @param os 輸出流
     * @throws NoSuchMethodException
     * @throws ClassNotFoundException
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     * @throws IOException
     */
    public static void writeExcel(SheetInfo sheetInfo, List<?> data, OutputStream os)throws NoSuchMethodException,ClassNotFoundException,IllegalAccessException, InvocationTargetException, IOException {
        String voClass = sheetInfo.getVoClass();
        List<ColumnInfo> columns = sheetInfo.getColumns();
        XSSFWorkbook workbook = new XSSFWorkbook();
        String sheetName = sheetInfo.getName();
        //創建sheet
        XSSFSheet sheet = workbook.createSheet(sheetName);
        //創建頁頭
        XSSFRow headRow = sheet.createRow(0);
          for(int i=0;i<columns.size();i++){
              ColumnInfo columnInfo = columns.get(i);
              XSSFCell cell = null;
              cell = headRow.createCell(i, CellType.STRING);
              cell.setCellValue(columnInfo.getDisplayName());
          }

            Class clazz = Class.forName(voClass);
            //遍歷data列表創建行
            for(int i=0;i<data.size();i++){
                XSSFRow row = sheet.createRow(i+1);
                // 遍歷columns列表創建列
                for(int j=0;j<columns.size();j++){
                    ColumnInfo columnInfo = columns.get(j);
                    String fieldName = columnInfo.getName();
                    //通過執行反射方法獲取對應的字段值
                    String methodName = "get"+upCaseFirstChar(fieldName);
                    Method method = clazz.getDeclaredMethod(methodName,null);
                    Object obj = method.invoke(data.get(i),null);
                    XSSFCell contentCell = row.createCell(j,CellType.STRING);
                    String value = obj.toString();
                    contentCell.setCellValue(value);
                }

            }
            //將工作簿寫到輸出流中
            workbook.write(os);
            workbook.close();

    }
    
    /**
    *屬性首字母轉大寫
    */
    public static String upCaseFirstChar(String filedName){
        char[] chs = filedName.toCharArray();
        chs[0]= (char) (chs[0]-32);
        return new String(chs);
    }


}

// 以上工具類還只完成了一個沒有樣式設置的Excel導出的公共方法邏輯,接下來回繼續完成Excel文件導入的公共方法邏輯並完善導出Excel文件的樣式設計

2.2 Controller層設計

     新建ExcelController類,並在其導出Excel文件接口中傳入sheetName的參數和一個HttpServletResponse對象參數,代碼如下

package com.example.mybatis.controller;

import com.example.mybatis.model.ServiceResponse;
import com.example.mybatis.service.IExcelService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;


@RestController
public class ExcelController {
    @Autowired
    private IExcelService excelService;

    /**
     * 導出Excel文件接口
     * @param sheetName 配置文件中的sheet名,sheetName必須與example.xml配置文件中sheet標籤中的name屬性一致
     * @param response 響應
     * @return
     */
    @GetMapping("/exportExcel")
    public ServiceResponse<String> exportExcel(@RequestParam("sheetName") String sheetName,HttpServletResponse response){
        logger.info("sheetName={}",sheetName);

        return excelService.exportSheet(sheetName,1,20,response);
    }
}

2.3 Service層設計

新建IExcelService接口類和ExcelService接口實現類,在實現類的Excel導出接口中調用工具類完成導出邏輯,代碼如下:

IExcelService 接口代碼:

package com.example.mybatis.service;

import com.example.mybatis.model.ServiceResponse;

import javax.servlet.http.HttpServletResponse;

public interface IExcelService {

    ServiceResponse<String> exportSheet(String sheetName, Integer page,Integer pageSize,HttpServletResponse response);
}

ExcelService類代碼:

package com.example.mybatis.service.impl;

import com.alibaba.fastjson.JSON;
import com.example.mybatis.business.IUserBusiness;
import com.example.mybatis.model.*;
import com.example.mybatis.service.IExcelService;

import com.example.mybatis.utils.ExcelReadWriteUtil;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletResponse;

import java.io.File;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.*;


@Service("excelService")
public class ExcelService implements IExcelService {


    private static WorkbookInfo workbookInfo;

    @Autowired
    private IUserBusiness userBusiness;
    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");;
    private static final Logger logger = LoggerFactory.getLogger(ExcelService.class);

    /**
     * 在靜態代碼塊中解析example.xml文件中的表和需要導出的列名
     */
    static{
        workbookInfo = new WorkbookInfo();
        Map<String, SheetInfo> sheetInfoMap = new HashMap<>();
        //1.創建saxReader對象
        long startTime = System.currentTimeMillis();
        SAXReader saxReader = new SAXReader();
        File file = new File("src/main/resources/excelConfig/example.xml");
        try {
            //2.加載xml文件
            Document document = saxReader.read(file);
            //3.獲取根節點
            Element rootElement = document.getRootElement();
            //4.獲取workbook的name屬性值
            String workbookName = rootElement.attributeValue("name");
            workbookInfo.setWorkbookName(workbookName);
            //4.獲取元素迭代器
            Iterator<Element> sheetIterator = rootElement.elementIterator("sheet");
            while(sheetIterator.hasNext()){
                Element sheetElement = sheetIterator.next();
                String sheetName = sheetElement.attributeValue("name");
                String voClass = sheetElement.attributeValue("voClass");
                SheetInfo sheetInfo = new SheetInfo(sheetName,voClass);
                Iterator<Element> columnIterator = sheetElement.elementIterator("column");
                while(columnIterator.hasNext()){
                    Element columnElement = columnIterator.next();
                    String columnName = columnElement.attributeValue("name");
                    String displayName = columnElement.attributeValue("displayName");
                    String type = columnElement.attributeValue("type");
                    Integer width = Integer.valueOf(columnElement.attributeValue("width"));
                    ColumnInfo columnInfo = new ColumnInfo(columnName,displayName,type,width);
                    sheetInfo.getColumns().add(columnInfo);
                }
                sheetInfoMap.put(sheetName,sheetInfo);
            }
            workbookInfo.setSheetInfoMap(sheetInfoMap);
            long endTime = System.currentTimeMillis();
            logger.info("dom4j resolve example.xml expense time:"+(endTime-startTime)+"ms");
            logger.info("workbookInfo={}", JSON.toJSONString(workbookInfo));

        } catch (DocumentException e) {
           throw new RuntimeException("resolve excel file error:"+e.getMessage());
        }
    }
     /**
      * sheetName: sheet名
      * page: 所在頁
      *  pageSize: 每頁記錄數
      * response:httpServlet響應對象
     **/
    @Override
    public ServiceResponse<String> exportSheet(String sheetName, Integer page, Integer pageSize, HttpServletResponse response) {
        logger.info("sheetName={}",sheetName);
        ServiceResponse<String> returnResponse = new ServiceResponse<>();
        Date now = new Date();
        String dateTime = sdf.format(now);
        String fileName = sheetName+dateTime+".xlsx";
        try {
            //導出excel文件需要在響應頭設置attachment類型和filename屬性
            response.setHeader("Content-Disposition","attachment;filename="+ URLEncoder.encode(fileName, "UTF-8"));

        } catch (UnsupportedEncodingException e) {
            returnResponse.setStatus(500);
            returnResponse.setMessage(e.getMessage());
            logger.error("encode fileName failed!",e);
            return returnResponse;
        }

//        String workbookName = workbookInfo.getWorkbookName();
        SheetInfo sheetInfo = workbookInfo.getSheetInfoMap().get(sheetName);

        try {
            List<UserTO> userTOList = userBusiness.selectAllUser(1,20);
            //從HttpServletResponse對象中獲取輸出流
            OutputStream os = response.getOutputStream();
            ExcelReadWriteUtil.writeExcel(sheetInfo,userTOList,os);
            returnResponse.setStatus(200);
            returnResponse.setMessage("success");
            returnResponse.setData("ok");
        } catch (Exception e) {
            returnResponse.setStatus(500);
            returnResponse.setMessage("exportSheet failed:"+e.getMessage());
            returnResponse.setData("error");
            logger.error("exportSheet failed!",e);
        }

        return returnResponse;
    }
}


#### 2.4 Business層代碼

```java
package com.example.mybatis.business;

import com.example.mybatis.model.UserTO;
import java.util.List;

public interface IUserBusiness {
    
    List<UserTO> selectAllUser(int startIndex,int pageSize);

}

package com.example.mybatis.business.impl;

import com.example.mybatis.business.IUserBusiness;
import com.example.mybatis.dao.IUserDao;
import com.example.mybatis.model.UserTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class UserBusiness implements IUserBusiness {
    @Autowired
    private IUserDao userDao;
   
    @Override
    public List<UserTO> selectAllUser(int startIndex,int pageSize) {

        return userDao.selectAllUser(startIndex,pageSize);
    }
}

2.5 Dao層代碼

IUserDao.java

@Repository
public interface IUserDao {
    /**
     *
     * @param startIndex
     * @param pageSize
     * @return
     */
    List<UserTO> selectAllUser(@Param("startIndex") int startIndex,@Param("pageSize") int pageSize);


}

src/resources/mapper/IUserDao.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mybatis.dao.IUserDao">
    <select id="selectAllUser" resultType="UserTO" >
            select a.id,a.user_account,a.password,a.nick_name,
                   a.dept_no,b.dept_name,a.phone_num,a.email_address,a.birth_day
            from userinfo a
            inner join dept b
            on a.dept_no=b.dept_no
            where a.id>=#{startIndex,jdbcType=INTEGER} limit #{pageSize,jdbcType=INTEGER}
    </select>
</mapper>

3 導出Excel文件測試

運行SpringBoot項目的啓動類成功後,在瀏覽器中調用GET請求類型接口
http://localhost:8080/exportExcel?sheetName=userInfo
接口調用成功後瀏覽器底腳會顯示下載帶文件名的excel文件,下載完成後瀏覽器右下角會有提示,下載後的文件一般會在本地電腦
C:\Users<主機名>\Downloads 目錄下,也是瀏覽器設置的下載內容位置(如下圖所示),也可通過瀏覽器高級設置選項中的更改下載內容位置對應的更改內容按鈕自定義文件下載位置

谷歌瀏覽器高級設置中下載內容默認位置
Excel文件下載成功後如下圖所示:
下載後打開的userInfo20191201011505.xlsx文件
接下來幾天,筆者將對POI中用於導出導入Excel文件的的一些常用Api進行解讀,併爲實現Excel文件的通用導入接口繼續寫一篇博文,
敬請期待。。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章