Skip to content

Design for Enhance the ObjectMapper to support Spring Boot's pattern to enable autoconfiguration

Muyao Feng edited this page Dec 23, 2022 · 2 revisions

Context

This design is to make our current ObjectMapper could follow Spring Boot's configuration pattern.

Users now use MessageConverters with the default ObjectMapper which was newed by us, and this ObjectMapper doesn't support Spring Boot's configuration.

@Bean
@ConditionalOnMissingBean
public EventHubsMessageConverter eventHubsMessageConverter() {
  return new EventHubsMessageConverter();
}
/**
 * Construct the message converter with default {@code ObjectMapper}.
 */
public EventHubsMessageConverter() {
  this(OBJECT_MAPPER);
}
public final class ObjectMapperHolder {
    private ObjectMapperHolder() {
    }
    public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
}

So this design is to enhance the ObjectMapper to let users also can work with Spring Boot's pattern, such as:

  • Configure properties
  • Customize Jackson2ObjectMapperBuilderCustomizer
  • Override ObjectMapper/Jackson2ObjectMapperBuilder

Investigation and Analysis

Spring Boot uses the default ObjectMapper created by JacksonObjectMapperConfiguration in JacksonAutoConfiguration, the ObjectMapper will be created if there is no other ObjectMapper in the ApplicationContext.

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
	static class JacksonObjectMapperConfiguration {

		@Bean
		@Primary
		@ConditionalOnMissingBean
		ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
			return builder.createXmlMapper(false).build();
		}

	}

And next step confirmed that JacksonAutoConfiguration was packaged in maven spring-boot-autoconfigure. which already exists in spring-cloud-azure-autoconfigure .

How do other open source projects use ObjectMapper?

For Spring-Boot 3.0.0 RC2 actuator endpoints: Use the ObjectMapper builded by Jackson2ObjectMapperBuilder in JacksonEndpointAutoConfiguration.

@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter(JacksonAutoConfiguration.class)
public class JacksonEndpointAutoConfiguration {
	@Bean
	@ConditionalOnProperty(name = "management.endpoints.jackson.isolated-object-mapper", matchIfMissing = true)
	@ConditionalOnClass({ ObjectMapper.class, Jackson2ObjectMapperBuilder.class })
	public EndpointObjectMapper endpointObjectMapper() {
		ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json()
				.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
						SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
				.serializationInclusion(Include.NON_NULL).build();
		return () -> objectMapper;
	}
}

For spring-data-commons: Use the SpringBoot's default ObjectMapper in SpringDataWebConfiguration

ObjectMapper mapper = context.getBeanProvider(ObjectMapper.class).getIfUnique(ObjectMapper::new);

For AWS: Same in S3AutoConfiguration.

@ConditionalOnMissingBean
@Bean
S3ObjectConverter s3ObjectConverter(Optional<ObjectMapper> objectMapper) {
	return new Jackson2JsonS3ObjectConverter(objectMapper.orElseGet(ObjectMapper::new));
}

For GCP: Same in PubSubJsonPayloadApplication

@Bean
public JacksonPubSubMessageConverter jacksonPubSubMessageConverter(ObjectMapper objectMapper) {
    return new JacksonPubSubMessageConverter(objectMapper);
}

For Alibaba: Seems doesn't use SpringBoot's default ObjectMapper, for example: SentinelAutoConfiguration

@ConditionalOnClass(ObjectMapper.class)
@Configuration(proxyBeanMethods = false)
protected static class SentinelConverterConfiguration {
	@Configuration(proxyBeanMethods = false)
	protected static class SentinelJsonConfiguration {
		private ObjectMapper objectMapper = new ObjectMapper();
                public SentinelJsonConfiguration() {
			objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);
		}
		@Bean("sentinel-json-flow-converter")
		public JsonConverter jsonFlowConverter() {
			return new JsonConverter(objectMapper, FlowRule.class);
		}
                ...

Solution Design

  1. Consider there will be some users may not want to use ObjectMapper from Spring context, so we could add a property to control whether to use it:

    Property Name Default Value Description
    spring.cloud.azure.message-converter.isolated-object-mapper true Whether to use an isolated object mapper to serialize/deserialize message in EventHubsMessageConverter/ServiceBusMessageConverter/StorageQueueMessageConverter.
  2. We could use two MessageConverter beans, one is using default ObjectMapper from SpringBoot JacksonAutoConfiguration, another is from ObjectMapperHolder:

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(value = "spring.cloud.azure.message-converter.isolated-object-mapper", havingValue = "true", matchIfMissing = true)
    EventHubsMessageConverter defaultEventHubsMessageConverter() {
        return new EventHubsMessageConverter(ObjectMapperHolder.OBJECT_MAPPER);
    }
    
    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(value = "spring.cloud.azure.message-converter.isolated-object-mapper", havingValue = "false")
    EventHubsMessageConverter eventHubsMessageConverter(ObjectMapper objectMapper) {
        return new EventHubsMessageConverter(objectMapper);
    }
  3. Add property info in additional-spring-configuration-metadata.json.

    {
       "name": "spring.cloud.azure.message-converter.isolated-object-mapper",
       "type": "java.lang.Boolean",
       "defaultValue": "true",
       "description": "Whether to use an isolated object mapper to serialize/deserialize message in EventHubsMessageConverter/ServiceBusMessageConverter/StorageQueueMessageConverter."
    }

Finally, after this design, users could use spring's way to configure ObjectMapper and converter message when setting the property as false, or users can use our old ObjectMapper with nothing to configure.

Other Thoughts

  1. we also consider whether to add more properties for each messageConverter, but it is relatively rare in terms of user usage scenarios and is a bit redundant in terms of design, so we won't use it at present.
Clone this wiki locally