SpEL KafkaListener. How can i inject custom deserializer through properties? - spring

I am using spring.
I have a configured ObjectMapper for the entire project and I use it to set up a kafka deserializer.
And then I need a custom kafka deserializer to be used in KafkaListener.
I'm configuring KafkaListener via autoconfiguration, not via #Configuration class.
#Component
#RequiredArgsConstructor
public class CustomMessageDeserializer implements Deserializer<MyMessage> {
private final ObjectMapper objectMapper;
#SneakyThrows
#Override
public MyMessage deserialize(String topic, byte[] data) {
return objectMapper.readValue(data, MyMessage.class);
}
}
If i do like this
#KafkaListener(
topics = {"${topics.invite-user-topic}"},
properties = {"value.deserializer=com.service.deserializer.CustomMessageDeserializer"}
)
public void receiveInviteUserMessages(MyMessage myMessage) {}
I received KafkaException: Could not find a public no-argument constructor
But with public no-argument constructor in CustomMessageDeserializer class i am getting NPE because ObjectMapper = null. It creates and uses a new class, not a spring component.
#KafkaListener supports SpEL expressions.
And I think that this problem can be solved using SpEL.
Do you have any idea how to inject spring bean CustomMessageDeserializer with SpEL?

There are no easy ways to do it with SPeL.
Analysis
To get started, see the JavaDoc for #KafkaListener#properties:
/**
*
* SpEL expressions must resolve to a String ...
*/
The value of value.deserializer is used to instantiate the specified deserializer class. Let's follow the call chain:
You specify this value in the #KafkaListener annotation, then you are probably not creating a bean of the ConsumerFactory.class. So Spring creates this bean class itself - see KafkaAutoConfiguration#kafkaConsumerFactory.
Next is the creation of the returned object new DefaultKafkaConsumerFactory(...) as ConsumerFactory<?,?> using the constructor for default delivery expressions keyDeserializer/valueDeserializer = () -> null
This factory is used to create a Kafka consumer (The entry point is the constructor KafkaMessageListenerContainer#ListenerConsumer, then KafkaMessageListenerContainer.this.consumerFactory.createConsumer...)
In the KafkaConsumer constructor, the valueDeserializer object is being created, because it is null (for the default factory of point 2 above):
if (valueDeserializer == null) {
this.valueDeserializer = config.getConfiguredInstance(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, Deserializer.class);
The implementation of config.getConfiguredInstance involves instantiating your deserializer class via a parameterless constructor using reflection and your String "com.service.deserializer.CustomMessageDeserializer" class name
Solutions
To use value.deserializer with your customized ObjectMapper, you must create the ConsumerFactory bean yourself using the setValueDeserializer(...) method. This is also mentioned in the second Important part of the JSON.Mapping_Types.Important documentation
If you don't want to create a ConsumerFactory bean, and also don't have complicated logic in your deserializer (you only have return objectMapper.readValue(data, MyMessage.class);), then register DefaultKafkaConsumerFactoryCustomizer:
#Bean
// inject your custom objectMapper
public DefaultKafkaConsumerFactoryCustomizer customizeJsonDeserializer(ObjectMapper objectMapper) {
return consumerFactory ->
consumerFactory.setValueDeserializerSupplier(() ->
new org.springframework.kafka.support.serializer.JsonDeserializer<>(objectMapper));
}
In this case, you don't need to create your own CustomMessageDeserializer class (remove it) and Spring will automatically parse the message into your MyMessage.
#KafkaListener annotation should also not contains the property properties = {"value.deserializer=com.my.kafka_test.component.CustomMessageDeserializer"}. This DefaultKafkaConsumerFactoryCustomizer bean will automatically be used to configure the default ConsumerFactory<?, ?> (see the implementation of the KafkaAutoConfiguration#kafkaConsumerFactory method)

Here how it works for me:
#KafkaListener(topics = "${solr.kafka.topic}", containerFactory = "batchFactory")
public void listen(List<SolrInputDocument> docs, #Header(KafkaHeaders.BATCH_CONVERTED_HEADERS) List<Map<String, Object>> headers, Acknowledgment ack) throws IOException {...}
And then I have 2 beans defined in my Configuration
#Profile("!test")
#Bean
#Autowired
public ConsumerFactory<String, SolrInputDocument> consumerFactory(KafkaProperties properties) {
Map<String, Object> props = properties.buildConsumerProperties();
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
DefaultKafkaConsumerFactory<String, SolrInputDocument> result = new DefaultKafkaConsumerFactory<>(props);
String validatedKeyDeserializerName = KafkaMessageType.valueOf(keyDeserializerName).toString();
ZiDeserializer<SolrInputDocument> deserializer = ZiDeserializerFactory.getInstance(validatedKeyDeserializerName);
result.setValueDeserializer(deserializer);
return result;
}
#Profile("!test")
#Bean
#Autowired
public ConcurrentKafkaListenerContainerFactory<String, SolrInputDocument> batchFactory(ConsumerFactory<String, SolrInputDocument> consumerFactory) {
ConcurrentKafkaListenerContainerFactory<String, SolrInputDocument> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory);
factory.setBatchListener(true);
factory.setConcurrency(2);
ExponentialBackOffWithMaxRetries backoff = new ExponentialBackOffWithMaxRetries(10);
backoff.setMultiplier(3); // Default is 1.5 but this seems more reasonable
factory.setCommonErrorHandler(new DefaultErrorHandler(null, backoff));
// Needed for manual commits
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
return factory;
}
Note that the interface ZiDeserializer<SolrInputDocument> deserializeris my interface and ZiDeserializerFactory.getInstance(validatedKeyDeserializerName); returns my custom implementation of ZiDeserializer. And ZiDeserializer extends org.apache.kafka.common.serialization.Deserializer. This works for me

Related

Does spring inject any beans provided in the #Configuration class

If I have a #Configuration class where I have a bean like below, will the dataMap be resolved in the constructor of the DataService class. What type of dependency injection is this? Is it by type because the name for sure doesn't match?
#Bean
public Map<String, List<Data>> data() {
final Map<String, List<Data>> dataMap = new HashMap<>();
readings.put("1", new Data());
return dataMap;
}
and a class
#Service
public class DataService {
private final Map<String, List<Data>> information;
public DataService(Map<String, List<Data>> information) {
this.information = information;
}
}
#Configuration annotation serves as a placeholder to mention that whichever classes annotated with #Configuration are holding the bean definitions!
When Spring application comes up, spring framework will read these definitions and create beans (or simply objects) in IOC (Inversion of control) container These would be Spring managed objects/beans !
To answer your question, it should create a bean and this is a setter based injection!
However, your #Bean must be some user defined or business entity class in ideal scenarios!
Few links for you to refer to:
https://www.codingame.com/playgrounds/2096/playing-around-with-spring-bean-configuration
https://www.linkedin.com/pulse/different-types-dependency-injection-spring-kashif-masood/

Spring AOP with prototype beans

I am using Spring AOP to fire metrics in our application. I have created an annotation #CaptureMetrics which has an #around advice associated with it. The advice is invoked fine from all the methods tagged with #CaptureMetrics except for a case when a method is invoked on a prototype bean.
The annotation has #Target({ElementType.TYPE, ElementType.METHOD})
PointCut expression:
#Around(value = "execution(* *.*(..)) && #annotation(captureMetrics)",
argNames = "joinPoint,captureMetrics")
Prototype bean creation
#Bean
#Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public DummyService getDummyServicePrototypeBean(int a, String b) {
return new DummyService(a, b);
}
DummyService has a method called dummyMethod(String dummyString)
#CaptureMetrics(type = MetricType.SOME_TYPE, name = "XYZ")
public Response dummyMethod(id) throws Exception {
// Do some work here
}
When dummyService.dummyMethod("123") is invoked from some other service, the #Around advice is not called.
Config class
#Configuration
public class DummyServiceConfig {
#Bean
public DummyServiceRegistry dummyServiceRegistry(
#Value("${timeout}") Integer timeout,
#Value("${dummy.secrets.path}") Resource dummySecretsPath) throws IOException {
ObjectMapper mapper = new ObjectMapper();
Map<String, String> transactionSourceToTokens = mapper.readValue(
dummySecretsPath.getFile(), new TypeReference<Map<String, String>>() {
});
DummyServiceRegistry registry = new DummyServiceRegistry();
transactionSourceToTokens.forEach((transactionSource, token) ->
registry.register(transactionSource,
getDummyServicePrototypeBean(timeout, token)));
return registry;
}
#Bean
#Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public DummyService getDummyServicePrototypeBean(int a, String b) {
return new DummyService(a, b);
}
}
Singleton Registry class
public class DummyServiceRegistry {
private final Map<String, DummyService> transactionSourceToService = new HashMap<>();
public void register(String transactionSource, DummyService dummyService) {
this.transactionSourceToService.put(transactionSource, dummyService);
}
public Optional<DummyService> lookup(String transactionSource) {
return Optional.ofNullable(transactionSourceToService.get(transactionSource));
}
}
Any advice on this please?
Note:
The prototype Dummy service is used to call a third party client. It is a prototype bean as it has a state that varies based on whose behalf it is going to call the third party.
A singleton registry bean during initialization builds a map of {source_of_request, dummyService_prototype}. To get the dummyService prototype it calls getDummyServicePrototypeBean()
The configuration, registry and prototype dummy bean were correct.
I was testing the flow using an existing integration test and there instead of supplying a prototype Bean, new objects of DummyService were instantiated using the new keyword. It wasn't a spring managed bean.
Spring AOP works only with Spring managed beans.

Class not able to access bean managed by Spring

I have Spring configuration file where I am defining beans but somehow this bean is not accessible from one of the class in same package, though same beans are accessible from Controller class which was annotated as #Controller. I was thinking may be this class was not managed by Spring but that's not the case.
1) Configuration class
#Bean
public FooConsumer fooConsumer() {
return new FooConsumer();
}
#Bean
public Map<String, ProxyConsumer> appProxyConsumerMap() {
Map<String, ProxyConsumer> proxyConsumer = new HashMap<String, ProxyConsumer>();
proxyConsumer.put(FOO_APP, FooConsumer());
return proxyConsumer;
}
#Bean
public FooEventConsumer fooEventConsumer() {
return new FooEventConsumer();
}
#Bean
public Map<String, FooConsumer> fooConsumerMap(){
Map<String, FooConsumer> fooEventConsumer = new HashMap<String, FooConsumer>();
fooEventConsumer.put(FOO_EVENT, fooEventConsumer());
}
2) Controller class
#Resource
#Qualifier("appProxyConsumerMap")
Map<String, ProxyConsumer> appProxyConsumerMap;
//proxyApp comes as path variable
ProxyConsumer consumer = appProxyConsumerMap.get(proxyApp);
//invoke consumer
boolean consumed = consumer.consumeEvent(eventRequest);
//here consumer is my FooConsumer class, till now all works fine.
3) now in FooConsumer class it tries to access Map bean named fooConsumerMap to get which event to call but somehow it returns null.
#Resource
#Qualifier("fooConsumerMap")
Map<String, FooConsumer> fooConsumerMap;
FooEventConsumer consumer = fooConsumerMap.get(eventType);
//Here fooConsumerMap comes as null in this class, though it comes as object in controller class , please advise.
In your configuration file, construct your FooConsumer bean with the FooConsumerMap bean declared in the same configuration.
You can autowire other beans into a configuration file, but to pull together beans within the file you pass them as constructor arguments.
Note that if you call a Bean annotated method multiple times, you will surprisingly always get the same instance even if the method logic constructs a new instance.
Check documentation at https://docs.spring.io/spring-javaconfig/docs/1.0.0.m3/reference/html/creating-bean-definitions.html

