Flink2 + iceberg 1.10.1 报错:"this.wrapped" is null

42 阅读3分钟

使用的环境: 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

github.com/EsotericSof…

issues.apache.org/jira/browse…

github.com/apache/flin…

简单来说: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("✅ 数据插入成功!");
    }
}