Arrays.asList(T... a)导致的事故

📚项目场景:

修改数据时,允许将非必填字段清空。

⛔问题描述:

由于使用的是Mybatis-Plus,只能使用LambdaUpdateWrapperUpdateWrapper通过set(column,val)来将字段清空;因为字段太多导致大量set放在一个方法,不符合部分规范,所以封装了一个SqlUtils工具来对字段进行set。

有人会问,为什么不用Mybatis-Plus@TableField(updateStrategy = FieldStrategy.IGNORED)?这不是可以直接加上注解修改的时候就会自己添加置空进去吗?

使用TableField注解确实可以达到你说的效果,但是得区分项目的整体情况;如果整个项目都是使用Mybatis-PlusLambdaUpdateWrapper来对数据的状态或者价格等字段进行更新的话,那么这个注解的可行性就少了一部分。因为有些场景下你只需要对某个字段进行更新而不是更新所有字段,例如:我这是一张申请单据,对应人送审成功,我需要更新未处理状态到处理中,这种情况下我们是不需要更新状态字段之外的其他字段,如果使用这种方式则会把其他数据清空。除非在更新前查一遍数据,然后拿这份数据去修改,这样对程序和数据库来说增加压力!

Java 复制代码
/**
 * sql处理工具
 *
 * @author Smallink *
 * @version Id: SqlUtils, v 0.1 2023/8/22 11:59 Smallink Exp $
 */
@Slf4j
@Component
public class SqlUtils<T> {

    private static final int FLAG_SERIALIZABLE = 1;

    private static final List<String> DEFAULT_EXCLUSION_FIELD = Arrays.asList("id", "createBy", "createName", "createTime", "updateBy", "updateName", "updateTime");

    /**
     * 将为空字段拼入sql
     *
     * @param wrapper 封装器
     * @param object  需要拼入对象
     * @return LambdaUpdateChainWrapper
     */
    public static UpdateWrapper packSqlUtils(UpdateWrapper<?> wrapper, Object object) {
        return (UpdateWrapper) packSqlUtils(wrapper, object, false, new ArrayList<>());
    }

    /**
     * 将为空字段拼入sql
     *
     * @param wrapper   封装器
     * @param object    需要拼入对象
     * @param fieldList 不需要拼入的字段
     * @return LambdaUpdateChainWrapper
     */
    public static UpdateWrapper packSqlUtils(UpdateWrapper<?> wrapper, Object object, List<String> fieldList) {
        return (UpdateWrapper) packSqlUtils(wrapper, object, false, CollectionUtil.isEmpty(fieldList) ? new ArrayList<>() : fieldList);
    }

    /**
     * 将为空字段拼入sql
     *
     * @param wrapper         封装器
     * @param object          需要拼入对象
     * @param ignoreNullValue 是否忽略值为空的字段
     * @param fieldList       不需要拼入的字段
     * @return LambdaUpdateChainWrapper
     */
    public static Update packSqlUtils(Update wrapper, Object object, boolean ignoreNullValue, List<String> fieldList) {
        if (ObjectUtils.isEmpty(object) || ObjectUtils.isEmpty(wrapper)) {
            log.info("mandatory parameters cannot be empty");
            throw new BizException(ErrCodeEnum.WRAPPER_OBJECT_IS_NULL_ERROR.getErrCode(), ErrCodeEnum.WRAPPER_OBJECT_IS_NULL_ERROR.getMsg());
        }

        // 添加默认排除字段
        fieldList.addAll(DEFAULT_EXCLUSION_FIELD);

        Class<?> aClass = object.getClass();
        // 将对象转换成Map
        Map<String, Object> objectMap = BeanUtil.beanToMap(object, new LinkedHashMap<>(), ignoreNullValue, key -> fieldList.contains(key) ? null : StrUtil.toUnderlineCase(key));

        // 将对象当需要修改值写入
        objectMap.forEach((key, value) -> {
            String fieldType = getFieldType(CamelUnderLineUtils.underLineToCamel(key), aClass);
            if (StringUtils.isNotEmpty(fieldType)) {
                wrapper.set(key, value);
            }
        });
        return wrapper;
    }

