软工项目->java 注解与反射机制实战:springboot+mybatis项目中的excel工具类
契机
这次开发的软工教学助手中,涉及excel文件的导入问题。在此之前的类似项目中,都是写一些复用性很差、很繁琐的代码来处理excel表格。此次软工课上系统地规划了一下时间,发现按照时间规划可以好好打磨一下这部分代码,写出一个方便快捷复用性高的工具类,就决定这样做了。
思路
之前在看到一位大神,写的工具类可以使用注解来标注实体类的属性,再利用反射机制很方便地把excel中的数据映射成对象或者把对象存储到excel中。正巧前段时间钻研了一下java的注解和反射机制,于是乎决定仿照大神的写法,写一个功能更轻量化的excel工具类。
自定义一个注解,来标注实体类的列名和列序号。
然后编写一个excel工具类,工具类中实现一个解析注解的方法,并且根据解析的结果,读取excel数据或者将excel数据导出。
代码
首先是自定义注解。由于我们需要利用反射机制来解析注解标注的实体类,所以我们自定义的注解一定要在运行时存活,所以其生命周期应当设置为@Retention(RetentionPolicy.RUNTIME)
。
除此之外,我们的自定义注解的标注对象只有实体类的属性,所以其标注对象应当设置为@Target(ElementType.FIELD)
而标注的内容则比较简单,只有列名和列序号。所以最终注解的代码为
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelAnnotation {
String name();
int col();
}
而使用起来也很简单,下面是student实体类
public class Student implements Serializable {
private Integer id;
@ExcelAnnotation(name = "学号",col = 0)
private Long stuID;
@ExcelAnnotation(name = "姓名",col = 1)
private String name;
@ExcelAnnotation(name = "密码",col= 2)
private String password;
@ExcelAnnotation(name = "方向id",col = 3)
private Integer p_field_id;
}
只要在属性上方使用注解标注列名和列号就可以了。
自定义注解结束后,我们要开始编写excel工具类。
首先工具类解析的目标实体类在整个处理流程是唯一不变的,所以我们在声明工具类时应当采用
public class ExcelUtil<T> {
}
除此之外,如果想利用反射机制来解析目标实体类,就要获取该实体类在JVM中的class对象,这个class对象可以作为我们工具类的成员来方便后续调用
public class ExcelUtil<T> {
private Class<T> tClass;
//构造函数
public ExcelUtil(Class<T> tClass){
this.tClass = tClass;
}
}
然后不管是excel解析的结果,还是可能用于导出的实体数据,我们都将其存入链表中,这时候就需要List了,而这些数据也可以作为我们工具类的成员来方便后续调用
public class ExcelUtil<T> {
private Class<T> tClass;
private List<T> data;
//构造函数
public ExcelUtil(Class<T> tClass){
this.tClass = tClass;
this.data = new ArrayList<T>();
}
}
紧接着我们还有可能解析出的属性集。这个我们也可以将其存入链表中,并作为一个成员来方便后续调用
public class ExcelUtil<T> {
private Class<T> tClass;
private List<T> data;
private List<Field> fields;
//构造函数
public ExcelUtil(Class<T> tClass){
this.tClass = tClass;
this.data = new ArrayList<T>();
this.fields = new ArrayList<Field>();
}
}
接下来,则是我们需要对注解进行解析的方法,而解析的结果可以存入成员变量fields中
//解析注解获取属性集的方法
public void parseAnnotation(Class cClass){
if(this.fields==null){
this.fields = new ArrayList<Field>();
}
Field[] fieldsArray = cClass.getDeclaredFields();
for(Field f:fieldsArray){
//将有excelAnnotation注解的属性加入属性列表
boolean hasAnnotation = f.isAnnotationPresent(ExcelAnnotation.class);
if(hasAnnotation){
//设置私有成员可以通过反射访问
f.setAccessible(true);
this.fields.add(f);
}
}
//如果本类存在超类(并且超类的类型为class),则递归解析超类
if(cClass.getSuperclass()!=null&&cClass.getSuperclass().equals(Object.class)){
parseAnnotation(cClass.getSuperclass());
}
}
然后我们对这个方法进行一下包装,因为我们最初传入的参数肯定是构造函数中初始化的class对象tClass,所以我们可以将这个成员作为默认参数
//重载parseAnnotation方法设置默认参数
public void parseAnnotation(){
parseAnnotation(this.tClass);
}
然后这个解析注解的方法可以放在构造函数中直接将成员fields的内容解析出来。所以整理后的代码如下
public class ExcelUtil<T> {
private Class<T> tClass;
private List<T> data;
private List<Field> fields;
//构造函数
public ExcelUtil(Class<T> tClass){
this.tClass = tClass;
this.data = new ArrayList<T>();
this.fields = new ArrayList<Field>();
//解析注解获取属性集
parseAnnotation();
}
public List<T> getData() {
return data;
}
//解析注解获取属性集的方法
public void parseAnnotation(Class cClass){
if(this.fields==null){
this.fields = new ArrayList<Field>();
}
Field[] fieldsArray = cClass.getDeclaredFields();
for(Field f:fieldsArray){
//将有excelAnnotation注解的属性加入属性列表
boolean hasAnnotation = f.isAnnotationPresent(ExcelAnnotation.class);
if(hasAnnotation){
//设置私有成员可以通过反射访问
f.setAccessible(true);
this.fields.add(f);
}
}
//如果本类存在超类(并且超类的类型为class),则递归解析超类
if(cClass.getSuperclass()!=null&&cClass.getSuperclass().equals(Object.class)){
parseAnnotation(cClass.getSuperclass());
}
}
//重载parseAnnotation方法设置默认参数
public void parseAnnotation(){
parseAnnotation(this.tClass);
}
}
到目前为止,这个工具类可以做到解析实体类注解了。如此我们便可以把注意力集中在excel的处理上。
对于读入excel文件,poi需要输入流和sheet名,所以我们的导入工具类也需要这两个参数。这里需要注意的地方是,对于读出来的excel数据我们需要构造成实体类的对象,而对象的属性的类型对我们来说是未知的,所以选择的处理办法如下:
- 先将excel的原始数据全部处理成字符串
- 再利用反射判断目标属性的类型,然后对字符串进行强制类型转换
//导入excel文件
public void importExcel(InputStream inputStream,String sheetName){
try {
Workbook workbook = WorkbookFactory.create(inputStream);
Sheet sheet;
//如果不设置表名默认获取第一张表
if(sheetName==null){
sheet = workbook.getSheetAt(0);
}else {
sheet = workbook.getSheet(sheetName);
}
//获取行数
int rows = sheet.getPhysicalNumberOfRows();
//不为空表时才进行处理
if(rows>0){
//认为第一行是表头,所以从第二行开始获取
for(int i = 1;i<rows;i++){
//获取本行数据
Row row = sheet.getRow(i);
T entity = null;
//获取各属性数据
for(Field f : this.fields){
//若属性为空,不予处理
if(f==null){
continue;
}
//读取对应列的单元格
Cell cell = row.getCell( f.getAnnotation(ExcelAnnotation.class).col() );
//如果单元格为空,不予处理
if(cell==null){
continue;
}
//根据单元格的类型设置值
String str;
if(cell.getCellTypeEnum()== CellType.NUMERIC){
str = String.valueOf(cell.getNumericCellValue());
}else if(cell.getCellTypeEnum()==CellType.BOOLEAN){
str = String.valueOf(cell.getBooleanCellValue());
}else {
str = cell.getStringCellValue();
}
//如果str为空,则不予处理
if( str==null || str.equals("")){
continue;
}
//存在实例则继续使用实例,不存在实例则实例化(首次循环无实例)
entity = (entity==null?this.tClass.newInstance():entity);
Class<?> fieldType = f.getType();
//根据当前属性的类型赋值
if(fieldType == String.class){
f.set(entity,str);
}else if(fieldType==Integer.TYPE||fieldType==Integer.class){
f.set(entity,new Integer((int)Double.parseDouble(str)));
}else if(fieldType==Float.TYPE||fieldType==Float.class){
f.set(entity,Float.parseFloat(str));
}else if(fieldType==Double.TYPE||fieldType==Double.class){
f.set(entity,Double.parseDouble(str));
}else if(fieldType==Short.TYPE||fieldType==Short.class){
f.set(entity,Short.parseShort(str));
}else if(fieldType==Long.TYPE||fieldType==Long.class){
f.set(entity,Long.parseLong(str));
}else if(fieldType==Byte.TYPE||fieldType==Byte.class){
f.set(entity,Byte.parseByte(str));
}else if(fieldType==Character.TYPE||fieldType==Character.class){
f.set(entity,str.charAt(0));
}
}
//记录本行数据
if(entity!=null){
this.data.add(entity);
}
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvalidFormatException e) {
e.printStackTrace();
}
}
由于我们大多数时候的数据源是springboot框架传入的MultipartFile对象,所以对于这个导入方法我们可以进行重载来适应MultipartFile对象
//重载importExcel方法改变参数
public void importExcel(MultipartFile file){
try {
importExcel(file.getInputStream(),null);
} catch (IOException e) {
e.printStackTrace();
}
}
既然已经使用了自定义注解,那么导出也变得很方便了,因为我们可以利用反射机制获取注解内容来构造导出的表头。
public HSSFWorkbook exportExcel(){
HSSFWorkbook workbook = new HSSFWorkbook();
HSSFSheet sheet = workbook.createSheet();
HSSFRow row;
HSSFCell cell;
row = sheet.createRow(0);
for(Field f:this.fields){
//属性为空,不予处理
if(f==null){
continue;
}
//获取列
int col = f.getAnnotation(ExcelAnnotation.class).col();
//生成单元格
cell = row.createCell(col);
//设置单元格值
cell.setCellValue(f.getAnnotation(ExcelAnnotation.class).name());
}
for(int i=0;i<this.data.size();i++){
row = sheet.createRow(i+1);
for(Field f:this.fields){
int col = f.getAnnotation(ExcelAnnotation.class).col();
HSSFCellStyle textStyle = workbook.createCellStyle();
HSSFDataFormat format = workbook.createDataFormat();
textStyle.setDataFormat(format.getFormat("@"));
cell = row.createCell(col);
cell.setCellStyle(textStyle);//设置单元格格式为"文本"
cell.setCellType(CellType.STRING);
try {
cell.setCellValue( f.get(this.data.get(i))==null?"":String.valueOf(f.get(this.data.get(i))) );
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return workbook;
}
整理后的完整工具类代码如下
public class ExcelUtil<T> {
private Class<T> tClass;
private List<T> data;
private List<Field> fields;
//构造函数
public ExcelUtil(Class<T> tClass){
this.tClass = tClass;
this.data = new ArrayList<T>();
this.fields = new ArrayList<Field>();
//解析注解获取属性集
parseAnnotation();
}
public List<T> getData() {
return data;
}
public void setData(List<T> data) {
this.data = data;
}
//解析注解获取属性集的方法
public void parseAnnotation(Class cClass){
if(this.fields==null){
this.fields = new ArrayList<Field>();
}
Field[] fieldsArray = cClass.getDeclaredFields();
for(Field f:fieldsArray){
//将有excelAnnotation注解的属性加入属性列表
boolean hasAnnotation = f.isAnnotationPresent(ExcelAnnotation.class);
if(hasAnnotation){
//设置私有成员可以通过反射访问
f.setAccessible(true);
this.fields.add(f);
}
}
//如果本类存在超类(并且超类的类型为class),则递归解析超类
if(cClass.getSuperclass()!=null&&cClass.getSuperclass().equals(Object.class)){
parseAnnotation(cClass.getSuperclass());
}
}
//重载parseAnnotation方法设置默认参数
public void parseAnnotation(){
parseAnnotation(this.tClass);
}
//导入excel文件
public void importExcel(InputStream inputStream,String sheetName){
try {
Workbook workbook = WorkbookFactory.create(inputStream);
Sheet sheet;
//如果不设置表名默认获取第一张表
if(sheetName==null){
sheet = workbook.getSheetAt(0);
}else {
sheet = workbook.getSheet(sheetName);
}
//获取行数
int rows = sheet.getPhysicalNumberOfRows();
//不为空表时才进行处理
if(rows>0){
//认为第一行是表头,所以从第二行开始获取
for(int i = 1;i<rows;i++){
//获取本行数据
Row row = sheet.getRow(i);
T entity = null;
//获取各属性数据
for(Field f : this.fields){
//若属性为空,不予处理
if(f==null){
continue;
}
//读取对应列的单元格
Cell cell = row.getCell( f.getAnnotation(ExcelAnnotation.class).col() );
//如果单元格为空,不予处理
if(cell==null){
continue;
}
//根据单元格的类型设置值
String str;
if(cell.getCellTypeEnum()== CellType.NUMERIC){
str = String.valueOf(cell.getNumericCellValue());
}else if(cell.getCellTypeEnum()==CellType.BOOLEAN){
str = String.valueOf(cell.getBooleanCellValue());
}else {
str = cell.getStringCellValue();
}
//如果str为空,则不予处理
if( str==null || str.equals("")){
continue;
}
//存在实例则继续使用实例,不存在实例则实例化(首次循环无实例)
entity = (entity==null?this.tClass.newInstance():entity);
Class<?> fieldType = f.getType();
//根据当前属性的类型赋值
if(fieldType == String.class){
f.set(entity,str);
}else if(fieldType==Integer.TYPE||fieldType==Integer.class){
f.set(entity,new Integer((int)Double.parseDouble(str)));
}else if(fieldType==Float.TYPE||fieldType==Float.class){
f.set(entity,Float.parseFloat(str));
}else if(fieldType==Double.TYPE||fieldType==Double.class){
f.set(entity,Double.parseDouble(str));
}else if(fieldType==Short.TYPE||fieldType==Short.class){
f.set(entity,Short.parseShort(str));
}else if(fieldType==Long.TYPE||fieldType==Long.class){
f.set(entity,Long.parseLong(str));
}else if(fieldType==Byte.TYPE||fieldType==Byte.class){
f.set(entity,Byte.parseByte(str));
}else if(fieldType==Character.TYPE||fieldType==Character.class){
f.set(entity,str.charAt(0));
}
}
//记录本行数据
if(entity!=null){
this.data.add(entity);
}
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvalidFormatException e) {
e.printStackTrace();
}
}
//重载importExcel方法改变参数
public void importExcel(MultipartFile file){
try {
importExcel(file.getInputStream(),null);
} catch (IOException e) {
e.printStackTrace();
}
}
//使用实体类表头导出excel
public HSSFWorkbook exportExcel(){
HSSFWorkbook workbook = new HSSFWorkbook();
HSSFSheet sheet = workbook.createSheet();
HSSFRow row;
HSSFCell cell;
row = sheet.createRow(0);
//构造表头
for(Field f:this.fields){
//属性为空,不予处理
if(f==null){
continue;
}
//获取列
int col = f.getAnnotation(ExcelAnnotation.class).col();
//生成单元格
cell = row.createCell(col);
//设置单元格值
cell.setCellValue(f.getAnnotation(ExcelAnnotation.class).name());
}
//填充数据
for(int i=0;i<this.data.size();i++){
row = sheet.createRow(i+1);
for(Field f:this.fields){
int col = f.getAnnotation(ExcelAnnotation.class).col();
HSSFCellStyle textStyle = workbook.createCellStyle();
HSSFDataFormat format = workbook.createDataFormat();
textStyle.setDataFormat(format.getFormat("@"));
cell = row.createCell(col);
cell.setCellStyle(textStyle);//设置单元格格式为"文本"
cell.setCellType(CellType.STRING);
try {
cell.setCellValue( f.get(this.data.get(i))==null?"":String.valueOf(f.get(this.data.get(i))) );
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return workbook;
}
}