fel表达式计算引擎用于excel导入识别

328 阅读6分钟

应用场景

    当外部多个模板数据需要统一导入到系统中时,起初代码各种判断针对不同模板编写不同的转换规则。
    随着外部模板不端增加代码便成了屎山难以维护。
    这时需要一个通用的解决方案不再每次添加模板增加代码。
    调研后发现表达式引擎fel满足需求。

参考文章:

1.Java动态脚本&规则引擎、计算/表达式引擎
    https://juejin.cn/post/6981474006293544991
2.Fel表达式计算引擎学习
    https://www.cnblogs.com/boothsun/p/7995387.html

代码实现:

study-thread/src/main/java/cn/zy/study/thread/fel

jel自定义函数封装

package com.hzqc.modules.imports;

import com.greenpineyu.fel.FelEngine;
import com.greenpineyu.fel.function.CommonFunction;
import com.greenpineyu.fel.function.Function;
import com.hzqc.exception.BadException400;//私包
import com.hzqc.utils.DateUtil;//私包
import com.hzqc.utils.StringUtils;//私包

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

/**
 * fel 扩展公共的自定义function
 * @author Yi.Zhang
 * @date 2023-05-28
 */
public class FelUtils {
    public static FelEngine getFelEngine() {
        FelEngine fel = FelEngine.instance;
        fel.addFun(objectResultIsNotNull());
        fel.addFun(booleanResultIsNotNull());
        fel.addFun(checkLength());
        fel.addFun(setDefault());
        fel.addFun(contains());
        fel.addFun(echoPhone());
        fel.addFun(echoVin());
        fel.addFun(dateFormatString());
        fel.addFun(setScale());
        fel.addFun(safeIntercept());
        return fel;
    }

    /**
     * 保险公司截取
     *
     * @return
     */
    public static Function safeIntercept() {
        return new CommonFunction() {
            @Override
            public String getName() {
                return "safeIntercept";
            }

            @Override
            public Object call(Object[] arguments) {
                if (arguments != null && arguments.length > 0) {
                    String bxgs = String.valueOf(arguments[0]);
                    if (StringUtils.isNotBlanks(bxgs) && bxgs.contains("送修")) {
                        bxgs = bxgs.split("送修").length == 0 ? "送修" : bxgs.split("送修")[0];
                        return bxgs + "送修";
                    }
                    return null;
                } else {
                    return null;
                }
            }
        };
    }

    /**
     * 设置 精度
     *
     * @return
     */
    public static Function setScale() {
        return new CommonFunction() {
            @Override
            public String getName() {
                return "setScale";
            }

            @Override
            public Object call(Object[] arguments) {
                if (arguments != null && arguments.length > 0) {
                    Object str = arguments[0];
                    Integer length = (Integer) arguments[1];
                    String typeName = str.getClass().getTypeName();
                    BigDecimal covertBigdecimal = new BigDecimal(String.valueOf(str)).setScale(length, RoundingMode.HALF_UP);
                    if ("java.math.BigDecimal".equals(typeName)) {
                        return covertBigdecimal;
                    } else if ("java.lang.Double".equals(typeName)) {
                        return covertBigdecimal.doubleValue();
                    } else if ("java.lang.Folat".equals(typeName)) {
                        return covertBigdecimal.floatValue();
                    } else {
                        return str;
                    }
                } else {
                    return null;
                }
            }
        };
    }

    /**
     * 使用示例:
     * dateFormatString(jsTime,true,"yyyy-MM-dd","结算日期")
     *
     * @return
     */
    public static Function dateFormatString() {
        return new CommonFunction() {
            @Override
            public String getName() {
                return "dateFormatString";
            }

            @Override
            public Object call(Object[] arguments) {
                if (arguments != null && arguments.length > 0) {
                    Object str = arguments[0];
                    Boolean isMust = (Boolean) arguments[1];
                    String erroFiled = (String) arguments[3];
                    String format = (String) arguments[2];
                    if (isMust && StringUtils.isBlanks(str)) {
                        throw new BadException400(erroFiled + ":不允许为空!");
                    }
                    return str == null ? null : DateUtil.format((Date) str, format);
                } else {
                    return null;
                }
            }
        };
    }

