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文件的通用导入接口继续写一篇博文,
敬请期待。。。。

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