我是如何通过反射机制写一个通用excel导入导出的【上篇--导入】

Nadia ·
更新时间:2024-11-14
· 779 次阅读

快一年没写推文了,感觉肚子里也积了点东西,今天就搞一搞个人觉得还是挺酷的东西--结合反射机制搞一个通用Excel导入,通用导出功能。

想了解下反射api的使用可以看下JAVASchool的说明:http://www.51gjie.com/java/782.html

以前写POI导入Excel的时候,基本都是来一个模板,就写一个解析读取接口,就下面像这样:

一列一列的取,取完再赋值给定义的实体类,这样来一个模板写一段代码去解析读取呢,其实也没啥问题,在需要读取的Excel很少的时候,写也很快。但是当需要读取的Excel越来越多的时候,每来一个模板就写一个Excel,来一个写一个的话,那就很难受了,效率也很低!像之前我搞的一个项目,需要读取解析的Excel模板四十多个的,一个一个写的话,那我要写到吐了。

  所以,写出一段可以解析不同Excel模板的代码,还是很有必要的,大体的实现思路如下:

1:根据不同的Excel文档模型,在数据库中定义一个解析模板

2:接口调用的时候把Excel文件与相应的Excel模板类型名传入,后台根据模板类型名找相应的解析模板去解析Excel

3:解析读取数据

4:拿到数据进行后续的处理

详细过程:

1:先在数据库中定义一个模板解析表(用来保存每个Excel的解析模板),sql如下:

CREATE TABLE `template_data` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `type` varchar(100) NOT NULL DEFAULT '' COMMENT '模板类型',
  `start_row` int(11) NOT NULL DEFAULT '0' COMMENT '开始行',
  `field` json DEFAULT NULL COMMENT '字段',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模板数据表';

提示一下,filed字段是json类型的,mysql 5.7 以上的版本才支持json类型,低于5.7的版本sql会执行失败,需要升级mysql。

表字段说明:

type:用来存模板类型,也可以说是这个模板的名称

start_row:表示从第几行开始读取,一般的Excel表前几行都会是描述或者表头,所以需要定义好从第几行开始读取数据,像我用来做测试用的张大炮的Excel样例,就是从第三行开始读取

field:用来存每一列的数据的类型,对应的java类属性名(关键点,决定在利用反射的时候赋值给java类中的哪个属性),是否允许为空等。张大炮Excel文档的一条模板解析sql如下:

INSERT INTO `test`.`template_data`(`id`, `type`, `start_row`, `field`) VALUES (1, 'ZhangDaPao', 2, '[{\"fieldLong\": 50, \"fieldName\": \"name\", \"fieldType\": \"String\", \"isNotNull\": \"是\", \"templateColumn\": 0}, {\"fieldLong\": 11, \"fieldName\": \"age\", \"fieldType\": \"Integer\", \"isNotNull\": \"是\", \"templateColumn\": 1}, {\"fieldLong\": 10, \"fieldName\": \"sex\", \"fieldType\": \"String\", \"isNotNull\": \"是\", \"templateColumn\": 2}, {\"fieldLong\": 255, \"fieldName\": \"address\", \"fieldType\": \"String\", \"isNotNull\": \"是\", \"templateColumn\": 3}, {\"fieldLong\": 1024, \"fieldName\": \"remark\", \"fieldType\": \"String\", \"isNotNull\": \"是\", \"templateColumn\": 4}]');
 

filed字段里面的json数据说明

fieldLong-用来限制Excel列的数据长度

filedName-对应的类属性名

fieldType-属性类型

isNotNull-是否不能为空

templateColumn-数据在Excel行中的第几列,下标从0开始。

之后每新增一个Excel模板,就在数据库中配置多一条sql就好了,接下来就是具体的代码实现了。

项目结构如下:

pom文件依赖:

org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-web org.apache.poi poi 3.17 org.apache.poi poi-ooxml 3.17 org.projectlombok lombok 1.18.6 com.alibaba druid-spring-boot-starter 1.1.0 mysql mysql-connector-java runtime javax.persistence javax.persistence-api 2.2 net.sf.json-lib json-lib 2.4 jdk15 org.apache.commons commons-lang3 com.baomidou mybatis-plus-boot-starter 3.3.1.tmp com.alibaba fastjson 1.2.49 javax javaee-web-api 7.0 org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine

