oracle转postgresql时,使用jpa(hibernate)报错: 不良的类型值 long

缘起

近日, 公司要和第三方对接, 而第三方不能和我方标准接口对接, 因此只能我方定时从他们的数据库中拉取数据, 然后调用我方标准接口将数据推送到我方服务器中去. 即要实现一个中间层的功能.

这个功能的实现思路是极为简单的, 就是写一个定时任务去第三方的数据库中拉取数据就是. 而且公司原本就有定时任务的代码, 可以copy. 但是公司的数据库是oracle的, 而第三方是pg数据库, 本想都是关系型的,最多数据类型有点不一致, 微调一下就行了, 结果在使用springboot+jpa读取第三方的pg数据库的时候报错

1
org.postgresql.util.PSQLException: 不良的类型值 long : \xffd8...

分析

遇到上述问题之后, 不论我怎么调整数据类型都无法解决, 报错依旧. 所以下班回家后在自己的电脑上搭建了一套测试的pg环境. 具体建表sql参见最后的demo例子中. 表结构如下

其中pg对于blob字段的数据类型是bytea(byte array字节数组). 上面报错的\xffd8… 显然是二进制字段——scene_stream造成的. 所以 初步断定是在读取该字段进行sql类型转换为java类型的时候报错. 网上资料甚少, 无奈, 只能阅读源码. 首先,具体的demo代码可以参见文末的github链接. 下面只列出关键部分.

首先是po对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.yfs.po;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Table;

import lombok.Data;

@Data
@Entity
@Table(name = "t_face_capture")
public class FaceCapture {

@Id
@GeneratedValue
private Long id;

private Long cid;

@Column(name = "device_name")
private String deviceName;

@Column(name = "capture_time")
private Long captureTime;

@Column(name = "capture_address")
private String captureAddress;

private Double longitude;

private Double latitude;

@Lob
@Column(name = "scene_stream")
private byte[] sceneStream;

@Lob
@Column(name = "small_stream")
private byte[] smallStream;

}

其次是pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
<version>9.4-1201-jdbc4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
<scope>provided</scope>
</dependency>
</dependencies>

可见, 是springboot整合jpa, 相应的application.yml文件如下

1
2
3
4
5
6
7
8
9
10
11
12
spring:
datasource:
username: postgres
password: postgres
url: jdbc:postgresql://localhost:5432/postgres
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
server:
port: 80

项目使用的hibernate-core版本是5.0.11. 要跟踪源码, 首先看报错的调用栈.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
org.postgresql.util.PSQLException: 不良的类型值 long : \xffd8...
at org.postgresql.jdbc2.AbstractJdbc2ResultSet.toLong(AbstractJdbc2ResultSet.java:2999) ~[postgresql-9.4-1201-jdbc4.jar:9.4]
at org.postgresql.jdbc2.AbstractJdbc2ResultSet.getLong(AbstractJdbc2ResultSet.java:2171) ~[postgresql-9.4-1201-jdbc4.jar:9.4]
at org.postgresql.jdbc2.AbstractJdbc2ResultSet.getBlob(AbstractJdbc2ResultSet.java:385) ~[postgresql-9.4-1201-jdbc4.jar:9.4]
at org.postgresql.jdbc2.AbstractJdbc2ResultSet.getBlob(AbstractJdbc2ResultSet.java:373) ~[postgresql-9.4-1201-jdbc4.jar:9.4]
at org.hibernate.type.descriptor.sql.BlobTypeDescriptor$1.doExtract(BlobTypeDescriptor.java:48) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
at org.hibernate.type.descriptor.sql.BasicExtractor.extract(BasicExtractor.java:47) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
at org.hibernate.type.AbstractStandardBasicType.nullSafeGet(AbstractStandardBasicType.java:238) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
at org.hibernate.type.AbstractStandardBasicType.nullSafeGet(AbstractStandardBasicType.java:234) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
at org.hibernate.type.AbstractStandardBasicType.nullSafeGet(AbstractStandardBasicType.java:224) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
at org.hibernate.type.AbstractStandardBasicType.hydrate(AbstractStandardBasicType.java:300) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
at org.hibernate.persister.entity.AbstractEntityPersister.hydrate(AbstractEntityPersister.java:2738) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
at org.hibernate.loader.Loader.loadFromResultSet(Loader.java:1729) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
...

