使用的环境: flink 2.0.1 + iceberg 1.10.1 + java 21
这是一个很简单的flink insert sql
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
public class ss {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 创建 catalog(如果尚未存在)
tableEnv.executeSql("""
CREATE CATALOG iceberg_hive_catalog WITH (
'type' = 'iceberg',
'catalog-type' = 'hive',
'uri' = 'thrift://10.0.0.2:9083',
'warehouse' = 's3a://bigdata/warehouse'
)
""");
// 切换到 warehouse catalog 和 ods database
tableEnv.useCatalog("iceberg_hive_catalog");
// 正确插入多行数据
String insertSql = """
INSERT INTO ods.ods_user_info
VALUES
('1001', '张三', '13800138000', 'zhangsan@example.com', '2025-12-29 18:00:00'),
('1002', '李四', '13900139000', 'lisi@example.com', '2025-12-29 18:05:00')
""";
// 对于 INSERT 这类 DML 操作,需要调用 await() 来等待作业完成
tableEnv.executeSql(insertSql).await();
System.out.println("✅ 数据插入成功!");
}
}
在探索flink2+iceberg构建数据仓库的过程中,我发现了下面的bug
Caused by: com.esotericsoftware.kryo.KryoException: java.lang.NullPointerException: Cannot invoke "java.util.Map.put(Object, Object)" because "this.wrapped" is null
Serialization trace:
lowerBounds (org.apache.iceberg.GenericDataFile)
dataFiles (org.apache.iceberg.io.WriteResult)
writeResult (org.apache.iceberg.flink.sink.FlinkWriteResult)
at com.esotericsoftware.kryo.serializers.ReflectField.read(ReflectField.java:146)
at com.esotericsoftware.kryo.serializers.FieldSerializer.read(FieldSerializer.java:130)
at com.esotericsoftware.kryo.Kryo.readClassAndObject(Kryo.java:877)
at com.esotericsoftware.kryo.serializers.DefaultArraySerializers$ObjectArraySerializer.read(DefaultArraySerializers.java:356)
at com.esotericsoftware.kryo.serializers.DefaultArraySerializers$ObjectArraySerializer.read(DefaultArraySerializers.java:299)
at com.esotericsoftware.kryo.Kryo.readObject(Kryo.java:796)
at com.esotericsoftware.kryo.serializers.ReflectField.read(ReflectField.java:124)
at com.esotericsoftware.kryo.serializers.FieldSerializer.read(FieldSerializer.java:130)
问题出现在flink默认使用kryo的序列化上的空指针
通过git issue
issues.apache.org/jira/browse…
简单来说:Flink 2.0 移除 Chill 库(FLINK-37546) → Kryo 实例化策略失效 → Iceberg 的自定义 Map 无法被正确初始化 → 触发 MapSerializer 的 NullPointerException(Kryo #1173)
但是导入chill的依赖后这个问题并没有解决。
后来我通过自定义序列化器解决了这个问题
// 文件路径: src/main/java/com/qlchat/serializer/IcebergMapSerializer.java
package com.qlchat.serializer;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.Serializer; // 注意:这里是继承抽象类
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import org.objenesis.Objenesis;
import org.objenesis.ObjenesisStd;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.Map;
/**
* 一个为特定 Kryo 版本定制的序列化器,用于处理 Iceberg 的包私有类。
*/
public class IcebergMapSerializer extends Serializer<Object> implements Serializable { // 泛型使用 Object
private static final Objenesis OBJENESIS = new ObjenesisStd(true);
private static final Field WRAPPED_FIELD;
static {
try {
// 1. 通过反射获取 Iceberg 的包私有类
Class<?> serializableByteBufferMapClass = Class.forName("org.apache.iceberg.SerializableByteBufferMap");
// 2. 通过反射获取该类中私有的 'wrapped' 字段
WRAPPED_FIELD = serializableByteBufferMapClass.getDeclaredField("wrapped");
WRAPPED_FIELD.setAccessible(true); // 3. 关键:允许访问私有字段
} catch (ClassNotFoundException | NoSuchFieldException e) {
// 如果在启动时就找不到类或字段,直接抛出运行时异常,因为后续无法工作
throw new RuntimeException("初始化 IcebergMapSerializer 失败!请检查 Iceberg 依赖。", e);
}
}
/**
* 序列化方法
*/
@Override
public void write(Kryo kryo, Output output, Object object) {
try {
// 从对象中获取 'wrapped' Map 字段的值
Map<?, ?> wrappedMap = (Map<?, ?>) WRAPPED_FIELD.get(object);
// 使用 Kryo 正常序列化这个 Map
kryo.writeClassAndObject(output, wrappedMap);
} catch (IllegalAccessException e) {
throw new RuntimeException("序列化 Iceberg 对象失败", e);
}
}
/**
* 反序列化方法
*/
@Override
public Object read(Kryo kryo, Input input, Class<? extends Object> type) {
// 1. 从输入流中读取之前序列化的 Map
Map<?, ?> wrappedMap = (Map<?, ?>) kryo.readClassAndObject(input);
// 2. 使用 Objenesis 创建目标类的实例(它能处理没有公共构造函数的类)
Object instance = OBJENESIS.newInstance(type);
try {
// 3. 将读取到的 Map 设置回新创建实例的 'wrapped' 字段中
WRAPPED_FIELD.set(instance, wrappedMap);
} catch (IllegalAccessException e) {
throw new RuntimeException("反序列化 Iceberg 对象失败", e);
}
// 4. 返回完全初始化的实例
return instance;
}
}
package com.qlchat;
import com.qlchat.serializer.IcebergMapSerializer;
import org.apache.flink.api.common.ExecutionConfig;
import org.apache.flink.api.common.serialization.SerializerConfigImpl;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
public class IcebergInsertDemo {
public static void main(String[] args) throws Exception {
// 1. 创建 Flink 流环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 通过反射和 Flink 的 ExecutionConfig 来注册序列化器
try {
// a. 使用 Class.forName() 通过字符串获取包私有类的 Class 对象
Class<?> icebergClass = Class.forName("org.apache.iceberg.SerializableByteBufferMap");
// b. 获取 Flink 的 ExecutionConfig
ExecutionConfig config = env.getConfig();
// c. 注册我们的自定义序列化器
// 这个方法会告诉 Flink,当需要序列化 icebergClass 类型的对象时,
// 就使用我们提供的 IcebergMapSerializer 的实例。
SerializerConfigImpl serializerConfig = (SerializerConfigImpl) config.getSerializerConfig();
serializerConfig.registerTypeWithKryoSerializer(
icebergClass,
new IcebergMapSerializer() // 注意:这里传入的是一个实例
);
System.out.println("✅ 成功为 org.apache.iceberg.SerializableByteBufferMap 注册了自定义序列化器。");
} catch (ClassNotFoundException e) {
System.err.println("❌ 错误:找不到类 org.apache.iceberg.SerializableByteBufferMap。请检查您的 Iceberg 依赖版本是否正确。");
throw e; // 如果找不到类,后续运行肯定会失败,直接抛出异常
}
// 3. 创建 TableEnvironment
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 4. 后续的 Catalog 创建、SQL 执行逻辑保持不变
tableEnv.executeSql("""
CREATE CATALOG iceberg_hive_catalog WITH (
'type' = 'iceberg',
'catalog-type' = 'hive',
'uri' = 'thrift://10.0.0.2:9083',
'warehouse' = 's3a://bigdata/warehouse'
)
""");
tableEnv.useCatalog("iceberg_hive_catalog");
String insertSql = """
INSERT INTO ods.ods_user_info
VALUES
('1001', '张三', '13800138000', 'zhangsan@example.com', '2025-12-29 18:00:00'),
('1002', '李四', '13900139000', 'lisi@example.com', '2025-12-29 18:05:00')
""";
tableEnv.executeSql(insertSql).await();
System.out.println("✅ 数据插入成功!");
}
}