    /**
     * 校验字段是否为空并返回
     * <p>
     * 使用示例:
     * objectResultIsNotNull(jsjeHsPrice,true,"结算金额(含税)")
     * <p>
     * 参数说明:
     * 参数1:jsjeHsPrice:要校验的数据
     * 参数2:true/false 是否必填校验
     * 参数3:校验的字段名
     *
     * @return 校验过后的数据
     */
    public static Function objectResultIsNotNull() {
        return new CommonFunction() {
            @Override
            public String getName() {
                return "objectResultIsNotNull";
            }

            @Override
            public Object call(Object[] arguments) {
                if (arguments != null && arguments.length > 0) {
                    Object str = arguments[0];
                    Boolean isMust = (Boolean) arguments[1];
                    String erroFiled = (String) arguments[2];
                    if (isMust && StringUtils.isBlanks(str)) {
                        throw new BadException400(erroFiled + ":不允许为空!");
                    }
                    return str;
                } else {
                    return null;
                }
            }
        };
    }

    /**
     * 使用示例:
     * booleanResultIsNotNull(jclc,false,"进厂里程")
     * <p>
     * 参数说明:
     * 参数1: jclc:要校验必填的数据
     * 参数2: 校验的字段名
     *
     * @return 校验的数据是否为空
     */
    public static Function booleanResultIsNotNull() {
        //自定义函数 ***注意 Function 引包
        return new CommonFunction() {
            @Override
            public String getName() {
                return "booleanResultIsNotNull";
            }

            @Override
            public Boolean call(Object[] arguments) {
                if (arguments != null && arguments.length > 0) {
                    Object str = arguments[0];
                    Boolean isMust = (Boolean) arguments[1];
                    String erroFiled = (String) arguments[2];
                    if (isMust && StringUtils.isBlanks(str)) {
                        throw new BadException400(erroFiled + ":不允许为空!");
                    }
                    return true;
                } else {
                    return false;
                }
            }
        };
    }

    /**
     * 校验Object长度,当长于规定长度报错,否则返回校验字段
     * <p>
     * 使用示例:
     * checkLength(yshj, 8,"校验字段名")
     *
     * @return
     */
    public static Function checkLength() {
        return new CommonFunction() {
            @Override
            public String getName() {
                return "checkLength";
            }

            @Override
            public Object call(Object[] arguments) {
                if (arguments != null && arguments.length > 0) {
                    String str = String.valueOf(arguments[0]);
                    Integer length = (Integer) arguments[1];
                    String erroFiled = (String) arguments[2];
                    if (str != null && str.length() > length) {
                        throw new BadException400(String.format("[%s]长度过长、当前长度[%s]、限制长度[%s]", erroFiled, str.length(), length));
                    }
                    return arguments[0];
                } else {
                    return null;
                }
            }
        };
    }

    public static Function echoPhone() {
        return new CommonFunction() {
            @Override
            public String getName() {
                return "echoPhone";
            }

            @Override
            public Object call(Object[] arguments) {
                if (arguments != null && arguments.length > 0) {
                    Object phone = arguments[0];
                    return StringUtils.echoPhone(String.valueOf(phone));
                } else {
                    return null;
                }
            }
        };
    }

    public static Function echoVin() {
        return new CommonFunction() {
            @Override
            public String getName() {
                return "echoVin";
            }

            @Override
            public Object call(Object[] arguments) {
                if (arguments != null && arguments.length > 0) {
                    Object vin = arguments[0];
                    return StringUtils.echoVin(String.valueOf(vin));
                } else {
                    return null;
                }
            }
        };
    }

