一、第一版简单的导入实现
最开始的准备工作
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,转载请注明。