Configure a Jackson's DeserializationProblemHandler in Spring environment [duplicate]

This question already has answers here:
Can't set ProblemHandler to ObjectMapper in Spring Boot
(2 answers)
Closed 4 years ago.
As I understood, Spring is already providing a bean for Jackson ObjectMapper. Therefore, instead of creating a new bean, I'm trying to customize this bean.
From this blog post, and then this Github project I used Jackson2ObjectMapperBuilder bean to achieve this customization.
#Bean
public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder(ApplicationContext context) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.findModulesViaServiceLoader(true);
return builder;
}
Then, I was trying to customize the deserializer in order to make it lenient: if an exception is raised when deserializing a property, I want the result object's property to be null and let the deserialization continue (default is to fail on first property that cannot be deserialized).
I've been able to achieve that with a class NullableFieldsDeserializationProblemHandler that extends DeserializationProblemHandler (I do not think the code is relevant but if needed, I can share it).
The simplest way to register this handler is to use the .addHandler() method of ObjectMapper. But of course, doing like this, I would need to set that every time I inject and use the ObjectMapper. I'd like to be able to configure handler so that every time the ObjectMapper is auto-wired, the handler is already present.
The best solution I came up with so far is to use a #PostConstruct annotation only to register the problem handler.
#Configuration
public class JacksonConfiguration implements InitializingBean {
#Autowired private ObjectMapper objectMapper;
#Bean
public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder(ApplicationContext context) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.findModulesViaServiceLoader(true);
return builder;
}
#Override
public void afterPropertiesSet() {
objectMapper.addHandler(new NullableFieldsDeserializationProblemHandler());
}
}
But the problem of this solution is that it seems I can still access an autowired ObjectMapper that doesn't have yet registered the problem handler (I can see it happening after when I need it in debug mode).
Any idea how I should register this handler? I've noticed Jackson2ObjectMapperBuilder has a .handlerInstantiator() but I couldn't figure out how to use it.
Note I've also tried with Jackson2ObjectMapperBuilderCustomizer since I'm using Spring Boot but had no better results.
It's not possible to directly add a DeserializationProblemHandler to the ObjectMapper via a Jackson2ObjectMapperBuilder or Jackson2ObjectMapperBuilderCustomizer. The handlerInstanciator() method is for something else.
However, it's possible to do so by registering a Jackson module:
the builder has a modules() method
the module has access via setupModule() to a SetupContext instance, which has a addDeserializationProblemHandler() method
This works:
#Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
return new Jackson2ObjectMapperBuilderCustomizer() {
#Override
public void customize(Jackson2ObjectMapperBuilder builder) {
builder.modules(new MyModule());
}
};
}
private static class MyModule extends SimpleModule {
#Override
public void setupModule(SetupContext context) {
// Required, as documented in the Javadoc of SimpleModule
super.setupModule(context);
context.addDeserializationProblemHandler(new NullableFieldsDeserializationProblemHandler());
}
}
What about writing a bean like this:
#Configuration
public class ObjectMapperConfiguration {
#Bean
ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
// jackson 1.9 and before
objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// or jackson 2.0
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return objectMapper;
}
}
This is for global configuration. If, instead, what you want to do is to configure the feature for specific a class, use this annotation above the class definition:
#JsonIgnoreProperties(ignoreUnknown = true)