实体类准备:

TemplateData:模板数据类

import lombok.Data; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; /** * 模板数据 */ @Data public class TemplateData { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; /**实体对象*/ private String type; /**开始行*/ private int startRow; /**字段*/ private String field; }

FieldTemplate:JSON字段实体类

import lombok.Data; /** * 字段模板 */ @Data public class FieldTemplate { /**字段名*/ private String fieldName; /**模板列*/ private int templateColumn; /**字段类型*/ private String fieldType; /**是否非空 (是 否)*/ private String isNotNull; /**字段长度限制*/ private int fieldLong; }

ZhangDaPao:张大炮实体类

import lombok.Data; @Data public class ZhangDaPao { private String name; private Integer age; private String sex; private String address; private String remark; }

Mybatis映射类:

TemplateDataMapper:模板数据映射类

import com.example.excel.template.entity.TemplateData; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.springframework.stereotype.Repository; @Mapper @Repository public interface TemplateDataMapper { @Select("select * from template_data where type = #{type}") TemplateData findByType(@Param("type") String type); }

业务层:ReportService,写了很多注释,希望对理解逻辑有帮助

主要逻辑:

1-获取解析模板

2-匹配模板字段与实体类的成员变量

3-校验数据列数据与赋值给成员变量

4-返回数据

