2

In my Java EE 7 program, I want to use @Alternative to inject different implementation depending on the context, production or test for example. What I did is to declare my class annotated with @Alternative in my beans.xml file. It works great and my alternative class is injected wherever I want instead of the default one. But I don't know if there is a way to skip this behavior and inject the default class other than removing the declaration in the beans.xml file. Which is not possible easily when the application is packaged. It would be great if I can choose if I want to use the default classes or the alternative ones in a configuration file, for example in my standalone.xml file of my WildFly server. Is this possible?

cheb1k4
  • 2,316
  • 7
  • 26
  • 39
  • Not sure if it's possible. Have you ever considered having a different `beans.xml` for each environment and packing the right one according to, for example, a Maven profile? – cassiomolin Feb 15 '17 at 13:51
  • Or to extend @CássioMazzochiMolin answer, use different qualifiers ```@Test``` and ```@Production``` – maress Feb 15 '17 at 14:30
  • @Cássio Mazzochi Molin I have not. Maybe I should dig into this direction. – cheb1k4 Feb 15 '17 at 14:49
  • @maress I can not because my case is more complicated than just test/production environment. I have 2 different production environment for the same project. – cheb1k4 Feb 15 '17 at 14:49
  • @cheb1k4 considering that, you can dynamically select a bean based on environment variables. You can also look into producer methods which will inject a specified type of bean based on which environment variable has been defined. look into ```Instance.select(Class)``` and ```Instance.select(Annotation qualifier)``` – maress Feb 15 '17 at 16:10
  • @cheb1k4 See my updated answer. I've tried a different approach and it worked fine. Hopefully it will fit your needs. – cassiomolin Feb 15 '17 at 18:47
  • Just for the record, another approach could be an extension which would, based on configuration of your choosing, disable alternatives. – Siliarus Feb 16 '17 at 07:40
  • @cheb1k4 I've just updated my [answer](http://stackoverflow.com/a/42252956/1426227), adding details on how to set system properties in the `standalone.xml` file. – cassiomolin Feb 16 '17 at 09:34

3 Answers3

2

I'm afraid it's not possible to be achieved with the plain @Alternative annotation. See below a couple of approaches you could try:

Using different beans.xml files

You could consider having a different beans.xml files for each environment and then pack the right one according to, for example, a Maven profile.

Writing your own alternative stereotype

You can define your own alternative stereotype and manage the injection with a CDI extension.

This approach is mentioned in this post from NightSpawN. I tested it on WildFly 10 and it worked as expected. Find the steps below:

Define an enumeration with your environment types:

public enum EnvironmentType {
    DEVELOPMENT, TESTING, STAGING, PRODUCTION;
}

Create your own @Alternative stereotype to hold meta-information about the environment:

@Stereotype
@Alternative
@Target(TYPE)
@Retention(RUNTIME)
public @interface EnvironmentAlternative  {
    EnvironmentType[] value();
}

And declare the alternative stereotype in the beans.xml:

<beans xmlns="http://java.sun.com/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">

    <alternatives>
        <stereotype>com.example.EnvironmentAlternative</stereotype>
    </alternatives>

</beans>

For example purposes, let's define a sample service:

public interface GreetingService {
    String sayGreeting();
}

Define a default implementation:

@Default
public class DefaultGreetingService implements GreetingService {

    @Override
    public String sayGreeting() {
        return "Hey!";
    }
}

Also define some alternative implementations using the @EnvironmentAlternative stereotype:

@EnvironmentAlternative(DEVELOPMENT)
public class DevelopmentGreetingService implements GreetingService {

    @Override
    public String sayGreeting() {
        return "Hey from a development environment!";
    }
}
@EnvironmentAlternative(PRODUCTION)
public class ProductionGreetingService implements GreetingService {

    @Override
    public String sayGreeting() {
        return "Hey from a production environment!";
    }
}

The @EnvironmentAlternative annotation also supports an array with multiple environment types:

@EnvironmentAlternative({ TESTING, STAGING })

Here's where the magic happens!

