2

I'm using a PollableMessageSource input to read from a Kafka topic. Messages on that topic are in Avro. use-native-decoding was set to true when those messages were published.

This is how I'm polling:

pollableChannels.inputChannel().poll(this::processorMethodName,
        new ParameterizedTypeReference<TypeClassName>() {
        });

pollableChannels is just an injected instance of this interface:

public interface PollableChannels {
  @Input("input-channel")
  PollableMessageSource inputChannel();
}

After seeing that the TypeClassName is not being formed properly (it's nested objects are set to null by mistake), I started debugging the poll method and I found that it's relying on the contentType header to select a converter, and since this has not been set (because the messages have been encoded natively), it's falling back to using the ApplicationJsonMessageMarshallingConverter which is clearly not the right option.

If I use a regular streamListener, the use-native-decoding config property is honored fine, so the messages seem to be published ok.

Therefore, my primary question here is how to force native decoding when using pollable consumers? My borader question could be asking if properties under spring.cloud.stream.bindings.channel-name.consumer are respected at all when using a pollable consumer?

Spring cloud stream version: 2.2.0.RELEASE
Spring Kafka: 2.2.5.RELEASE
Confluent version for the serializer: 5.2.1

Update:

Relevant config:

spring:
  cloud.stream:
    bindings:  
      input-channel:
        content-type: application/*+avro
        destination: "topic-name"
        group: "group-name"
        consumer:
          partitioned: true
          concurrency: 3
          max-attempts: 1
          use-native-decoding: true
    kafka:
      binder:
        configuration:
          key.serializer: org.apache.kafka.common.serialization.StringSerializer
          value.serializer: io.confluent.kafka.serializers.KafkaAvroSerializer
          key.deserializer: org.apache.kafka.common.serialization.StringDeserializer
          value.deserializer: io.confluent.kafka.serializers.KafkaAvroDeserializer

Milad
  • 608
  • 1
  • 6
  • 14

1 Answers1

2

The ParameterzedTypeReference. argument is intended to help the message converter to convert the payload to the required type. When using native decoding, the "conversion" is done by the deserializer instead and conversion is not necessary.

So, just remove the second argument to the poll() method and conversion will be skipped.

That said, starting with version 3.0.8 (and Spring Framework 5.2.9), the conversion is a no-op, as can be seen in the example below.

However, it's still more efficient to omit the argument to avoid any attempt at conversion.

else if (targetClass.isInstance(payload)) {
    return payload;
}

I just tested it without any problems (tested on 3.0.8, but I don't believe there have been any changes in this area). In fact, you don't even need useNativeDecoding for this case.

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 + "]";
    }

}


@SpringBootApplication
@EnableBinding(Polled.class)
public class So64554618Application {

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

    @Autowired
    PollableMessageSource source;


    @Bean
    public ApplicationRunner runner(KafkaTemplate<byte[], byte[]> template) {
        return args -> {
            template.send("input", "{\"bar\":\"baz\"}".getBytes());
            Thread.sleep(5_000);
            source.poll(msg -> {
                System.out.println(msg);
            }, new ParameterizedTypeReference<Foo>() { });
        };
    }

}

interface Polled {

    @Input
    PollableMessageSource input();

}
#spring.cloud.stream.bindings.input.consumer.use-native-decoding=true
spring.cloud.stream.bindings.input.group=foo

spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer
spring.kafka.consumer.properties.spring.json.trusted.packages=*
spring.kafka.consumer.properties.spring.json.value.default.type=com.example.demo.Foo
GenericMessage [payload=Foo [bar=baz], headers={kafka_offset=2, ...
Gary Russell
  • 166,535
  • 14
  • 146
  • 179
  • Yes, the payload is already the required type. However, I can't see the no-op logic that you mentioned. In which class does that lie? – Milad Oct 27 '20 at 23:56
  • Debugging, I can see that it's coming to `super.convertFromInternal(message, targetClass, conversionHint)` inside the `convertFromInternal` method of the `ApplicationJsonMessageMarshallingConverter` class since all the previous if conditions are not met in its own `convertToInternal`. That line triggers Jackson to be used which creates the problem. – Milad Oct 27 '20 at 23:59
  • That doesn’t make sense. Jackson can’t take an Object as input. In `convertFromInternal`, `convertParameterizedType` returns `null`, so we fall back to `result = super.convertFromInternal(message, targetClass, conversionHint);`, which takes the path I put in the answer (just return the payload since it's already the required type). – Gary Russell Oct 28 '20 at 00:24
  • Jackson is not taking the object directly. This is what's happening in `MappingJackson2MessageConverter` (superclass of `ApplicationJsonMessageMarshallingConverter`): `this.objectMapper.readValue(payload.toString(), javaType)`. So the payload is converted into String first then Jackson is used. I also updated my original question with the config. Thanks! – Milad Oct 28 '20 at 00:46
  • Where is that being called from? You said `ApplicationJsonMessageMarshallingConverter` in your original post. I am testing with 3.0.8. 2.0.0 is ancient. Supported versions are shown here: https://spring.io/projects/spring-cloud-stream#learn – Gary Russell Oct 28 '20 at 00:57
  • That is called from within ApplicationJsonMessageMarshallingConverter's `convertToInternal`'s last line when the super `convertToInternal` is triggered. – Milad Oct 28 '20 at 01:03
  • So would you say that this might not occur in newer versions? – Milad Oct 28 '20 at 01:04
  • Found it - so the test right above that is failing for some reason `else if (targetClass.isInstance(payload)) {` - so, somehow, your class converted by the deserializer does not match the paramterized type - perhaps a classloader issue? Are you using devtools? – Gary Russell Oct 28 '20 at 01:05
  • hmm... I'm still struggling to find the test you mentioned. I'm looking inside [this](https://github.com/spring-cloud/spring-cloud-stream/blob/master/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/converter/ApplicationJsonMessageMarshallingConverter.java). – Milad Oct 28 '20 at 04:50
  • `convertParameterizedType` in there seems to be checking for byte[]/String only. Am I missing something? – Milad Oct 28 '20 at 04:53
  • Aha - sorry about that - it was added in [Spring Framework 5.2.9](https://github.com/spring-projects/spring-framework/blob/69921b49a5836e412ffcd1ea2c7e20d41f0c0fd6/spring-messaging/src/main/java/org/springframework/messaging/converter/MappingJackson2MessageConverter.java#L222-L224) which is the version used by 3.0.8 - hence it works for me. I wrongly assumed that code was always there. – Gary Russell Oct 28 '20 at 12:49
  • Just remove the parameterized type reference - it's not needed when using native decoding - it's a conversion hint. – Gary Russell Oct 28 '20 at 13:16
  • Yes, removing the parameterized type reference did the job. Thanks! Could you please update your answer so I can mark it as accepted? – Milad Oct 29 '20 at 03:32