import com.alibaba.fastjson.JSON; import com.example.excel.template.entity.FieldTemplate; import com.example.excel.template.entity.TemplateData; import com.example.excel.template.entity.ZhangDaPao; import com.example.excel.template.mapper.TemplateDataMapper; import lombok.extern.slf4j.Slf4j; import net.sf.json.JSONArray; import org.apache.commons.lang3.StringUtils; import org.apache.poi.ss.usermodel.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; @Service @Slf4j public class ReportService { @Autowired private TemplateDataMapper templateDataMapper; /** * @param file:需要解析的Excel * @param type:文件模板名 * @return */ public Object dataUpload(MultipartFile file, String type) { if (file == null || file.isEmpty()) { log.error("上报文件为空"); return "上报文件为空"; } Workbook workbook = null; try { workbook = WorkbookFactory.create(file.getInputStream()); } catch (Exception e) { e.printStackTrace(); } if (workbook == null) { log.error("workbook == null"); return "workbook == null"; } return execute(workbook, type); } /** * 上报处理 */ public Object execute(Workbook hssfWorkbook, String type) { TemplateData templateData = null; switch (type) { case "ZhangDaPao": //根据模板类型去数据库取相应的解析模板 templateData = templateDataMapper.findByType("ZhangDaPao"); return addData(templateData, hssfWorkbook, new ZhangDaPao()); default: log.error("没有找到对应的模板..."); } return null; } /** * @param templateData * @param hssfWorkbook * @param t 与Excel中数据对应的实体类 * @param * @return */ public List addData(TemplateData templateData, Workbook hssfWorkbook, T t) { JSONArray jsonArray = JSONArray.fromObject(new ArrayList()); //读第一个Excel表 Sheet hssfSheet = hssfWorkbook.getSheetAt(0); //拿解析模板中的field字段的json数据列,转为字段模板列表 List fieldTemplateList = JSON.parseArray(templateData.getField(), FieldTemplate.class); //从模板中定义的开始行开始遍历读取行 for (int rowNum = templateData.getStartRow(); rowNum <= hssfSheet.getLastRowNum(); rowNum++) { Row hssfRow = hssfSheet.getRow(rowNum); if (hssfRow != null) { T data = t; //遍历field字段中的json数据列 for (FieldTemplate sqlFt : fieldTemplateList) { //获取泛型的class,再用getDeclaredFields()获取类的成员变量 //遍历成员变量 for (Field field : t.getClass().getDeclaredFields()) { //如果成员变量跟数据库中定义的属性名匹配,进入数据读取解析 //也就是sql中,field定义的 "fieldName" 与实体ZhangDaPao中的属性名相同,进入数据读取 if (field.getName().equals(sqlFt.getFieldName())) { Cell cell = hssfRow.getCell(sqlFt.getTemplateColumn()); /*Excel数据列的校验,读取,赋值操作 * cell:数据列 * sqlFt:数据库中field字段中的json模板 * field:反射中的成员变量 * rowNum:读取的Excel是哪一行,用来抛异常的时候记录是第几行有问题 * data:与Excel数据对应的实体类 * */ verifyType(cell, sqlFt, field, rowNum, data); } } } //把读取成功的data加到json数组中,用于后续泛型转换 jsonArray.add(data); } } //转换为相应的实体列表返回 return (List) JSONArray.toCollection(jsonArray, t.getClass()); } /** * 校验,读取 * @param cell:数据列 * * @param sqlFt:数据库中field字段中的json模板,格式是下面这样的 * {"fieldLong": 50, "fieldName": "name", "fieldType": "String", "isNotNull": "是", "templateColumn": 0} * * @param field:反射中获取到的成员变量 * @param rowNum:读取的Excel是哪一行,用来抛异常的时候记录是第几行有问题 * @param data:与Excel数据对应的实体类 */ private T verifyType(Cell cell, FieldTemplate sqlFt, Field field, int rowNum, T data) { // 校验非空 if (sqlFt.getIsNotNull().equals("是")) { if (cell == null || StringUtils.isBlank(cell.toString())) { log.error("数据格式错误:行:" + (rowNum + 1) + ";列:" + (sqlFt.getTemplateColumn() + 1) + ",必要数据不能为空"); } } // 校验长度 if (cell != null && cell.toString().length() >= sqlFt.getFieldLong()) { log.error("数据格式错误:行:" + (rowNum + 1) + ";列:" + (sqlFt.getTemplateColumn() + 1) + ",长度超出限制"); } try { //打破封装 //一般成员变量我们都是定义为private的,在反射机制中,需要设置该属性允许被操作 field.setAccessible(true); //根据数据库中json数据中定义的该列是什么数据类型,进行相应的数据处理,这里只弄了String跟Integer的 if (sqlFt.getFieldType().equals("String")) { //String类型,直接toString赋值 field.set(data, cell.toString()); } else if (sqlFt.getFieldType().equals("Integer")) { //Integer类型,读取到的格式是这样的 30.0 先转 double再转为int类型 field.set(data, new Double(cell.toString()).intValue()); } } catch (Exception e) { log.error("数据格式错误:行:" + (rowNum + 1) + ";列:" + (sqlFt.getTemplateColumn() + 1)); } return data; } }

控制类:

import com.example.excel.template.service.ReportService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @RestController @Slf4j public class TemplateUploadController { @Autowired private ReportService reportService; @RequestMapping(value = "/upload", method = RequestMethod.POST) public void illegalBuildingUpload(MultipartFile file, @RequestParam String type) { log.info("文件上传---------"); reportService.dataUpload(file, type); } }

postman调用测试:调用成功,返回了张大炮等人的信息

接下来我们增加一个Excel模板,如下

新增Teacher实体类如下:

import lombok.Data; @Data public class Teacher { private String name; private String school; private String course; }

新增一条模板数据到数据库

INSERT INTO `test`.`template_data`(`id`, `type`, `start_row`, `field`) VALUES (2, 'Teacher', 2, '[{\"fieldLong\": 50, \"fieldName\": \"name\", \"fieldType\": \"String\", \"isNotNull\": \"是\", \"templateColumn\": 0}, {\"fieldLong\": 50, \"fieldName\": \"school\", \"fieldType\": \"String\", \"isNotNull\": \"是\", \"templateColumn\": 1}, {\"fieldLong\": 50, \"fieldName\": \"course\", \"fieldType\": \"String\", \"isNotNull\": \"是\", \"templateColumn\": 2}]');
 

在ReportService的execute中加入Teacher类型的捕获,如下:

具体代码:

case "Teacher": templateData = templateDataMapper.findByType("Teacher"); return addData(templateData, hssfWorkbook, new Teacher());

postman调用:

获取数据成功,后续继续新增Excel模板的时候,只需要在数据库中配置新的模板,再建一个实体类就可以了。

通用的Excel导入就写到这,哪天心血来潮了再继续写通用导出的。

程序员-就得搞搞偷懒式的代码

老敷 原创文章 15获赞 16访问量 2万+ 关注 私信 展开阅读全文
作者:老敷



反射 反射机制

需要 登录 后方可回复, 如果你还没有账号请 注册新账号