Mystery0の小站

Mystery0の小站

神通数据库 Method com/oscar/jdbc/OscarResultSetV2.getObject is abstract

2024-05-15更新

根本原因不是日期时间类型不匹配,正确来说,应该是“日期类型不匹配”会导致这个问题

并不是只有“类型不匹配”才会导致这个问题

问题表现

Java连接神通数据库执行SQL报错

相同的代码已经适配了达梦、金仓数据库了,但是在神通数据库的环境中执行SQL的时候报错:

Caused by: java.lang.AbstractMethodError: Method com/oscar/jdbc/OscarResultSetV2.getObject(Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object; is abstract
        at com.oscar.jdbc.OscarResultSetV2.getObject(OscarResultSetV2.java)
        at com.zaxxer.hikari.pool.HikariProxyResultSet.getObject(HikariProxyResultSet.java)
        at com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler.getNullableResult(MybatisEnumTypeHandler.java:118)
        at com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler.getNullableResult(MybatisEnumTypeHandler.java:49)
        at org.apache.ibatis.type.BaseTypeHandler.getResult(BaseTypeHandler.java:85)
        at com.baomidou.mybatisplus.core.handlers.CompositeEnumTypeHandler.getResult(CompositeEnumTypeHandler.java:62)
        at com.baomidou.mybatisplus.core.handlers.CompositeEnumTypeHandler.getResult(CompositeEnumTypeHandler.java:37)
        at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.applyAutomaticMappings(DefaultResultSetHandler.java:572)
        at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getRowValue(DefaultResultSetHandler.java:409)
        at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValuesForSimpleResultMap(DefaultResultSetHandler.java:361)
        at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValues(DefaultResultSetHandler.java:335)
        at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSet(DefaultResultSetHandler.java:308)
        at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSets(DefaultResultSetHandler.java:201)
        at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:65)
        at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79)
......

问题分析

乍一看不知道是什么错误,像是框架自己报的

通过 Google 搜索找到以下几篇文章:

