【Jackson 填坑日记】“把 CLOB 直接扔给前端?MonitorLock 当场社死”

作者:@Neoest
时间:2025-08-15
关键词:Jackson、oracle.sql.CLOB、InvalidDefinitionException、FAIL_ON_EMPTY_BEANS、DTO、序列化


1. 问题现场

开发新写的「慢 SQL 诊断接口」上线即 500,日志狂飙:

Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
No serializer found for class oracle.jdbc.internal.Monitor$CloseableLock
and no properties discovered to create BeanSerializer
(through reference chain: java.util.HashMap["rows"]
  -> java.util.ArrayList[0]
  -> java.util.HashMap["memoryTip"]
  -> oracle.sql.CLOB["physicalConnection"]
  -> oracle.jdbc.driver.T4CConnection["monitorLock"])

一句话:Jackson 想把 JDBC 驱动内部对象序列化成 JSON,结果被拒绝


2. 先定位根因

层级 对象 说明
Controller 返回值 List<Map<String,Object>> 直接把 ResultSetMap
Map 里的值 oracle.sql.CLOB 驱动实现的 CLOB 接口
CLOB 内部字段 T4CConnection MonitorLock(驱动私有)
Jackson 默认策略 序列化 所有 public getter 遇到无 getter 的内部类直接抛错

3. 错误示范(千万别学)

@GetMapping("/slow-sql")
public List<Map<String, Object>> slowSql() {
    return jdbcTemplate.queryForList(sql);  // ❌ 直接把 ResultSet → Map
}

4. 三种根治姿势

✅ 方案 A:DTO 收口,禁止裸 Map(推荐)

  1. 定义前端真正需要的数据结构:
@Data
public class SlowSqlVO {
    private Long id;
    private String sqlText;   // CLOB → String
    private Long elapsedMs;
}
  1. 手动把 CLOBString
@Mapper
public interface SlowSqlMapper {
    @Select("select id, sql_fulltext, elapsed_time from v$sql_monitor")
    @Results({
        @Result(column = "sql_fulltext", property = "sqlText",
                typeHandler = ClobStringTypeHandler.class)
    })
    List<SlowSqlVO> list();
}
  1. Jackson 只序列化干净的 DTO,从根源隔离驱动对象

✅ 方案 B:关闭 FAIL_ON_EMPTY_BEANS(临时止血)

spring:
  jackson:
    serialization:
      fail-on-empty-beans: false

或在代码里:

objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);

副作用:

  • 只是把异常降级为空对象 {},前端仍看不到真实数据
  • 驱动内部字段依旧在网络里裸奔,不安全
  • 治标不治本,不推荐上线

✅ 方案 C:自定义 ClobSerializer + @JsonSerialize

如果必须保留 Map,可以注册 全局模块

public class ClobSerializer extends JsonSerializer<CLOB> {
    @Override
    public void serialize(CLOB value, JsonGenerator gen,
                          SerializerProvider serializers) throws IOException {
        try (Reader reader = value.getCharacterStream()) {
            gen.writeString(IOUtils.toString(reader));
        } catch (SQLException e) {
            throw new IOException(e);
        }
    }
}

@Configuration
public class JacksonConfig {
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer clobCustomizer() {
        return builder -> builder.serializerByType(CLOB.class, new ClobSerializer());
    }
}
  • 仍然比裸 Map 安全,但不如 DTO 语义清晰

5. 小结

方案 场景 侵入性 备注
DTO + TypeHandler 所有项目 官方推荐
关闭 FAIL_ON_EMPTY_BEANS 本地调试 临时方案
自定义 Serializer 遗留代码 需维护

6. 一键复制版补丁(方案 A)

@Data
public class SlowSqlVO {
    private Long id;
    private String sqlText;
    private Long elapsedMs;
}

// MyBatis-Plus 写法
@Mapper
public interface SlowSqlMapper extends BaseMapper<SlowSqlVO> {
    @Select("select id, sql_fulltext, elapsed_time from v$sql_monitor")
    @Results({
        @Result(column = "sql_fulltext", property = "sqlText",
                typeHandler = ClobStringTypeHandler.class)
    })
    List<SlowSqlVO> list();
}

7. 参考资料

  • Jackson 官方 Wiki:Handling Oracle CLOB
  • MyBatis-Plus 文档:自定义 TypeHandler
  • 《Effective Java》第 3 版——第 51 条:谨慎设计可序列化接口

如果本文帮到你,记得点赞收藏!评论区一起交流更多 ORM 序列化坑位~

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