Spring整合Kafka(十四)----序列化、反序列化和消息转换

一、概述

Apache Kafka提供了一个高级API来序列化和反序列化记录值以及它们的键(keys)。它存在于org.apache.kafka.common.serialization.Serializer和org.apache.kafka.common.serialization.Deserializer抽象和一些内置实现中。同时,我们可以通过使用Producer或Consumer配置属性来指定序列化器和反序列化器类。下面的例子展示了如何这样做:

props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
...
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);

对于更复杂或更特殊的情况,KafkaConsumer和KafkaProducer提供重载构造函数,以分别接受键和值的序列化器和反序列化器实例。
当你使用此API时,DefaultKafkaProducerFactory和DefaultKafkaConsumerFactory也提供属性(通过构造函数或setter方法),以将自定义序列化程序和反序列化程序实例注入到目标Producer或Consumer中。此外,你可以通过构造函数传入Supplier或Supplier实例-这些Supplier在创建每个Producer或Consumer时被调用。

二、String序列化和反序列化String Serialization/Deserialization

Spring for Apache Kafka提供了ToStringSerializer和ParseStringDeserializer类,它们使用实体的String表示。它们依赖于toString方法和一些Function或BiFunction<String, Headers>来解析String并填充实例的属性。通常,这将调用类上的一些静态方法,例如parse:

ToStringSerializer<Thing> thingSerializer = new ToStringSerializer<>();
//...
ParseStringDeserializer<Thing> deserializer = new ParseStringDeserializer<>(Thing::parse);

默认情况下,ToStringSerializer被配置为在record Headers传递关于序列化实体的类型信息。你可以通过将addTypeInfo属性设置为false来禁用此功能。接收端的ParseStringDeserializer可以使用该信息。

  • ToStringSerializer.ADD_TYPE_INFO_HEADERS (默认true):你可以将其设置为false以禁用ToStringSerializer(设置addTypeInfo属性)上的此功能。
ParseStringDeserializer<Object> deserializer = new ParseStringDeserializer<>((str, headers) -> {
    byte[] header = headers.lastHeader(ToStringSerializer.VALUE_TYPE).value();
    String entityType = new String(header);

    if (entityType.contains("Thing")) {
        return Thing.parse(str);
    }
    else {
        // ...parsing logic
    }
});

你可以配置用于将String转换为byte[]的字符集,默认值为UTF-8。
你可以使用ConsumerConfig属性并使用parser方法的名称配置反序列化程序:

  • ParseStringDeserializer.KEY_PARSER
  • ParseStringDeserializer.VALUE_PARSER
    属性必须包含类的完全限定名,后跟用点分隔的方法名。该方法必须是静态的,并且具有(String, Headers)或(String)的签名。
    框架还提供了ToFromStringSerde,用于Kafka Streams。

三、JSON序列化和反序列化Serialization/Deserialization

Spring for Apache Kafka还提供了基于Jackson JSON对象映射器的JsonSerializer和JsonDeserializer实现。JsonSerializer允许将任何Java对象写入JSON byte[]。JsonDeserializer 需要一个额外的Class<?> targetType参数允许将已消费的byte[]反序列化为正确的目标对象。下面的例子展示了如何创建JsonDeserializer:

JsonDeserializer<Thing> thingDeserializer = new JsonDeserializer<>(Thing.class);

你可以使用ObjectMapper自定义JsonSerializer和JsonDeserializer。你还可以继承它们以在configure(Map<String, ?> configs, boolean isKey)方法中实现一些特定的配置逻辑。
默认情况下,所有支持JSON的组件都使用“JacksonUtils.enhancedObjectMapper()”实例进行配置,该实例附带“MapperFeature.default_VIEW_INCLUSION”和“DeserializationFeature.FAIL_ON_UNNOWN_PROPERTIES”“功能已禁用。此外,此类实例还提供了用于自定义数据类型的众所周知的模块,如Java time和Kotlin支持。为了在网络上实现平台间兼容性,此方法还注册了一个org.springframework.kafka.support.JacksonMimeTypeModule,以便将org.springframework.util.MimeType对象序列化为纯字符串。JacksonMimeTypeModule可以在应用程序上下文中注册为bean,并将被自动配置到Spring Boot ObjectMapper实例中。
JsonDeserializer提供了基于TypeReference的构造函数,以更好地处理目标通用容器类型。
你可以在record Headers中传递类型信息,从而允许处理多种类型。此外,你还可以使用以下Kafka属性来配置序列化程序和反序列化程序。如果您分别为KafkaProducer和KafkaConsumer提供了Serializer和Deserializer实例,则它们不会起作用。

3.1 配置属性Configuration Properties

  • JsonSerializer.ADD_TYPE_INFO_HEADERS(默认为true):你可以将其设置为false以禁用JsonSerializer上的此功能(设置addTypeInfo属性)。
  • JsonSerializer.TYPE_MAPPINGS(默认为empty):请参见3.2映射类型。
  • JsonDeserializer.USE_TYPE_INFO_HEADERS(默认为true):你可以将其设置为false以忽略序列化程序设置的headers。
  • JsonDeserializer.REMOVE_TYPE_INFO_HEADERS(默认为true):您可以将其设置为false以保留序列化程序设置的headers。
  • JsonDeserializer.KEY_DEFAULT_TYPE:如果不存在header信息,则用于反序列化键(keys)的回退类型。
  • JsonDeserializer.VALUE_DEFAULT_TYPE:如果不存在header信息,则用于反序列化值(values)的回退类型。
  • JsonDeserializer.TRUSTED_PACKAGES(默认java.util,java.lang):允许反序列化的包patterns的逗号分隔列表。*意味着反序列化所有。
  • JsonDeserializer.TYPE_MAPPINGS(默认为空):请参见3.2映射类型。
  • JsonDeserializer.KEY_TYPE_METHOD(默认为空):请参见3.3使用方法确定类型。
  • JsonDeserializer.VALUE_TYPE_METHOD(默认为空):请参阅3.3使用方法确定类型。

反序列化程序将删除headers中的类型信息(如果由序列化程序添加)。通过直接在反序列化程序上设置或使用前面描述的配置属性可以将removeTypeHeaders属性设置为false,以改变前述的行为。
如果您以编程方式构建序列化器或反序列化器,如3.4Programmatic Construction中所示,则只要你没有显式设置任何属性(使用set*()方法或使用fluent API),工厂就会应用上述属性。

3.2 映射类型Mapping Types

当使用JSON时,可以通过使用前面列表中的属性来提供类型映射。映射由逗号分隔的成对token:className列表组成。在出站(outbound)时,payload的类名被映射到相应的token。在入站(inbound)时,header中的token被映射到相应的类名。
以下示例创建一组映射:

senderProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
senderProps.put(JsonSerializer.TYPE_MAPPINGS, "cat:com.mycat.Cat, hat:com.myhat.hat");
...
consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
consumerProps.put(JsonDeSerializer.TYPE_MAPPINGS, "cat:com.yourcat.Cat, hat:com.yourhat.hat");

相应的对象必须兼容。

如果使用Spring Boot,则可以在application.properties(或yaml)文件中提供这些属性。以下示例显示了如何执行此操作:

spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer
spring.kafka.producer.properties.spring.json.type.mapping=cat:com.mycat.Cat,hat:com.myhat.Hat

你只能使用properties执行简单配置。对于更高级的配置(例如在序列化程序和反序列化程序中使用自定义的ObjectMapper),应该使用接受预构建的序列化程序和反串行程序的生产者和消费者工厂构造函数。以下Spring Boot示例覆盖默认工厂:

@Bean
public ConsumerFactory<String, Thing> kafkaConsumerFactory(JsonDeserializer customValueDeserializer) {
    Map<String, Object> properties = new HashMap<>();
    // properties.put(..., ...)
    // ...
    return new DefaultKafkaConsumerFactory<>(properties,
        new StringDeserializer(), customValueDeserializer);
}

@Bean
public ProducerFactory<String, Thing> kafkaProducerFactory(JsonSerializer customValueSerializer) {

    return new DefaultKafkaProducerFactory<>(properties.buildProducerProperties(),
        new StringSerializer(), customValueSerializer);
}