    /**
     * 当校验字段为空或者空字符串时 设置一个默认值 否则返回校验的数据
     * <p>
     * 使用示例:
     * setDefault(yshj, 0)
     *
     * @return
     */
    public static Function setDefault() {
        //自定义函数 ***注意 Function 引包
        return new CommonFunction() {
            @Override
            public String getName() {
                return "setDefault";
            }

            @Override
            public Object call(Object[] arguments) {
                if (arguments != null && arguments.length > 0) {
                    Object filed = arguments[0];
                    Object defaultValue = arguments[1];
                    return (filed == null || filed == "") ? defaultValue : filed;
                } else {
                    return null;
                }
            }
        };
    }

    /**
     * 是否包含某个字段
     * 包含返回true 不包含返回 false
     * <p>
     * 使用示例:
     * contains('张三,李四,王五,赵六', \"张三\")   ---->true
     *
     * @return
     */
    public static Function contains() {
        //自定义函数 ***注意 Function 引包
        return new CommonFunction() {
            @Override
            public String getName() {
                return "contains";
            }

            @Override
            public Boolean call(Object[] arguments) {
                if (arguments != null && arguments.length > 0) {
                    String listString = (String) arguments[0];
                    String[] stringArray = listString.split(",");
                    List<String> list = Arrays.asList(stringArray);
                    Object currentValue = String.valueOf(arguments[1]);
                    return list.contains(currentValue);
                } else {
                    return false;
                }
            }
        };
    }
}

数据库表设计