getObject(Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object; is abstract解决

【踩坑记录】Method oracle/jdbc/driver/OracleResultSetImpl.getObject is abstract 错误

真正原因请看这里

实际上就是上面两篇文章写的原因:

表中字段是DATE类型,实体类用的LocalDateTime  在做字段映射时出现异常

真正原因

MybatisPlus的类型处理器在处理的时候,会调用数据库驱动的 getObject 方法进行数据查询,但是在神通数据库中,这个方法存在单个参数的,却不存在两个参数的

  • MybatisPlus期望调用的:

public <T> T getObject(String columnLabel, Class<T> type) throws SQLException;
  • 神通数据库驱动实际存在的:

public Object getObject(int columnIndex) throws SQLException

出现这种情况一般是 神通数据库 编译时使用的 java.sql.ResultSet 与 当前使用的 java.sql.ResultSet 不匹配,这种不匹配可能是JDK版本不同、Jar版本不同等等

所以在报错信息中才会写到:

Method com/oscar/jdbc/OscarResultSetV2.getObject(Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object; is abstract

解析:

OscarResultSetV2类中的getObject函数,接收两个参数的(String, Class),这个函数是抽象的,也就是没有实现的

理论上说这种问题一般升级依赖版本就能解决,例如把神通数据库升级到最新之类的,但是我司统一了依赖的版本,所以不能通过升级版本进行解决

问题解决⚡真!

从堆栈信息我们可以看出,我这里是在处理枚举的时候遇到的问题,因此要解决这个问题,我们可以从枚举处理器出发进行相关的方法替换,将调用getObject方法的地方根据Class的类型替换为getIntgetString之类的

因此我们需要根据MybatisPlus的文档自行写一个枚举处理器,也就是写一个类,继承BaseTypeHandler,然后将枚举的类型放进去,代码如下:

public class MyEnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {
    private static final Map<String, String> TABLE_METHOD_OF_ENUM_TYPES = new ConcurrentHashMap<>();
    private static final ReflectorFactory REFLECTOR_FACTORY = new DefaultReflectorFactory();
    private final Class<E> enumClassType;
    private final Class<?> propertyType;
    private final Invoker getInvoker;

    public MyEnumTypeHandler(Class<E> enumClassType) {
        if (enumClassType == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        } else {
            this.enumClassType = enumClassType;
            MetaClass metaClass = MetaClass.forClass(enumClassType, REFLECTOR_FACTORY);
            String name = findEnumValueFieldName(this.enumClassType).orElseThrow(() -> new IllegalArgumentException(String.format("Could not find @EnumValue in Class: %s.", this.enumClassType.getName())));

            this.propertyType = ReflectionKit.resolvePrimitiveIfNecessary(metaClass.getGetterType(name));
            this.getInvoker = metaClass.getGetInvoker(name);
        }
    }

    public static Optional<String> findEnumValueFieldName(Class<?> clazz) {
        if (clazz != null && clazz.isEnum()) {
            String className = clazz.getName();
            return Optional.ofNullable(CollectionUtils.computeIfAbsent(TABLE_METHOD_OF_ENUM_TYPES, className, (key) -> {
                Optional<Field> fieldOptional = findEnumValueAnnotationField(clazz);
                return fieldOptional.map(Field::getName).orElse(null);
            }));
        } else {
            return Optional.empty();
        }
    }

    private static Optional<Field> findEnumValueAnnotationField(Class<?> clazz) {
        return Arrays.stream(clazz.getDeclaredFields()).filter((field) -> field.isAnnotationPresent(MyEnumDbValue.class)).findFirst();
    }

    public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
        if (jdbcType == null) {
            ps.setObject(i, this.getValue(parameter));
        } else {
            ps.setObject(i, this.getValue(parameter), jdbcType.TYPE_CODE);
        }
    }

    public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
        //这个函数的代码被改了
        Object value;
        if (DbTypeDetector.getDbType() == DbType.OSCAR) {
            if (this.propertyType == Integer.class) {
                value = rs.getInt(columnName);
            } else if (this.propertyType == String.class) {
                value = rs.getString(columnName);
            } else {
                value = rs.getObject(columnName, this.propertyType);
            }
        } else {
            value = rs.getObject(columnName, this.propertyType);
        }
        return null == value && rs.wasNull() ? null : this.valueOf(value);
    }

    public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        //这个函数的代码被改了
        Object value;
        if (DbTypeDetector.getDbType() == DbType.OSCAR) {
            if (this.propertyType == Integer.class) {
                value = rs.getInt(columnIndex);
            } else if (this.propertyType == String.class) {
                value = rs.getString(columnIndex);
            } else {
                value = rs.getObject(columnIndex, this.propertyType);
            }
        } else {
            value = rs.getObject(columnIndex, this.propertyType);
        }
        return null == value && rs.wasNull() ? null : this.valueOf(value);
    }

    public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        //这个函数的代码被改了
        Object value;
        if (DbTypeDetector.getDbType() == DbType.OSCAR) {
            if (this.propertyType == Integer.class) {
                value = cs.getInt(columnIndex);
            } else if (this.propertyType == String.class) {
                value = cs.getString(columnIndex);
            } else {
                value = cs.getObject(columnIndex, this.propertyType);
            }
        } else {
            value = cs.getObject(columnIndex, this.propertyType);
        }
        return null == value && cs.wasNull() ? null : this.valueOf(value);
    }

    private E valueOf(Object value) {
        E[] es = this.enumClassType.getEnumConstants();
        return Arrays.stream(es).filter((e) -> this.equalsValue(value, this.getValue(e))).findAny().orElse(null);
    }

    protected boolean equalsValue(Object sourceValue, Object targetValue) {
        String sValue = StringUtils.toStringTrim(sourceValue);
        String tValue = StringUtils.toStringTrim(targetValue);
        return sourceValue instanceof Number && targetValue instanceof Number && (new BigDecimal(sValue)).compareTo(new BigDecimal(tValue)) == 0 || Objects.equals(sValue, tValue);
    }

    private Object getValue(Object object) {
        try {
            return this.getInvoker.invoke(object, new Object[0]);
        } catch (ReflectiveOperationException e) {
            throw ExceptionUtils.mpe(e);
        }
    }
}

其中的大部分代码都是从 com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler 类里面拷贝的,仅仅修改了三个getNullableResult方法的实现,增加了对神通数据库的判断(我这里只有神通数据库遇到这个问题,如果你有其他数据库也需要同样的问题,可以参考着微调一下代码)

注意🚧:MybatisPlus针对枚举处理器有一些特殊的判断逻辑,以下逻辑仅针对 com.baomidou:mybatis-plus-boot-starter:3.5.3.1 版本,新版本有可能已经针对此做了修改

特殊逻辑如下:

在对枚举进行处理时,会进入 com.baomidou.mybatisplus.core.handlers.CompositeEnumTypeHandler 这个类型处理器,在构造函数中接收枚举的类型,然后去处理器缓存中查询有没有现成的处理器,如果没有则创建一个。

