How to autowire an object passing application properties with Spring Boot? - spring

I have a class like this:
#Component
public class KafkaConsumer {
...
#KafkaListener(id = "${my-id}", topics = "${my-topics}")
public void receive(String myMessage) {
...
}
...
}
and I want to reuse it as it is but changing id and topics params. These params should come from the application.yml, in this way:
consumer1:
id: id1
topics: topic1
consumer2:
id: id2
topics: topic2
and from another service doing something like:
#Service
class MyService {
#Autowired(my-id="{consumer1.id}", my-topics="{consumer1.topics}")
KafkaConsumer consumer1;
#Autowired(my-id="{consumer2.id}", my-topics="{consumer2.topics}")
KafkaConsumer consumer2;
...
}
what is the closest way to do that?

Related

How to validate Spring Boot's External Configuration used in Placeholders

I am aware of possibility to validate the external configuration of a Spring Boot configuration done by the help of configuration properties (#ConfigurationProperties) and bean validation.
But how can I validate properties used in placeholders like in this code snippet:
#Component
public class Receiver {
#JmsListener(destination = "${jms.queuename}", containerFactory = "myFactory")
public void receiveMessage(Email email) {
System.out.println("Received <" + email + ">");
}
}
The only workaround I can imagine is to use the same property also in a configuration properties class like this.
#ConfigurationProperties(prefix = "jms")
#Validated
public class JMSProperties {
#NotNull
String queuename;
}
Any hint?

SpringBoot kafka request/reply with dynmaic topic name for #KafkaListener

i am writing a microservices application based on kafka request/reply semantic, so i got configured ReplyingKafkaTemplate as message producer and #KafkaListener with #SendTo annotations method as the service request listener. I need to create a class with method marked with #KafkaListeners and #SendTo annotations dynamically based on topic name. It should be something like this:
#Component
class KafkaReceiver {
// LISTENER FOR OTHER MICROSERVICE REQUESTS
#KafkaListener("#{topicProvider.getTopic()})
#SendTo
public Response listen(Request request) {
... some logic
return response;
}
}
#Component
class KafkaRecevierFactory {
public KafkaReceiver createListener(String topic) {
...
return kafkaReceiver;
}
//OR SOMETHING LIKE THIS:
public void runListenerContainer(String topic, ReqeustProcessor processor) {
Container container = ContainerFactory.create(topic)
container.setListener( request -> {
Response resp = processor.process(request);
return resp;
});
container.start();
}
}
Is there anyway i can do this?

Spring-Kafka: How to pass the kafka topic from the application.yml

I have a small project in Spring Kafka
I wish I could pass my kafka topic from application.yml and avoid a hard-coding problem. For the moment I have this situation:
public class KafkaConsumer {
#Autowired
private UserRepository userRepository;
#KafkaListener(topics = "myTopic")
public void listen(#Validate UserDto userDto) {
User user= new User(userDto);
userRepository.save(userDto.getAge(), user);
}
}
at this moment I have the static kafka topic (being a string) is it possible to put it in the application.yml and have it read from there? Thanks everyone for any help
You can post your topic in the application.yml :
kafka:
template:
default-topic: "MyTopic"
In your KafkaListerner :
#KafkaListener(topics = "#{'${spring.kafka.template.default-topic}'}")
So you should solve the problem of the "Attribute Value" failing to take a dynamic value
This worked for me.
You can use below entry in application.yml file
Usually we use #Value as below to pick data from properties/yaml files for a specified key in you Java class as below.
#Value("${kafka.topic.name}")
private String TOPIC_NAME;
Since Kafka Listener expects constant here, you can use directly as below
public class KafkaConsumer {
#Autowired
private UserRepository userRepository;
#KafkaListener(topics = "${kafka.topic.name}")
public void listen(#Validate UserDto userDto) {
User user= new User(userDto);
userRepository.save(userDto.getAge(), user);
}
}

Mapping an array of properties in spring boot and injecting into a prototype scope

I am very new to spring boot and here is how my yaml looks like
configs:
-
collection: col1
groupId: groupId1
topic: topic1
-
collection: col2
groupId: groupId2
topic: topic2
I would like to have 3 classes with their scope defined as a prototype and use the property from the yaml something like
#Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class mongoListener {
public mongoListener(#Value("${config.collection}") String collectionName) {
//do something
}
}
Similarly, have 2 more classes that would use groupID and topic as well. I am stuck here on how to design.
It will be of great help if anyone can explain how to achieve this.
Please try:
Put your application.yaml that consists of your yaml configuration into /src/main/resources/.
Use the property by #Value annotation like:
public mongoListener(#Value("${configs[0].collection}") String collectionName) {
// collectionName is 'col1'
//do something
}
[EDIT 1]
You can create singleton beans different from each other in name like:
#Configuration
public class MyConfiguration {
#Bean("mongoListener_A")
public mongoListener mongoListener_A(#Value("${configs[0].collection}") String collectionName) {
return new mongoListener(collectionName);
}
#Bean("mongoListener_B")
public mongoListener mongoListener_B(#Value("${configs[1].collection}") String collectionName) {
return new mongoListener(collectionName);
}
// ... and so on
}
[EDIT 2]
You can read your yaml configurations into java objects by using #ConfigurationProperties annotation.
#Data //lombok annotation.
#Component("myConfigs")
#ConfigurationProperties
public class Configs {
private final List<Config> configs;
#Data //lombok annotation.
public static class Config {
private String collection;
private String groupId;
private String topic;
}
}
You can create your mongoListeners from that objects like:
#Configuration
public class MyConfiguration {
#Bean
public List<mongoListener> mongoListeners(#Qualifier("myConfigs") Configs configs) {
return configs.getConfigs().stream()
.map(Config::getCollection)
.map(mongoListener::new)
.collect(Collectors.toList());
}
// ... and so on
}
The beans are still singleton but you can create an arbitrary number of instances depending on your yaml file.
See Also
Use YAML for External Properties - Spring Boot Reference Documentation “How-to” Guides
Spring #Value with arraylist split and obtain the first value

