Dynamically add new queues, bindings and exchanges as beans - spring

I'm currently working on a rabbit-amqp implementation project and use spring-rabbit to programmatically setup all my queues, bindings and exchanges. (spring-rabbit-1.3.4 and spring-framework versions 3.2.0)
The declaration in a javaconfiguration class or xml-based configuration are both quite static in my opinion declared. I know how to set a more dynamic value (ex. a name) for a queue, exchange
or binding like this:
#Configuration
public class serverConfiguration {
private String queueName;
...
#Bean
public Queue buildQueue() {
Queue queue = new Queue(this.queueName, false, false, true, getQueueArguments());
buildRabbitAdmin().declareQueue(queue);
return queue;
}
...
}
But I was wondering if it was possible to create a undefined amount instances of Queue and
register them as beans like a factory registering all its instances.
I'm not really familiar with the Spring #Bean annotation and its limitations, but I tried
#Configuration
public class serverConfiguration {
private String queueName;
...
#Bean
#Scope("prototype")
public Queue buildQueue() {
Queue queue = new Queue(this.queueName, false, false, true, getQueueArguments());
buildRabbitAdmin().declareQueue(queue);
return queue;
}
...
}
And to see if the multiple beans instances of Queue are registered I call:
Map<String, Queue> queueBeans = ((ListableBeanFactory) applicationContext).getBeansOfType(Queue.class);
But this will only return 1 mapping:
name of the method := the last created instance.
Is it possible to dynamically add beans during runtime to the SpringApplicationContext?

You can add beans dynamically to the context:
context.getBeanFactory().registerSingleton("foo", new Queue("foo"));
but they won't be declared by the admin automatically; you will have to call admin.initialize() to force it to re-declare all the AMQP elements in the context.
You would not do either of these in #Beans, just normal runtime java code.

Related

Autowire a Bean in spring-boot-starter library which is not created yet | TransformerSupplier Kafka Streams

I am creating a spring-boot-starter kafka streams library.
In that I am defining a configuration class which will have a list of storeBuilders.
class CustomConfiguration {
private String A;
private String B;
private Set<StoreBuilder<?>> transformationStoreBuilders;
private Set<StoreBuilder<?>> processorStoreBuilders;
// Constructor and Getter
}
The client which will import this spring-boot-starter library will have to create a bean of the above configuration class.
Now I am creating a custom TransformerSupplier and I want to auto-set the stores by autowiring stores from CustomConfiguration.
I am doing something like this:
public abstract class CustomTransformerSupplier<A, B, C, D> implements TransformerSupplier<A, B, KeyValue<C, D>> {
#Autowired
private CustomConfiguration configuration;
public abstract CustomTransformer<A, B, KeyValue<C, D>> get();
public Set<StoreBuilder<?>> stores() {
return configuration.getTransformationStoreBuilders();
}
}
However the CustomTransformerSupplier bean will be created by the client and we cannot Autowire bean in a non Bean class.
How do I auto set the store builders in the transformer supplier?
Why am I following the approach mentioned is because:
The client can have n store builders
There is going to be a ProcessorSupplier and a TransformerSupplier. They can share common state stores.
I want the client to just create the state stores and not worry about injecting the state stores in the TransformerSupplier and the processorSupplier.
Thanks in advance!
See if #Configurable may answer to your question: https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-atconfigurable

#Value annotation unable to read properties file in spring boot camel application

I have a spring-boot application where I read data from queue and send data to transformation class using .bean()
Integration.java
class Integration {
#Value("${someURL}")
private String someURL; //able to read someURL from property file
from("queue")
// some intermediate code
.bean(new TransformationClass(), "transformationMethod")
// other code
}
Now, Inside TransformationClass I have #Value annotation to read values from properties file but it always returns a null.
TransformationClass.java
#Component
class TransformationClass {
#Value("${someURL}")
private String someURL; //someURL return null though there is key-value associated in props file.
public void transformationMethod(Exchange exchange) {
// other stuff related to someURL
}
}
Note - I am able to read values from property file in class Integration.java but unable to read from class TransformationClass.java
I am using spring boot version - 2.7.2 and camel version - 3.18.1 jdk - 17
I tried to read using camel PropertiesComponent but it did not worked.
Problem here is, that new TransformationClass() is not a "spring managed instance", thus all #Autowire/Value/Inject/...s have no effect.
Since TransformationClass is (singleton, spring-managed) #Component and is needed by Integration, wiring these makes sense:
Via field... :
class Integration {
#Autowired
private TransformationClass trnsObject;
// ...
Or constructor injection:
class Integration {
private final TransformationClass trnsObject;
public Integration(/*im- /explicitely #Autowired*/ TransformationClass pTrnsObject) {
trnsObject = pTrnsObject;
}
// ...
// then:
doSomethingWith(trnsObject); // has correct #Values
}

SpringBoot RabbitMQ - how to reduce boilerplate for many topics (events)?

I wonder if there is a way to reduce amount of boilerplate code when initializing many RabbitMQ queues/bindings in SpringBoot?
Following event-driven approach, my app produces like 50 types of events (it will be split into several smaller apps later, but still).
Each event goes to exchange with type "topic".
Some events are getting consumed by other apps, some events additionally consumed by the same app which is sending them.
Lets consider that publishing-and-self-consuming case.
In SpringBoot for each event I need to declare:
routing key name in config (like "event.item.purchased")
queue name to consume that event inside the same app
("queue.event.item.purchased")
matching configuration properties class field or a variable itemPurchasedRoutingKey or constant in code which keeps property name (like ${event.item.purchased})
bean for Queue creation (with a name featuring event name) like
itemPurchasedQueue
bean for Binding creation (with a name featuring
event name) and routing key name. like itemPurchasedBinding which is
constructed with itemPurchasedQueue.bind(...itemPurchasedRoutingKey)
RabbitListener for event, with annotation containing queue name
(can't be defined in runtime)
So - 6 places where "item purchased" is mentioned in one or another form.
The amount of boilerplate code is just killing me :)
If there are 50 events, its very easy to make a mistake - when adding new event, you need to remember to add it to 6 places.
Ideally, for each event I'd like to:
specify routing key in config. Queue name can be built upon it by appending common prefix (specific to the app).
use some annotation or alternative RabbitListener which automatically declares queue (by routing key + prefix), binds to it, and listens to events.
Is there a way to optimize it?
I thought about custom annotations, but RabbitListener doesn't like dynamic queue names, and spring boot can't find beans for queues and bindings if I declare them inside some util method.
Maybe there is a way to declare all that stuff in code, but it's not a Spring way, I believe :)
So I ended up using manual bean declaration and using 1 bind() method for each bean
#Configuration
#EnableConfigurationProperties(RabbitProperties::class)
class RabbitConfiguration(
private val properties: RabbitProperties,
private val connectionFactory: ConnectionFactory
) {
#Bean
fun admin() = RabbitAdmin(connectionFactory)
#Bean
fun exchange() = TopicExchange(properties.template.exchange)
#Bean
fun rabbitMessageConverter() = Jackson2JsonMessageConverter(
jacksonObjectMapper()
.registerModule(JavaTimeModule())
.registerModule(Jdk8Module())
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL)
)
#Value("\${okko.rabbit.queue-prefix}")
lateinit var queuePrefix: String
fun <T> bind(routingKey: String, listener: (T) -> Mono<Void>): SimpleMessageListenerContainer {
val queueName = "$queuePrefix.$routingKey"
val queue = Queue(queueName)
admin().declareQueue(queue)
admin().declareBinding(BindingBuilder.bind(queue).to(exchange()).with(routingKey)!!)
val container = SimpleMessageListenerContainer(connectionFactory)
container.addQueueNames(queueName)
container.setMessageListener(MessageListenerAdapter(MessageHandler(listener), rabbitMessageConverter()))
return container
}
internal class MessageHandler<T>(private val listener: (T) -> Mono<Void>) {
// NOTE: don't change name of this method, rabbit needs it
fun handleMessage(message: T) {
listener.invoke(message).subscribeOn(Schedulers.elastic()).subscribe()
}
}
}
#Service
#Configuration
class EventConsumerRabbit(
private val config: RabbitConfiguration,
private val routingKeys: RabbitEventRoutingKeyConfig
) {
#Bean
fun event1() = handle(routingKeys.event1)
#Bean
fun event2() = handle(routingKeys.event2)
...
private fun<T> handle(routingKey: String): Mono<Void> = config.bind<T>(routingKey) {
log.debug("consume rabbit event: $it")
... // handle event, return Mono<Void>
}
companion object {
private val log by logger()
}
}
#Configuration
#ConfigurationProperties("my.rabbit.routing-key.event")
class RabbitEventRoutingKeyConfig {
lateinit var event1: String
lateinit var event2: String
...
}

Does spring kafka support auto-deserialization in consumer side with no explicit type provided?