以上场景也可以使用setters,作为使用这些构造函数的替代方案。
你可以显式配置反序列化程序以使用提供的目标类型,并通过使用具有布尔值useHeadersIfPresent(默认情况下为true)的重载构造函数之一来忽略headers中的类型信息。以下示例显示了如何执行此操作:

DefaultKafkaConsumerFactory<Integer, Cat1> cf = new DefaultKafkaConsumerFactory<>(props,
        new IntegerDeserializer(), new JsonDeserializer<>(Cat1.class, false));

3.3 使用方法确定类型Using Methods to Determine Types

你现在可以通过properties配置反序列化程序,以调用一个方法来确定目标类型。如果存在,这将覆盖上面讨论的任何其他技术。如果数据是由不使用Spring序列化程序的应用程序发布的,并且您需要根据数据或其他headers将数据反序列化为不同的类型,那么这可能会很有用。将这些属性设置为方法名——一个完全限定的类名,后跟方法名,用点分隔。该方法必须声明为public static,具有三个签名之一(String topic, byte[] data, Headers headers), (byte[] data, Headers headers) 或者 (byte[] data),并返回Jackson JavaType。

  • JsonDeserializer.KEY_TYPE_METHOD : spring.json.key.type.method
  • JsonDeserializer.VALUE_TYPE_METHOD : spring.json.value.type.method
    你可以使用任意headers或inspect数据来确定类型。
JavaType thing1Type = TypeFactory.defaultInstance().constructType(Thing1.class);

JavaType thing2Type = TypeFactory.defaultInstance().constructType(Thing2.class);

public static JavaType thingOneOrThingTwo(byte[] data, Headers headers) {
    // {"thisIsAFieldInThing1":"value", ...
    if (data[21] == '1') {
        return thing1Type;
    }
    else {
        return thing2Type;
    }
}

对于更复杂的数据inspection,可以考虑使用JsonPath或类似方法,但确定类型的测试越简单,过程就越高效。
以下是以编程方式创建反序列化程序的示例(当在构造函数中为consumer工厂提供反序列化程序时):

JsonDeserializer<Object> deser = new JsonDeserializer<>()
        .trustedPackages("*")
        .typeResolver(SomeClass::thing1Thing2JavaTypeForTopic);

...

public static JavaType thing1Thing2JavaTypeForTopic(String topic, byte[] data, Headers headers) {
    ...
}

3.4 程序化构建Programmatic Construction

当以编程方式构建序列化器/反序列化器以供生产者/消费者工厂使用时,你可以使用fluent API,这简化了配置。

@Bean
public ProducerFactory<MyKeyType, MyValueType> pf() {
    Map<String, Object> props = new HashMap<>();
    // props.put(..., ...)
    // ...
    DefaultKafkaProducerFactory<MyKeyType, MyValueType> pf = new DefaultKafkaProducerFactory<>(props,
        new JsonSerializer<MyKeyType>()
            .forKeys()
            .noTypeInfo(),
        new JsonSerializer<MyValueType>()
            .noTypeInfo());
    return pf;
}

@Bean
public ConsumerFactory<MyKeyType, MyValueType> cf() {
    Map<String, Object> props = new HashMap<>();
    // props.put(..., ...)
    // ...
    DefaultKafkaConsumerFactory<MyKeyType, MyValueType> cf = new DefaultKafkaConsumerFactory<>(props,
        new JsonDeserializer<>(MyKeyType.class)
            .forKeys()
            .ignoreTypeHeaders(),
        new JsonDeserializer<>(MyValueType.class)
            .ignoreTypeHeaders());
    return cf;
}

要以编程方式提供类型映射,类似于3.3使用方法确定类型,请使用typeFunction属性。

JsonDeserializer<Object> deser = new JsonDeserializer<>()
        .trustedPackages("*")
        .typeFunction(MyUtils::thingOneOrThingTwo);

如果你不使用fluent API来配置属性,也不使用set*()方法来设置属性,工厂就会使用配置属性来配置序列化器/反序列化器;参见3.1配置属性。

四、委托序列化器和反序列化器Delegating Serializer and Deserializer

4.1 使用头Using Headers

