快一年没写推文了,感觉肚子里也积了点东西,今天就搞一搞个人觉得还是挺酷的东西--结合反射机制搞一个通用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万+
关注
私信
展开阅读全文
作者:老敷