I'm using spring-integration-kafka.
I have an abstract interface Event, which has dozens of concrete implementations, say, AEvent, BEvent, CEvent, etc.. And I want one only consumer listener to handle all incoming Events, such as fun handle(message: Message<>) { message.payload... }
After reading the documentation, I find no way to support auto-deserialization with no explicit type provided in consumer side.
Any suggestion will be appreciated.
Using JdkSerializationRedisSerializer from spring-data-redis meet my requirements.
public class GenericObjectSerializer implements Serializer<Object> {
private final JdkSerializationRedisSerializer serializer =
new JdkSerializationRedisSerializer();
}
public class GenericObjectDeserializer implements Deserializer<Object> {
private final JdkSerializationRedisSerializer serializer =
new JdkSerializationRedisSerializer();
}

How to replace a constructor injected object with mocked object in spock

I know that the question is very big but I just want to clear the situation i am into.
I am working on an application that consumes the JMS messages from the message broker.
We are using camel route on the consumer side. All the object required in route builder are injected through constructor injection using spring .
I want to mock the behavior of the actual processing, Once the consumer receives the message from the queue. All the classes gets loaded via the spring configuration.
Below are the three classes:
CustomRouteBuilder.java
public CustomRouteBuilder extends RouteBuilder{
private CustomRouteAdapter customAdapter;
public CustomRouteBuilder (CustomRouteAdapter customAdapter){
this.customAdapter = customAdapter
}
public void configure(RouteDefinition route){
route.bean(customAdapter);
}
}
CustomRouteAdapter.java
public class CustomRouteAdapter {
private Orchestrator orchestrator;
public CustomRouteAdapter (Orchestrator orchestrator){
this.orchestrator = orchestrator;
}
#Handler
public void process(String message){
orchestrator.generate(message) ;
}
}
Orchestrator.java
public class Orchestrator{
private Service service;
public Orchestrator(Service service){
this.service = service;
}
public void generateData(String message){
service.process(message);
}
}
As per our requirement we have to load this configuration file and then write the functional test using spock.
Below is my
CustomRouteBuilderTest.groovy file.
import org.springframework.test.util.ReflectionTestUtils
import spock.lang.Specification
#ContextConfiguration(classes=[CustomRouteBuilderTest.Config.class])
class CustomRouteBuilderTest extends Specification{
private static final String message = "Hello";
Orchestrator orchestrator;
#Autowired
CustomRouteAdapter customRouteAdapter;
def setup(){
orchestrator = Mock(Orchestrator)
ReflectionTestUtils.setField(customRouteAdapter,"orchestrator",orchestrator)
orchestrator.generate(message )
}
private String getMessageAsJson() {
//return json string;
}
private String getMessage() {
// return message;
}
private Map<String, Object> doMakeHeaders() {
//Create message headers
}
private void doSendMessage(){
Thread.sleep(5000)
Map<String,Object> messageHeader = doMakeHeaders()
byte [] message = getMessageAsJson().getBytes()
CamelContext context = new DefaultCamelContext()
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(jmsBrokerUrl)
context.addComponent("activeMQComponent",JmsComponent.jmsComponent(connectionFactory))
ProducerTemplate template = context.createProducerTemplate()
context.start();
template.sendBodyAndHeaders("queueName", message, messageHeader)
}
def "test message consumption"(){
given:
doSendMessage()
}
#Configuration
#Import([FunctionalTestCommonConfig.class,CustomRouteBuilderConfig.class])
#PropertySource(value="classpath:test.properties")
static class Config{
}
}
The problem that here is even though I inject the mocked object to the adapter using ReflectionTestUtils , I am not able to define its behavior correctly.
And when the message is received the orchestrator tries to process it.
My Requirement is that:
Adapter should be called from the camel route automatically which happens but
when the orechestrator.generate is called from the adapter then nothing should happen it should simply return.
But here nothing like that is going on.
Each time I send a message the consumer(RouteBuilder) receives it and calls the handler function which then calls the
orchestrator.generate(message)
function and the orchestrator starts processing and throws an exception from service level.
Any one can please help me on this.
I suppose your beans have been proxified by Spring, and this proxy use cglib (because you see CustomRouteBuilder$$EnhancerBySpringCGLIB$$ad2783ae).
If it's really the case, you didn't #Autowired in your test the real instance of your CustomRouteAdapter but a cglib proxy: Spring creates a new class, extending the realclass, and overriding all the methods of this class. The new method delegate to the real instance.
When you change the orchestrator field, you are in reality changing the orchestrator field of the proxy, which is not used by the real instance.
There are severals ways to achieve what you want to do:
add a setOrchestrator method in CustomRouteAdapter
create the mock in your spring configuration and let spring inject this mock instead of a real instance of Orchestrator
Inject the orchestrator in the real instance (ugly - I didn't recommend you that, it didn't help in the testability of your code!)
customRouteAdapter.targetSource.target.orchestrator = _themock_

Resources