-
Notifications
You must be signed in to change notification settings - Fork 1.6k
GH-2068: Add lightweight converting adapter #2165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not fully sure in the purpose behind this class, but here you are some my review at a glance.
Thanks
*/ | ||
public ConvertingAndDelegatingMessageListenerAdapter(Object delegate, Class<V> desiredValueType) { | ||
validateMessageListener(delegate); | ||
Objects.requireNonNull(desiredValueType); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We prefer to use org.springframework.util.Assert
instead of blind NullPointerException
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Solved in 603202f
|
||
private void validateMessageListener(Object messageListener) { | ||
Objects.requireNonNull(messageListener); | ||
if (!(messageListener instanceof MessageListener)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why then just don't require a MessageListener delegate
in the constructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Solved in 603202f
* @since 3.0.0 | ||
* | ||
*/ | ||
class ConvertingAndDelegatingMessageListenerAdapterTest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We prefer to name test classes as *Tests
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Solved in 603202f
* @since 3.0 | ||
* @see AcknowledgingConsumerAwareMessageListener | ||
*/ | ||
public class ConvertingAndDelegatingMessageListenerAdapter<T, U, V> implements AcknowledgingConsumerAwareMessageListener<T, U> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would call it just ConvertingMessageListener
. Doesn't look like it adapts to something not compatible in API: https://www.baeldung.com/java-adapter-pattern
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Solved in 603202f
} | ||
} | ||
|
||
private ConsumerRecord<T, V> convertConsumerRecord(ConsumerRecord<T, U> data) { // NOSONAR |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we care somehow about T
& U
in the logic of this class.
In the end what we only need is a V
for the data type we would like to convert to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to replace T and U with just ?, but then it turned out that I can't override onMessage method without compiler yelling at me with communicate "same erasure, yet neither overrides the other". Also from code readability perspective it's kinda useful in my opinion, because it's clearly visible, that key (T) stays the same and U is at the end converted to V. Anyway, if you don't feel convinced I'll try to put some additional effort to investigate how could I avoid using T and U keeping only V as a type of a value after conversion.
* @since 3.0 | ||
* @see AcknowledgingConsumerAwareMessageListener | ||
*/ | ||
public class ConvertingMessageListener<T, U, V> implements AcknowledgingConsumerAwareMessageListener<T, U> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I look at this from the end-users perspective where they have to think what exactly need to be specified for the T
and U
when they really don't care about them since T
is always going to be an original one and U
is always going to be converted to V
independently of U
.
Therefore I find them redundant and want to avoid questions about them and trying to explain that we need them merely to satisfy an internal code consistency for compiler.
I believe we still can find some hard casting behavior internally even if it could be handled with a rawtypes
suppression...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed in eeb9961
* @param messageConverter the {@link MessageConverter} to use for conversion. | ||
* @param desiredValueType the {@link Class} setting desired type of {@link ConsumerRecord}'s value. | ||
*/ | ||
public ConvertingMessageListener(MessageListener<T, V> delegate, MessageConverter messageConverter, Class<V> desiredValueType) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If messageConverter
is optional and we have a default one, it is better to extract it into a setter instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed in eeb9961
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please, stop making comments like this.
You just push commit to the PR and we are notified.
We also will see your changes and how they are related to the review we have left before.
Otherwise it is spamming my mail box: first your new commit and this "duplication" comments in their individual emails 😄
} | ||
|
||
@Override | ||
public void onMessage(ConsumerRecord<T, U> data, Acknowledgment acknowledgment, Consumer<?, ?> consumer) { // NOSONAR |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure that we really need a // NOSONAR
over here. The method doesn't look so complicated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed in eeb9961
return rebuildConsumerRecord(data, converted); | ||
} | ||
|
||
private ConsumerRecord<T, V> rebuildConsumerRecord(ConsumerRecord<T, U> data, V converted) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method should be static
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed in eeb9961
this.delegate = delegate; | ||
this.desiredValueType = desiredValueType; | ||
|
||
this.messageConverter = new SimpleMessageConverter(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SimpleMessageConverter
does not make too much sense for a logic of this listener.
It just does this: return (ClassUtils.isAssignableValue(targetClass, payload) ? payload : null);
. So, if we expect in this converter whatever is there has been deserialized for us, then we just don't need to use this converter at all.
Perhaps a GenericMessageConverter
would be better...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed in eeb9961
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm OK with this one.
Let's see what @garyrussell thinks and into what version we merge it.
Thanks
Hi @garyrussell, what's your opinion about this PR? |
I will try to look at it this week. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the contribution; I was anticipating a call to a POJO after conversion, but this is a valid implementation too (creating a new ConsumerRecord
).
We can add a POJO adapter later.
* @see AcknowledgingConsumerAwareMessageListener | ||
*/ | ||
@SuppressWarnings("rawtypes") | ||
public class ConvertingMessageListener<V> implements AcknowledgingConsumerAwareMessageListener<Object, Object> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also implement DelegatingMessageListener
so the container can determine what type of listener the delegate is.
this.delegate.onMessage(convertedConsumerRecord, acknowledgment); | ||
} | ||
|
||
this.delegate.onMessage(convertedConsumerRecord); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs to be in an else
block, otherwise the listener is called twice.
data.partition(), | ||
data.offset(), | ||
data.key(), | ||
converted |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about the headers and other metadata (timestamp etc.) ?
private ConsumerRecord convertConsumerRecord(ConsumerRecord data) { | ||
Header[] headerArray = data.headers().toArray(); | ||
Map<String, Object> headerMap = Arrays.stream(headerArray) | ||
.collect(Collectors.toMap(Header::key, Header::value)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use a KafkaHeaderMapper
here (configurable, with SimpleKafkaHeaderMapper
default).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I analysed the code one more time and figured out that creating GenericMessage with headers is pointless, because they don't play any role in conversion of message payload. Also in newly built ConsumerRecord they are just copied form received record. Based on that I deleted these lines related to extracting headers and take care only about payload in conversion. What's your opinion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only thought I had was if someone might want access to the headers to decide the conversion strategy.
Let's say, the listener is expecting ConsumerRecord<String, Foo>
where Foo
is an interface (or abstract class) with two implementations Bar
and Baz
; there might be type information in the headers to help the converter decide which one to instantiate.
But, I agree, it would be unfortunate to take the overhead of mapping the headers when the converter doesn't need them.
I would therefore recommend supporting a header mapper property, but only map the headers if one is provided.
…ata and fix doubled invokation of message listener
…cover it with tests
Hi @garyrussell , will this PR be merged? |
Yes, sorry; I have been busy and was not working last week. |
I've added lightweight message listener adapter converting consumer records to desired type specified by developer as mentioned in #2068