Excel导入简易版(一)

/ java / 没有评论 / 479浏览

写在前面

需要说明的是,这篇文章的主旨不是要实现一个完美的Excel导入功能,而是希望做到抛砖引玉以及帮助部分同学展开自我思考的思路。

好了,话不多说,我们直接进入正题。

背景定义

我们在完成功能前一般需要梳理一下需求,由于本篇文章旨在引导,所以需求一般会比较简单。

我们定义如下背景:

某公司由于业务需要,现想要自主研发管理系统,这就需要将公司部分数据从原有系统导出(现有系统已支持),然后导入到新系统中。由于公司体量庞大,业务和数据较多,所以公司领导层希望研发部逐步完成数据转移。研发部的第一个功能就是人员信息数据转移,已知原有人员信息模本中盖内容包括:所在部门,姓名,性别,出生日期,年龄,户籍所在地,居住地,就职时间,婚姻状况,教育程度,联系电话,紧急联系人,紧急联系人电话。

由于研发部主要都是Java开发人员,所以研发部决定先用java开发一版demo,于是采集了部分样本数据:人员信息

alt

由于业务庞大,为分工明确,研发部将数据导入分为多个模块(eg: 文件上传,数据转换,数据填充,相关业务初始化,数据入库)。

分析需求

其他模块暂且不分析,我们简要分析一下上述背景中关于数据转换需求:

貌似这个功能很简单,我们就直接开始吧。

创建Demo Project

我们尽量精简一点,我们使用Eclipse(或者idea)创建一个Maven项目
alt

初始化好框架代码

读取Excel文件

需要注意的是,我们虽然是要读取文件,所以第一步就是找到文件,所以我们先创建一个FileUtil类
然后完成第一个方法,读取文件:

package cn.com.pfinfo.excel.util;

import java.io.File;
import java.io.FileNotFoundException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;

/**
 * 文件操作工具类
 * 
 * @author cuitpf
 *
 */
public class FileUtil {

    /**
     * 根据文件路径找到对应的文件,并返回文件对象。
     * 
     * @param fileLocation 文件本地路径
     * @return 文件对象
     * @throws FileNotFoundException
     */
    public static File getFile(String fileLocation) throws FileNotFoundException {
        if (fileLocation == null) {
            throw new RuntimeException("文件地址不能为空");
        }
        ClassLoader cl = FileUtil.class.getClassLoader();
        URL url = (cl != null ? cl.getResource(fileLocation) : ClassLoader.getSystemResource(fileLocation));
        if (url == null) {
            throw new FileNotFoundException("本地文件不存在");
        }
        // Load file by URL
        try {
            URI uri = new URI(url.toString().replaceAll(" ", "%20"));
            return new File(uri.getSchemeSpecificPart());
        } catch (URISyntaxException ex) {
            // Fallback for URLs that are not valid URIs (should hardly ever happen).
            return new File(url.getFile());
        }
    }

}

加载工作簿

这一步怎么都绕不过去的就是依赖apatch-poi。这里给出maven配置依赖的方式

    <!-- https://mvnrepository.com/artifact/org.apache.poi/poi -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>${poi.version}</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>${poi.version}</version>
    </dependency>

其他直接导入jar包的或者其他构建工具的读者请自行添加依赖。

添加了依赖后,我们就可以开始编码了:

package cn.com.pfinfo.excel.util;

import static cn.com.pfinfo.excel.config.ConstantConfig.XLS;
import static cn.com.pfinfo.excel.config.ConstantConfig.XLSX;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;

import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import cn.com.pfinfo.excel.config.ConstantConfig;
import cn.com.pfinfo.excel.exception.ImportExcelBaseException;

/**
 * 文件操作工具类
 * 
 * @author cuitpf
 *
 */
public class FileUtil {

