Skip to content

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

Merged
merged 5 commits into from
Jun 1, 2022

Conversation

breader124
Copy link
Contributor

I've added lightweight message listener adapter converting consumer records to desired type specified by developer as mentioned in #2068

Copy link
Member

@artembilan artembilan left a 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);
Copy link
Member

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

Copy link
Contributor Author

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)) {
Copy link
Member

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?

Copy link
Contributor Author

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 {
Copy link
Member

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.

Copy link
Contributor Author

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> {
Copy link
Member

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

Copy link
Contributor Author

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
Copy link
Member

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.

Copy link
Contributor Author

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> {
Copy link
Member

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...

Copy link
Contributor Author

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) {
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in eeb9961

Copy link
Member

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
Copy link
Member

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.

Copy link
Contributor Author

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) {
Copy link
Member

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.

Copy link
Contributor Author

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();
Copy link
Member

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...

Copy link
Contributor Author

@breader124 breader124 Mar 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in eeb9961

Copy link
Member

@artembilan artembilan left a 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

@breader124
Copy link
Contributor Author

Hi @garyrussell, what's your opinion about this PR?

@garyrussell
Copy link
Contributor

I will try to look at it this week.

Copy link
Contributor

@garyrussell garyrussell left a 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> {
Copy link
Contributor

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);
Copy link
Contributor

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
Copy link
Contributor

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));
Copy link
Contributor

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).

Copy link
Contributor Author

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?

Copy link
Contributor

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.

@breader124 breader124 requested a review from garyrussell April 8, 2022 10:24
@breader124
Copy link
Contributor Author

Hi @garyrussell , will this PR be merged?

@garyrussell
Copy link
Contributor

Yes, sorry; I have been busy and was not working last week.

@garyrussell garyrussell merged commit 8034c2c into spring-projects:main Jun 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants