自定义 Jackson Serializer 解决无法序列化 SerialClob 的问题

由于种种原因,项目上在做数据查询时,没有选择任何的 ORM 框架而是使用了 JdbcTemplate 进行数据库查询。查询出来也没做太多事情,只是简单地对数据做了下封装,然后直接返回给了前端。

1
2
3
4
5
6
7
8
9
10
11
12
List<Map<String, Object>> data = new ArrayList<>();
JdbcTemplate jdbcTemplate = new JdbcTemplate(ds);
SqlRowSet sqlRowSet = jdbcTemplate.queryForRowSet(sql);

int columnSize = sqlRowSet.getMetaData().getColumnNames().length;
while (sqlRowSet.next()) {
Map<String, Object> tmp = new LinkedHashMap<>();
for (int i = 1; i <= columnSize; i++) {
tmp.put(i + "", sqlRowSet.getObject(i));
}
data.add(tmp);
}

本来一直岁月静好,知道某一天查了一张新表,然后后台报了异常:

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
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class java.io.ByteArrayInputStream and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: com.demo.web.rest.util.ResponseEnvelope["data"]->com.demo.service.dto.SqlQueryDto["data"]->java.util.ArrayList[0]->java.util.LinkedHashMap["LOG_MSG"]->javax.sql.rowset.serial.SerialClob["asciiStream"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77)
at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1300)
at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:400)
at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.failForEmpty(UnknownSerializer.java:46)
at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.serialize(UnknownSerializer.java:29)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:728)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:774)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178)
at com.fasterxml.jackson.databind.ser.std.MapSerializer.serializeFields(MapSerializer.java:808)
at com.fasterxml.jackson.databind.ser.std.MapSerializer.serializeWithoutTypeInfo(MapSerializer.java:764)
at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:720)
at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:35)
at com.fasterxml.jackson.datatype.hibernate5.PersistentCollectionSerializer.serialize(PersistentCollectionSerializer.java:244)
at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:119)
at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:79)
at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:18)
at com.fasterxml.jackson.datatype.hibernate5.PersistentCollectionSerializer.serialize(PersistentCollectionSerializer.java:244)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:728)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:774)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:728)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:774)
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178)
at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:480)
at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:319)
at com.fasterxml.jackson.databind.ObjectWriter$Prefetch.serialize(ObjectWriter.java:1518)
at com.fasterxml.jackson.databind.ObjectWriter.writeValue(ObjectWriter.java:1007)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:454)
... 123 common frames omitted

乍一看,可以通过提示所说的设置 SerializationFeature.FAIL_ON_EMPTY_BEANSfalse 来避免这个问题,但这个问题其实远远没有这么简单。

尝试按照提示解决

既然异常中已经给出了解决方案,所以我们可以先尝试用给出的方案解决。由于我们是 spring 项目,所以我们可以很简单地通过在 yaml 中添加配置而不是采用写大段代码的方式来设置这个序列化属性:

1
2
3
4
spring:
jackson:
serialization:
FAIL_ON_EMPTY_BEANS: false

让我们再调用下接口。好,没问题,不报错了!再看眼返回值,等等,这是个啥?

这个字段明明在数据库中是有值的,而且正常也不会变成这种嵌套结构,所以看来这个解决办法并不可行。

寻找问题根源

所以简单的解决方式不行,就只能按部就班先看问题出在哪里了。废话不多说,既然我们知道了问题出在哪个字段,就直接打个断点先看眼程序拿到的返回值是什么样子。

已知数据库是 H2,对应的字段类型为 CHARACTER LARGE OBJECT,再看眼拿到的值:

可以看到,程序所拿到的返回值是 javax.sql.rowset.serial.SerialClob 的对象,而这个对象里面所包含的 clob 字段就是 H2 的 org.h2.jdbc.JdbcClob 对象。

也就是说,默认情况下 Jackson 无法序列化 SerialClob 这个类所创建的对象,是不是由于 clob 字段所包含的具体实现类导致的并没有进一步研究,因为数据库的种类很多,我们需要着手做的是解决这个不能序列化的问题。

自定义 Serializer 解决问题

我们知道 Jackson 可以通过扩展 Module 来支持更多类型的序列化操作,所以我们也可以采用同样的方式来增加对 SerialClob 的支持。

不过我们不需要自定义 Module,使用自带的 SimpleModule 即可,要做的只是自定义对应的 Serializer:

1
2
3
4
5
6
7
8
9
10
11
@Slf4j
private static class SerialClobSerializer extends JsonSerializer<SerialClob> {
@Override
public void serialize(SerialClob clob, JsonGenerator gen, SerializerProvider serializers) throws IOException {
try {
gen.writeString(clob.getCharacterStream(), (int) clob.length());
} catch (SerialException e) {
log.error("Serialize SerialClob error", e);
}
}
}

最后要做的就是将自定义的 SerialClobSerializer 注册到 ObjectMapper 中去,通常网上的方法都是自定义创建全局的 ObjectMapper 供 Spring 使用。这个方法定制化强,完全由自己来创建 ObjectMapper,但个人希望在可以应用 Spring 默认创建的 ObjectMapper 属性的同时,将 SerialClobSerializer 注册进去,所以采用下面的方法:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class JacksonConfiguration {

@Resource
private ObjectMapper objectMapper;

@PostConstruct
public void customSerializerSupport() {
objectMapper.registerModule(new SimpleModule().addSerializer(SerialClob.class, new SerialClobSerializer()));
}
}

到此,问题就得到了解决,调用接口后不再报错,且可以返回正确的结果。