最近有这么一个需求,前端页面选择Excel文件,将文件中的数据存到数据库中,然而这些Excel的模板有30+种,也就是每个Excel模板对应一张表,最简单的解决办法就是根据Excel模板类型写30+个if{}else{},基本步骤就是,判断类型,选择实体类,从Excel中取出数据,往实体中注入数据,调用插入方法。然后后边是无穷无尽的else.然而对于拒绝搬砖的我,当然拒绝了这个办法,也是为了方便以后功能的拓展,决定另辟蹊径,下面是我的心路历程。
一 反射
为了避免大量的if else ,当然首选设计模式中的策略模式,利用反射,灵活的选择需要注入数据的实体类和调用插入方法。而不是在代码中硬编码。于是我找到了jdk自带的通过反射注入属性值–PropertyDescriptor (具体内容可参考:https://blog.csdn.net/weixin_42069143/article/details/82119724)
package com.peidasoft.instrospector;
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
public class BeanInfoUtil {
// 设置bean的某个属性值
public static void setProperty(UserInfo userInfo, String userName) throws Exception {
// 获取bean的某个属性的描述符
PropertyDescriptor propDesc = new PropertyDescriptor(userName, UserInfo.class);
// 获得用于写入属性值的方法
Method methodSetUserName = propDesc.getWriteMethod();
// 写入属性值
methodSetUserName.invoke(userInfo, "wong");
System.out.println("set userName:" + userInfo.getUserName());
}
// 获取bean的某个属性值
public static void getProperty(UserInfo userInfo, String userName) throws Exception {
// 获取Bean的某个属性的描述符
PropertyDescriptor proDescriptor = new PropertyDescriptor(userName, UserInfo.class);
// 获得用于读取属性值的方法
Method methodGetUserName = proDescriptor.getReadMethod();
// 读取属性值
Object objUserName = methodGetUserName.invoke(userInfo);
System.out.println("get userName:" + objUserName.toString());
}
}
然而我在测试的过程中,遇到了一个很麻烦的问题,Excel中的数据取出时是按照String类型存储在map中的,然后对于数字类型和datetime类型,实体类中是Integer和dateTime类型,这个不能很好的进行类型转换(所以只在测试阶段用了一下,没能实际运用到项目中。)查了一下,可以转换,需要写工具类,我就是闲麻烦,又在找资料,于是我又找到了Apache的common-beanutils。
二、common-beanutils 基于反射实现灵活的封装数据
首先这是maven链接:https://mvnrepository.com/artifact/commons-beanutils/commons-beanutils
这个使用起来就非常方便。下面是测试内容,一些疑问和尝试都在注释中了。
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import java.lang.reflect.InvocationTargetException;
public class BeanUtilsCommonReject {
public static void main(String[] args) {
User userInfo = new User();
Dept dept = new Dept();
dept.setDeptId(1L);
try {
/**
* BeanUtils.setProperty()属性分别为实体类,属性名和属性值
*/
BeanUtils.setProperty(userInfo, "userName", "peida");
System.out.println("set userName:" + userInfo.getUserName());
System.out.println("get userName:" + BeanUtils.getProperty(userInfo, "userName"));
/**
* 如果是参数的值得类型无法转换为目标属性的类型,则取值为目标属性的默认值
* 比如此处userID为long型,参数为“aaaa",字母无法转换为long,则此处userID取默认值为0
*/
BeanUtils.setProperty(userInfo, "userId", "18");
System.out.println("set userId:" + userInfo.getUserId());
System.out.println("get userId:" + BeanUtils.getProperty(userInfo, "userId"));
System.out.println("get userName type:" +
BeanUtils.getProperty(userInfo, "userName").getClass().getName());
System.out.println("get userId type:" +
BeanUtils.getProperty(userInfo, "userId").getClass().getName());
/**
* 可以支持直接赋值对象,也可以支持嵌套赋值直接赋值内置对象的属性值
*/
BeanUtils.setProperty(userInfo,"createTime", DateUtils.getNowDate());
System.out.println("get createTime:"+BeanUtils.getProperty(userInfo,"createTime"));
//赋值对象
BeanUtils.setProperty(userInfo,"dept",dept);
//赋值对象的属性值
BeanUtils.setProperty(userInfo,"dept.deptId","1");
System.out.println("get dept:"+dept);
/**
* PropertyUtils类和BeanUtils不同在于,运行getProperty、setProperty操作时,没有类型转换,
* 使用属性的原有类型或者包装类。
* 由于userId属性的数据类型是long,所以方法
* PropertyUtils.setProperty(userInfo,”userId”, “8”)会爆出数据类型不匹配,
* 无法将值赋给属性。
*/
PropertyUtils.setProperty(userInfo, "userId", 8);
System.out.println(PropertyUtils.getProperty(userInfo, "userId"));
System.out.println(PropertyUtils.getProperty(userInfo, "userId").getClass().getName());
PropertyUtils.setProperty(userInfo, "userId", 8); // IllegalArgumentException
} catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
应用到实际中是这样的
//读取第一页
ExcelReader reader = ExcelUtil.getReader(file.getInputStream());
List<Map<String,Object>> readAll = reader.readAll();
List<List<Object>> readAll1 = reader.read();
Object map11 = readAll1.get(0);
UploadTemplete uploadTemplete =
uploadTempleteService.selectUploadTempleteByCode(recordEntity.getTemplateCode());
//去掉空格
uploadTemplete.setExcelHeaderName(uploadTemplete.getExcelHeaderName().replace(" ",""));
uploadTemplete.setTableColumnName(uploadTemplete.getTableColumnName().replace(" ",""));
//验证模板是否一致
if (!uploadTemplete.getExcelHeaderName().equals(
StringUtils.strip(map11.toString(),"[]").replace(" ",""))){
result.put("state", "error");
result.put("msg", "上传的excel文件与模板不一致,请检查");
return result;
}
if(readAll.size()<=0) {
result.put("state", "error");
result.put("msg", "上传的excel文件数据不能为空");
return result;
}
//包名首字母小写
String packageName = StringUtils.toCamelCase(uploadTemplete.getTableName());
//实体类首字母大写
String domainName = StringUtils.convertToCamelCase(uploadTemplete.getTableName());
//实体类,拼接实体类的路径和类名,利用反射加载实体类的class对象
Class<?> clazzDomain = Class.forName("com.aaa.aaa." + packageName + ".domain." + domainName);
//实例化实体对象
Object objectDomain = clazzDomain.newInstance();
//service类,拼接实体类的路径和类名
Class<?> clazzService = Class.forName(
"com.aaa.aaa." + packageName + ".service.I" + domainName + "Service");
//由于service已经交给spring控制了,所以service实例从spring容器中取出
Object objectService = SpringUtils.getBean(clazzService);
//service类中插入方法
Method serviceMethod = clazzService.getMethod("insert"+domainName, clazzDomain);
//因为不能采用硬编码方式,所以Excel列名不能存在在代码中,所以将列放在了数据库中,用逗号隔开,
//取出时通过转换成数组
String[] excelHeaderArr = Convert.toStrArray(uploadTemplete.getExcelHeaderName());
//这里边存的是与Excel模板相对应的数据库中表的列的名字
String[] columnArr = Convert.toStrArray(uploadTemplete.getTableColumnName());
for(int i=0;i<readAll.size();i++) {
Map<String, Object> readMap = readAll.get(i);
//给实体属性注入值
BeanUtils.setProperty(objectDomain,"code",UUIDUtil.getUUID32());
//循环取出Excel中每一行的数据,
for (int j=0;j<columnArr.length;j++){
BeanUtils.setProperty(objectDomain,StringUtils.toCamelCase(columnArr[j]),
readMap.get(excelHeaderArr[j]).toString());
}
BeanUtils.setProperty(objectDomain,"del_flag","0");
BeanUtils.setProperty(objectDomain,"createCode",recordEntity.getCreateCode());
BeanUtils.setProperty(objectDomain,"createTime",DateUtils.getNowDate());
//执行插入方法
serviceMethod.invoke(objectService,objectDomain);
}
然而这个方法还是有很致命的硬伤,数据单条插入,数据量太大的话,会花费很长时间,给用户不好的体验。所以首先想到的就是批量插入,但是现在每个表的mybatis xml文件还没有批量插入的方法,如果写批量插入的话,要写30+个,所以就先放弃了。采用了另一种方法,利用map代替实体。
三、map代替实体类
利用实体类进行封装数据没问题,其实已经很好了,但是批量插入的话,要写30+条批量插入的语句,而且每个还都不一样,所以想到了用map做到进一步解耦。
下面代码就简单多了,就是将实体类换成了map,代码如下:
String[] columnArr = Convert.toStrArray(uploadTemplete.getTableColumnName());
String[] excelHeaderArr = Convert.toStrArray(uploadTemplete.getExcelHeaderName());
//因为没有实体关联,不能调用具体service中的插入方法,所以这里需要注入tableName控制插入的那个表中
String tableName = uploadTemplete.getTableName();
for(int i=0;i<readAll.size();i++) {
Map<String, Object> readMap = readAll.get(i);
Map<String, Object> map = new HashMap<>();
//循环取出Excel中每一行的数据
for (int j=0;j<columnArr.length;j++){
map.put(columnArr[j],readMap.get(excelHeaderArr[j]));
}
map.put("del_flag","0");
map.put("create_code",recordEntity.getCreateCode());
map.put("create_time",DateUtils.getNowDate());
uploadTempleteService.insertRecordsByTableName(map,tableName);
当然了,对于这一块,只有逻辑代码是不够的,下面请看一下dao层的代码和sql
首先是dao接口的方法
public int insertRecordsByTableName(@Param("map") Map<String,Object> map,
@Param("tableName) tableName);
下面是sql ,在这我又遇到了难点,怎么循环取出map中所有的key和value,幸好mybatis 中foreach标签提供了循环map的功能,
insert into ${tableName}
//在foreach循环map的时候,collection使用map.entrySet()取出map中所有的键值对,然后index存的是key,
//item存的是value
<foreach item="value" index="key" collection="map.entrySet()" open="(" separator="," close=")">
${key}
</foreach>
values
<foreach item="value" index="key" collection="map.entrySet()" open="(" separator="," close=")">
<if test="key != 'tableName'">
#{value}
</if>
</foreach>
这样就实现了功能与实体主键的解耦。
然后,,,这样就结束了吗,还没有我们上边说的批量插入还没有实现。下边就是批量插入了。
四、List<Map<String,Object>实现批量插入
首先是逻辑代码也是很简单,就是将第三部的map放到list中实现批量插入。
List<Map<String,Object>> list = new ArrayList<>();
String[] columnArr = Convert.toStrArray(uploadTemplete.getTableColumnName());
String[] excelHeaderArr = Convert.toStrArray(uploadTemplete.getExcelHeaderName());
for(int i=0;i<readAll.size();i++) {
Map<String, Object> readMap = readAll.get(i);
Map<String, Object> map = new HashMap<>();
for (int j=0;j<columnArr.length;j++){
if(StringUtils.isEmpty(readMap.get(excelHeaderArr[j]).toString())){
return returnMsg(excelHeaderArr[j],0,(i+2));
}
map.put(columnArr[j],readMap.get(excelHeaderArr[j]));
}
map.put("del_flag","0");
map.put("create_code",recordEntity.getCreateCode());
map.put("create_time",DateUtils.getNowDate());
list.add(map);
}
//批量插入
uploadTempleteService.batchInsertRecordsByTableName(list,uploadTemplete.getTableName());
逻辑代码依然是如此简单,重点还是在sql。在写这个sql的时候又遇到了一个难题,怎么在mybatis中实现双重循环,因为上边的map需要循环取出列的数据,现在又套了一层list,所以需要双层循环。请看代码
public int batchInsertRecordsByTableName(@Param("list") List<Map<String,Object>> list,
@Param("tableName") String tableName);
insert into ${tableName}(
<foreach collection="list" separator="," index="index" item="map">
//因为sql中需要插入的列名只需要取出一次就可以了,所以使用list的index控制循环次数。
<if test="index == 1">
<trim suffixOverrides=",">
<foreach item="value" index="key" collection="map.entrySet()">
${key},
</foreach>
</trim>
</if>
</foreach>
)values
<foreach collection="list" separator="," index="index" item="map">
<foreach item="value" index="key" collection="map.entrySet()" open="(" separator="," close=")">
#{value}
</foreach>
</foreach>
五、总结
这就是我对于开头提出的问题的一个解决历程。如此高度的封装虽然简化了代码,使代码看起来没有臃肿,很简洁,但是也会有一种问题,这么多模板不可能每一个都没有特殊的情况,比如控制一些必填项啊,数据累类型检验啊,对于添加这些功能来说有些许困难。
好了,今天分享就到这了。
参考:https://blog.csdn.net/weixin_42069143/article/details/82119724