How to create multiple beans (same type) in one Spring Boot java config class (#Configuration)?

In my yaml file, I have config values as below:
myapp:
rest-clients:
rest-templates:
- id: myService1
username: chris
password: li
base-url: http://localhost:3000/service1
read-timeout: 2s
connect-timeout: 1s
- id: myService2
username: chris
password: li
base-url: http://localhost:3000/service1
read-timeout: 2s
connect-timeout: 1s
I want to Spring Boot 2 app register a RestTemplate for each config items.
My configuration is bean is below:
#Configuration
#AllArgsConstructor
public class MyAppRestClientsConfiguration {
private MyAppRestClientsProperties properties;
private GenericApplicationContext applicationContext;
private RestTemplateBuilder restTemplateBuilder;
#PostConstruct
public void init() {
properties.getRestTemplates().forEach(this::registerRestTemplate);
}
private void registerRestTemplate(MyAppRestTemplateConfig config) {
// do some work
applicationContext.registerBean(config.getId(), RestTemplate.class, () -> restTemplate)
}
}
The problem is that when I inject my registered RestTemplate via #Autowire, this config bean has not finished init yet. So there is no RestTemplate bean could be injected.
#Autowired
#Qualifier("myService1")
private RestTemplate client1;
The injection point has the following annotations:
- #org.springframework.beans.factory.annotation.Autowired(required=true)
- #org.springframework.beans.factory.annotation.Qualifier(value=myService1)
Is there any correct way to implement this requirement?
The problem with registering new beans in a #PostConstruct annotated method is that Spring is already past that particular point in the Spring life cycle (more info on the Spring life cycle). Sometimes an annotation such as #DependsOn (already mentioned), #Order, or #Lazy might help. However, as you mentioned you'd rather not force (spring) implementation details upon projects that make use of your library, I've written a BeanFactoryPostProcessor that registers a RestTemplate bean:
#Component
public class DemoBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
#Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) {
GenericBeanDefinition genericBeanDefinition = new GenericBeanDefinition();
genericBeanDefinition.setBeanClass(RestTemplate.class);
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setReadTimeout(Integer.valueOf(configurableListableBeanFactory.resolveEmbeddedValue("${rest-templates[0].read-timeout}")));
factory.setConnectTimeout(Integer.valueOf(configurableListableBeanFactory.resolveEmbeddedValue("${rest-templates[0].connect-timeout}")));
// etc
ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
constructorArgumentValues.addGenericArgumentValue(factory);
genericBeanDefinition.setConstructorArgumentValues(constructorArgumentValues);
String beanId = configurableListableBeanFactory.resolveEmbeddedValue("${rest-templates[0].id}");
((DefaultListableBeanFactory) configurableListableBeanFactory).registerBeanDefinition(beanId, genericBeanDefinition);
}
}
application.yml:
rest-templates:
- id: myService1
username: chris
password: li
base-url: http://localhost:3000/service1
read-timeout: 2000
connect-timeout: 1000
- id: myService2
username: chris
password: li
base-url: http://localhost:3000/service1
read-timeout: 2000
connect-timeout: 1000
Accompanying test:
#RunWith(SpringRunner.class)
#SpringBootTest
public class DemoApplicationTests {
#Autowired
#Qualifier("myService1")
private RestTemplate restTemplate;
#Test
public void demoBeanFactoryPostProcessor_shouldRegisterBean() {
String stackOverflow =
restTemplate.getForObject("https://stackoverflow.com/questions/57122343/how-to-create-multiple-beans-same-type-in-one-spring-boot-java-config-class", String.class);
Assertions.assertThat(stackOverflow).contains("How to create multiple beans (same type) in one Spring Boot java config class (#Configuration)?");
}
}
As the BeanFactoryPostProcessor is invoked before the application context is fully set up, I had to find a different way to retrieve the application properties. I used the method ConfigurableListableBeanFactory#resolveEmbeddedValue to retrieve placeholder values instead of having them injected by an #Value annotation or environment#getProperty. Furthermore, I rewrote the property value 2s to 2000 as the HttpComponentsClientHttpRequestFactory required an int value.
You can levarage the binding mechanism that Spring Boot uses for the #ConfigurationPropeties. The corresponding beans can then be generated dynamically from a map of configurations
#Configuration
public class MyConfiguration implements BeanFactoryPostProcessor, EnvironmentAware {
private Environment environment;
#Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
#Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) {
Binder.get(environment)
.bind("com.example", FooMapProps.class)
.get()
.getMap()
.forEach((name, props) -> configurableListableBeanFactory.registerSingleton(name + "FooService", new FooService(props.getGreeting())));
}
}
In this way you can create multiple FooServices with different configurations based on your YAML config
com:
example:
map:
hello:
greeting: 'hello there!'
hey:
greeting: 'hey ho :)'
The corresponding properties class looks like this
#ConfigurationProperties(prefix = "com.example")
public class FooMapProps {
private Map<String, FooProps> map;
public Map<String, FooProps> getMap() {
return map;
}
public void setMap(Map<String, FooProps> map) {
this.map = map;
}
public static class FooProps {
private String greeting;
public String getGreeting() {
return greeting;
}
public void setGreeting(String greeting) {
this.greeting= greeting;
}
}
}
In this example two FooService beans has been created with different greetings as constructor paramters. The beans can be accessed by the qualifiers 'helloFooService' and 'heyFooService'.

Resources