public CompositeEnumTypeHandler(Class<E> enumClassType) {
    if (enumClassType == null) {
        throw new IllegalArgumentException("Type argument cannot be null");
    }
    //判断枚举类型在不在MP_ENUM_CACHE缓存中,如果存在则直接取出来缓存数据进行返回,如果不存在则新建一个处理器
    if (CollectionUtils.computeIfAbsent(MP_ENUM_CACHE, enumClassType, MybatisEnumTypeHandler::isMpEnums)) {
        delegate = new MybatisEnumTypeHandler<>(enumClassType);
    } else {
        delegate = getInstance(enumClassType, defaultEnumTypeHandler);
    }
}

本身这个逻辑没有什么问题,但是这里调用了computeIfAbsent方法,并且在computeIfAbsent方法中调用了isMpEnums方法

🚧 注意了!

也就是说,这里的完整逻辑是:

  1. 判断枚举类型在不在MP_ENUM_CACHE缓存中,如果存在则直接取出来缓存数据进行返回

  2. 如果不存在则判断这个枚举类“是否应该交给”MybatisEnumTypeHandler处理

  3. 如果是,则新建一个MybatisEnumTypeHandler处理器

  4. 如果不是,则读取配置文件中定义的 mybatis-plus.configuration.default-enum-type-handler 配置,然后新建一个对应的处理器

看完这个逻辑应该理解我说的“特殊判断逻辑”是什么意思了吧,如果你屁颠屁颠的照着MybatisPlus的文档,直接写一个类,实现 BaseTypeHandler ,然后一运行:

  • ????

  • 怎么不生效呢?怎么还是走的 MybatisEnumTypeHandler 呢?

至于我这里说它是“特殊操作”而不是“坑”,更没有说它是“BUG”的原因,因为从MybatisPlus的角度来说,这是正确的处理逻辑,或者说是一个Feature。

我们再详细看一下MybatisPlus的文档

这两种方式也正好对应 MybatisEnumTypeHandler::isMpEnums 方法的逻辑

大家都看得懂Java代码,我就不挨个解读了。

所以,我们上面的修复方法,还有地方需要修改,实际上代码已经改了,如果大家直接拷贝过去,运行会报错,报错信息应该是“找不到MyEnumDbValue.class

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
public @interface MyEnumDbValue {
}

我这里照着MybatisPlusEnumValue注解自己搞了一个一样的,因为根据MybatisPlus的逻辑,如果你用了EnumValue注解或者实现了IEnum接口,就会强制走内置的枚举处理器,因此我们需要在使用上避开这个检测,不然我们自定义的枚举处理器不生效。

仔细看代码的同学应该也能看出来,我自定义的枚举处理器删除了对于IEnum的检测,原因是我这里用不到,就干掉了,有需要的话可以自行加回来。

最后

看到这里,相信大家应该对这个问题的原因以及修复方案都了解了,文章中的代码可能并不是100%适用于你的场景,因此在你做修复的时候,最好是根据自己的使用场景对代码进行调整。

日期类型不匹配也可能导致这个问题,解决方案和这个一样(自己写一个类型处理器 TypeHandler),只是不需要像枚举处理器一样需要搞这么多麻烦的操作了。

直接写好处理器然后在实体类上标记注解就行了

@TableField(fill = FieldFill.INSERT, typeHandler = InstantTypeHandler.class)
private Instant createTime;

@TableField(fill = FieldFill.INSERT_UPDATE, typeHandler = InstantTypeHandler.class)
private Instant updateTime;

以下为第一版原文:

问题解决(假

最简单的方法就是,把对应的PO实体类类型改了,把用到LocalDateTime、Instant、LocalDate、LocalTime的地方全部改成Date,为了和其他代码保持兼容,可以额外原类型的setter、getter,详细方案如下:

  • 改之前:

@TableField(fill = FieldFill.INSERT)
private Instant createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Instant updateTime;
  • 改之后:

@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;

public void setCreateTime(Instant createTime) {
    if (createTime == null) {
        this.createTime = null;
    } else {
        this.createTime = Date.from(createTime);
    }
}

public Instant getCreateTime() {
    if (createTime == null) {
        return null;
    }
    return createTime.toInstant();
}

public void setUpdateTime(Instant updateTime) {
    if (updateTime == null) {
        this.updateTime = null;
    } else {
        this.updateTime = Date.from(updateTime);
    }
}

public Instant getUpdateTime() {
    if (updateTime == null) {
        return null;
    }
    return updateTime.toInstant();
}