The idea is to have a configuration listener in the application and allows it to request the application to re-read the configuration. I will say one word about it at the end.

In the previous post we saw that DeltaSpike configuration looked like:

@ApplicationScoped
public class MyConfig{
   @Inject
   @ConfigProperty(name = "app.weather.url")
   private String weatherUrl;

   // getter
}

Of course, to ensure the value can be reloaded, you can migrate it to:


@ApplicationScoped
public class MyConfig{
   @Inject
   @ConfigProperty(name = "app.weather.url")
   private Provider<String> weatherUrl;

   public String getWeatherUrl() {
     return weatherUrl.get();
   }
}

This is great but quite inefficient in terms of performances. Thus, you will need to add, on top of that, some caching/eviction. It would work fine but you will need to maintain an event to reload the configuration (the “listener”) and another one for the eviction in all beans having some configuration.

We can surely do better!

@Reloadable: mark instance to be destroyable

The first step to enhance it a bit is to create a marker annotation for our config clients (the instances getting configuration injected):

@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD})
public @interface Reloadable {
}

Our configuration bean will then become as follow:

@Reloadable
@ApplicationScoped
public class MyConfig{
   @Inject
   @ConfigProperty(name = "app.weather.url")
   private String weatherUrl;

   // getter
}

Find the @Reloadable instance

To find the reloadable instances, we will use the same logic as in the previous post in a new extension, but for this new annotation:

public class ReloadExtension implements Extension {
   private final Collection<Bean<?>> beans = new ArrayList<>();


   void findReloadableBeans(@Observes final ProcessBean<?> bean, final BeanManager bm) {
       if (!bean.getAnnotated().isAnnotationPresent(Reloadable.class)) {
           return;
       }
       final Bean<?> b = bean.getBean();
       if (!AlterableContext.class.isInstance(bm.getContext(b.getScope()))) {
           throw new IllegalArgumentException(
                   "Only AlterableContext are supported, " + b.getScope() + " for " +
                           bean.getBean() + " is not using such a scope");
       }
       beans.add(b);
   }

   public Collection<Bean<?>> getBeans() {
       return beans;
   }
}

So, we basically capture all the beans decorated with @Reloadable and validate that the related scope/context is an AlterableContext (you will understand soon).

Lose coupling: destroy on event!

Then the second step is to have a way to trigger the bean destruction. To ensure it integrates with whatever listener we use, a Reload event will be created:

public class Reload {
}

Yes, this class is quite simple and is just there to mark the event.

Reload the instances!

Finally we need an observer for this event resetting marked instances.

Before seeing the observer implementation and to ensure the extension jar doesn’t need to be scanned to work, we will add a small method to our extension:

void registerInternalBeans(@Observes final BeforeBeanDiscovery bbd, final BeanManager bm) {
   bbd.addAnnotatedType(bm.createAnnotatedType(ReloadObserver.class));
}

This method simply enforces CDI to know the ReloadObserver bean even if not scanned. This is important for us since we rely on that observer for @Reloadable to work.

The observer implementation will count on CDI 1.1 new methods which allows to destroy an instance from a Bean. For that, we get, from the BeanManager, the Context (scope) related to the @Reloadable bean we want to destroy and just invoke its destroy method with this bean.

To get the beans, we inject the extension, which is used there as a @Reloadable bean repository:

@ApplicationScoped
public class ReloadObserver {
   @Inject
   private BeanManager bm;

   @Inject
   private ReloadExtension extension;

   public void observe(@Observes final Reload reload) {
       extension.getBeans().forEach(this::destroy);
   }

   private void destroy(final Bean<?> bean) {
       AlterableContext.class.cast(bm.getContext(bean.getScope()))
           .destroy(bean);
   }
}

Limitation

There is no advanced locking there, so if your beans use @PreDestroy, then the reloading thread can destroy the bean while it is used. Of course you can use a ReadWriteLock to solve this issue, or simply move the configuration in a bean without that issue.

Going further

Obviously, the API can be enhanced to support conditions in @Reloadable or even scan the injection points to automatically find these conditions. For instance, you can scan @ConfigProperty in an extension to filter the beans to reload depending on the config keys:

public void observe(@Observes final Reload reload) {
   extension.getBeans().stream()
        .filter(reload::accept)
        .forEach(this::destroy);
}

public class Reload {
   private final Collection<Bean<?>> beans;

   // constructor

   public boolean accept(Bean<?> bean) {
     return beans.contains(bean);
   }
}

And when you trigger the Reload event, you just find the beans that needs to be reloaded thanks to the extension which can own the list of beans by config key.

Listeners

My original description of the initial event spoke about “listeners”, and you probably thought of a “ConfigListener” API or a “FileWatcher” for instance. This would be perfectly valid but it can also be a web hook, i.e. you deploy an HTTP endpoint in your application (secured or not) and an external actor can trigger this event. This is particularly useful when the configuration storage is external and the configuration writers are not your own application, but ops team for instance. In such a case they will update the configuration, call your endpoint and you will request your application to reload the needed beans with your Reload event :)

OpenWebBeans case

OpenWebBeans caches @ApplicationScoped instances once retrieved. This is a clever idea - even if not 100% spec compliant - but this prevents the destroy() to be propagated to all injections. Essentially, if you got an @ApplicationScoped instance injected and you already used it, even if you fire Reload event, you will still use the same instance.

To solve it, you need to configure in openwebbeans.properties what follows:

org.apache.webbeans.proxy.mapping.javax.enterprise.context.ApplicationScoped = org.apache.webbeans.intercept.NormalScopedBeanInterceptorHandler

For tomee, you can alternatively just set in conf/system.properties like this:

openejb.cdi.applicationScope.cached = false

@ApplicationScoped invocations will be a little bit too slower : overhead should be close to two “get()” in two maps so I am not sure you should even care about it. But this use case will perfectly work.

From the same author:

In the same category: