2
votes

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.

2
What are you trying to accomplish - can you enrich the question with your specific use case? I would guess that one solution could be using configuration profiles and, maybe, a producer. But knowing the exact problem might yield more interesting solutions.Nikos Paraskevopoulos
You can use ProfileManager.getActiveProfile() to execute custom logic according to active profile.iabughosh
I've updated my question with a bit more detail.Jim Wharton

2 Answers

2
votes

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();
  }

}
1
votes

My solution is to create the final bean on my own inside a @javax.ws.rs.ext.Provider. Not as elegant as Micronaut @Requires, but well, it works.

Note that instance of SomeProvider is not a "bean", you have to care for the lifecycle on your own (dependency injection, PostConstruct, no PreDestroy, ...).

org.acme.SomeProvider.java

package org.acme;

import javax.enterprise.context.ApplicationScoped;

public interface SomeProvider {

  void providerMethod();

  @ApplicationScoped
  class ProdProviderRequirement {
    void foo() {}
  }

  class ProdProvider implements SomeProvider {

    private final ProdProviderRequirement prodProviderRequirement;

    ProdProvider(final ProdProviderRequirement prodProviderRequirement) {
      this.prodProviderRequirement = prodProviderRequirement;
    }

    @Override
    public void providerMethod() {
      prodProviderRequirement.foo();
    }
  }

  class DevProvider implements SomeProvider {
    @Override
    public void providerMethod() {}
  }
}

org.acme.SomeProviderFactory.java

package org.acme;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
import javax.inject.Inject;
import javax.ws.rs.ext.Provider;
import org.acme.SomeProvider.DevProvider;
import org.acme.SomeProvider.ProdProvider;
import org.acme.SomeProvider.ProdProviderRequirement;

@Provider
class SomeProviderFactory {

  SomeProvider someProvider;

  @Inject
  SomeProviderFactory(final ProdProviderRequirement prodProviderRequirement) {
    final var someCondition = true;
    someProvider = someCondition ? new DevProvider() : new ProdProvider(prodProviderRequirement);
  }

  @Produces
  @ApplicationScoped
  SomeProvider someProvider() {
    return someProvider;
  }
}