CREATE TABLE `upload_templet` (  
  `id` bigint(20NOT NULL AUTO_INCREMENT,  
  `table_name` varchar(100COLLATE utf8mb4_general_ci NOT NULL COMMENT '模板对应表名',  
  `table_remark` varchar(100COLLATE utf8mb4_general_ci NOT NULL COMMENT '表备注',  
  `diff_field` varchar(100COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '模板区分字段名称',  
  `diff_field_value` varchar(100COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '模板区分字段值',  
  `templet_name` varchar(100COLLATE utf8mb4_general_ci NOT NULL COMMENT '模板名称',  
  `enabled` bit(1NOT NULL DEFAULT b'0' COMMENT '是否启用:0禁用 1启用',  
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',  
  `create_user` varchar(20CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '创建人',  
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',  
  `update_user` varchar(20CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '修改人',  
  PRIMARY KEY (`id`)  
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='导入模板';

CREATE TABLE `upload_filed_mapping` (  
  `id` bigint(20NOT NULL AUTO_INCREMENT,  
  `temp_id` bigint(20NOT NULL COMMENT '(upload_templet) id',  
  `is_database` bit(1NOT NULL DEFAULT b'1' COMMENT '是否数据库字段',  
  `field_name` varchar(50CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '字段名',  
  `field_remark` varchar(100CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '字段名描述',  
  `mapping_column` varchar(50COLLATE utf8mb4_general_ci NOT NULL COMMENT '映射excel列名',  
  `filed_type` varchar(10CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '数据库字段类型',  
  `sorts` int(3DEFAULT NULL COMMENT '排序',  
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',  
  `create_user` varchar(20CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '创建人',  
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',  
  `update_user` varchar(20CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '修改人',  
  PRIMARY KEY (`id`),  
  UNIQUE KEY `temp_id_is_database_field_name` (`temp_id`,`is_database`,`field_name`) USING BTREE  
) ENGINE=InnoDB AUTO_INCREMENT=45 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='模板表字段映射';

CREATE TABLE `upload_terms_mapping` (  
  `id` bigint(20NOT NULL AUTO_INCREMENT,  
  `filed_id` bigint(20NOT NULL COMMENT 'updload_filed_mapping id',  
  `terms_exp` varchar(500CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'true' COMMENT '条件表达式',  
  `result_exp` varchar(500CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '结果表达式',  
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',  
  `create_user` varchar(20CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '创建人',  
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',  
  `update_user` varchar(20CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '修改人',  
  PRIMARY KEY (`id`)  
) ENGINE=InnoDB AUTO_INCREMENT=39 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='条件映射表';

导入工具

package com.hzqc.modules.imports;

import cn.hutool.core.collection.CollUtil;
import com.alibaba.fastjson.JSON;
import com.greenpineyu.fel.FelEngine;
import com.greenpineyu.fel.context.FelContext;
import com.hzqc.exception.BadException400;//私包
import com.hzqc.exception.BadExceptionNoMsg;//私包
import com.hzqc.model.ExcelMap;//私包
import com.hzqc.modules.imports.domain.UploadFiledMapping;
import com.hzqc.modules.imports.domain.UploadTemplet;
import com.hzqc.modules.imports.domain.UploadTermsMapping;
import com.hzqc.modules.imports.repository.UploadFiledMappingRepository;
import com.hzqc.modules.imports.repository.UploadTempletRepository;
import com.hzqc.modules.imports.repository.UploadTermsMappingRepository;
import com.hzqc.modules.mnt.model.ExcelSocketMsg;//私包
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author Yi.Zhang
 * @date 2023-05-28
 */
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class ImportService {

    private final UploadTempletRepository uploadTempletRepository;

    private final UploadFiledMappingRepository uploadFiledMappingRepository;

    private final UploadTermsMappingRepository uploadTermsMappingRepository;

    public <T> List<T> covertList(List<ExcelMap> list, Long templetId, Class<T> beanClass, ExcelSocketMsg msg) throws Exception {
        List<T> listResult = new ArrayList<>();
        int row = 1;
        try {
            for (ExcelMap map : list) {
                row++;
                T covert = covert(map, templetId, beanClass);
                listResult.add(covert);
                msg.sendMsg("已组装" + listResult.size() + "条数据");
            }
            return listResult;
        } catch (Exception e) {
            msg.sendError(row, e);
            throw new BadException400("excel数据转换异常!");
        }
    }

    /**
     * 将一行excel数据根据模板配置转换成对象
     * @param map       excel 行数据
     * @param templetId 模板id
     * @param beanClass 转换的目标对象
     * @param <T>
     * @return
     * @throws Exception
     */
    public <T> T covert(ExcelMap map, Long templetId, Class<T> beanClass) throws Exception {
        UploadTemplet uploadTemplet = uploadTempletRepository.findById(templetId)
                .orElseThrow(() -> new BadExceptionNoMsg("导入excel模板不存在"));
        log.info("导入数据模板为:" + JSON.toJSONString(uploadTemplet));
        //查询所有的映射字段
        List<UploadFiledMapping> listFiledMapings = uploadFiledMappingRepository.findByTempId(uploadTemplet.getId());
        //排序
        List<UploadFiledMapping> sortListFiledMappings = listFiledMapings.stream().sorted(Comparator.comparingInt(UploadFiledMapping::getSorts)).collect(Collectors.toList());
        //映射map
        Map<String, Object> map1 = new HashMap<>();
        FelEngine felEngine = FelUtils.getFelEngine();
        FelContext felContext = felEngine.getContext();
        //需要特殊规则映射的字段
        sortListFiledMappings.forEach(filedMapping -> {
            //这里是映射规则
            List<UploadTermsMapping> listTermsMappings = uploadTermsMappingRepository.findByFiledId(filedMapping.getId());
            Object value = getObject(map, filedMapping);
            if (CollUtil.isNotEmpty(listTermsMappings)) {
                value = mappingProcessing(listTermsMappings, felEngine, felContext);
            }
            log.info("当前转换字段:" + filedMapping.getFieldName() + ".转换后的值:" + value);
            map1.put(filedMapping.getFieldName(), value);
            felContext.set(filedMapping.getFieldName(), value);
        });
        return beanClass.cast(mapToBean(map1, beanClass));
    }

    private Object mappingProcessing(List<UploadTermsMapping> listTermsMappings, FelEngine felEngine, FelContext felContext) {
        Object value = null;
        try {
            if (listTermsMappings.size() == 1) {
                //一对一
                UploadTermsMapping uploadTermsMapping = listTermsMappings.get(0);
                String termsExp = uploadTermsMapping.getTermsExp();
                String resultExp = uploadTermsMapping.getResultExp();
                Boolean eval = (Boolean) felEngine.eval(termsExp, felContext);
                if (eval) {
                    value = felEngine.eval(resultExp, felContext);
                }
            } else {
                //一对多
                Boolean flag = false;
                for (UploadTermsMapping listTermsMapping : listTermsMappings) {
                    String termsExp = listTermsMapping.getTermsExp();
                    String resultExp = listTermsMapping.getResultExp();
                    Boolean eval = (Boolean) felEngine.eval(termsExp, felContext);
                    if (eval) {
                        value = felEngine.eval(resultExp, felContext);
                        flag = true;
                        break;
                    }
                }
                if (!flag) {
                    log.error("未匹配到正确的条件!");
                }
            }
        } catch (Exception e) {
            throw new BadException400(String.format("表达式识别异常:异常表达式:[%s].异常信息:[%s].", JSON.toJSONString(listTermsMappings), e.getMessage()));
        }
        return value;
    }

    /**
     * map 转对象
     *
     * @param map
     * @param beanClass
     * @param <T>
     * @return
     * @throws Exception
     */
    public <T> T mapToBean(Map<String, Object> map, Class<T> beanClass) throws InstantiationException, IllegalAccessException {
        T object = beanClass.newInstance();
        Field[] fields = object.getClass().getDeclaredFields();
        for (Field field : fields) {
            int mod = field.getModifiers();
            if (Modifier.isStatic(mod) || Modifier.isFinal(mod)) {
                continue;
            }
            ReflectionUtils.makeAccessible(field);
            if (map.containsKey(field.getName())) {
                if (map.get(field.getName()) != null) {
                    String mapTypeName = map.get(field.getName()).getClass().getTypeName();
                    String beanTypeName = field.getType().getTypeName();
                    if (!mapTypeName.equals(beanTypeName)) {
                        if ("java.lang.Double".equals(mapTypeName) && "java.math.BigDecimal".equals(beanTypeName)) {
                            ReflectionUtils.setField(field, object, BigDecimal.valueOf((Double) map.get(field.getName())));
                            continue;
                        }
                        if ("java.lang.Integer".equals(mapTypeName) && "java.math.BigDecimal".equals(beanTypeName)) {
                            ReflectionUtils.setField(field, object, new BigDecimal(Integer.parseInt(map.get(field.getName()).toString())));
                            continue;
                        }
                        throw new BadException400(String.format("反射转换实体时类型对应异常!异常字段名:[%s].异常字段mapType:[%s].异常字段beanType:[%s].", field.getName(), mapTypeName, beanTypeName));
                    }
                }
                ReflectionUtils.setField(field, object, map.get(field.getName()));
            }
        }
        return object;
    }

    /**
     * 对象转map
     *
     * @param object
     * @return
     * @throws IllegalAccessException
     */
    public static <T> Map<String, Object> beanToMap(T object) throws IllegalAccessException {
        Map<String, Object> map = new HashMap<>();
        Field[] fields = object.getClass().getDeclaredFields();
        for (Field field : fields) {
            ReflectionUtils.makeAccessible(field);
            map.put(field.getName(), field.get(object));
        }
        return map;
    }

    public static Object getObject(ExcelMap map, UploadFiledMapping filedMapping) {
        String mappingColumn = filedMapping.getMappingColumn();
        if (!"无".equals(mappingColumn) && map.get(mappingColumn) == null) {
            throw new BadException400("表头无法对应请检查模板是否正确!异常表头:" + mappingColumn);
        }
        String filedType = filedMapping.getFiledType();
        Object value;
        switch (filedType) {
            case "Long":
                value = map.getLong(mappingColumn);
                break;
            case "Integer":
                value = map.getInteger(mappingColumn);
                break;
            case "BigDecimal":
                value = map.getBigDecimal(mappingColumn);
                break;
            case "Date":
                value = map.getDate(mappingColumn);
                break;
            case "String":
                value = map.getString(mappingColumn);
                break;
            case "Boolean":
                value = map.getBoolean(mappingColumn);
                break;
            default:
                throw new BadException400("类型配置异常!-->" + filedType + "-->" + mappingColumn);
        }
        return value;
    }

}