框架提供了DelegatingSerializer和DelegatingDeserializer两个类,它们允许生成和消费具有不同键(key )或值(value)类型的记录。生产者必须将标头“DelegatingSerializer.VALUE_SERIALIZATION_SELECTOR”设置为选择器值,该选择器值用于选择要用于该值的序列化程序,并将标头“DelegatingSerializer.KEY_SERIALIZATION_SELECTOR”设置为键(key);如果没有找到匹配项,则抛出IllegalStateException。
对于传入的记录,反序列化程序使用相同的headers来选择要使用的反序列化程序;如果未找到匹配项或header不存在,则返回原始byte[]。
你可以通过构造函数配置选择器map到Serializer/Deserializer,也可以通过key值“DelegatingSerializer.VALUE_SERIALIZATION_SELECTOR_CONFIG”和“DelegatingSerializer.KEY_SERIALIZATION_SELECTOR_CONFIG”为Kafka生产者/消费者属性进行配置。对于序列化程序,生产者属性可以是Map<String, Object>,其中键(key)是选择器,值(value)是序列化程序实例、序列化程序类或类名。该属性也可以是逗号分隔的map entries的字符串,如下所示。
对于反序列化程序,consumer属性可以是Map<String, Object>,其中键(key)是选择器,值(value)是反序列化程序实例、反序列化程序类或类名。该属性也可以是逗号分隔的map entries的字符串,如下所示。
要使用属性(properties)进行配置,请使用以下语法:

producerProps.put(DelegatingSerializer.VALUE_SERIALIZATION_SELECTOR_CONFIG,
    "thing1:com.example.MyThing1Serializer, thing2:com.example.MyThing2Serializer")

consumerProps.put(DelegatingDeserializer.VALUE_SERIALIZATION_SELECTOR_CONFIG,
    "thing1:com.example.MyThing1Deserializer, thing2:com.example.MyThing2Deserializer")

然后,生产者将设置DelegatingSerializer.VALUE_SERIALIZATION_SELECTOR header为thing1或thing2。
此技术支持向同一topic(或不同topics)发送不同类型。
如果类型(key or value)是Serdes支持的标准类型之一(Long, Integer等),则无需设置选择器header。相反,序列化程序会将header设置为该类型的类名。没有必要为这些类型配置序列化程序或反序列化程序,它们将被动态创建(一次)。
有关向不同主题发送不同类型的另一种技术,请参阅使用RoutingKafkaTemplate

4.2 根据类型By Type

框架提供了DelegatingByTypeSerializer:

@Bean
public ProducerFactory<Integer, Object> producerFactory(Map<String, Object> config) {
    return new DefaultKafkaProducerFactory<>(config,
            null, new DelegatingByTypeSerializer(Map.of(
                    byte[].class, new ByteArraySerializer(),
                    Bytes.class, new BytesSerializer(),
                    String.class, new StringSerializer())));
}

你可以配置序列化程序来检查map key是否可以从目标对象分配,这在委托序列化程序可以序列化子类时很有用。在这种情况下,如果存在不明确的匹配,则应提供有序的Map,例如LinkedHashMap。

4.3 根据主题By Topic

从版本2.8开始,DelegatingByTopicSerializer和DelegatingByTopicDeserializer允许根据主题(topic)名称选择序列化程序/反序列化程序。Regex Pattern用于查找要使用的实例。map可以使用构造函数进行配置,也可以通过属性(逗号分隔的pattern:serializer列表)进行配置。

producerConfigs.put(DelegatingByTopicSerializer.VALUE_SERIALIZATION_TOPIC_CONFIG,
            "topic[0-4]:" + ByteArraySerializer.class.getName()
        + ", topic[5-9]:" + StringSerializer.class.getName());
...
ConsumerConfigs.put(DelegatingByTopicDeserializer.VALUE_SERIALIZATION_TOPIC_CONFIG,
            "topic[0-4]:" + ByteArrayDeserializer.class.getName()
        + ", topic[5-9]:" + StringDeserializer.class.getName());

将其用于键(key)时,请使用“KEY_SERIALIZATION_TOPIC_CONFIG”。

@Bean
public ProducerFactory<Integer, Object> producerFactory(Map<String, Object> config) {
    return new DefaultKafkaProducerFactory<>(config,
            null,
            new DelegatingByTopicSerializer(Map.of(
                    Pattern.compile("topic[0-4]"), new ByteArraySerializer(),
                    Pattern.compile("topic[5-9]"), new StringSerializer())),
                    new JsonSerializer<Object>());  // default
}