    // --------------------------------------------- Lambda

    /**
     * 将为空字段拼入sql
     *
     * @param wrapper 封装器
     * @param object  需要拼入对象
     * @return LambdaUpdateChainWrapper
     */
    public LambdaUpdateWrapper packLambdaSqlUtils(LambdaUpdateWrapper<T> wrapper, T object) {
        return (LambdaUpdateWrapper) packLambdaSqlUtils(wrapper, object, false, new ArrayList<>());
    }

    /**
     * 将为空字段拼入sql
     *
     * @param wrapper 封装器
     * @param object  需要拼入对象
     * @return LambdaUpdateChainWrapper
     */
    public LambdaUpdateChainWrapper packLambdaSqlUtils(LambdaUpdateChainWrapper<T> wrapper, T object) {
        return (LambdaUpdateChainWrapper) packLambdaSqlUtils(wrapper, object, false, new ArrayList<>());
    }

    /**
     * 将为空字段拼入sql
     *
     * @param wrapper   封装器
     * @param object    需要拼入对象
     * @param fieldList 不需要拼入的字段
     * @return LambdaUpdateChainWrapper
     */
    public LambdaUpdateWrapper packLambdaSqlUtils(LambdaUpdateWrapper<T> wrapper, T object, List<String> fieldList) {
        return (LambdaUpdateWrapper) packLambdaSqlUtils(wrapper, object, false, CollectionUtil.isEmpty(fieldList) ? new ArrayList<>() : fieldList);
    }

    /**
     * 将为空字段拼入sql
     *
     * @param wrapper   封装器
     * @param object    需要拼入对象
     * @param fieldList 不需要拼入的字段
     * @return LambdaUpdateChainWrapper
     */
    public LambdaUpdateChainWrapper packLambdaSqlUtils(LambdaUpdateChainWrapper<T> wrapper, T object, List<String> fieldList) {
        return (LambdaUpdateChainWrapper) packLambdaSqlUtils(wrapper, object, false, CollectionUtil.isEmpty(fieldList) ? new ArrayList<>() : fieldList);
    }

    /**
     * 将为空字段拼入sql
     *
     * @param wrapper         封装器
     * @param object          需要拼入对象
     * @param ignoreNullValue 是否忽略值为空的字段
     * @param fieldList       不需要拼入的字段
     * @return LambdaUpdateChainWrapper
     */
    public Update packLambdaSqlUtils(Update<?, SFunction<T, ?>> wrapper, T object, boolean ignoreNullValue, List<String> fieldList) {
        if (ObjectUtils.isEmpty(object) || ObjectUtils.isEmpty(wrapper)) {
            log.info("mandatory parameters cannot be empty");
            throw new BizException(ErrCodeEnum.WRAPPER_OBJECT_IS_NULL_ERROR.getErrCode(), ErrCodeEnum.WRAPPER_OBJECT_IS_NULL_ERROR.getMsg());
        }
        // 添加默认排除字段
        fieldList.addAll(DEFAULT_EXCLUSION_FIELD);

        Class<?> aClass = object.getClass();
        // 将对象转换成Map
        Map<String, Object> objectMap = BeanUtil.beanToMap(object, new LinkedHashMap<>(), ignoreNullValue, key -> fieldList.contains(key) ? null : key);
        // 将对象当需要修改值写入
        objectMap.forEach((key, value) -> {
            String fieldType = getFieldType(key, aClass);
            SFunction sFunction = StringToFunction(key, fieldType, aClass);
            wrapper.set(sFunction, value);
        });
        return wrapper;
    }

