1

I am doing deserialization at the Listener in Spring Kafka. But this assumes that the type information was included or sent by a Spring Kafka producer. In my case the Json is being sent across by the Debezium MySQLConnector and it does not add this meta data. So I would like to add it to the requests. I understand its placed in the request somewhere in the JsonSerializer, and I looked at the source code but could not figure out exactly how to use this to add meta data type during serialization generically to a request. In particular what field holds this type information? And is it the class name of the java object that was serialized? I dont think just setting a default serializer is going to work because I have multiple consumers listening on different topics as one would expect. Except for the simplest cases this is just not going to work to set one default as i have many consumers and types that I am deserializing to. So this answer is not going to work in my case Kafka - Deserializing the object in Consumer

Update tried using Method Types on deserializer but have another issue: Kafka Spring Deserialzer returnType static method never called

Roger Alkins
  • 125
  • 11

1 Answers1

3

See

public abstract class AbstractJavaTypeMapper implements BeanClassLoaderAware {

    /**
     * Default header name for type information.
     */
    public static final String DEFAULT_CLASSID_FIELD_NAME = "__TypeId__";

    /**
     * Default header name for container object contents type information.
     */
    public static final String DEFAULT_CONTENT_CLASSID_FIELD_NAME = "__ContentTypeId__";

    /**
     * Default header name for map key type information.
     */
    public static final String DEFAULT_KEY_CLASSID_FIELD_NAME = "__KeyTypeId__";

    /**
     * Default header name for key type information.
     */
    public static final String KEY_DEFAULT_CLASSID_FIELD_NAME = "__Key_TypeId__";

    /**
     * Default header name for key container object contents type information.
     */
    public static final String KEY_DEFAULT_CONTENT_CLASSID_FIELD_NAME = "__Key_ContentTypeId__";

    /**
     * Default header name for key map key type information.
     */
    public static final String KEY_DEFAULT_KEY_CLASSID_FIELD_NAME = "__Key_KeyTypeId__";

2 sets of headers (keys and values).

TypeId is for simple classes

If TypeId is a container List<?>

ContentTypeId is the contained type.

If TypeId is a Map

Key_TypeId is the key type.

This allows you to reconstruct a Map<Foo, Bar>.

These headers can either contain fully qualified class names, or tokens that map to class names via the classIdMappings map.

However, since version 2.5, it would be easier to use the new

Using Methods to Determine Types.

That way, you can set your own headers and examine them in the method.

EDIT

Here is a simple example:

@SpringBootApplication
public class Gitter76Application {

    public static void main(String[] args) {
        SpringApplication.run(Gitter76Application.class, args);
    }

    @Bean
    public NewTopic topic() {
        return TopicBuilder.name("gitter76").partitions(1).replicas(1).build();
    }

    @KafkaListener(id = "gitter76", topics = "gitter76")
    public void listen(Foo in) {
        System.out.println(in);
    }

}
public class Foo {

    private String bar;

    public Foo() {
    }

    public Foo(String bar) {
        this.bar = bar;
    }

    public String getBar() {
        return this.bar;
    }

    public void setBar(String bar) {
        this.bar = bar;
    }

