一、第一版简单的导入实现
最开始的准备工作
1.数据库相关
CREATE TABLE `dynamic_import_excel` ( `id` int(11) NOT NULL, `function_number` int(11) DEFAULT NULL COMMENT '功能编号', `function_name` varchar(100) DEFAULT NULL COMMENT '功能名字', `field_name` varchar(100) DEFAULT NULL COMMENT '字段名字(中文)', `property_name` varchar(100) DEFAULT NULL COMMENT '字段名字(英文)', `verify_mode` varchar(100) DEFAULT NULL COMMENT '校验模式(如果使用多个,使用数字逗号隔开,具体校验模式查看:VerifyModeEnum 枚举类)', `index_num` int(11) DEFAULT NULL COMMENT '表头排序下标(从0开始)', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
小例子
2.实体类 —— DynamicImportExcel
点击查看完整 DynamicImportExcel 内容
import lombok.Data; import java.io.Serializable; /** * @author DBC * @date 2024/6/3 11:09 * @network dbc655.top */ @Data public class DynamicImportExcel implements Serializable { private Integer id; /** * 功能编号 */ private Integer functionNumber; /** * 功能名字 */ private String functionName; /** * 字段名字(中文) */ private String fieldName; /** * 字段名字(英文) */ private String propertyName; /** * 校验模式(如果使用多个,使用数字逗号隔开,具体校验模式查看:VerifyModeEnum 枚举类) */ private String verifyMode; /** * 表头排序下标(从0开始) */ private Integer indexNum; }
关键Mapper
DynamicImportExcelMapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.test05.demo.model.DynamicImportExcel; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.List; /** * @author DBC * @date 2024/6/3 11:12 * @network dbc655.top */ @Mapper public interface DynamicImportExcelMapper extends BaseMapper<DynamicImportExcel> { Integer saveBatch(@Param("tableName") String tableName, @Param("columns") List<String> columns, @Param("data") List<List<String>> data); }
DynamicImportExcelMapper.xml
<!-- Integer saveBatch(@Param("tableName") String tableName,--> <!-- @Param("columns") List<String> columns,--> <!-- @Param("data") List<List<String>> data);--> <select id="saveBatch" resultType="java.lang.Integer"> INSERT INTO ${tableName} ( <foreach collection="columns" item="column" separator=","> ${column} </foreach> ) VALUES <foreach collection="data" item="data" separator=","> ( <foreach collection="data" item="item" separator=","> #{item} </foreach> ) </foreach> </select>
3.核心service
DynamicImportExcelService
点击查看完整 DynamicImportExcelService 内容
import cn.hutool.core.util.ReflectUtil; import com.alibaba.excel.EasyExcel; import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.event.AnalysisEventListener; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.example.test05.demo.mapper.DynamicImportExcelMapper; import com.example.test05.demo.mapper.UserMapper; import com.example.test05.demo.model.DynamicImportExcel; import com.example.test05.demo.tool.JsonData; import lombok.SneakyThrows; import org.apache.commons.collections4.CollectionUtils; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.io.IOException; import java.lang.reflect.Field; import java.util.*; import java.util.stream.Collectors; /** * @author DBC * @date 2024/6/6 17:30 * @network dbc655.top */ @Service public class DynamicImportExcelService { @Resource private DynamicImportExcelMapper dynamicImportExcelMapper; public <T> JsonData getJsonData(MultipartFile file, Class<T> clazz, BaseMapper<T> baseMapper) throws InstantiationException, IllegalAccessException, ClassNotFoundException, IOException { List<DynamicImportExcel> dynamicImportExcels = dynamicImportExcelMapper.selectList(new LambdaQueryWrapper<DynamicImportExcel>().eq(DynamicImportExcel::getFunctionNumber, 1)); // 检查是否存在模版,由于强关联,则就暂时不兼容无模版空导入了 if (CollectionUtils.isEmpty(dynamicImportExcels)) { return JsonData.buildError("暂未配置模版,请稍后再试"); } // 获得正确的表头 用于后面的表头校验相关 Map<Integer, DynamicImportExcel> trueIndexHeadMap = dynamicImportExcels.stream() .collect(Collectors.toMap(DynamicImportExcel::getIndexNum, v -> v)); Map<String, DynamicImportExcel> trueNameHeadMap = dynamicImportExcels.stream() .collect(Collectors.toMap(DynamicImportExcel::getPropertyName, v -> v)); // 获得需要导入的列 Set<String> propertyNameSet = dynamicImportExcels.stream() .map(DynamicImportExcel::getPropertyName) .collect(Collectors.toSet()); EasyExcel.read(file.getInputStream(), new AnalysisEventListener<Map<String, Object>>() { // 定义临时集合 List<T> cachedDataList = new ArrayList<>(); private static final int BATCH_COUNT = 100; @Override public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) { // 读取到的表头数据,其key是以0开始的索引 trueIndexHeadMap.forEach((k, v) -> { if (!headMap.get(k).equals(v.getFieldName())) { throw new RuntimeException("表头数据不一致"); } }); } @SneakyThrows @Override public void invoke(Map<String, Object> data, AnalysisContext context) { T className = clazz.newInstance(); for (Object o : data.keySet()) { String propertyName = trueIndexHeadMap.get(o).getPropertyName(); Object o1 = data.get(o); ReflectUtil.setFieldValue(className, propertyName, o1); } cachedDataList.add(className); if (cachedDataList.size() >= BATCH_COUNT) { saveData(); cachedDataList.clear(); } } @Override public void doAfterAllAnalysed(AnalysisContext context) { saveData(); } /** * 加上存储数据库 */ private void saveData() { try { T className = clazz.newInstance(); // 获取表名 String tableName = getTableName(className); // 获取到需要导入的字段列 List<String> propertyNameList = getColumns(className, propertyNameSet); // 得到最终数据 List<List<String>> data = cachedDataList.stream() .map(object -> propertyNameList.stream() .map(ppn -> { Object o = ReflectUtil.getFieldValue(object, ppn); return o != null ? o.toString() : null; }) .collect(Collectors.toList())) .collect(Collectors.toList()); // 得到最终列名 List<String> columns = propertyNameList.stream().map(StringUtils::camelToUnderline).collect(Collectors.toList()); dynamicImportExcelMapper.saveBatch(tableName, columns, data); } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); } } }).sheet().doRead(); return JsonData.buildSuccess(); } /** * 通过实体类构造一个sql的字段列数组,且必须存在于 propertyNameSet * * @param className * @param propertyNameSet * @param <T> * @return */ private <T> List<String> getColumns(T className, Set<String> propertyNameSet) { return Arrays.stream(className.getClass().getDeclaredFields()) .filter(field -> propertyNameSet.contains(field.getName())) .map(Field::getName) .collect(Collectors.toList()); } /** * 获取 className 上的@TableName 注解内容 如果没有就用类名 * * @param className * @param <T> * @return */ private static <T> String getTableName(T className) { return className.getClass().getAnnotation(TableName.class) == null ? className.getClass().getSimpleName() : className.getClass().getAnnotation(TableName.class).value(); } }
4.实现效果 小例子
@Resource private UserMapper userMapper; @Resource private DynamicImportExcelService dynamicImportExcelService; @SneakyThrows @RequestMapping("/importExcel") public JsonData importExcel(MultipartFile file) throws IOException, ClassNotFoundException { dynamicImportExcelService.getJsonData(file, UserDO.class, userMapper); return JsonData.buildSuccess(); }
导入后效果
二、第二版简单实现 —— 增加动态的校验
1.简单的策略工厂 —— DynamicImportFactory
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.Map; /** * @author DBC * @date 2024/5/31 10:43 * @network dbc655.top */ @Component public class DynamicImportFactory { @Autowired private Map<String, DynamicImportInterface> dynamicImportInterfaceMap; public DynamicImportInterface getMsgStrategy(String type) throws Exception { DynamicImportInterface payInterface = dynamicImportInterfaceMap.get(type); if(payInterface == null){ throw new Exception("错误的选项!"); } return payInterface; } }
2.策略接口类 —— DynamicImportInterface
import com.fasterxml.jackson.core.JsonProcessingException; import java.util.Objects; /** * @author DBC * @date 2024/5/31 10:22 * @network dbc655.top */ public interface DynamicImportInterface { Boolean check(Object o) throws JsonProcessingException; }
3.三个简单的策略类 DynamicImportIdCardStrategy、DynamicImportIdChineseStrategy、DynamicImportNumberStrategy
点击查看完整 DynamicImportIdCardStrategy 内容
import cn.hutool.core.date.DateUnit; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.IdcardUtil; import com.example.test05.demo.constants.DynamicImportConstants; import com.fasterxml.jackson.core.JsonProcessingException; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.Date; import java.util.Objects; import java.util.concurrent.TimeUnit; /** * @author DBC * @date 2024/5/31 10:25 * @network dbc655.top */ @Component(DynamicImportConstants.IS_ID_CARD) public class DynamicImportIdCardStrategy implements DynamicImportInterface { @Override public Boolean check(Object object) throws JsonProcessingException { // 如果object类型是String 那么进入下面的判断 if (object instanceof String) { return IdcardUtil.isValidCard(String.valueOf(object)); } return false; } }
点击查看完整 DynamicImportIdChineseStrategy 内容
import cn.hutool.core.lang.Validator; import cn.hutool.core.util.CharUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.pinyin.PinyinUtil; import com.example.test05.demo.constants.DynamicImportConstants; import com.fasterxml.jackson.core.JsonProcessingException; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.Objects; /** * @author DBC * @date 2024/6/3 9:20 * @network dbc655.top */ @Component(DynamicImportConstants.IS_CHINESE) public class DynamicImportIdChineseStrategy implements DynamicImportInterface { @Override public Boolean check(Object o) throws JsonProcessingException { try { // 判断是否为中文 使用hutool return Validator.isChinese(o.toString()); }catch (Exception e){ return false; } } }
点击查看完整 DynamicImportNumberStrategy 内容
import cn.hutool.core.util.NumberUtil; import com.example.test05.demo.constants.DynamicImportConstants; import com.fasterxml.jackson.core.JsonProcessingException; import org.springframework.stereotype.Component; /** * @author DBC * @date 2024/5/31 10:25 * @network dbc655.top */ @Component(DynamicImportConstants.IS_NUMBER) public class DynamicImportNumberStrategy implements DynamicImportInterface { @Override public Boolean check(Object object) throws JsonProcessingException { // 判断object 是否是纯数字 try { return NumberUtil.isNumber((CharSequence) object); }catch (Exception e){ return false; } } }
4.一个简单的枚举类 —— VerifyModeEnum
点击查看完整 VerifyModeEnum 内容
import com.example.test05.demo.constants.DynamicImportConstants; import lombok.Getter; /** * @author DBC * @date 2024/6/7 11:27 * @network dbc655.top */ public enum VerifyModeEnum { IS_ID_CARD(1001, "判断身份证是否正确", "身份证错误,请检查后重试", DynamicImportConstants.IS_ID_CARD), IS_CHINESE(1002, "判断是否为中文", "{}不合法,请检查后重试", DynamicImportConstants.IS_CHINESE), IS_NUMBER(1003, "判断是否为数字", "{}必须为数值,请检查后重试",DynamicImportConstants.IS_NUMBER), ; /** * 功能类型 */ @Getter private String functionName; @Getter private int code; /** * 错误信息 */ @Getter private String errorMessage; /** * 对应策略类 */ @Getter private String strategy; VerifyModeEnum(int code, String functionName, String errorMessage, String strategy) { this.code = code; this.functionName = functionName; this.errorMessage = errorMessage; this.strategy = strategy; } public static VerifyModeEnum getByCode(int code) { for (VerifyModeEnum value : VerifyModeEnum.values()) { if (value.getCode() == code) { return value; } } return null; } }
5.核心 Service 类
点击查看完整 DynamicImportExcelService 内容
import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import com.alibaba.excel.EasyExcel; import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.event.AnalysisEventListener; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.example.test05.demo.enums.VerifyModeEnum; import com.example.test05.demo.mapper.DynamicImportExcelMapper; import com.example.test05.demo.mapper.UserMapper; import com.example.test05.demo.model.DynamicImportExcel; import com.example.test05.demo.strategy.DynamicImportFactory; import com.example.test05.demo.strategy.DynamicImportInterface; import com.example.test05.demo.tool.JsonData; import lombok.SneakyThrows; import org.apache.commons.collections4.CollectionUtils; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.io.IOException; import java.lang.reflect.Field; import java.util.*; import java.util.stream.Collectors; /** * @author DBC * @date 2024/6/6 17:30 * @network dbc655.top */ @Service public class DynamicImportExcelService { @Resource private DynamicImportExcelMapper dynamicImportExcelMapper; @Resource private DynamicImportFactory dynamicImportFactory; public <T> JsonData getJsonData(MultipartFile file, Class<T> clazz, BaseMapper<T> baseMapper) throws InstantiationException, IllegalAccessException, ClassNotFoundException, IOException { List<DynamicImportExcel> dynamicImportExcels = dynamicImportExcelMapper.selectList(new LambdaQueryWrapper<DynamicImportExcel>().eq(DynamicImportExcel::getFunctionNumber, 1)); // 检查是否存在模版,由于强关联,则就暂时不兼容无模版空导入了 if (CollectionUtils.isEmpty(dynamicImportExcels)) { return JsonData.buildError("暂未配置模版,请稍后再试"); } // 获得正确的表头 用于后面的表头校验相关 Map<Integer, DynamicImportExcel> trueIndexHeadMap = dynamicImportExcels.stream() .collect(Collectors.toMap(DynamicImportExcel::getIndexNum, v -> v)); Map<String, DynamicImportExcel> trueNameHeadMap = dynamicImportExcels.stream() .collect(Collectors.toMap(DynamicImportExcel::getPropertyName, v -> v)); // 获得需要导入的列 Set<String> propertyNameSet = dynamicImportExcels.stream() .map(DynamicImportExcel::getPropertyName) .collect(Collectors.toSet()); EasyExcel.read(file.getInputStream(), new AnalysisEventListener<Map<String, Object>>() { // 定义临时集合 List<T> cachedDataList = new ArrayList<>(); // 错误集合 List<List<String>> cachedErrorDataList = new ArrayList<>(); private static final int BATCH_COUNT = 100; @Override public void doAfterAllAnalysed(AnalysisContext context) { saveData(); saveErrorData(); } @Override public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) { // 读取到的表头数据,其key是以0开始的索引 trueIndexHeadMap.forEach((k, v) -> { if (!headMap.get(k).equals(v.getFieldName())) { throw new RuntimeException("表头数据不一致"); } }); } @SneakyThrows @Override public void invoke(Map<String, Object> data, AnalysisContext context) { // 定义初始数据相关 T className = clazz.newInstance(); Boolean isTrue = true; List<String> errorList = new ArrayList<>(); StringBuilder errorMessage = new StringBuilder(); for (Object index : data.keySet()) { // 实体类的属性名 String propertyName = trueIndexHeadMap.get(index).getPropertyName(); // 实体类的属性值 Object propertyData = data.get(index); // 预先配置错误信息 (空间换时间) errorList.add(propertyData.toString()); // 通过数据库定义的校验规则来做动态的校验 if (StringUtils.isNotBlank(trueNameHeadMap.get(propertyName).getVerifyMode())) { // 获取到校验规则 String verifyMode = trueNameHeadMap.get(propertyName).getVerifyMode(); // 校验规则可能是一个,分割的数组 String[] split = verifyMode.split(","); for (String vm : split) { // 策略模式 调用对应的校验策略类 DynamicImportInterface msgStrategy = dynamicImportFactory .getMsgStrategy(VerifyModeEnum.getByCode(Integer.parseInt(vm)).getStrategy()); Boolean check = msgStrategy.check(propertyData); // 判断值是否正确 if (check){ ReflectUtil.setFieldValue(className, propertyName, propertyData); }else { // 不正确则拼接错误信息 errorMessage.append(StrUtil .format(VerifyModeEnum.getByCode(Integer.parseInt(vm)).getErrorMessage(), trueNameHeadMap.get(propertyName).getFieldName())).append(";"); isTrue = false; } } }else { // 不需要校验的直接赋值即可 ReflectUtil.setFieldValue(className, propertyName, propertyData); } } // 开始保存临时数据 saveDataList(className, isTrue, errorList, errorMessage.toString()); } /** * 保存临时数据 * @param className 实体类 * @param isTrue 是否是正确的数据 * @param errorList 错误的临时数据信息 * @param errorMessage 错误的原因 */ private void saveDataList(T className, Boolean isTrue, List<String> errorList, String errorMessage) { if (isTrue){ cachedDataList.add(className); }else { // 去掉最后的; errorMessage = errorMessage.substring(0, errorMessage.length()-1); errorList.add(errorMessage); cachedErrorDataList.add(errorList); } if (cachedDataList.size() >= BATCH_COUNT) { saveData(); cachedDataList.clear(); } if (cachedErrorDataList.size() >= BATCH_COUNT) { saveErrorData(); cachedErrorDataList.clear(); } } private void saveErrorData() { System.out.println(cachedErrorDataList.toString()); } /** * 加上存储数据库 */ private void saveData() { try { T className = clazz.newInstance(); // 获取表名 String tableName = getTableName(className); // 获取到需要导入的字段列 List<String> propertyNameList = getColumns(className, propertyNameSet); // 得到最终数据 List<List<String>> data = cachedDataList.stream() .map(object -> propertyNameList.stream() .map(ppn -> { Object o = ReflectUtil.getFieldValue(object, ppn); return o != null ? o.toString() : null; }) .collect(Collectors.toList())) .collect(Collectors.toList()); // 得到最终列名 List<String> columns = propertyNameList.stream().map(StringUtils::camelToUnderline).collect(Collectors.toList()); if (data.size() == 0) { return; } dynamicImportExcelMapper.saveBatch(tableName, columns, data); } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); } } }).sheet().doRead(); return JsonData.buildSuccess(); } /** * 通过实体类构造一个sql的字段列数组,且必须存在于 propertyNameSet * * @param className * @param propertyNameSet * @param <T> * @return */ private <T> List<String> getColumns(T className, Set<String> propertyNameSet) { return Arrays.stream(className.getClass().getDeclaredFields()) .filter(field -> propertyNameSet.contains(field.getName())) .map(Field::getName) .collect(Collectors.toList()); } /** * 获取 className 上的@TableName 注解内容 如果没有就用类名 * * @param className * @param <T> * @return */ private static <T> String getTableName(T className) { return className.getClass().getAnnotation(TableName.class) == null ? className.getClass().getSimpleName() : className.getClass().getAnnotation(TableName.class).value(); } }
6.小例子,以及最终效果
导入模版
导入结果
错误数组
三、第三版优化,代码小重构,加入重复数据相关校验
改动到的相关类
VerifyModeEnum
点击查看完整内容
import com.example.test05.demo.strategy.*; import lombok.Getter; /** * @author DBC * @date 2024/6/7 11:27 * @network dbc655.top */ public enum VerifyModeEnum { IS_ID_CARD(1001, "判断身份证是否正确", "身份证错误,请检查后重试", DynamicImportIdCardStrategy.class), IS_CHINESE(1002, "判断是否为中文", "{}不合法,请检查后重试", DynamicImportIdChineseStrategy.class), IS_NUMBER(1003, "判断是否为数字", "{}必须为数值,请检查后重试", DynamicImportNumberStrategy.class), // 判断是否重复关键字段 IS_REPEAT_DATA(101, "判断是否重复", "导入数据重复,请检查后重试", DynamicImportRepeatDataStrategy.class); ; /** * 功能类型 */ @Getter private String functionName; @Getter private int code; /** * 错误信息 */ @Getter private String errorMessage; /** * 对应策略类 */ @Getter private Class<? extends DynamicImportInterface> strategyClass; VerifyModeEnum(int code, String functionName, String errorMessage, Class<? extends DynamicImportInterface> strategyClass) { this.code = code; this.functionName = functionName; this.errorMessage = errorMessage; this.strategyClass = strategyClass; } public static VerifyModeEnum getByCode(int code) { for (VerifyModeEnum value : VerifyModeEnum.values()) { if (value.getCode() == code) { return value; } } return null; } public DynamicImportInterface getStrategyInstance(DynamicImportFactory dynamicImportFactory) throws Exception { return dynamicImportFactory.getMsgStrategy(strategyClass); } }
DynamicImportExcelService
点击查看完整内容
import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import com.alibaba.excel.EasyExcel; import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.event.AnalysisEventListener; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.example.test05.demo.enums.VerifyModeEnum; import com.example.test05.demo.mapper.DynamicImportExcelMapper; import com.example.test05.demo.mapper.UserMapper; import com.example.test05.demo.model.DynamicImportExcel; import com.example.test05.demo.strategy.DynamicImportFactory; import com.example.test05.demo.strategy.DynamicImportInterface; import com.example.test05.demo.strategy.VerifyContext; import com.example.test05.demo.tool.JsonData; import lombok.SneakyThrows; import org.apache.commons.collections4.CollectionUtils; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.io.IOException; import java.lang.reflect.Field; import java.util.*; import java.util.stream.Collectors; /** * @author DBC * @date 2024/6/6 17:30 * @network dbc655.top */ @Service public class DynamicImportExcelService { @Resource private DynamicImportExcelMapper dynamicImportExcelMapper; @Resource private DynamicImportFactory dynamicImportFactory; @Resource private VerifyContext verifyContext; public <T> JsonData getJsonData(MultipartFile file, Class<T> clazz, BaseMapper<T> baseMapper) throws InstantiationException, IllegalAccessException, ClassNotFoundException, IOException { List<DynamicImportExcel> dynamicImportExcels = dynamicImportExcelMapper.selectList(new LambdaQueryWrapper<DynamicImportExcel>().eq(DynamicImportExcel::getFunctionNumber, 1)); // 检查是否存在模版,由于强关联,则就暂时不兼容无模版空导入了 if (CollectionUtils.isEmpty(dynamicImportExcels)) { return JsonData.buildError("暂未配置模版,请稍后再试"); } // 获得正确的表头 用于后面的表头校验相关 Map<Integer, DynamicImportExcel> trueIndexHeadMap = dynamicImportExcels.stream() .collect(Collectors.toMap(DynamicImportExcel::getIndexNum, v -> v)); Map<String, DynamicImportExcel> trueNameHeadMap = dynamicImportExcels.stream() .collect(Collectors.toMap(DynamicImportExcel::getPropertyName, v -> v)); // 获得需要导入的列 Set<String> propertyNameSet = dynamicImportExcels.stream() .map(DynamicImportExcel::getPropertyName) .collect(Collectors.toSet()); // 定义一个用来判断是否重复数据的set Set<String> repeatDataSet = new HashSet<>(); EasyExcel.read(file.getInputStream(), new AnalysisEventListener<Map<String, Object>>() { // 定义临时集合 List<T> cachedDataList = new ArrayList<>(); // 错误集合 List<List<String>> cachedErrorDataList = new ArrayList<>(); private static final int BATCH_COUNT = 100; @Override public void doAfterAllAnalysed(AnalysisContext context) { saveData(); saveErrorData(); } @Override public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) { // 读取到的表头数据,其key是以0开始的索引 trueIndexHeadMap.forEach((k, v) -> { if (!headMap.get(k).equals(v.getFieldName())) { throw new RuntimeException("表头数据不一致"); } }); } @SneakyThrows @Override public void invoke(Map<String, Object> data, AnalysisContext context) { // 定义初始数据相关 T className = clazz.newInstance(); Boolean isTrue = true; List<String> errorList = new ArrayList<>(); StringBuilder errorMessage = new StringBuilder(); for (Object index : data.keySet()) { // 实体类的属性名 String propertyName = trueIndexHeadMap.get(index).getPropertyName(); // 实体类的属性值 Object propertyData = data.get(index); // 预先配置错误信息 (空间换时间) errorList.add(propertyData.toString()); // 通过数据库定义的校验规则来做动态的校验 if (StringUtils.isNotBlank(trueNameHeadMap.get(propertyName).getVerifyMode())) { // 获取到校验规则 String verifyMode = trueNameHeadMap.get(propertyName).getVerifyMode(); // 校验规则可能是一个,分割的数组 String[] split = verifyMode.split(","); for (String vm : split) { // 策略模式 调用对应的校验策略类 Boolean check = true; VerifyModeEnum byCode = VerifyModeEnum.getByCode(Integer.parseInt(vm)); if (byCode != null) { check = verifyContext.executeStrategy(byCode, propertyData, repeatDataSet); // 判断值是否正确 if (check) { ReflectUtil.setFieldValue(className, propertyName, propertyData); } else { errorMessage.append(StrUtil .format(byCode.getErrorMessage(), trueNameHeadMap.get(propertyName).getFieldName())).append(";"); isTrue = false; } } } }else { // 不需要校验的直接赋值即可 ReflectUtil.setFieldValue(className, propertyName, propertyData); } } // 开始保存临时数据 saveDataList(className, isTrue, errorList, errorMessage.toString()); } /** * 保存临时数据 * @param className 实体类 * @param isTrue 是否是正确的数据 * @param errorList 错误的临时数据信息 * @param errorMessage 错误的原因 */ private void saveDataList(T className, Boolean isTrue, List<String> errorList, String errorMessage) { if (isTrue){ cachedDataList.add(className); }else { // 去掉最后的; errorMessage = errorMessage.substring(0, errorMessage.length()-1); errorList.add(errorMessage); cachedErrorDataList.add(errorList); } if (cachedDataList.size() >= BATCH_COUNT) { saveData(); cachedDataList.clear(); } if (cachedErrorDataList.size() >= BATCH_COUNT) { saveErrorData(); cachedErrorDataList.clear(); } } private void saveErrorData() { System.out.println(cachedErrorDataList.toString()); } /** * 加上存储数据库 */ private void saveData() { try { T className = clazz.newInstance(); // 获取表名 String tableName = getTableName(className); // 获取到需要导入的字段列 List<String> propertyNameList = getColumns(className, propertyNameSet); // 得到最终数据 List<List<String>> data = cachedDataList.stream() .map(object -> propertyNameList.stream() .map(ppn -> { Object o = ReflectUtil.getFieldValue(object, ppn); return o != null ? o.toString() : null; }) .collect(Collectors.toList())) .collect(Collectors.toList()); // 得到最终列名 List<String> columns = propertyNameList.stream().map(StringUtils::camelToUnderline).collect(Collectors.toList()); if (data.size() == 0) { return; } dynamicImportExcelMapper.saveBatch(tableName, columns, data); } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); } } }).sheet().doRead(); return JsonData.buildSuccess(); } /** * 通过实体类构造一个sql的字段列数组,且必须存在于 propertyNameSet * * @param className * @param propertyNameSet * @param <T> * @return */ private <T> List<String> getColumns(T className, Set<String> propertyNameSet) { return Arrays.stream(className.getClass().getDeclaredFields()) .filter(field -> propertyNameSet.contains(field.getName())) .map(Field::getName) .collect(Collectors.toList()); } /** * 获取 className 上的@TableName 注解内容 如果没有就用类名 * * @param className * @param <T> * @return */ private static <T> String getTableName(T className) { return className.getClass().getAnnotation(TableName.class) == null ? className.getClass().getSimpleName() : className.getClass().getAnnotation(TableName.class).value(); } }
DynamicImportBaseStrategy
点击查看完整内容
import com.fasterxml.jackson.core.JsonProcessingException; import java.util.Set; /** * @author DBC * @date 2024/6/11 14:26 * @network dbc655.top * 定义基类校验 */ public class DynamicImportBaseStrategy implements DynamicImportInterface{ /** * 由子类自行实现 * @param o * @return * @throws JsonProcessingException */ @Override public Boolean check(Object o) throws JsonProcessingException { return null; } /** * 默认由父类实现 * @param o * @param repeatDataSet * @return * @throws JsonProcessingException */ @Override public Boolean check(Object o, Set<String> repeatDataSet) throws JsonProcessingException { return null; } }
DynamicImportFactory
点击查看完整内容
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.Map; /** * @author DBC * @date 2024/5/31 10:43 * @network dbc655.top */ @Component public class DynamicImportFactory { @Autowired private Map<String, DynamicImportInterface> dynamicImportInterfaceMap; public DynamicImportInterface getMsgStrategy(Class<? extends DynamicImportInterface> strategyClass) throws Exception { // 第一个字母小写 String strategyClassName = strategyClass.getSimpleName().toLowerCase().charAt(0) + strategyClass.getSimpleName().substring(1); DynamicImportInterface payInterface = dynamicImportInterfaceMap.get(strategyClassName); if(payInterface == null){ throw new Exception("错误的选项!"); } return payInterface; } }
DynamicImportIdCardStrategy
点击查看完整内容
import cn.hutool.core.date.DateUnit; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.IdcardUtil; import com.fasterxml.jackson.core.JsonProcessingException; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.Date; import java.util.Objects; import java.util.concurrent.TimeUnit; /** * @author DBC * @date 2024/5/31 10:25 * @network dbc655.top */ @Component() public class DynamicImportIdCardStrategy extends DynamicImportBaseStrategy implements DynamicImportInterface { @Override public Boolean check(Object object) throws JsonProcessingException { // 如果object类型是String 那么进入下面的判断 if (object instanceof String) { return IdcardUtil.isValidCard(String.valueOf(object)); } return false; } }
DynamicImportIdChineseStrategy
点击查看完整内容
import cn.hutool.core.lang.Validator; import cn.hutool.core.util.CharUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.pinyin.PinyinUtil; import com.fasterxml.jackson.core.JsonProcessingException; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.Objects; /** * @author DBC * @date 2024/6/3 9:20 * @network dbc655.top */ @Component() public class DynamicImportIdChineseStrategy extends DynamicImportBaseStrategy implements DynamicImportInterface { @Override public Boolean check(Object o) throws JsonProcessingException { try { // 判断是否为中文 使用hutool return Validator.isChinese(o.toString()); }catch (Exception e){ return false; } } }
DynamicImportInterface
点击查看完整内容
import com.fasterxml.jackson.core.JsonProcessingException; import java.util.Objects; import java.util.Set; /** * @author DBC * @date 2024/5/31 10:22 * @network dbc655.top */ public interface DynamicImportInterface { /** * 通用单个校验 * @param o * @return * @throws JsonProcessingException */ Boolean check(Object o) throws JsonProcessingException; /** * 基类判断是否重复 * @param o * @param repeatDataSet * @return * @throws JsonProcessingException */ Boolean check(Object o, Set<String> repeatDataSet) throws JsonProcessingException; }
DynamicImportNumberStrategy
点击查看完整内容
import cn.hutool.core.util.NumberUtil; import com.fasterxml.jackson.core.JsonProcessingException; import org.springframework.stereotype.Component; /** * @author DBC * @date 2024/5/31 10:25 * @network dbc655.top */ @Component() public class DynamicImportNumberStrategy extends DynamicImportBaseStrategy implements DynamicImportInterface { @Override public Boolean check(Object object) throws JsonProcessingException { // 判断object 是否是纯数字 try { return NumberUtil.isNumber((CharSequence) object); }catch (Exception e){ return false; } } }
DynamicImportRepeatDataStrategy
点击查看完整内容
import com.fasterxml.jackson.core.JsonProcessingException; import org.springframework.stereotype.Component; import java.util.Set; /** * @author DBC * @date 2024/6/11 11:55 * @network dbc655.top */ @Component() public class DynamicImportRepeatDataStrategy extends DynamicImportBaseStrategy implements DynamicImportInterface{ public Boolean check(Object data, Set<String> repeatDataSet) throws JsonProcessingException { if (repeatDataSet.contains(data)){ return false; }else { repeatDataSet.add(String.valueOf(data)); return true; } } }
VerifyContext
点击查看完整内容
import com.example.test05.demo.enums.VerifyModeEnum; import org.springframework.stereotype.Component; import java.util.Set; /** * @author DBC * @date 2024/6/11 14:45 * @network dbc655.top */ @Component public class VerifyContext { private final DynamicImportFactory dynamicImportFactory; public VerifyContext(DynamicImportFactory dynamicImportFactory) { this.dynamicImportFactory = dynamicImportFactory; } public boolean executeStrategy(VerifyModeEnum verifyMode, Object propertyData, Set<String> repeatDataSet) throws Exception { DynamicImportInterface strategy = verifyMode.getStrategyInstance(dynamicImportFactory); if (verifyMode == VerifyModeEnum.IS_REPEAT_DATA) { return strategy.check(propertyData, repeatDataSet); } else { return strategy.check(propertyData); } } }
具体改造的代码位置
for (String vm : split) { // 策略模式 调用对应的校验策略类 Boolean check = true; VerifyModeEnum byCode = VerifyModeEnum.getByCode(Integer.parseInt(vm)); if (byCode != null) { check = verifyContext.executeStrategy(byCode, propertyData, repeatDataSet); // 判断值是否正确 if (check) { ReflectUtil.setFieldValue(className, propertyName, propertyData); } else { errorMessage.append(StrUtil .format(byCode.getErrorMessage(), trueNameHeadMap.get(propertyName).getFieldName())).append(";"); isTrue = false; } } }
代码更加的简洁了,从设计层面上来看,更加的清晰、简洁。
导入一览
导入模版
导入后的效果
红框位置即为新增的功能,到此,第三版开发完成,距离第一个正式版本更近一步![aru_44]
四、动态导入模版
点击查看完整内容
public ResponseEntity<FileSystemResource> getTemplate(Integer funNumber) { List<DynamicImportExcel> dynamicImportExcels = dynamicImportExcelMapper.selectList(new LambdaQueryWrapper<DynamicImportExcel>().eq(DynamicImportExcel::getFunctionNumber, funNumber)); // String exportPathStr = exportCachePath + File.separator + taskId + File.separator; String exportPathStr = "C:\\Users\\Administrator\\Desktop\\q"; File exportPath = new File(exportPathStr); if (!exportPath.exists()) { exportPath.mkdirs(); } String fileName = dynamicImportExcels.get(0).getFunctionName() + "_" + "导入模版" + ".xlsx"; File file = new File(exportPathStr + File.separator + fileName); // 构造一个表头 List<List<String>> headList = dynamicImportExcels.stream().map(dynamicImportExcel -> Collections.singletonList(dynamicImportExcel.getFieldName())).collect(Collectors.toList()); // 构建第一行示例 List<List<String>> exampleList = Collections.singletonList(dynamicImportExcels.stream().map(DynamicImportExcel::getTemplateRequire).collect(Collectors.toList())); // 导出 getExcel(dynamicImportExcels.get(0).getFunctionName(), file, headList, exampleList); HttpHeaders headers = new HttpHeaders(); // 对fileName进行URL编码,确保跨浏览器兼容性 try { String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString()).replace("+", "%20"); // 导出Excel名字使用 fileName headers.add("Content-Disposition", "attachment; filename*=utf-8''" + encodedFileName); } catch (UnsupportedEncodingException e) { // 处理编码异常 throw new RuntimeException("Failed to encode file name", e.getCause()); } headers.add("Cache-Control", "no-cache, no-store, must-revalidate"); headers.add("Pragma", "no-cache"); headers.add("Expires", "0"); headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); return ResponseEntity .ok() .headers(headers) .contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) .body(new FileSystemResource(file)); } /** * 导出excel */ public void getExcel(String fileName, File file, List<List<String>> headList, List<List<String>> dataList) { excelWrite(EasyExcel.write(file), headList, dataList, fileName, new HorizontalCellStyleStrategy(getHeadStyle(), getWriteStyle())); } /** * 写出数据 */ private void excelWrite(ExcelWriterBuilder file, List<List<String>> headList, List<List<String>> dataList, String fileName, HorizontalCellStyleStrategy horizontalCellStyleStrategy) { file .head(headList) .sheet(fileName) .registerWriteHandler(horizontalCellStyleStrategy) .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) .doWrite(dataList); } /** * 获取头的样式 * * @return */ private WriteCellStyle getHeadStyle() { WriteCellStyle headWriteCellStyle = new WriteCellStyle(); headWriteCellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex()); WriteFont headWriteFont = new WriteFont(); headWriteFont.setFontHeightInPoints((short) 15); headWriteCellStyle.setWriteFont(headWriteFont); return headWriteCellStyle; } /** * 设置内容的样式 * * @return */ private WriteCellStyle getWriteStyle() { WriteCellStyle contentWriteCellStyle = new WriteCellStyle(); // 设置内容水平居中 contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER); //自动换行 contentWriteCellStyle.setWrapped(true); //垂直居中 contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER); //设置左边框 contentWriteCellStyle.setBorderLeft(BorderStyle.THIN); //设置右边框 contentWriteCellStyle.setBorderRight(BorderStyle.THIN); //设置上边框 contentWriteCellStyle.setBorderTop(BorderStyle.THIN); //设置下边框 contentWriteCellStyle.setBorderBottom(BorderStyle.THIN); return contentWriteCellStyle; }
导出后的效果
即可实现动态导入模版,搭配使用基本完成大半此功能![aru_51]
五、灵活的错误Excel导出
关键实现代码
private void saveErrorData() { // 构造一个表头 List<List<String>> headList = dynamicImportExcels.stream() .map(dynamicImportExcel -> Arrays.asList(dynamicImportExcel.getFunctionName()+" —— 错误数据Excel", dynamicImportExcel.getFieldName())) .collect(Collectors.toList()); headList.add(Arrays.asList(headList.get(0).get(0), "错误原因")); File exportPath = new File(exportPathStr); if (!exportPath.exists()) { exportPath.mkdirs(); } String fileName = dynamicImportExcels.get(0).getFunctionName() + "_" + "错误数据" + ".xlsx"; File file = new File(exportPathStr + File.separator + fileName); // 导出 getExcel(dynamicImportExcels.get(0).getFunctionName(), file, headList, cachedErrorDataList); }
实现效果
至此,基本已经完成了动态导入的代码编写,可以说是v0.9的版本了,接下来需要对代码进行相关的调整,个性化的校验类能够实现热拔插的效果即可完成整个功能[aru_36]
六、计划改造对应的保存和校验位置代码,实现更灵活的封装
本文作者为DBC,转载请注明。