How can you extend behavior of the spring boot autoconfiguration?

I am looking to extend JacksonAutoConfiguration specifically when the builder is created, i would like to set that ObjectMapper to a Util class which has static setter for the ObjectMapper. Look at the line before returning builder where I would like to set ObjectMpper to a static class.
#Bean
#ConditionalOnMissingBean(Jackson2ObjectMapperBuilder.class)
public Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.applicationContext(this.applicationContext);
if (this.jacksonProperties.getSerializationInclusion() != null) {
builder.serializationInclusion(this.jacksonProperties
.getSerializationInclusion());
}
configureFeatures(builder, this.jacksonProperties.getDeserialization());
configureFeatures(builder, this.jacksonProperties.getSerialization());
configureFeatures(builder, this.jacksonProperties.getMapper());
configureFeatures(builder, this.jacksonProperties.getParser());
configureFeatures(builder, this.jacksonProperties.getGenerator());
configureDateFormat(builder);
configurePropertyNamingStrategy(builder);
configureModules(builder);
**ObjectMapperUtils.setObjectMapper( builder.build() );**
return builder;
}
The ObjectMapper created from the auto-configured Jackson2ObjectMapperBuilder is exposed as a bean in the JacksonAutoConfiguration. You could simply create another #Configuration class, get a reference to the ObjectMapper (via auto-wiring) and use an #PostConstruct method to set the ObjectMapper in your ObjectMapperUtils class.
Another suggestion would be to refactor ObjectMapperUtils so that it is created as a Spring bean itself, then you can auto-wire ObjectMapper directly into it.

Resources