Search code examples
cdiquarkus

Injecting a different bean during local development with Quarkus


With Spring and Micronaut, there are very concise ways to inject a different bean depending on what environment/profile an application is running in. I'm trying to do the same with Quarkus.

I've read this post: https://quarkus.io/blog/quarkus-dependency-injection/. And the process is alluded to in this StackOverflow post: How can I override a CDI bean in Quarkus for testing?. That last post says, "create bean in test directory".

My problem is slightly different. I'd like to inject a bean when in "development". In production, I'd like the default bean injected. From the docs, I can't see a way to have the app make this distinction.

If I have a default class like this:

@DefaultBean
@ApplicationScoped
class ProdProvider : SomeProvider {}

And I want to override it like this:

@Alternative
@Priority(1)
class DevProvider : SomeProvider {}

How can I make this happen only in dev mode?

In one case, I have a credential provider class that sets up Google's PubSub emulator while in local development. In production, I use a class that implements the same interface, but a real credential provider. The particular case that led me to asking this question, though is a a class that implements one method:

@ApplicationScoped
class VaultLoginJwtProvider : LoginJwtProvider {
  @ConfigProperty(name = "vault.tokenPath")
  private val jwtPath: String? = null

  companion object {
    val logger: Logger = LoggerFactory.getLogger("VaultTokenProvider")
  }

  override fun getLoginJwt(): Optional<String> {
    logger.info("Using Vault Login JWT")

    return try {
      Optional.of(String(Files.readAllBytes(Paths.get(jwtPath))).trim { it <= ' ' })
    } catch (e: Exception) {
      logger.error("Could not read vault token at $jwtPath")
      logger.error(e.printStackTrace().toString())
      Optional.empty()
    }
  }
}

That class is injected into another class via constructor injection:

@Singleton
class JwtServiceImpl(
  @RestClient val vaultClient: VaultClient,
  @Inject val loginJwtProvider: LoginJwtProvider
) {
  private var serviceJwt: String? = null

  companion object {
    val logger: Logger = LoggerFactory.getLogger("JwtServiceImpl")
  }

  private fun getLoginToken(): String? {
    val vaultLogin = VaultLogin(
      role = "user-service",
      jwt = loginJwtProvider.getLoginJwt().get()
    )

    val loginResponse = vaultClient.login(vaultLogin)

    return loginResponse.auth.clientToken
  }
}

I'd like to inject more of a "mock" class while in development that just returns a static string. I could use ProfileManager.getActiveProfile(), but that has me mixing development concerns into my logic. And I don't feel that that has any place in my compiled production code.

This is possible in Micronaut by using the annotation @Requires(env = ["dev", "test"]). I did briefly look at using @Produces but the Oracle EE docs seemed a little bit difficult for me to grasp. If that's the solution, I'll dig in.


Solution

  • In case anybody else comes across this, this is how to do it: https://quarkus.io/guides/cdi-reference#enabling-beans-for-quarkus-build-profile

    For example:

    import javax.enterprise.inject.Produces;
    
    import com.oi1p.common.EmailSender;
    import com.oi1p.common.ErrorEmailSender;
    import com.oi1p.common.LogOnlyEmailSender;
    
    import io.quarkus.arc.DefaultBean;
    import io.quarkus.arc.profile.IfBuildProfile;
    
    @ApplicationScoped
    public class Producers {
    
      @Produces
      @IfBuildProfile("dev")
      public EmailSender logOnlyEmailSender() {
        return new LogOnlyEmailSender();
      }
    
      @Produces
      @DefaultBean
      public EmailSender errorEmailSender() {
        // TODO: implement a real email sender.  This one explodes when poked.
        return new ErrorEmailSender();
      }
    
    }