Skip to content

Feature request: Allow factory beans to have a @PreDestroy method to match/mirror a @Bean method #749

Closed
@ascopes

Description

@ascopes

Edit: This is an edit to the summary as a TLDR on what this enhancement request has become.

The problem is specific to @Factory components that have methods that create @Bean components, and wanting to have a @PreDestroy method invoked on those beans.

The use case is that using Vertx the desired PreDestroy is actually a chained method invocation of vertx.close().blockingAwait();.

Change 1 - Change @Bean(destroyMethod) to support method chaining

So for example @Bean(destroyMethod="close().blockingAwait()") now works. This change was added via #754

Change 2 - Allow Factories to have a @PreDestroy method that matches a @Bean method

Factories naturally have @Bean methods to create components and it seems natural to allow those factories to have a @PreDestroy method that would match, for example:

@Factory
final class MyFactory {

  @Bean 
  Vertx vertx() {
    var opts = new VertexOptions().setUseDaemonThread(true);
    return Vertx.vertx(opts);
  }
  
  @PreDestroy
  void destroy(Vertx vertx) {
    vertx.close().blockingAwait();
  }
}

This is supported via #756

Also changing the title to reflect that this issue relates specifically to factories only. Normal @Singleton components can and should have their own @PreDestroy methods and shutdown logic (and should not have an extra dependencies required to execute the shutdown logic - aka normal @PreDestroy methods are not allowed to have arguments).


First off, thanks for the awesome annotation processor library! This has slotted really nicely into some work I am doing to make some components much easier to manage without the overhead and hassle of a full runtime CDI framework like Spring!

I have a small issue and I am wondering if a feature could be introduced to help address it.

Background

In my application, I am making use of Vert.x RXJava3 components. I have some configuration resembling the following:

import io.avaje.inject.Bean;
import io.avaje.inject.Factory;
import io.vertx.core.VertxOptions;
import io.vertx.rxjava3.core.Vertx;

@Factory
public class MyContext {
  @Bean 
  public Vertx vertx() {
    var opts = new VertexOptions().setUseDaemonThread(true);
    return Vertx.vertx(opts);
  }
}

My issue revolves around the ability to safely destroy the vertx bean so that the threadpool is closed prior to the BeanScope being fully closed. This is useful as it means I do not have to worry about Vertx retaining ports in the background between integration test cases.

Since this is a reactive shim around "regular" Vert.x, methods for object closure are non-blocking, as one may expect.

This means that the following or something similar has to be invoked to shut down my event loop:

vertx.close().blockingAwait();

Unfortunately, I cannot find a "nice" way of handling this with Avaje. In Spring, I'd usually "abuse" the cglib method proxying to allow referencing the bean within the component, like so:

@Configuration
public class MyConfiguration {
  @Bean 
  public Vertx vertx() { ... }
  
  @PreDestroy
  public void close() {
    // calls to the bean are intercepted by spring and injected instead.
    vertx().close().blockingAwait();
  }
}

In Avaje, as expected, this will not work as cglib proxies that modify method call behaviours are not used.

Feature request

What I'd like to be able to say is something like this:

import io.avaje.inject.Bean;
import io.avaje.inject.Factory;
import io.vertx.core.VertxOptions;
import io.vertx.rxjava3.core.Vertx;

@Factory
public class MyContext {
  @Bean 
  public Vertx vertx() {
    var opts = new VertexOptions().setUseDaemonThread(true);
    return Vertx.vertx(opts);
  }
  
  @PreDestroy
  public void destroy(Vertx vertx) {
    vertx.close().blockingAwait();
  }
}

i.e. where dependencies can be injected into the signature of @PreDestroy hooks.

What I have tried

I have tried some workarounds:

Storing Vertx in a field

@Factory
public class MyContext {
  private final Vertx vertx;
  
  public MyContext() {
    var opts = new VertexOptions().setUseDaemonThread(true);
    vertx = Vertx.vertx(opts);
  }

  @Bean 
  public Vertx vertx() {
    return vertx;
  }
  
  @PreDestroy
  public void destroy(Vertx vertx) {
    vertx.close().blockingAwait();
  }
}

This works but feels like it violates the purpose of factory methods.

Registering destroy hooks with the BeanScope

The BeanScope itself does not let me add hooks by the looks of things, so I instead tried this:

@Factory
public class MyContext {
 
  @Bean 
  public Vertx vertx(BeanScopeBuilder beanScopeBuilder) {
    var opts = new VertexOptions().setUseDaemonThread(true);
    var vertx = Vertx.vertx(opts);
    beanScopeBuilder.addPreDestroy(() -> vertx.close().blockingAwait());
    return vertx;
  }
}

...however, the BeanScopeBuilder does not appear to be able to inject itself, so I get a NullPointerException.

Registering a sibling bean

@Factory
public class MyContext {
 
  @Bean 
  public Vertx vertx() {
    var opts = new VertexOptions().setUseDaemonThread(true);
    return Vertx.vertx(opts);
  }
  
  @Bean
  public Object vertxReaper(Vertx vertx) {
    return new Object() {
      @PreDestroy
      public void destroy() {
        vertx.close().blockingAwait();
      }
    };
  }
}

...this never appeared to be invoked.

Registering a singleton

@Factory
public class MyContext {
 
  @Bean 
  public Vertx vertx() {
    var opts = new VertexOptions().setUseDaemonThread(true);
    return Vertx.vertx(opts);
  }
}
@Singleton
public class VertxReaper {
  private final Vertx vertx;
  
  @Inject
  public VertxReaper(Vertx vertx) {
    this.vertx = vertx;
  }
  
  @PreDestroy
  public void destroy() {
    vertx.close().blockingAwait();
  }
}

This works, but feels very clunky and verbose.

Hacking around with destroyMethod hooks on the @Bean annotation

@Factory
public class MyContext {
 
  @Bean(destroyMethod="close().blockingAwait")
  public Vertx vertx() {
    var opts = new VertexOptions().setUseDaemonThread(true);
    return Vertx.vertx(opts);
  }
}

This does not work (and would be a fragile hack regardless).

Closing Vertx outside the bean scope

public class Main {
  public static void main(String[] args) {
    try (var ctx = BeanScope.builder().modules(new Mymodule()).build()) {
      try {
        ctx.get(ThingThatRuns.class).await();
      } finally {
        ctx.get(Vertx.class).close().blockingAwait();
      }
    }
  }
}

This works but means my other classes now have to be called from outside the CDI context, which feels like it is creating fragile cross-cutting concerns.


If you any suggestions on this, it would be greatly appreciated!

Thanks

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions