-
Notifications
You must be signed in to change notification settings - Fork 1.6k
GH-1781: Configure ObjectMapper in JsonSerializer/JsonDeserializer #1782
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
…Deserializer Fixes spring-projectsGH-1781 (spring-projects#1781) * Add ObjectMapperCustomizer interface for customizing local ObjectMapper * Implement ObjectMapperCustomizer in JsonSerializer / JsonDeserializer * call customizeObjectMapper() in ctor so ObjectWriter is properly constructed * added tests / docs
@cnygardtw Please sign the Contributor License Agreement! Click here to manually synchronize the status of this Pull Request. See the FAQ for frequently asked questions. |
@cnygardtw Thank you for signing the Contributor License Agreement! |
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.
Thank you for contribution!
Please, find some review on code lines: I really have some doubts about proposed solution...
====== Customize the ObjectMapper | ||
|
||
Occasionally, the only way to configure the way information is serialized is by customizing the ObjectMapper used in (de)serialization. | ||
Because Kafka uses its own ObjectMapper separate from the normal Spring-Boot ObjectMapper, customizing the ObjectMapper typically involved (re)implementing various Kafka factories. |
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, call the trade mark as Apache Kafka
.
[[serdes-customize-object-mapper]] | ||
====== Customize the ObjectMapper | ||
|
||
Occasionally, the only way to configure the way information is serialized is by customizing the ObjectMapper used in (de)serialization. |
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 ObjectMapper
should be wrapped into a code snippet using backtick symbol
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.fasterxml.jackson.databind.SerializationFeature; | ||
|
||
import org.springframework.kafka.support.serializer.JsonSerializer; |
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 think we can live without imports and package
declarations in the samples shown in the docs...
* @return configured (or replaced) ObjectMapper instance | ||
* @since 2.7.1 | ||
*/ | ||
public ObjectMapper customizeObjectMapper(ObjectMapper objectMapper) { |
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 this is what @garyrussell has meant in the issue.
We can't call non-final
methods from the ctors.
Probably the point was to inject an ObjectMapperCustomizer
into such a serialize impl...
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.
Good catch @artembilan ; yes a Function<ObjectMapper, ObjectMapper>
setter would be safer.
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 alternative is to stop creating the ObjectWriter in the ctor and instead construct in the serialize() function (which is what JsonDeserializer does currently). If this were done, then there's probably a better way to just hook into the configure() function. The root cause was that the ObjectWriter was private, and there seemed no other way that wasn't brittle and likely to break in future versions. If that's the preferred change, I can re-look at the implementation.
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.
As an alternative, I added Map<String, ?>
to the customize function, and call it first thing from the configure() function instead of calling in the constructor. I'm assuming that everywhere a serializer is instantiated, the configure() function will get called appropriately. The other bit is to create the writer in serialize(). updated patch shortly.
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; we have a day off tomorrow so it will probably be next week before we continue.
Thanks again.
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!
Just some minor comments.
* @return configured (or replaced) ObjectMapper instance | ||
* @since 2.7.1 | ||
*/ | ||
public ObjectMapper customizeObjectMapper(ObjectMapper objectMapper) { |
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 can be a default
method on the interface.
Please change all sinces to 2.5.13; we will be backporting this to 2.6, 2.5.
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.
That was my original thought, but I took guidance from JsonTypeResolver. If we provide default implementation, then @FunctionalInterface goes away. Is that what you intend?
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 this needs to be a FunctionalInterface
the type resolver can be used as lambda expression, or method reference:
Line 371 in bbc5003
.typeResolver(JsonSerializationTests::fooBarJavaTypeForTopic); |
This is just a super interface for the (de)serializer.
* @return configured (or replaced) ObjectMapper instance | ||
* @since 2.7.1 | ||
*/ | ||
public ObjectMapper customizeObjectMapper(ObjectMapper objectMapper) { |
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.
See comment in serializer - pull up to interface.
Because Kafka uses its own ObjectMapper separate from the normal Spring-Boot ObjectMapper, customizing the ObjectMapper typically involved (re)implementing various Kafka factories. | ||
Since 2.7.1, a new interface `ObjectMapperCustomizer` has been introduced and implemented by the `JsonSerializer<T>` and `JsonDeserializer<T>` classes. | ||
This makes it simpler to configure the ObjectMapper without losing the convenience of the Spring property configuration capabilities. | ||
|
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.
Suggested rewording - the Apache Foundation require us to mention Apache whenever we reference Kafka
.
$ git diff
diff --git a/spring-kafka-docs/src/main/asciidoc/kafka.adoc b/spring-kafka-docs/src/main/asciidoc/kafka.adoc
index 2b1cd6a2..faa0a1e1 100644
--- a/spring-kafka-docs/src/main/asciidoc/kafka.adoc
+++ b/spring-kafka-docs/src/main/asciidoc/kafka.adoc
@@ -3795,6 +3795,7 @@ Also since version 2.7 `ConsumerPartitionPausedEvent` and `ConsumerPartitionResu
[[serdes]]
==== Serialization, Deserialization, and Message Conversion
+[[serdes-overview]]
===== Overview
Apache Kafka provides a high-level API for serializing and deserializing record values as well as their keys.
@@ -3924,8 +3925,9 @@ You can revert to the previous behavior by setting the `removeTypeHeaders` prope
====== Customize the ObjectMapper
Occasionally, the only way to configure the way information is serialized is by customizing the ObjectMapper used in (de)serialization.
-Because Kafka uses its own ObjectMapper separate from the normal Spring-Boot ObjectMapper, customizing the ObjectMapper typically involved (re)implementing various Kafka factories.
-Since 2.7.1, a new interface `ObjectMapperCustomizer` has been introduced and implemented by the `JsonSerializer<T>` and `JsonDeserializer<T>` classes.
+When instantiated by Apache Kafka, the Spring JSON Serializer and Deserializer use a locally declared `ObjectMapper`.
+The only way to customize the `ObjectMapper` was to inject instances into the producer and consumer factories as discussed <<serdes-overview,here>> rather than by using properties.
+Since version 2.5.13, a new interface `ObjectMapperCustomizer` has been introduced and implemented by the `JsonSerializer<T>` and `JsonDeserializer<T>` classes.
This makes it simpler to configure the `ObjectMapper` without losing the convenience of the Spring property configuration capabilities.
The following is an example of how to provide a simple customized serializer class (derived from `JsonSerializer<T>`) to do the configuration.
@@ -3958,7 +3960,7 @@ public class CustomJsonSerializer<T> extends JsonSerializer<T> {
----
====
-Then in your properties file, configure your `CustomJsonSerializer<T>` class as the `value-serializer`:
+Then in your Spring Boot properties file, for example, configure your `CustomJsonSerializer<T>` class as the `value-serializer`:
====
[source, properties]
- delay creation of objectreader/writer - add local cache object to minimize construction - shift objectmapper configuration to configure() function - pass configuration map to objectmapper configuration
@@ -718,4 +717,27 @@ private JsonTypeResolver buildTypeResolver(String methodProperty) { | |||
}; | |||
} | |||
|
|||
protected class CachedObjectReader { |
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 do we need this?
Why just regular volatile
property which we set first time call in that deserialize()
not enough for us?
Looks like a this.objectMapper
and this.targetType
always produce the same state, so we even don't need to worry about synchronization.
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 problems are:
- can't create ObjectWriter in ctor, because there's no way to provide customization of ObjectMapper in ctor
- can't create ObjectWriter in configure() function, because unit tests indicate it is not guaranteed that configure() will get called
- construct ObjectWriter at point of need, and possibly suffer performance issues
Therefore making no assumptions about how costly it is to construct an ObjectWriter. Considering that the current serializer constructs the ObjectWriter just once in the constructor, I took the conservative approach and tried to at least cache the object so that if conditions are right it would still only get constructed once. On the other hand, hashcode does have a cost, so if you don't think it matters to performance, I can pull it out.
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.
OK. Probably you just didn't see through my suggestion:
private volatile ObjectReader objectReader;
...
public T deserialize(...) {
if (this.objectReader == null) {
this.objectReader = this.objectMapper.readerFor(this.targetType);
}
}
Does it make sense?
Technically your caching is just the same, but without volatile
.
And that's why I don't see a reason in that extra instance if we just can deal with volatile
and plain if()
.
Sorry for not being clear enough from the beginning.
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.
That is certainly simpler, but I'm not familiar enough with all the supported code flows to decide if it's sufficient or not.
Are there any flows where a serializer might get reconfigured? With this code, once the ObjectReader is constructed, changes to the ObjectMapper will not have any effect on the serialization (which was the original reason for the PR). If that's an acceptable constraint, I'll make the change.
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 so: you call customizeObjectMapper()
from the configure(Map<String, ?> configs, boolean isKey)
which is a Kafka Client API, which is called once from Kafka Client instance, e.g.:
if (valueSerializer == null) {
this.valueSerializer = config.getConfiguredInstance(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
Serializer.class);
this.valueSerializer.configure(config.originals(Collections.singletonMap(ProducerConfig.CLIENT_ID_CONFIG, clientId)), false);
} else {
config.ignore(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG);
this.valueSerializer = valueSerializer;
}
And you see: they also set config.ignore(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG);
, which means do not look into that property any more. So, any changes to the ObjectMapper
at runtime are not going to be visible anyway.
Sounds reasonable?
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.
On the flip side, changes to the ObjectMapper may or may not be reflected in the hashcode. If not, then the CachedObjectReader won't work any better (or worse) than a volatile.
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.
ok, will update.
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.
Looking closely to an issue I would say that it is probably better just to reject all of these changes in favor of plain Java inheritance:
public class CustomJsonSerializer extends JsonSerializer<Object> {
public CustomJsonSerializer() {
super(customizedObjectMapper());
}
private static ObjectMapper customizedObjectMapper() {
ObjectMapper mapper = JacksonUtils.enhancedObjectMapper();
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}
and this is going to work as expected for both Java configuration and Apache Kafka properties setting.
What am I missing?
not sure, I'll try that also. |
That worked as far as my tests required it. Reject the PR. Do you perhaps want a different PR with just documentation updates? |
I wonder why, since we talk just about a regular Java inheritance. Thanks for understanding! |
Point me to the docs, and I'll reward you with a facepalm ;) As for docs, I was thinking something in the tips section. There's quite a lot of related SO questions/answers, and the answers are all over the place, so this might help resolve some confusion. And no worries, a solution that doesn't require a PR is a solution I'm happy with. |
Sure thing! This chapter should be a good one for such a simple example: https://docs.spring.io/spring-kafka/docs/current/reference/html/#tips-n-tricks. Also we won't mind if you add something for custom (de)serialization use-case into this directory: https://github.com/spring-projects/spring-kafka/tree/main/samples But yeah: different story - deserves different PR! |
@cnygardtw Closing this; I opened this issue #1784 Thanks for your efforts anyway. |
Fixes GH-1781 (#1781)