当没有pattern匹配时,可以使用“DelegatingByTopicSerialization.KEY_SERIALIZATION_TOPIC_DEFAULT”和“DelegatingByTopicSerialization.VALUE_SERIALIZATION_TOPIC_DEFAULT.”指定要使用的默认序列化程序/反序列化程序。
当设置为false时,属性“DelegatingByTopicSerialization.CASE_SENSITIVE”(默认为true)会使topic查找不区分大小写。

五、重试反序列化器Retrying Deserializer

在反序列化过程中可能出现短暂错误(如网络问题)时,RetryingDeserializer使用委托Deserializer和RetryTemplate来重试反序列化。

ConsumerFactory cf = new DefaultKafkaConsumerFactory(myConsumerConfigs,
    new RetryingDeserializer(myUnreliableKeyDeserializer, retryTemplate),
    new RetryingDeserializer(myUnreliableValueDeserializer, retryTemplate));

请参考spring-retry项目,了解带有重试策略、退回(back off)策略等的RetryTemplate的配置。

六、Spring Messaging消息转换 Message Conversion

尽管从相对底层的Kafka Consumer和Producer的角度来看,Serializer和Deserializer API非常简单和灵活,但当使用@KafkaListener或Spring Integration的Apache Kafka Support时,你可能需要在Spring Messaging级别有更大的灵活性。为了方便你转换到org.springframework.messaging.Message和从其转换,Spring for Apache Kafka提供了一个MessageConverter抽象,MessagingMessageConverter实现和它的JsonMessageConverter(及其子类)自定义的实现。你可以直接将MessageConverter注入到KafkaTemplate实例中,并使用@KafkaListener.containerFactory()属性的AbstractKafkaListenerContainerFactory bean定义。以下示例显示了如何执行此操作:

@Bean
public KafkaListenerContainerFactory<?> kafkaJsonListenerContainerFactory() {
    ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
        new ConcurrentKafkaListenerContainerFactory<>();
    factory.setConsumerFactory(consumerFactory());
    factory.setRecordMessageConverter(new JsonMessageConverter());
    return factory;
}
...
@KafkaListener(topics = "jsonData",
                containerFactory = "kafkaJsonListenerContainerFactory")
public void jsonListener(Cat cat) {
...
}

当使用Spring Boot时,只需将转换器定义为@Bean,Spring Boot自动配置就会将其注入到自动配置的template和容器工厂中。
当你使用@KafkaListener时,会向消息转换器提供参数类型以帮助进行转换。
只有在方法级别声明@KafkaListener注解时,才能实现这种类型推断。对于类级别的@KafkaListener,payload类型用于选择要调用的@KafkaHandler方法,因此在选择该方法之前,它必须已经被转换。
在consumer端,你可以配置JsonMessageConverter;它可以处理byte[]、Bytes和String类型的ConsumerRecord值,因此应与ByteArrayDeserializer、BytesDeserializer或StringDeserializer一起使用。(byte[]和Bytes更高效,因为它们避免了不必要的byte[]到String的转换)。如果你愿意,还可以配置与反序列化程序相对应的JsonMessageConverter的特定子类。
在producer端,当你使用Spring Integration或KafkaTemplate.send(Message<?> message)方法(请参阅使用KafkaTemplate)时,你必须配置一个与配置的Kafka Serializer兼容的消息转换器。

  • StringJsonMessageConverter with StringSerializer
  • BytesJsonMessageConverter with BytesSerializer
  • ByteArrayJsonMessageConverter with ByteArraySerializer