于是, 我们定位到上面报错的第8行作为入口点. 进入的源码是

1
2
3
protected final T nullSafeGet(ResultSet rs, String name, WrapperOptions options) throws SQLException {
return remapSqlTypeDescriptor( options ).getExtractor( javaTypeDescriptor ).extract( rs, name, options );
}

注意上面源码的第2行, 分成三部分来看

  1. remapSqlTypeDescriptor( options ) 其结果记为A
  2. A.getExtractor( javaTypeDescriptor ) 其结果记为B
  3. B.extract( rs, name, options );

分析A的代码, 其进入到的源码是AbstractStandardBasicType 中的

1
2
3
protected SqlTypeDescriptor remapSqlTypeDescriptor(WrapperOptions options) {
return options.remapSqlTypeDescriptor( sqlTypeDescriptor );
}

继续跟上面的 options.remapSqlTypeDescriptor( sqlTypeDescriptor ), 进到WrapperOptionsImpl的remapSqlTypeDescriptor(SqlTypeDescriptor)方法, 源码如下

1
2
3
4
5
6
7
@Override
public SqlTypeDescriptor remapSqlTypeDescriptor(SqlTypeDescriptor sqlTypeDescriptor) {
final SqlTypeDescriptor remapped = sqlTypeDescriptor.canBeRemapped()
? session.getFactory().getDialect().remapSqlTypeDescriptor( sqlTypeDescriptor )
: sqlTypeDescriptor;
return remapped == null ? sqlTypeDescriptor : remapped;
}

其中记 session.getFactory().getDialect() 的结果为A1,则A1.remapSqlTypeDescriptor的源码是

Dialect的如下源码

1
2
3
4
5
6
7
8
9
10
11
public SqlTypeDescriptor remapSqlTypeDescriptor(SqlTypeDescriptor sqlTypeDescriptor) {
if ( sqlTypeDescriptor == null ) {
throw new IllegalArgumentException( "sqlTypeDescriptor is null" );
}
if ( ! sqlTypeDescriptor.canBeRemapped() ) {
return sqlTypeDescriptor;
}

final SqlTypeDescriptor overridden = getSqlTypeDescriptorOverride( sqlTypeDescriptor.getSqlType() );
return overridden == null ? sqlTypeDescriptor : overridden;
}

其中getSqlTypeDescriptorOverride方法的源码是org.hibernate.dialect.PostgreSQL81Dialect的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public SqlTypeDescriptor getSqlTypeDescriptorOverride(int sqlCode) {
SqlTypeDescriptor descriptor;
switch ( sqlCode ) {
case Types.BLOB: {
// Force BLOB binding. Otherwise, byte[] fields annotated
// with @Lob will attempt to use
// BlobTypeDescriptor.PRIMITIVE_ARRAY_BINDING. Since the
// dialect uses oid for Blobs, byte arrays cannot be used.
descriptor = BlobTypeDescriptor.BLOB_BINDING;
break;
}
case Types.CLOB: {
descriptor = ClobTypeDescriptor.CLOB_BINDING;
break;
}
default: {
descriptor = super.getSqlTypeDescriptorOverride( sqlCode );
break;
}
}
return descriptor;
}

这个类的父类是org.hibernate.dialect.Dialect,他是一个抽象父类. 旗下各种方言子类, 目的就是每种数据库都有自己的类型描述符. 因此需要覆写.对于pg的bytea类型的字段, 返回的类型描述符是

1
org.hibernate.type.descriptor.sql.BlobTypeDescriptor$4

注意, $ 表示这是 BlobTypeDescriptor的一个内部类.

