java反射和注解在项目中的应用 [案例:excel批量导入封装对象]

硅谷探秘者 1634 0 0

        最近在项目中遇到了一个批量导入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数据,遍历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解析不了");
    }
}

评论区
请写下您的评论...
暂无评论...
猜你喜欢
weblog 1273 java使easypoiexcel实体类:importcn.afterturn.easypoi.excel.annotation.Excel
算法基础 2269 什么是jxlsjxls是一个简单、轻excel出库,使特定标记excel模板文件来定义输出格式布局。其实java成熟excel出工具有pol、jxl,但他们都是使java
spring/springmvc 6012 springmvc启动时从数据库初始化系统常设计标是,把系统常配置,放数据库初始化时从获取配置信息,利技术,把key-value值自动进配置类。1
算法基础 2861 Java机制可以让我们获取某个类所有信息,本文要聊是如何获取某个类,某个类属性,某个类方法修饰符。或判断某类、属性、方法上是否有某个修饰符。Class、Field、Method这三
spring/springmvc 1630 "springioc容器获取aop受理",这句话是什么意思呢?有时候我们会spring一下类进行代理,比如我们会springaop自定义一些接口访问添加日志,再比如
java基础 3236 asm实方法1.需要jar包2.我们需要通过asm生成标类如下:packageclub.jiajia.test3;publicclassExamp5
weblog 6102 java使easypoiexcel时进行值替换如出性别:数据库储存是数字,1代表男,2代表女。时候需要是汉字。实体类使@Excel。 @Excel(name="性别
框架 5491 springmvc,如一个页面调另一个接口会产生跨域问题403。于一个接口而言很好决跨域问题,springmvc只需要接口上加一个。@CrossOrigin
归档
2018-11  12 2018-12  33 2019-01  28 2019-02  28 2019-03  32 2019-04  27 2019-05  33 2019-06  6 2019-07  12 2019-08  12 2019-09  21 2019-10  8 2019-11  15 2019-12  25 2020-01  9 2020-02  5 2020-03  16 2020-04  4 2020-06  1 2020-07  7 2020-08  13 2020-09  9 2020-10  5 2020-12  3 2021-01  1 2021-02  5 2021-03  7 2021-04  4 2021-05  4 2021-06  1 2021-07  7 2021-08  2 2021-09  8 2021-10  9 2021-11  16 2021-12  14 2022-01  7 2022-05  1 2022-08  3 2022-09  2 2022-10  2 2022-12  5 2023-01  3 2023-02  1 2023-03  4 2023-04  2 2023-06  3 2023-07  4 2023-08  1 2023-10  1 2024-02  1 2024-03  1 2024-04  1 2024-08  1
标签
算法基础 linux 前端 c++ 数据结构 框架 数据库 计算机基础 储备知识 java基础 ASM 其他 深入理解java虚拟机 nginx git 消息中间件 搜索 maven redis docker dubbo vue 导入导出 软件使用 idea插件 协议 无聊的知识 jenkins springboot mqtt协议 keepalived minio mysql ensp 网络基础 xxl-job rabbitmq haproxy srs 音视频 webrtc javascript 加密算法
目录
没有一个冬天不可逾越,没有一个春天不会来临。最慢的步伐不是跬步,而是徘徊,最快的脚步不是冲刺,而是坚持。