同样,使用byte[]或Bytes更有效,因为它们避免了String到byte[]的转换。
为了方便起见,框架还提供了StringOrBytesSerializer,它可以序列化所有三种值类型,因此可以与任何消息转换器一起使用。
消息payload转换可以委托给spring-messaging SmartMessageConverter;例如,这使得转换能够基于MessageHeaders.CONTENT_TYPE header。
调用KafkaMessageConverter.fromMessage()方法将出站(outbound)转换为ProducerRecord,消息payload位于ProducerRecord.value()属性中。调用KafkaMessageConverter.toMessage()方法从ConsumerRecord进行入站(inbound)转换,payload为ConsumerRecord.value()属性。调用SmartMessageConverter.toMessage()方法以创建新的出站Message<?>,消息来自传递给fromMessage()的消息(通常由KafkaTemplate.send(Message<?>msg))。类似地,在KafkaMessageConverter.toMessage()方法中,在转换器从ConsumerRecord创建了一个新的Message<?>之后,调用SmartMessageConverter.fromMessage()方法,然后使用新转换的payload创建最终入站消息。在任何一种情况下,如果SmartMessageConverter返回null,则使用原始消息。
当默认转换器在KafkaTemplate和监听器容器工厂中使用时,你可以通过在template上调用setMessagingConverter()和通过@KafkaListener方法上的contentMessageConverter属性来配置SmartMessageConverter。
示例:

template.setMessagingConverter(mySmartConverter);
@KafkaListener(id = "withSmartConverter", topics = "someTopic",
    contentTypeConverter = "mySmartConverter")
public void smart(Thing thing) {
    ...
}

七、使用Spring Data Projection接口Using Spring Data Projection Interfaces

你可以将JSON转换为Spring Data Projection接口,而不是具体类型。这允许对数据进行非常选择性和低耦合的绑定,包括从JSON文档中的多个位置查找值。例如,以下接口可以定义为payload载荷类型:

interface SomeSample {

  @JsonPath({ "$.username", "$.user.name" })
  String getUsername();

}
@KafkaListener(id="projection.listener", topics = "projection")
public void projection(SomeSample in) {
    String username = in.getUsername();
    ...
}

默认情况下,Accessor方法将用于在收到的JSON文档中查找属性名称作为字段。@JsonPath表达式允许自定义值查找,甚至可以定义多个JSON Path表达式,从多个位置查找值,直到表达式返回实际值。
要启用此功能,请使用配置了适当委托转换器(用于出站转换和转换non-projection接口)的ProjectingMessageConverter。您还必须将spring-data:spring-data-commons和com.jayway.jsonpath:json-path添加到类路径中。
当用作@KafkaListener方法的参数时,接口类型会正常自动传递给转换器。

八、使用Using ErrorHandlingDeserializer

当反序列化程序无法反序列化消息时,Spring无法处理此问题,因为它发生在poll()返回之前。为了解决这个问题,引入了ErrorHandlingDeserializer。这个反序列化程序委托给一个真正的反序列化程序(键或值)。如果委托未能反序列化记录内容,ErrorHandlingDeserializer将在包含原因和raw bytes的header中返回一个null值和一个DeserializationException。使用记录级(record-level )MessageListener时,如果ConsumerRecord包含键或值的DeserializationException header,则会使用失败的ConsumerRecord调用容器的ErrorHandler。记录record不会传递给监听器。
或者,您可以配置ErrorHandlingDeserializer,通过提供failedDeserializationFunction来创建自定义值,该函数是一个Function<FailedDeserializationInfo, T>。调用此函数以创建T的实例,该实例以通常的方式传递给监听器。FailedDeserializationInfo类型的对象(包含所有上下文信息)被提供给函数。你可以在headers中找到DeserializationException(作为序列化的Java对象)。
你可以使用DefaultKafkaConsumerFactory构造函数,该构造函数接受键和值反序列化器对象,并注入到你使用适当委托配置的相应ErrorHandlingDeserializer实例中。或者,您可以使用consumer配置属性(由ErrorHandlingDeserializer使用)来实例化委托。属性名称为ErrorHandlingDeserializer.KEY_DESERIALIZER_CLASS和ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS。属性值可以是类或类名。以下示例展示了如何设置这些属性:

... // other props
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
props.put(ErrorHandlingDeserializer.KEY_DESERIALIZER_CLASS, JsonDeserializer.class);
props.put(JsonDeserializer.KEY_DEFAULT_TYPE, "com.example.MyKey")
props.put(ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, JsonDeserializer.class.getName());
props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, "com.example.MyValue")
props.put(JsonDeserializer.TRUSTED_PACKAGES, "com.example")
return new DefaultKafkaConsumerFactory<>(props);

下面的示例使用了failedDeserializationFunction。

public class BadFoo extends Foo {