    @Override
    public String toString() {
        return "Foo [bar=" + this.bar + "]";
    }

}
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer

spring.kafka.consumer.properties.spring.json.trusted.packages=com.example.demo
$ kafkacat -P -b localhost:9092 -t gitter76 -H __TypeId__=com.example.demo.Foo
{"bar":"baz"}
^C
2020-08-08 09:32:10.034  INFO 58146 --- [ gitter76-0-C-1] o.s.k.l.KafkaMessageListenerContainer    : gitter76: partitions assigned: [gitter76-0]
Foo [bar=baz]
Gary Russell
  • 166,535
  • 14
  • 146
  • 179
  • Ok, is there a way to handle all of this on the Listener side when the type information is not there? – Steven Smart Aug 02 '20 at 09:18
  • In other words how would I utilize Using Methods to Determine Types from the Listener side without modifying the producer? How exactly would I apply this? – Steven Smart Aug 02 '20 at 09:19
  • 1
    The only way to do that, as mentioned in the doco link, is to inspect the data (e.g. run a JsonPath query against it, or convert it to a map) looking for the presence of fields that are unique to one type or another; if there is any ambiguity, and you can't determine it that way, you are out of luck. – Gary Russell Aug 02 '20 at 14:23
  • Ok, I can easily do this by including a type field in the json itself and inspect that however, I am still not clear on how to apply the deserializer to the listener(s) purely on the listener side? Is there a field on the listener to set the deserializer? Or the factory used for it? And if not and it needs to be in a bean will this throw off the spring boot autoconfig? do I then have to turn off autoconfig? Keep in mind I also have request/replying templates in my config. – Steven Smart Aug 02 '20 at 16:08
  • The link clearly shows how to construct the deserializer and have it use the method but whats less clear is how that deserializer gets applied to a given listener? Where do I declare this deserializer and what connects it to a listener? and in doing so will it effect the autoconfig or not? – Steven Smart Aug 02 '20 at 16:10
  • I was also looking at https://github.com/SpringOnePlatform2016/grussell-spring-kafka example @Bean public ConcurrentKafkaListenerContainerFactory jsonKafkaListenerContainerFactory() { ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); factory.setConsumerFactory(consumerFactory()); factory.setMessageConverter(new StringJsonMessageConverter()); return factory; }. And I again wonder if using this bean is going to effect spring autoconfig in a config where I have request/reply templates etc – Steven Smart Aug 02 '20 at 16:17
  • Don't put code in comments; it is hard to read; it's better to ask a new question. See [this answer](https://stackoverflow.com/questions/63108344/spring-kafka-requirements-for-supporting-multiple-consumers/63117327#63117327). The deserializer can be provided to the consumer factory bean directly, by class name in the Boot consumer properties, or overridden at the listener level. – Gary Russell Aug 02 '20 at 17:03
  • If you are using a Json deserializer you shouldn't use a `JsonMessageConverter` or you'll be trying to decode JSON twice. Use a String deserializer with the message converter - that is used to automatically determine the type from the method parameter. – Gary Russell Aug 02 '20 at 17:03
  • Ok, I just need one last clarification. Sometimes the type information is there in the header and sometimes not. Since there are multiple fields for the header in Spring Kafka like for Key etc how do use default behavior in the case where there is header information already? I see the headers are passed yes, there are up to three fields used in this and I am not sure how to recreate the full type based on these three header fields. – Steven Smart Aug 03 '20 at 02:17
  • In other words how do I get to JavaType from having TypeId, ContentTypeId, and the Key_Type_Id ? – Roger Alkins Aug 03 '20 at 02:23
  • When you provide a method for type determination, all other type resolution is disabled. If you want a hybrid approach (sometimes headers, sometimes not) you should delegate to a `DefaultJackson2JavaTypeMapper`, or copy [its code](https://github.com/spring-projects/spring-kafka/blob/000ce9a9c41adc514772c1f6ab035ecdf1d45825/spring-kafka/src/main/java/org/springframework/kafka/support/converter/DefaultJackson2JavaTypeMapper.java#L94-L116). It has the logic to handle simple, container, and map types. – Gary Russell Aug 03 '20 at 02:33
  • Also how do you compare the JsonMessageCoverter with String deserializer to the type method on deserializer approach? Which do you think is best option when there is missing type information on some topics from connector but not all topics? – Roger Alkins Aug 03 '20 at 03:59
  • The admins here don't like extended commentary; it's best to ask a new question. `String/ByteArrayDeserializer` with a `String/ByteArrayMessageConverter` (`byte[]` preferred) is the simplest approach with concrete types, since we can determine the type from the method and need no headers. However, if the parameter type is abstract (e.g. different event types, or an interface) it won't work and you need to do the conversion in the deserializer. https://docs.spring.io/spring-kafka/docs/2.6.0-SNAPSHOT/reference/html/#messaging-message-conversion – Gary Russell Aug 03 '20 at 13:11
  • Ok, here is what happens when I try to implement the method type on deserializer https://stackoverflow.com/questions/63263947/kafka-spring-deserialzer-returntype-static-method-never-called – Roger Alkins Aug 05 '20 at 11:14
  • Well I just added the TypeId and gave it a class but still getting the no type id found on the Listener side?? I used Kafkacat to check and the headers are there on the consumer side? Is TypeId enough ? – Roger Alkins Aug 08 '20 at 11:15
  • I can't "guess" wha you are doing wrong; you need to show code, configuration and test data. This stuff just works; it's been around for several years and we have used a similar technique in spring-amqp (Spring for RabbitMQ) for over a decade. – Gary Russell Aug 08 '20 at 13:13
  • I added a simple, working, example to my answer; I tested it with Boot 2.1; no problems. Bear in mind that Boot 2.1 goes [end-of-life soon](https://spring.io/blog/2019/12/10/spring-boot-2-1-x-eol-november-1st-2020). – Gary Russell Aug 08 '20 at 16:00