    /**
     * 将字段名转换为对应的函数式
     *
     * @param name        字段名
     * @param fieldType   字段类型
     * @param entityClass 类
     * @return
     */
    private static SFunction StringToFunction(String name, String fieldType, Class<?> entityClass) {
        Class<?> rType = getRType(fieldType);
        SFunction func;
        String getMethod = "get" + name.substring(0, 1).toUpperCase() + name.substring(1);
        final MethodHandles.Lookup lookup = MethodHandles.lookup();
        //po的返回Integer的一个方法
        MethodType methodType = MethodType.methodType(rType, entityClass);
        final CallSite site;
        try {
            //方法名叫做:getSecretLevel  转换为 SFunction function interface对象
            site = LambdaMetafactory.altMetafactory(lookup,
                    "invoke",
                    MethodType.methodType(SFunction.class),
                    methodType,
                    lookup.findVirtual(entityClass, getMethod, MethodType.methodType(rType)),
                    methodType, FLAG_SERIALIZABLE);
            func = (SFunction) site.getTarget().invokeExact();
            //数据小于这个级别的都查出来
            //    mpjLambdaWrapper.le(func, secretLevel);
            return func;
        } catch (Throwable e) {
            log.error("获取getSecretLevel方法错误", e);
        }
        return null;
    }

    /**
     * 类型转换
     *
     * @param fieldType 类型
     * @return
     */
    private static Class<?> getRType(String fieldType) {
        Class<?> rtype = null;
        switch (fieldType) {
            case "class java.lang.String":
                rtype = String.class;
                break;
            case "class java.lang.Integer":
                rtype = Integer.class;
                break;
            case "class java.lang.Double":
                rtype = Double.class;
                break;
            case "class java.lang.Boolean":
                rtype = Boolean.class;
                break;
            case "class java.util.Date":
                rtype = Date.class;
                break;
            case "class java.time.LocalDate":
                rtype = LocalDate.class;
                break;
            case "class java.time.LocalDateTime":
                rtype = LocalDateTime.class;
                break;
            case "class java.math.BigDecimal":
                rtype = BigDecimal.class;
                break;
            case "class java.lang.Long":
                rtype = Long.class;
                break;
        }
        return rtype;
    }

    /**
     * 获取字段类型
     *
     * @param fieldName 字段名
     * @param clazz     类
     * @return
     */
    private static String getFieldType(String fieldName, Class<?> clazz) {
        Field[] fields = ReflectUtils.getAllFields(clazz);
        for (Field f : fields) {
            boolean exist = false;
            TableField tableField = f.getAnnotation(TableField.class);
            if (tableField != null) {
                exist = !tableField.exist();
            }
            if (fieldName.equals(f.getName()) && !exist) {
                f.setAccessible(true);
                return f.getGenericType().toString();
            }
        }
        return null;
    }

}

工具是支持传入参数来排除指定字段修改为空(没有选择使用自定义注解,自定义注解增加了功能的复杂难度与维护,所以使用了最便捷的方式),以下是示例 需要结合SqlUtils工具看

Java 复制代码
    @Resource
    private SqlUtils sqlUtils;

    // 对象中不需要封装的字段名,
    public static List<String> SALE_QUOTE_EXCLUSION_FIELD = Arrays.asList("customerNo","customerName","quoteStatus","productCategory","productType","deliveryMethod");

    public BusinessResponse updateQuote(Object obj) {
        LambdaUpdateWrapper<Object> detailPOLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
        // 调用SqlUtils工具来封装需要置空的字段
        sqlUtils.packLambdaSqlUtils(detailPOLambdaUpdateWrapper, obj, SALE_QUOTE_DATEIL_EXCLUSION_FIELD);
        detailPOLambdaUpdateWrapper.eq(BasePO::getId, id);
        baseMapper.update(new Object(), detailPOLambdaUpdateWrapper);
    }

除了指定排除字段之外,还有默认字段所以使用了add()来添加,然后就导致了异常:

乍一看,这不是空指针嘛,so easy啊(内心os:明明就有值,怎么会报这个错误呢?而且编译时也没有报错)

仔细一瞧,这UnsupportedOperationException是个什么玩意?