  private final FailedDeserializationInfo failedDeserializationInfo;

  public BadFoo(FailedDeserializationInfo failedDeserializationInfo) {
    this.failedDeserializationInfo = failedDeserializationInfo;
  }

  public FailedDeserializationInfo getFailedDeserializationInfo() {
    return this.failedDeserializationInfo;
  }

}

public class FailedFooProvider implements Function<FailedDeserializationInfo, Foo> {

  @Override
  public Foo apply(FailedDeserializationInfo info) {
    return new BadFoo(info);
  }

}

本例使用的配置如下:

...
consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
consumerProps.put(ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, JsonDeserializer.class);
consumerProps.put(ErrorHandlingDeserializer.VALUE_FUNCTION, FailedFooProvider.class);
...

如果consumer配置了ErrorHandlingDeserializer,那么为KafkaTemplate和它的producer配置一个序列化器是很重要的,这个序列化器可以处理普通对象和原始的byte[]值,这些值是由反序列化异常引起的。template的泛型值类型应该是Object。一种技术是使用DelegatingByTypeSerializer;一个例子如下:

@Bean
public ProducerFactory<String, Object> producerFactory() {
  return new DefaultKafkaProducerFactory<>(producerConfiguration(), new StringSerializer(),
    new DelegatingByTypeSerializer(Map.of(byte[].class, new ByteArraySerializer(),
          MyNormalObject.class, new JsonSerializer<Object>())));
}

@Bean
public KafkaTemplate<String, Object> kafkaTemplate() {
  return new KafkaTemplate<>(producerFactory());
}

在使用ErrorHandlingDeserializer和batch监听器时,必须检查消息头中的反序列化异常。当与DefaultBatchErrorHandler一起使用时,你可以使用该头来确定异常在哪个记录上失败,并通过BatchListenerFailedException与error handler通信。

@KafkaListener(id = "test", topics = "test")
void listen(List<Thing> in, @Header(KafkaHeaders.BATCH_CONVERTED_HEADERS) List<Map<String, Object>> headers) {
    for (int i = 0; i < in.size(); i++) {
        Thing thing = in.get(i);
        if (thing == null
                && headers.get(i).get(SerializationUtils.VALUE_DESERIALIZER_EXCEPTION_HEADER) != null) {
            try {
                DeserializationException deserEx = SerializationUtils.byteArrayToDeserializationException(this.logger,
                        headers.get(i).get(SerializationUtils.VALUE_DESERIALIZER_EXCEPTION_HEADER));
                if (deserEx != null) {
                    logger.error(deserEx, "Record at index " + i + " could not be deserialized");
                }
            }
            catch (Exception ex) {
                logger.error(ex, "Record at index " + i + " could not be deserialized");
            }
            throw new BatchListenerFailedException("Deserialization", deserEx, i);
        }
        process(thing);
    }
}

“SerializationUtils.byteArrayToDeserializationException()”可用于将header转换为DeserializationException。
消费“List<ConsumerRecord<?, ?>”时,将使用“SerializationUtils.getExceptionFromHeader()”:

@KafkaListener(id = "kgh2036", topics = "kgh2036")
void listen(List<ConsumerRecord<String, Thing>> in) {
    for (int i = 0; i < in.size(); i++) {
        ConsumerRecord<String, Thing> rec = in.get(i);
        if (rec.value() == null) {
            DeserializationException deserEx = SerializationUtils.getExceptionFromHeader(rec,
                    SerializationUtils.VALUE_DESERIALIZER_EXCEPTION_HEADER, this.logger);
            if (deserEx != null) {
                logger.error(deserEx, "Record at offset " + rec.offset() + " could not be deserialized");
                throw new BatchListenerFailedException("Deserialization", deserEx, i);
            }
        }
        process(rec.value());
    }
}

如果您也在使用DeadLetterPublishingRecoverer,则为DeserializationException发布的记录将具有类型为byte[]的record.value();这不应该被序列化。考虑使用DelegatingByTypeSerializer,该序列化程序配置为对byte[]使用ByteArraySerializer,对所有其他类型使用普通序列化程序(Json、Avro等)。

九、使用batch监听器进行payload转换Payload Conversion with Batch Listeners

