java反射和注解在项目中的应用 [案例:excel批量导入封装对象]
最近在项目中遇到了一个批量导入excel的功能,excel导入用到的是esaypoi,可以轻松将excel中的数据封装成对象,但是不知为何,突然转换对象的过程变得很慢,一万条数据得转换一分钟。无奈只能手动去写这些逻辑。最终把它封装成一个可以方便直接转换成对象得工具类。
现有一个excel表,如下图:
现需要把excel表中的每一行都封装在一个java对象中,当然如果用poi的api可以很轻松的完成这些工作,但是如果对于不同的excel和对应的类,我们必须编写不同的逻辑代码。怎么才能写一个工具类框架来应对不同的excel和对象的转换呢。下面就实现一下,并认真分析一下实现的过程,测试结果还可以,10000条数据3秒左右吧~。
excel表已经有了,还需要一个类,这个类需要满足excel的列和类属性要对应如下:
class Student{
private Integer id;
private String name;
private Integer sex;
}
可以看到性别这一个属性在excel中是中文字符,但是属性确是Integer类型,其实还有一个需求就是可以将 男变成1,女变成0储存在sex属性中。很明显目前只有excel和类还无法满足需求,因为类的属性和excel的列无法对应。所以还需要一个注解,用这个注解将属性和列对应起来,如下:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface JExcel{
public String name();
public String[] replace() default {};
}
目前就以这个简单的注解为例;那么最后的类应该是这样的。
class Student{
@JExcel(name="序号")
private Integer id;
@JExcel(name="姓名")
private String name;
@JExcel(name="性别",replace = {"男_1","女_2"})
private Integer sex;
@Override
public String toString() {
return "Student [id=" + id + ", name=" + name + ", sex=" + sex + "]";
}
}
注解的replace属性的意思就是将列中的男,变成数字1,女变成数字2。
首先我们要用poi接口解析excel。
public <T> List<T> readExcelContentByList(InputStream is,Class<T> clazz) throws InterruptedException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException, InvalidFormatException {
List<T> list = new ArrayList<T>();
Workbook wb=null;
try {
//创建excel
wb = create(is);
} catch (IOException e) {
e.printStackTrace();
}
Sheet sheet = wb.getSheetAt(0);
// 得到总行数
int rowNum = sheet.getLastRowNum();
Row row = sheet.getRow(0);//获取第0行对象,这一行是标题
// 得到总列数
int colNum = row.getPhysicalNumberOfCells();
...
}
这里的代码不完整,下面有完整的代码,这一步获取了excel对象,并且可以获取到excel的行数rowNum和列数colNum,现在默认第一列为标题。
第二步我们要解析类的结构,可以看到传进来的参数有一个clazz,这就是我们需要转换的目标对象的类,首先要获取该类的所有含有注解的属性,把这些属性进一步封装一下放入一个map集合,方便查找,没有注解的就不考虑了。
//属性集合,有注解的属性全部拿出来,为了后面做缓存使用
Map<String,FieldEntity> fmap=new HashMap<String, FieldEntity>();
Field[] fses=clazz.getDeclaredFields(); //获取所有属性
for(Field f:fses) {//遍历属性
JExcel e=f.getAnnotation(JExcel.class);//获取属性的Excel注解
if(e!=null) {
String[] g=e.replace();//如果有注解的话,获取注解上的 replace 属性
//用Excel注解的名字作为主键存放在map里面
fmap.put(e.name(),new FieldEntity(f,g.length==0?false:true,g));
}
}
封装属性的类为:
protected class FieldEntity{
public FieldEntity(Field f,boolean b,String[] g) {
this.field=f; //方法属性
this.b=b; //该方法的注解上是否有 replace 属性
this.g=g; //这是 replace 属性的值 例如:{"男_1", "女_0"}
}
public Field field;
public boolean b;
public String[] g;
}
这一步应该说很关键,众所周知,java反射的效率一般是很低的,如果我们在遍历excel数据的时候才去解析类,找到对应的属性,然后再去反射赋值的话,这样效率是很底的,速度是很慢的。所以为例提高效率需要做两件事:
- 第一, 缓存目标类的属性,避免每次再去从目标类身上获取属性,提高效率。
- 第二,可以用excel列的序号(下标)和目标类的属性完成映射,说白了就是用一个数组,数组储存的是目标类的属性,数组的下表呢就是excel的列的序号(下表)。这样在遍历excel列的时候,可以通过列的序号(下表)来快速定位到该列对应的目标类的属性。可以用一张图来解释一下。
代码实现:
//fs是列序号和属性的对应关系
//比如第一列对应id属性,那么fs[0]就是存放的id属性
FieldEntity[] fs=new FieldEntity[colNum];
//遍历所有标题
//这里遍历的目的是和单元格的每一列的标题(或者说和每一列的序号)对应,这样可以理解为是一个缓存
//然后把这一个对应关系存放在fs集合里边
//这样做的好处是后面遍历每一列的单元格的时候可以直接通过每一列的序号,快速找到该列对应的属性。
for(int j=0;j<colNum;j++) {//遍历列明
String name=getVal(row.getCell(j));//获取单元格的值,也是每一列的标题
if(fmap.containsKey(name)) {
fs[j]=fmap.get(name);//通过这个标题找到fmap集合里面对应的属性
}
}
经过这一步就完成了属性和列名的映射。
这是最后一步解析excel数据,遍历excel的每一行每一列,将其封装进对象里。因为代码用到了很多反射的知识,再次不进一步解释了,代码中有注释。
// 正文内容应该从第二行开始,第一行为表头的标题
for (int i = 1; i <= rowNum; i++) {
row = sheet.getRow(i);//获取行
int j = 0;//从第0列开始
boolean b=false;
T ise=clazz.newInstance();//创建目标对象
while (j < colNum) {
//遍历所有列,c对象就是第j列的单元格
Cell c = row.getCell((short)j);
if(c!=null) {
String s=getVal(c);//获取单元格的值
if(s!=null&&!"".equals((s=s.trim()))) {
b=true;
if(fs[j]!=null) {
//执行fs[j].field.setAccessible(true);以后会使私有属性可以被访问,否则你是无法访问私有属性的
fs[j].field.setAccessible(true);
if(fs[j].b) {//如果该属性有需要替换的值,也就是replace有值
//那就去解析,并返回对应的值
fs[j].field.set(ise,trans(fs[j].g,s,fs[j].field.getType()));
}else {
//直接赋值,但是得保证属性必须是String类型
Class<?> ty=fs[j].field.getType();//获取属性的类型
Constructor<?> con = ty.getConstructor(String.class);//获取该类型的构造方法(要求改类的构造方法必须有一个String类型的参数)
Object obj=con.newInstance(s);//创建属性对象
fs[j].field.set(ise,obj);//给ise对象的属性赋值
}
}
}
}
j++;
}
if(b) {
list.add(ise);//将对象添加进list集合
}
}
//返回所有对象
return list;
}
/**
* 获取单元格的值
* @param c
* @return
*/
protected String getVal(Cell c) {
c.setCellType(CellType.STRING);
return c.getStringCellValue();
}
还有一个方法就是解析替换的方法。。。不多说了看代码把~
/**
* 转换并返回 需要 替换的对象
* g:替换的方式 例如 {'男_1','女_2'}
* s:对比的值 比如 男
* c:替换后数据的类型,也是属性的类型,比如:
* @Excel(name = "性别",replace = {"男_1", "女_0"})
* private Integer sex;
* //sex是Integer类型,那么c就是Integer.class
*/
protected Object trans(String [] g,String s,Class<?> c) throws InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
//遍历 注解中的 replace 属性
for(int i=0;i<g.length;i++) {
String [] sc = g[i].split("_");
if(sc[0].equals(s)) {//单元格的值是否和替换的值相等
//获取目标属性数据类型的构造方法,该方法接收一个String类型的参数
Constructor<?> con=c.getConstructor(String.class);
//创建目标对象并返回
return con.newInstance(sc[1]);
}
}
//没有任何符合条件的 就 返回null
return null;
}
就解释这么多把~
下面看完整代码:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.poi.POIXMLDocument;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
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;
/**
* 注解
* @author LENOVO
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface JExcel{
public String name();
public String[] replace() default {};
}
/**
* 学生类
* @author LENOVO
*
*/
class Student{
@JExcel(name="序号")
private Integer id;
@JExcel(name="姓名")
private String name;
@JExcel(name="性别",replace = {"男_1","女_2"})
private Integer sex;
@Override
public String toString() {
return "Student [id=" + id + ", name=" + name + ", sex=" + sex + "]";
}
}
public class ExcelImportUtilForJia {
public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException, InvalidFormatException, FileNotFoundException, InterruptedException {
long l=System.currentTimeMillis();
List<Student> list=new ExcelImportUtilForJia().readExcelContentByList(new FileInputStream(new File("E:\\test\\a\\test.xlsx")),Student.class);
long l2=System.currentTimeMillis();
System.out.println(l2-l);
}
public <T> List<T> readExcelContentByList(InputStream is,Class<T> clazz) throws InterruptedException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException, InvalidFormatException {
List<T> list = new ArrayList<T>();
Workbook wb=null;
try {
//创建excel
wb = create(is);
} catch (IOException e) {
e.printStackTrace();
}
Sheet sheet = wb.getSheetAt(0);
// 得到总行数
int rowNum = sheet.getLastRowNum();
Row row = sheet.getRow(0);//获取第0行对象,这一行是标题
// 得到总列数
int colNum = row.getPhysicalNumberOfCells();
//属性集合,有注解的属性全部拿出来,为了后面做缓存使用
Map<String,FieldEntity> fmap=new HashMap<String, FieldEntity>();
Field[] fses=clazz.getDeclaredFields(); //获取所有属性
for(Field f:fses) {//遍历属性
JExcel e=f.getAnnotation(JExcel.class);//获取属性的Excel注解
if(e!=null) {
String[] g=e.replace();//如果有注解的话,获取注解上的 replace 属性
//用Excel注解的名字作为主键存放在map里面
fmap.put(e.name(),new FieldEntity(f,g.length==0?false:true,g));
}
}
//fs是列序号和属性的对应关系
//比如第一列对应id属性,那么fs[0]就是存放的id属性
FieldEntity[] fs=new FieldEntity[colNum];
//遍历所有标题
//这里遍历的目的是和单元格的每一列的标题(或者说和每一列的序号)对应,这样可以理解为是一个缓存
//然后把这一个对应关系存放在fs集合里边
//这样做的好处是后面遍历每一列的单元格的时候可以直接通过每一列的序号,快速找到该列对应的属性。
for(int j=0;j<colNum;j++) {//遍历列明
String name=getVal(row.getCell(j));//获取单元格的值,也是每一列的标题
if(fmap.containsKey(name)) {
fs[j]=fmap.get(name);//通过这个标题找到fmap集合里面对应的属性
}
}
// 正文内容应该从第二行开始,第一行为表头的标题
for (int i = 1; i <= rowNum; i++) {
row = sheet.getRow(i);//获取行
int j = 0;//从第0列开始
boolean b=false;
T ise=clazz.newInstance();//创建目标对象
while (j < colNum) {
//遍历所有列,c对象就是第j列的单元格
Cell c = row.getCell((short)j);
if(c!=null) {
String s=getVal(c);//获取单元格的值
if(s!=null&&!"".equals((s=s.trim()))) {
b=true;
if(fs[j]!=null) {
//执行fs[j].field.setAccessible(true);以后会使私有属性可以被访问,否则你是无法访问私有属性的
fs[j].field.setAccessible(true);
if(fs[j].b) {//如果该属性有需要替换的值,也就是replace有值
//那就去解析,并返回对应的值
fs[j].field.set(ise,trans(fs[j].g,s,fs[j].field.getType()));
}else {
//直接赋值,但是得保证属性必须是String类型
Class<?> ty=fs[j].field.getType();//获取属性的类型
Constructor<?> con = ty.getConstructor(String.class);//获取该类型的构造方法(要求改类的构造方法必须有一个String类型的参数)
Object obj=con.newInstance(s);//创建属性对象
fs[j].field.set(ise,obj);//给ise对象的属性赋值
}
}
}
}
j++;
}
if(b) {
list.add(ise);//将对象添加进list集合
}
}
//返回所有对象
return list;
}
/**
* 转换并返回 需要 替换的对象
* g:替换的方式 例如 {'男_1','女_2'}
* s:对比的值 比如 男
* c:替换后数据的类型,也是属性的类型,比如:
* @Excel(name = "性别",replace = {"男_1", "女_0"})
* private Integer sex;
* //sex是Integer类型,那么c就是Integer.class
*/
protected Object trans(String [] g,String s,Class<?> c) throws InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
//遍历 注解中的 replace 属性
for(int i=0;i<g.length;i++) {
String [] sc = g[i].split("_");
if(sc[0].equals(s)) {//单元格的值是否和替换的值相等
//获取目标属性数据类型的构造方法,该方法接收一个String类型的参数
Constructor<?> con=c.getConstructor(String.class);
//创建目标对象并返回
return con.newInstance(sc[1]);
}
}
//没有任何符合条件的 就 返回null
return null;
}
/**
* 封装方法 和 需要替换的对象
* @author LENOVO
*
*/
protected class FieldEntity{
public FieldEntity(Field f,boolean b,String[] g) {
this.field=f; //方法属性
this.b=b; //该方法的注解上是否有 replace 属性
this.g=g; //这是 replace 属性的值 例如:{"男_1", "女_0"}
}
public Field field;
public boolean b;
public String[] g;
}
/**
* 获取单元格的值
* @param c
* @return
*/
protected String getVal(Cell c) {
c.setCellType(CellType.STRING);
return c.getStringCellValue();
}
/**
* 判断excel的版本
* @param in
* @return
* @throws IOException
* @throws InvalidFormatException
*/
protected static Workbook create(InputStream in) throws IOException,InvalidFormatException {
if (!in.markSupported()) {
in = new PushbackInputStream(in, 8);
}
if (POIFSFileSystem.hasPOIFSHeader(in)) {
return new HSSFWorkbook(in);
}
if (POIXMLDocument.hasOOXMLHeader(in)) {
return new XSSFWorkbook(OPCPackage.open(in));
}
throw new IllegalArgumentException("你的excel版本目前poi解析不了");
}
}