分析完毕A,下面分析B. 因为A的结果是 BlobTypeDescriptor,所以B=A.getExtractor,我们自然要调查BlobTypeDescriptor的getExtractor方法. 源码如下(位于BlobTypeDescriptor中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public <X> ValueExtractor<X> getExtractor(final JavaTypeDescriptor<X> javaTypeDescriptor) {
return new BasicExtractor<X>( javaTypeDescriptor, this ) {
@Override
protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException {
return javaTypeDescriptor.wrap( rs.getBlob( name ), options );
}

@Override
protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException {
return javaTypeDescriptor.wrap( statement.getBlob( index ), options );
}

@Override
protected X doExtract(CallableStatement statement, String name, WrapperOptions options)
throws SQLException {
return javaTypeDescriptor.wrap( statement.getBlob( name ), options );
}
};
}

所以B得到的是这里new出来的BasicExtractor,它覆写了doExtract方法. 所以分析B.extract( rs, name, options ) 找到BasicExtractor的

1
public J extract(ResultSet rs, String name, WrapperOptions options)

方法. 其第一行源码就是

1
final J value = doExtract( rs, name, options );

而doExtract方法就是上面覆写的doExtract方法. 所以我们来到上面的代码块的第6行. 其中rs是AbstractJdbc2ResultSet, 其getBlob方法源码如下

1
2
3
4
5
6
7
8
public Blob getBlob(int i) throws SQLException
{
checkResultSet(i);
if (wasNullFlag)
return null;

return makeBlob(getLong(i));
}

其中i代表第几个字段, 这里debug发现是8, 恰好是scene_stream这个字段. 注意第7行的getLong,其源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public long getLong(int columnIndex) throws SQLException
{
checkResultSet(columnIndex);
if (wasNullFlag)
return 0; // SQL NULL

if (isBinary(columnIndex)) {
int col = columnIndex - 1;
int oid = fields[col].getOID();
if (oid == Oid.INT8) {
return ByteConverter.int8(this_row[col], 0);
}
return readLongValue(this_row[col], oid, Long.MIN_VALUE,
Long.MAX_VALUE, "long");
}

Encoding encoding = connection.getEncoding();
if (encoding.hasAsciiNumbers()) {
try {
return getFastLong(columnIndex);
} catch (NumberFormatException ex) {
}
}
return toLong( getFixedString(columnIndex) );
}

注意其中第7行,isBinary方法的源码是

1
2
3
protected boolean isBinary(int column) {
return fields[column - 1].getFormat() == Field.BINARY_FORMAT;
}

但是debug发现, fields是一个org.postgresql.core.Field的数组, 每个数组元素的format属性有2种

1
2
public static final int TEXT_FORMAT = 0;
public static final int BINARY_FORMAT = 1;

但是debug发现,不论是bytea字段还是其他什么字段, 全部是TEXT_FORMAT(例如数字类型也是采用字符串的方式读取的),而不是BINARY_FORMAT. 所以导致这里都是走第20行代码. 而getFastLong方法会抛异常. 所以最后还是会走第24行的toLong方法. 而显然就会报错,导致抛出不良类型 long.

解决

网上看过一个解决方法(参见后面的参考【1】)是覆写PostgreSQL81Dialect的getSqlTypeDescriptorOverride方法. 但是这里并不奏效, 我估计是版本的问题(我这里的hibernate-core版本是5.0.11).因为如上源码所见, 不论是你是BlobTypeDescriptor(它是一个抽象类, 构造函数还是private的, 这就导致根本不可能extends该类, 而只能使用该类提供的几个枚举实例)的哪一个枚举实例,它总共提供4个枚举实例

  1. DEFAULT
  2. PRIMITIVE_ARRAY_BINDING
  3. BLOB_BINDING
  4. STREAM_BINDING

都会走到org.hibernate.type.descriptor.sql.BlobTypeDescriptor.getExtractor(JavaTypeDescriptor)方法. 进而调用上面的罪魁祸首——doExtract方法. 也就是这个方法是不论使用哪一个枚举实例都会走的路. 而且你还无法继承BlobTypeDescriptor来提供新的枚举实例, 进而我们可以覆写

1
org.hibernate.dialect.PostgreSQL81Dialect.getSqlTypeDescriptorOverride(int)

来选择使用我们自定义的枚举实例来解决问题. 无奈, 我最后只能使用原生的jdbc+quartz来处理问题. 这其实就非常的简单和暴力了.

参考

【1】https://www.cnblogs.com/wggj/p/8023222.html

demo

https://github.com/yfsyfs/backend/tree/master/oracle2pg