神通数据库 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
的类型替换为getInt
、getString
之类的
因此我们需要根据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
方法
🚧 注意了!
也就是说,这里的完整逻辑是:
判断枚举类型在不在
MP_ENUM_CACHE
缓存中,如果存在则直接取出来缓存数据进行返回如果不存在则判断这个枚举类“是否应该交给”
MybatisEnumTypeHandler
处理如果是,则新建一个
MybatisEnumTypeHandler
处理器如果不是,则读取配置文件中定义的
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 {
}
我这里照着MybatisPlus
的EnumValue
注解自己搞了一个一样的,因为根据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();
}
- 1
- 0
-
分享