Cleaning up after PRODUCER METHOD beans
Producer method beans are a useful way of creating multiple instances of a class with different qualifiers, without having to
provide multiple separate implementations. However, if the produced beans require you to close some resources manually, you might
run into the issue that the @PreDestroy
annotation, which can be used to clean up on bean exit, does not work with producer method
beans.
Creating multiple bean instances of a class via producer method
We’ll use Google’s pub/sub client library for our example. We want our application to be able to publish messages to multiple
topics. For this, we’ll wrap the client library into a PubsubPublisher
class, and create multiple instances via a producer
method to handle different topics:
public class PubsubPublisher {
private final Publisher publisher;
public PubsubPublisher(String topicName) {
String projectId =
ConfigProvider.getConfig().getValue("google-project-id", String.class);
this.publisher =
Publisher.newBuilder(TopicName.of(projectId, topicName))
.build();
}
...
void destroy() throws InterruptedException {
publisher.shutdown();
publisher.awaitTermination(1, TimeUnit.MINUTES);
}
}
Next, we’ll create a factory / provider class that will create the PubsubPublisher
beans via a producer method. We’ll use
a custom @PubsubTopic
annotation, which takes a value through which we can provide the topicName
.
@ApplicationScoped
public class PubsubPublisherFactory {
@Produces
@PubsubTopic
PubsubPublisher create(InjectionPoint injectionPoint) {
String topicName = injectionPoint.getAnnotated()
.getAnnotation(PubsubTopic.class)
.value();
return new PubsubPublisher(topicName);
}
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface PubsubTopic {
@Nonbinding
String value() default "";
}
}
We can now use our implementation to publish to any number of pub/sub topic in a very concise way:
@ApplicationScoped
public class MyService {
private final PubsubPublisher fridgeTopic;
private final PubsubPublisher shoppingListTopic;
public MyService(
@PubsubTopic("fridge-topic") PubsubPublisher fridgeTopic,
@PubsubTopic("shopping-list-topic") PubsubPublisher shoppingListTopic) {
this.fridgeTopic = fridgeTopic;
this.shoppingListTopic = shoppingListTopic;
}
...
}
The producer method PubsubPublisher create(InjectionPoint injectionPoint)
will create bean instances for us based on the
provided value at the injection point ("fridge-topic"
and "shopping-list-topic"
in this case).
Cleaning up after producer method beans
As mentioned before, the pub/sub client library has to be gracefully shut down. You might be tempted to annotated the
PubsubPublisher.destroy()
method with a @PreDestroy
annotation. However, since the PubsubPublisher
class is not a bean,
this won’t work. We’ll have to use a different approach.
Option 1: Cleaning up underyling resources with @PreDestroy
As it turns out, we can still use the @PreDestroy
annotations 🙃 The trick is to not use it in the
PubsubPublisher
class, but rather track the bean instances created via the producer method, and clean up after the beans
once Quarkus removes the parent PubsubPublisherFactory
class.
First, we’ll add a map to PubsubPublisherFactory
to hold the references to the created beans
private final ConcurrentHashMap<String, PubsubPublisher> publisherMap =
new ConcurrentHashMap<>();
Next, we’ll modify the producer method to add the newly created PubsubPublisher
to the map, as well as add a
@Dependent
annotation to the producer, such that we tie the lifecycle of the created bean to the parent
PubsubPublisherFactory
. Otherwise the beans could be killed off before we shut down the pub/sub client.
@Produces
@Dependent <----
@PubsubTopic
PubsubPublisher create(InjectionPoint injectionPoint) {
String topicName = injectionPoint.getAnnotated()
.getAnnotation(PubsubTopic.class)
.value();
return publisherMap.computeIfAbsent(topicName, PubsubPublisher::new); <----
}
Lastly, we add an annotated destroy()
method to the PubsubPublisherFactory
, which will gracefully shut down each
PubsubPublisher
once the factory goes out of scope:
@PreDestroy
void destroy() throws InterruptedException {
for (PubsubPublisher publisher : publisherMap.values()) {
publisher.destroy();
}
}
Option 2: Cleaning up underlying resources with Disposer
methods
Instead of going with the ConcurrentHashMap
approach, we can also take advantage of Disposer
methods 1. We simply have to
add a matching disposer method for the producer method, and Quarkus should automagically clean up the resources once the
beans go out of scope.
@ApplicationScoped
public class PubsubPublisherFactory {
@Produces
@PubsubTopic
PubsubPublisher create(InjectionPoint injectionPoint) {
String topicName = injectionPoint.getAnnotated()
.getAnnotation(PubsubTopic.class)
.value();
return new PubsubPublisher(topicName);
}
void destroy(@Disposes @PubsubTopic PubsubPublisher publisher)
throws InterruptedException {
publisher.destroy();
}
...
}
This approach requires less code and certainly looks cleaner, however you have to know that the Disposer
methods exist
(which I found out only after implementing the first pattern 😂) and how to use them.
Conclusion
With everything put together, we can now conveniently create any number of PubsubPublisher
beans, and we have control
over the cleaning process of the underlying pub/sub client for each created bean instance 💪