Esses dias me deparei com o seguinte desafio: listar no properties
de um projeto Spring Boot as filas que são utilizadas no projeto e criá-las no RabbitMQ de acordo com essa lista. Até então eu criava as filas uma a uma declarando uma @Bean
(Exemplo 1) e assim toda vez que a aplicação é iniciada as devidas filas são criadas, caso já não existam. O problema aqui é a necessidade de alterar código toda vez que for necessário criar uma nova fila.
Exemplo 1
@Bean
public Queue example1Queue() {
log.info("example1Queue created");
return QueueBuilder.durable("example1Queue").build();
}
@Bean
public Queue example2Queue() {
log.info("example2Queue created");
return QueueBuilder.durable("example2Queue").build();
}
Criar uma @Bean
que retorna uma Queue
é o jeito básico de criar filas com o Spring, mas se precisamos de algo mais flexível e dinâmico precisamos outro meio... precisamos da classe Declarables
.
A ideia geral do desafio era obter flexibilidade e centralizar as informações das filas em uma lista no arquivo properties do projeto, sem ser necessário alterar código Java para configurar uma nova fila. Nesse contexto seria interessante poder criar várias filas de uma vez só e descobri que isso é possível, utilizando a classe Declarables
. Declarables
é uma coleção que suporta implementações de Declarable
, como Bind
, Queue
e Exchange
(e creio que outras). Para utilizá-la seguimos a mesma lógica do exemplo 1, declarando uma @Bean
, só que em vez de retornar um componente especifico, retornamos uma instância de Declarables
com tudo que precisamos criar. No exemplo 2 eu recrio o exemplo 1 usando Declarables
.
Exemplo 2
@Bean
public Declarables queues() {
return buildQueues();
}
private Declarables buildQueues() {
return new Declarables(
QueueBuilder.durable("example1Queue")
.build(),
QueueBuilder.durable("example2Queue")
.build()
);
}
Vejam como é interessante, simples e nos abre possibilidades, como pegar uma lista de filas de um lugar qualquer, iterá-las e montar as filas. É isso que vamos fazer! Mas cuidado! Alerto que devemos utilizar responsabilidade. Por exemplo no projeto aqui da firma eu criei muitas coisas com ela, Queues, Binds e Exchanges, mas separadamente, para não virar bagunça. Flexibilidade deve ser adicionada com cuidado para não virar zorra e deixar o projeto impossível de manter.
Agora que sabemos utilizar a classe Declarables
vamos guardar nossa lista de filas no properties do projeto.
Agora vamos guardar as filas no arquivo properties (usando yaml) do projeto para podermos recuperar elas na nossa Bean e criá-las. Para isso eu pensei na estrutura do exemplo 3.
Exemplo 3
queues:
example1Queue:
name: example1Queue
tll: 1000
example2Queue:
name: example2Queue
tll: 1000
Usei a estrutura de keys e valores mas também poderíamos usar uma lista de strings (exemplo 4) com os nomes das filas ou uma lista de objetos sem chave (exemplo 5). Adotei a estrutura do exemplo 3 para nos permitir referenciar as filas em outros locais, como nas anotações @RabbitListener
(exemplo: @RabbitListener(queues = "${queues.example1Queue.name}")
). A estrutura que sugeri (e a do exemplo 5) também permite adicionar outras propriedades pertinente as filas, como o ttl .
Exemplo 4
queues: example1Queue, example2Queue
Exemplo 5
queues:
-
name: example1Queue
tll: 1000
-
name: example2Queue
tll: 1000
Agora que temos nossas filas registradas no properties devemos mapear elas para uma classe para podermos recuperar as informações onde precisamos.
Para recuperar os valores da lista de filas vamos criar a classe QueuesProperties
(exemplo 6). Nela temos uma classe que representa a Queue
e temos um Map
de Queue
. A classe Queue
tem os atributos name
e tll
. Na propriedade queues eu tentei utilizar lista mas não deu certo, então criei um getQueues()
que retorna uma lista.
Exemplo 6
@Data
public class QueuesProperties {
private Map<String, Queue> queues;
public Collection<Queue> getQueues() {
if (CollectionUtils.isEmpty(queues)) return new ArrayList<>();
return queues.values();
}
@Data
public static class Queue {
private String name;
private int tll;
}
}
Depois de montarmos nosso Pojo para armazenas as filas, devemos dizer ao Spring que essa classe representa propriedades, para podemos utilizar a injeção de dependências. Para isso utilizamos as anotações @Component
e @ConfigurationProperties("propertie")
(exemplo 7). Na @ConfigurationProperties
teríamos que informar o prefixo da nossa propriedade, mas como ela está no topo da hierarquia, não precisamos.
Exemplo 7
@Data
@Component
@ConfigurationProperties
public class QueuesProperties {
...
}
Também podemos validar nossas anotações utilizando o Spring Validation e impedir que a aplicação seja executada caso esteja faltando alguma informação. Sabemos por exemplo que o atributo name
da Queue
deve ser obrigatório, então podemos anotar ele com @NotBlank
e anotamos as classes com @Validated
, para que o Spring entenda que deve validar elas (exemplo 8).
Exemplo 8
...
@Data
@Validated
public static class Queue {
@NotBlank
private String name;
private int tll;
}
...
Feito tudo isso, nossa classe completa ficou assim:
Exemplo 9 (QueuesProperties.java)
@Data
@Validated
@Component
@ConfigurationProperties
public class QueuesProperties {
private Map<String, Queue> queues;
public Collection<Queue> getQueues() {
if (CollectionUtils.isEmpty(queues)) return new ArrayList<>();
return queues.values();
}
@Data
@Validated
public static class Queue {
@NotBlank
private String name;
private int tll;
}
}
Agora podemos injetar nossa classe de propriedades usando injeção de dependência do Spring em qualquer classe que tenha suporte.
Para criar nossas filas vamos criar uma classe anotada com @Component
chamada QueuesInitializer
, onde declararemos nossa @Bean
de criação de filas. Nela injetaremos nossas propriedades utilizando uma variável final (é necessário criar um construtor para que o Spring possa instanciá-la, para isso eu uso a anotação @RequiredArgsConstructor
do Lombok
).
Depois declaramos a nossa @Bean
que retorna uma Declarables
com nossas filas construídas. Como mostra o exemplo 10:
Exemplo 10 (QueuesInitializer.java)
@Component
@RequiredArgsConstructor
public class QueuesInitializer {
private final QueuesProperties queuesProperties;
@Bean
public Declarables queues() {
return buildQueues();
}
private Declarables buildQueues() {
List<Queue> queues = queuesProperties.getQueues().stream().map(queue -> {
return QueueBuilder.durable(queue.getName())
.ttl(queue.getTll())
.build();
}).collect(Collectors.toList());
return new Declarables(queues);
}
}
Pronto, é isso, simples e fácil.
Você pode encontrar o código apresentado neste repositório.