Create a CDI Extension to observe CDI lifecycle events. The processAnotated() method gets called for each annotated type the container processes, and if it is annotated with @EnvironmentAlternative and the current environemnt is not in the specified environments, event's veto() method is called, preventing the type from being processed any further:

public class EnvironmentAlternativesExtension implements Extension {

    private EnvironmentType currentEnvironment = PRODUCTION;

    public <T> void processAnotated(@Observes ProcessAnnotatedType<T> event) {
        EnvironmentAlternative alternative = 
            event.getAnnotatedType().getJavaClass()
                 .getAnnotation(EnvironmentAlternative.class);
        if (alternative != null && !containsCurrentEnvironment(alternative.value())) {
            event.veto();
        }
    }

    private boolean containsCurrentEnvironment(EnvironmentType[] environments) {
        for (EnvironmentType environment : environments) {
            if (environment == currentEnvironment) {
                return true;
            }
        }
        return false;
    }
}

The default implementation will be used when no suitable alternatives are found.

Next, register the CDI extension as a service provider by creating a file named javax.enterprise.inject.spi.Extension under the META-INF/services folder. The content of the file will be just the canonical name of the extension class:

com.example.EnvironmentAlternativesExtension

Finally inject and use the above defined service:

@Inject
private GreetingService service;
String greeting = service.sayGreeting();

In a real application you won't hardcode the value of the currentEnvironment field. You could, for example, use a system property to determine the environemnt where the application is running.

To set a system property in the standalone.xml, use the <system-properties> tag under the <server> tag:

<server xmlns="urn:jboss:domain:4.2">

    ...

    <system-properties>
        <property name="environment" value="PRODUCTION"/>
    </system-properties>

    ...

</server>

Then use the following piece of code to get the value of the environemnt variable and set the value of the currentEnvironment field:

String environment = System.getProperty("environment");
currentEnvironment = EnvironmentType.valueOf(environment);
cassiomolin
  • 124,154
  • 35
  • 280
  • 359
2

Elaborating on my comment, you can do the following.

Define a single qualifier

@Inherited
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, FIELD, METHOD, PARAMETER})
public @interface BeanSelector {

    @NonBinding
    private String environment;

}

Define an annotation literal

public class BeanSelectorImpl extends AnnotationLiteral<BeanSelector> implements BeanSelector {

    private final String environment;

    public BeanSelectorImpl(final String environment) {
        this.environment = environment;
    }

    public String environment() {
        return this.environment;
    }
}

Create a producer that reads from the environment

@ApplicationScoped
public class BeanSelectorProducer {

   @Any
   @Inject
   private Instance<MyBeanService> myBeanServices;

   @Produces
   @BeanSelector(environment = "")
   public MyBeanService produceService() {
      final String env = System.getProperty("env.property");
      final BeanSelector beanSelector = new BeanSelectorImpl(env);

      //You may wish to handle exceptions.
      return myBeanServices.select(beanSelector).get();
   }
}

The negative side of this implementation is that all your beans will be in service. The other option of defining different beans.xml for every environment is probably a better option.

maress
  • 3,533
  • 1
  • 19
  • 37
1

In my opinion the simpliest solution was to create a maven profile like it's mentionned in some comments.

In my pom.xml file, I added a resources filtering section and a profile:

<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <filtering>true</filtering>
            <includes>
                <include>**/*.*</include>
            </includes>
       </resource>
   </resources>
</build>

<profiles>
    <profile>
        <id>default</id>
        <properties>
            <MyBean></MyBean>
        </properties>
    </profile>
    <profile>
        <id>alternative</id>
        <properties>
            <MyBean><![CDATA[<class>com.MyBean</class>]]></MyBean>
        </properties>
    </profile>
</profiles>

The beans.xml file is now like that:

<beans ...>
    <alternatives>
        ${MyBean}
    </alternatives>
</beans>

Finally I just need to execute the maven command with the useful profil: mvn package -P alternative.

cheb1k4
  • 2,316
  • 7
  • 26
  • 39