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 💪