    /**
     * 根据文件路径找到对应的文件,并返回文件对象。
     * 
     * @param fileLocation 文件本地路径
     * @return 文件对象
     * @throws FileNotFoundException
     */
    public static File getFile(String fileLocation) throws FileNotFoundException {
        if (fileLocation == null) {
            throw new RuntimeException("文件地址不能为空");
        }
        ClassLoader cl = FileUtil.class.getClassLoader();
        URL url = (cl != null ? cl.getResource(fileLocation) : ClassLoader.getSystemResource(fileLocation));
        if (url == null) {
            throw new FileNotFoundException("本地文件不存在");
        }
        // Load file by URL
        try {
            URI uri = new URI(url.toString().replaceAll(" ", "%20"));
            return new File(uri.getSchemeSpecificPart());
        } catch (URISyntaxException ex) {
            // Fallback for URLs that are not valid URIs (should hardly ever happen).
            return new File(url.getFile());
        }
    }

    /**
     * 根据文件路径获取文件中的工作簿,如果文件不是{@link ConstantConfig#XLS}或者{@link ConstantConfig#XLSX}格式的,会返回{@code null}对象
     *
     * @param localFilePath 文件路径
     * @return 如果文件格式是excel格式,返回工作簿对象,否则,返回null对象
     * @throws ImportExcelBaseException 文件如果受损导致不能打开,将抛出自定义异常。
     */
    public static Workbook getWorkbook(String localFilePath) throws ImportExcelBaseException {
        try (FileInputStream fileInputStream = new FileInputStream(getFile(localFilePath))) {
            if (localFilePath.endsWith(XLS)) {
                return new HSSFWorkbook(fileInputStream);
            } else if (localFilePath.endsWith(XLSX)) {
                return new XSSFWorkbook(fileInputStream);
            }
            return null;
        } catch (IOException e) {
            throw new ImportExcelBaseException(e);
        }
    }

}

建立映射关系

excel如何转换为java对象呢?我们需要建立一个映射关系,告诉程序,比如:当你发现姓名这一列的时候,它对应的对象属性为name。

这样的映射关系我们传统编码中,是这样写的(代码太长,我就用伪代码表示一下了):

read line
  read cell from line
     get cell.index of line
     if(index = 1)
         user.name = cell.value
      else if (index = 2)
          user.sex = cell.value
      ...
return user

这种方式当然也能实现功能,但如果某天不需要某个属性了,模版中删除了一列,这里的代码改动可能不会太小。所以上面的代码可能不太优雅,我们需要使用优雅的代码来解决问题。

由于jdk1.5推出了注解这个概念,所以在那之后多数公司在实现excel导入的时候,都采用注解的方式进行协助。我们这里也采用注解协助开发。

在此之前我们需要了解几个概念。java是面向对象的编程语言,万物皆对象。所以我们需要先分析编码在我们可能会接触到的对象概念:工作簿,工作表,行,列,单元格。

我们导入数据的时候,就是把一个个工作表导入成为一个个对应的集合,表中的一行数据对应集合中的一个元素。

反过来,我们一个元素就是描述的某个工作表的某一行数据,元素的属性就是对应的这一行的一个个单元格的数据,具体对应关系就是元素属性和列的对应关系

通常,我们会将第一行标记为表头,就像这样:

alt

所以,属性的数据就是对应的表头所在列的数据。

最后总结一下:

现在可以创建注解了。

首先是类注解:

package cn.com.pfinfo.excel.anno;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 工作表注解,用于描述实体Bean对应的工作表
 * @author cuitpanfei
 *
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Sheet {    

    String value() defult "Sheet1";

}

然后是属性注解:

package cn.com.pfinfo.excel.anno;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 工作表注解,用于描述实体Bean对应的工作表
 * @author cuitpanfei
 *
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Cell {    

    /** 对应的列名称 */
    String name() default "";

    /** 列序号 */
    int index();

    /** 字段类型对应的格式 */
    String format() default "";

    /** 校验是否可以为空 */
    boolean isEmpty() default true;

}

读取数据

这一步比较冗长,我们先写一个大致代码框架出来(不必要的代码这里都已经略过了)

public <T> List<T> map(String sheetName, Class<T> clazz){
    //获取表头映射关系

    //检查获取到的映射关系

    //加载工作表

    //获取工作表 表头

    //检查表头和映射关系是否匹配

    //根据映射关系和表头的关系加载表数据

    //映射数据为泛型数据集合

    //返回映射结果
    return null;
}

最后

大体的脉络我们清楚了,后续主要是填充好就好了。
(未完待续)