Java 复制代码
java.lang.UnsupportedOperationException: null
	at java.util.AbstractList.add(AbstractList.java:148) ~[na:1.8.0_151]
	at java.util.AbstractList.add(AbstractList.java:108) ~[na:1.8.0_151]

🔍原因分析:

秉持出现问题,解决问题的思想 (哈哈哈)。断点调试后,还是有问题;接下来就是看源码了,请展示:

Java 复制代码
    @SafeVarargs
    @SuppressWarnings("varargs")
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

    private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {
        private static final long serialVersionUID = -2764017481108945198L;
        private final E[] a;

        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }

        @Override
        public int size() {
            return a.length;
        }

        @Override
        public Object[] toArray() {
            return a.clone();
        }

        @Override
        @SuppressWarnings("unchecked")
        public <T> T[] toArray(T[] a) {
            int size = size();
            if (a.length < size)
                return Arrays.copyOf(this.a, size,
                                     (Class<? extends T[]>) a.getClass());
            System.arraycopy(this.a, 0, a, 0, size);
            if (a.length > size)
                a[size] = null;
            return a;
        }

        @Override
        public E get(int index) {
            return a[index];
        }

        @Override
        public E set(int index, E element) {
            E oldValue = a[index];
            a[index] = element;
            return oldValue;
        }

        @Override
        public int indexOf(Object o) {
            E[] a = this.a;
            if (o == null) {
                for (int i = 0; i < a.length; i++)
                    if (a[i] == null)
                        return i;
            } else {
                for (int i = 0; i < a.length; i++)
                    if (o.equals(a[i]))
                        return i;
            }
            return -1;
        }

        @Override
        public boolean contains(Object o) {
            return indexOf(o) != -1;
        }

        @Override
        public Spliterator<E> spliterator() {
            return Spliterators.spliterator(a, Spliterator.ORDERED);
        }

        @Override
        public void forEach(Consumer<? super E> action) {
            Objects.requireNonNull(action);
            for (E e : a) {
                action.accept(e);
            }
        }

        @Override
        public void replaceAll(UnaryOperator<E> operator) {
            Objects.requireNonNull(operator);
            E[] a = this.a;
            for (int i = 0; i < a.length; i++) {
                a[i] = operator.apply(a[i]);
            }
        }

        @Override
        public void sort(Comparator<? super E> c) {
            Arrays.sort(a, c);
        }
    }

可以从源码中看出,此ArrayList非彼java.util.ArrayListArrays.asList()使用的是类部类中的ArrayList;它继承的是AbstractList,并没有实现AbstractList里面的add()方法!!!

看到这里相信各位同学应该知道问题了,没错就是没有实现里面的add()所以导致了《空指针》异常🌚

💡问题解决:

既然知道了问题,那么就好解决了!

换一个初始化List集合的工具就行啦!

我换成了hutool的CollectionUtil.toList

📝问题总结:

经验太少...哈哈哈哈

相关推荐
花哥码天下5 分钟前
apifox登录后设置token到环境变量
java·后端
浩瀚地学23 分钟前
【Java】常用API(二)
java·开发语言·经验分享·笔记·学习
hashiqimiya1 小时前
springboot事务触发滚动与不滚蛋
java·spring boot·后端
因我你好久不见1 小时前
Windows部署springboot jar支持开机自启动
windows·spring boot·jar
PPPHUANG1 小时前
一次 CompletableFuture 误用,如何耗尽 IO 线程池并拖垮整个系统
java·后端·代码规范
恩创软件开发2 小时前
创业日常2026-1-8
java·经验分享·微信小程序·小程序
无关86882 小时前
SpringBootApplication注解大解密
spring boot
想用offer打牌2 小时前
一站式了解Spring AI Alibaba的流式输出
java·人工智能·后端
Lonely丶墨轩2 小时前
从登录入口窥见架构:一个企业级双Token认证系统的深度拆解
java·数据库·sql