当你使用batch监听器容器工厂时,也可以在BatchMessagingMessageConverter中使用JsonMessageConverter来转换batch消息。
默认情况下,转换的类型是从监听器参数推断出来的。如果使用DefaultJackson2TypeMapper配置JsonMessageConverter,并且其TypePrecedence设置为TYPE_ID(而不是默认的INFERRED),则转换器将使用headers中的类型信息(如果存在)。例如,这允许使用接口而不是具体类来声明监听器方法。此外,类型转换器支持映射,因此反序列化可以是与源不同的类型(只要数据兼容)。当您使用类级别的@KafkaListener实例时,这也很有用,在这些实例中,payload必须已经转换,以确定要调用哪个方法。以下示例创建使用此方法的bean:

@Bean
public KafkaListenerContainerFactory<?> kafkaListenerContainerFactory() {
    ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
            new ConcurrentKafkaListenerContainerFactory<>();
    factory.setConsumerFactory(consumerFactory());
    factory.setBatchListener(true);
    factory.setBatchMessageConverter(new BatchMessagingMessageConverter(converter()));
    return factory;
}

@Bean
public JsonMessageConverter converter() {
    return new JsonMessageConverter();
}

请注意,要使其工作,转换目标的方法签名必须是具有单一泛型参数类型的容器对象,例如:

@KafkaListener(topics = "blc1")
public void listen(List<Foo> foos, @Header(KafkaHeaders.OFFSET) List<Long> offsets) {
    ...
}

注意,你仍然可以访问batch headers。如果batch转换器具有支持它的record转换器,则还可以接收消息列表,其中根据泛型类型转换payloads。下面的例子展示了如何这样做:

@KafkaListener(topics = "blc3", groupId = "blc3")
public void listen1(List<Message<Foo>> fooMessages) {
    ...
}

十、转换服务自定义ConversionService Customization

org.springframework.core.covert.ConversionService被默认o.s.messaging.handler.annotation.support.MessageHandlerMethodFactory使用,用于解析监听器方法调用参数,它与实现以下任意接口的bean一起提供:

  • org.springframework.core.convert.converter.Converter
  • org.springframework.core.convert.converter.GenericConverter
  • org.springframework.format.Formatter
    这样,你可以进一步自定义监听器反序列化,而无需更改ConsumerFactory和KafkaListenerContainerFactory的默认配置。
    通过KafkaListenerConfigurer bean在KafkaListenerEndpointRegistrar上设置自定义MessageHandlerMethodFactory将禁用此功能。

十一、为@KafkaListener添加自定义HandlerMethodArgumentResolver

现在,你可以添加自己的HandlerMethodArgumentResolver,并解析自定义方法参数。你所需要做的就是实现KafkaListenerConfigurer并使用类KafkaListenerEndpointRegistrar中的setCustomMethodArgumentResolvers()方法。

@Configuration
class CustomKafkaConfig implements KafkaListenerConfigurer {

    @Override
    public void configureKafkaListeners(KafkaListenerEndpointRegistrar registrar) {
        registrar.setCustomMethodArgumentResolvers(
            new HandlerMethodArgumentResolver() {

                @Override
                public boolean supportsParameter(MethodParameter parameter) {
                    return CustomMethodArgument.class.isAssignableFrom(parameter.getParameterType());
                }

                @Override
                public Object resolveArgument(MethodParameter parameter, Message<?> message) {
                    return new CustomMethodArgument(
                        message.getHeaders().get(KafkaHeaders.RECEIVED_TOPIC, String.class)
                    );
                }
            }
        );
    }

}

你还可以通过向KafkaListenerEndpointRegistrar bean添加自定义MessageHandlerMethodFactory来完全替换框架的参数解析。如果你这样做,并且你的应用程序需要处理tombstone记录,而且值为null(例如来自compacted topic),你应该向工厂添加一个KafkaNullAwarePayloadArgumentResolver;它必须是最后一个解析程序,因为它支持所有类型,并且可以在没有@Payload注释的情况下匹配参数。如果你使用的是DefaultMessageHandlerMethodFactory,将此resolver设置为最后一个自定义解析程序;工厂将确保在标准PayloadMethodArgumentResolver之前使用此解析器,标准resolver没有KafkaNull payload的信息。

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值