随时随地阅读更多技术实战干货,获取项目源码、学习资料,请关注源代码社区公众号(ydmsq666)
在实际项目中,有时候会遇到表格数据导入数据库的需求,如果数据量非常大,那么采用人工的方式显然是不可取的,非常耗费时间和人力,这时候我们可以写一段代码来实现自动导入,只要制定好了规则,就非常方便,在java中Apache POI开源项目提供了非常好的实现,使用起来也非常简单方便,下面用一个简单示例来说明。
首先,我们把一行数据对应为一个对象,对象是java封装的精髓,下面以学生信息为例:
Student.java:
public class Student {
/** 姓名 */
private String name;
/** 年龄 */
private String age;
/** 学号 */
private String number;
public Student() {
super();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
@Override
public String toString() {
return "name:" + name + "--" + "age:" + age + "--number:" + number;
}
}
然后就是测试的表格数据,和对象属性是一一对应的:
然后就是核心代码,操作Excel的工具类,先上代码再解析:
ExcelUtil.java:
package com.home.base.file;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DateUtil;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import com.home.base.reflect.ReflectUtil;
/**
* Excel操作工具类,保证格式标准,没有空行空列,第一行为标题,第二行为内容,目前所有内容都读取,没做过滤
*
* @author fengjian
*
*/
public class ExcelUtil {
private Logger logger = Logger.getLogger(ExcelUtil.class);
private Workbook wb;
private Sheet sheet;
private Row row;
/** 当前读取的sheet下标,默认为0 */
private int sheetIndex = 0;
/**
* 构造方法,传入文件路径
*
* @param filepath
*/
public ExcelUtil(String filepath) {
if (!FileUtil.isExist(filepath)) {
logger.error("文件不存在:" + filepath);
return;
}
String ext = filepath.substring(filepath.lastIndexOf("."));
logger.info("ext:" + ext);
try {
InputStream is = new FileInputStream(filepath);
if (".xls".equals(ext)) {
wb = new HSSFWorkbook(is);
} else if (".xlsx".equals(ext)) {
wb = new XSSFWorkbook(is);
} else {
wb = null;
}
} catch (Exception e) {
logger.error("", e);
}
}
/**
* 读取Excel表格表头的内容
*
* @param InputStream
* @return String 表头内容的数组
*/
@SuppressWarnings("deprecation")
public String[] readExcelTitle() throws Exception {
if (wb == null) {
throw new Exception("Workbook对象为空!");
}
sheet = wb.getSheetAt(sheetIndex);
row = sheet.getRow(0);
// 标题总列数
int colNum = row.getPhysicalNumberOfCells();
String[] title = new String[colNum];
for (int i = 0; i < colNum; i++) {
row.getCell(i).setCellType(Cell.CELL_TYPE_STRING);
title[i] = row.getCell(i).getStringCellValue();
}
return title;
}
/**
* 赋值接口
*
* @author fengjian
*
* @param <T>
*/
public interface AssignCallback<T> {
/**
* 赋值
*
* @param map
*/
public T assign(Map<String, Object> map);
}
/**
* 读取Excel数据内容
*
* @param cls
* 泛型类模板
* @param callback
* 回调接口,用于生成每行数据的对象
* @return 把每行数据封装成一个对象,然后返回所有行对象的数据集合
* @throws Exception
*/
public <T> List<T> readExcel(Class<T> cls, AssignCallback<T> callback) throws Exception {
if (wb == null) {
throw new Exception("Workbook对象为空!");
}
List<T> content = new ArrayList<T>();
sheet = wb.getSheetAt(sheetIndex);
// 得到总行数
int rowNum = sheet.getLastRowNum();
row = sheet.getRow(0);
String[] titles = readExcelTitle();
// 正文内容应该从第二行开始,第一行为表头的标题
for (int i = 1; i <= rowNum; i++) {
row = sheet.getRow(i);
if (row != null) {
int j = 0;
Map<String, Object> cellValue = new HashMap<String, Object>();
while (j < titles.length) {
Object obj = getCellFormatValue(row.getCell(j));
if (obj != null) {
cellValue.put(titles[j], obj);
}
j++;
}
// 利用回调接口完成赋值
T t = callback.assign(cellValue);
content.add(t);
}
}
return content;
}
/**
* 读取Excel数据内容
*
* @param cls
* @param callback
* @return 把每行数据封装成一个对象,然后返回所有行对象的数据集合
* @throws Exception
*/
public <T> List<T> readExcel(Class<T> cls) throws Exception {
if (wb == null) {
throw new Exception("Workbook对象为空!");
}
List<T> content = new ArrayList<T>();
sheet = wb.getSheetAt(sheetIndex);
// 得到总行数
int rowNum = sheet.getLastRowNum();
row = sheet.getRow(0);
String[] titles = readExcelTitle();
// 正文内容应该从第二行开始,第一行为表头的标题
T t;
for (int i = 1; i <= rowNum; i++) {
row = sheet.getRow(i);
if (row != null) {
int j = 0;
t = cls.newInstance();
List<Object> cellValue = new ArrayList<Object>();
while (j < titles.length) {
Object obj = getCellFormatValue(row.getCell(j));
if (obj != null) {
cellValue.add(obj);
}
j++;
}
ReflectUtil.setField(t, cls.getName(), cellValue);
content.add(t);
}
}
return content;
}
/**
*
* 根据Cell类型设置数据
*
* @param cell
* @return
*/
@SuppressWarnings("deprecation")
private Object getCellFormatValue(Cell cell) {
Object cellvalue = "";
if (cell == null) {
return cellvalue;
}
// 判断当前Cell的Type
switch (cell.getCellType()) {
case Cell.CELL_TYPE_NUMERIC:// 如果当前Cell的Type为NUMERIC
case Cell.CELL_TYPE_FORMULA: {
// 判断当前的cell是否为Date
if (DateUtil.isCellDateFormatted(cell)) {
cellvalue = cell.getDateCellValue();
} else {// 如果是纯数字
cellvalue = String.valueOf(cell.getNumericCellValue());
}
break;
}
case Cell.CELL_TYPE_STRING:// cell的Type为STRING
// 取得当前的Cell字符串
cellvalue = cell.getRichStringCellValue().getString();
break;
default:// 默认的Cell值
cellvalue = "";
}
return cellvalue;
}
public int getSheetIndex() {
return sheetIndex;
}
/**
* 设置当前读取的sheet下标,第一个为0,以此类推
*
* @param sheetIndex
*/
public void setSheetIndex(int sheetIndex) {
this.sheetIndex = sheetIndex;
}
}
第一步,构造方法,完成读取表格的初始化处理,将表格文件以流的方式关联到Workbook对象中,不同的表格扩展名构建不同的Workbook实现对象,方便后续使用。
第二步,在读取表格数据时,先要读取表格的表头,对应方法为readExcelTitle,方便后面对数据进行归类和获取。
第三步,读取表格实际内容,这里提供了两种方式。
1:public <T> List<T> readExcel(Class<T> cls, AssignCallback<T> callback)
第一个参数传入每行表格要封装的对象,就是上面的Student,这里使用Java的泛型,便于在实际需求中扩展为任意的对象类型。
第二个参数是一个回调接口AssignCallback,这个接口的作用是将读取到的一行数据转换为具体的实例对象,简单说就是把表格的一行数据封装成一个Student对象。接口原型如下:
public interface AssignCallback<T> {
/**
* 赋值
*
* @param map
*/
public T assign(Map<String, Object> map);
}
map是表格的一行数据,键就是表格标题。
方法的思路很简单,就是将表格进行遍历,将每一行数据转成对象,然后返回所有数据对象的集合,这样,外部调用者获取到数据集合后,就可以进行入库操作,入库操作在这里就省略了。
2:public <T> List<T> readExcel(Class<T> cls)
第二种读取方式,少了回调接口,那怎样完成赋值呢,这里采用的java的反射机制,将读出来的数据进行赋值,考虑到标题多数为中文,程序代码里面是英文,所以这里没有做表格数据和对象属性的映射,要求表格的标题顺序和对象的属性顺序一致,这样就省掉了第一个方法回调接口里面根据表格标题对应对象属性的步骤。相比较而言,第二种方式虽然少了赋值的步骤,但是灵活性比第一种差多了,对对象的属性和顺序要求严格,在实际中还是以第一种为主,也比较方便控制业务的异常。
其它实现方式和第一个方法一样,都是采用的POI库里面提供的读取接口。
这里用到反射赋值,相关代码如下:
ReflectUtil.java:
/**
* 设置属性的所有值,要求值的集合顺序和属性的定义属性一致,并且个数长度一致
*
* @param object
* 目标对象,如果是静态域本参数传入null即可
* @param clazzName
* 目标类名(带完整包名)
* @param values
* 需要设置的属性的值的集合
* @return 是否设置成功
*/
public static boolean setField(Object object, String clazzName, List<Object> values) {
if (isEmpty(clazzName)) {
logger.error("参数异常,clazzName=" + clazzName);
return false;
}
if (isEmpty(values)) {
logger.error("参数异常,values=" + values);
return false;
}
try {
Class<?> clazz = Class.forName(clazzName);
List<Field> fields = getFields(clazz);
if (isEmpty(fields)) {
logger.error("没有属性,fields=" + fields);
return false;
}
if (fields.size() != values.size()) {
logger.error("属性和参数集合的长度不一致,field.size=" + fields.size() + "--values.size=" + values.size());
return false;
}
for (int i = 0; i < fields.size(); i++) {
boolean result = setField(object, clazz, fields.get(i), values.get(i));
if (result == false) {
return false;
}
}
return true;
} catch (Exception e) {
logger.error("Exception:" + e);
}
return false;
}
/**
* 获取目标类的所有Field
*
* @param clazz
* 目标class
* @return 所有 Fields的集合
*/
public static List<Field> getFields(Class<?> clazz) {
if (clazz == null) {
logger.error("参数异常,clazz=" + clazz);
return null;
}
List<Field> fields = new ArrayList<Field>();
Field[] f = clazz.getDeclaredFields();
Collections.addAll(fields, f);
return fields;
}
/**
* 设置Field值
*
* @param object
* 目标对象,如果是静态域本参数传入null即可
* @param clazz
* 目标类名(带完整包名)
* @param field
* 目标Field
* @param value
* 新的Field值
* @return true表示设置Field值成功
*/
public static boolean setField(Object object, Class<?> clazz, Field field, Object value) {
if (object == null || clazz == null || field == null) {
logger.error("参数异常,object=" + object + ",clazz=" + clazz + ",field=" + field);
return false;
}
try {
field.setAccessible(true);
field.set(object, value);
return true;
} catch (Exception e) {
logger.error("Exception:" + e);
}
return false;
}
/**
* 判断集合是否有数据
*
* @param list
* @return
*/
public static boolean isEmpty(Collection<?> collection) {
return (collection == null || collection.isEmpty());
}
这个是常规的反射赋值,就不做多的解释了。
使用到的读取表格的库分别如下,poi-3.17-beta1.jar,poi-ooxml-3.17-beta1.jar,poi-ooxml-schemas-3.17-beta1.jar,这个在网上很容易下载到。
测试代码:
public static void main(String[] args) throws Exception {
String filepath = "F:/test.xls";
ExcelUtil reader = new ExcelUtil(filepath);
// 第二种方式
// List<Student> list = reader.readExcel(Student.class);
// 第一种方式
List<Student> list = reader.readExcel(Student.class, new ExcelUtil.AssignCallback<Student>() {
@Override
public Student assign(Map<String, Object> map) {
// 构造每一条数据的对象,这种方式比较灵活
Student student = new Student();
student.setName(map.get("姓名").toString());
student.setAge(map.get("年龄").toString());
student.setNumber(map.get("学号").toString());
return student;
}
});
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
测试结果: