CDI (Part 2): Qualifiers in Java: Polymorphism in DI
Hello!
This is the Part 2 of the CDI Series in Java that contains:
- Part 1: Factory in CDI with @Produces annotation
- Part 2: Polymorphism with CDI Qualifiers
- Part 3: Events and Observers in CDI
- Part 4: CDI and Lazy Initialization
- Part 5: Interceptors in CDI
- Part 6: CDI Dependency Injection and Alternatives
- Part 7: Working with CDI Decorators
In this part, we're going to explore how CDI Qualifier works and how it can help us in Java with Dependency Injection and Polymorphism!
We'll use the scenario from the previous part, when we talked about @Produces annotations.
Let's remember that scenario and add a few more steps
- Create a Checkout class and log on the console when a checkout is finished
- Create a Simple Logger class that will have the logic to print a log
- Create the Logger object choosing the desired Log mode; Debug, Info, or Warn mode
Are you ready?
Step 1: Using CDI 2.0 in a Standalone Mode
It's a good time to review the code from the previous part of this series that runs CDI in a standalone mode.
First of all, let's create the MainApplication class that will start the CDI Container. Just a reminder, in the following code we're using CDI 2.0, but Qualifiers can be used from version 1.0.
public class MainApplication {
public static void main(String[] args) {
try (CDI<object> container = new Weld().initialize()) {
Checkout checkout = container.select(Checkout.class).get();
checkout.finishCheckout();
}
}
}
This code doesn't compile yet since we don't have the Checkout class.
Step 2: Creating the Checkout Class
Let's create the Checkout class with a simple logger:
public class Checkout {
@Inject
private SimpleLogger logger;
public void finishCheckout() {
logger.log("Finishing Checkout");
}
}
That code doesn't compile either. Let's create the SimpleLogger class.
Step 3: Creating the SimpleLogger Class
This class will just use a log a message based on a configuration mode.
public class SimpleLogger {
private LogConfiguration configuration;
public SimpleLogger(LogConfiguration configuration) {
this.configuration = configuration;
}
public void log(String message) {
if (configuration.isInDebugMode()) {
System.out.println("DEBUG LOG: " + message);
}
else if (configuration.isInInfoMode()) {
System.out.println("DEBUG LOG: " + message);
} else {
System.out.println("DEFAULT LOG: " + message);
}
}
}
As you can see, we should have the class LogConfiguration to be able to configure the mode to be used when printing a log.
Step 4: Creating the LogConfiguration class
This class will contain 3 modes to be passed to the SimpleLogger class:
public class LogConfiguration {
private boolean infoMode;
private boolean debugMode;
private boolean warnMode;
public LogConfiguration(boolean infoMode, boolean debugMode, boolean warnMode) {
this.infoMode = infoMode;
this.debugMode = debugMode;
this.warnMode = warnMode;
}
public boolean isInInfoMode() {
return infoMode;
}
public boolean isInDebugMode() {
return debugMode;
}
public boolean isInWarnMode() {
return warnMode;
}
}
Notice that the SimpleLogger class should have a LogConfiguration object built.
In this case, we should create a Factory to teach CDI how to create a SimpleLogger object.
Let's move on!
Step 5: Factory With @Produces in CDI
We're going to use the @Produces annotation to create a Factory to build a SimpleLogger object.
This object will be built with the boolean debugMode as true to indicate that a message should be printed in the debug mode.
public class SimpleLoggerFactory {
@Produces
public SimpleLogger createLogger() {
LogConfiguration logInDebugMode = new LogConfiguration(false, true, false);
return new SimpleLogger(logInDebugMode);
}
}
Great! So far we have just made an overview from the previous post.
Step 6: More Logger Modes
As you may notice, so far, we have just 1 mode to work with the Logger, the Debug mode!
But we'd like to use another Logger mode, for example, Info mode. How can we do that?
The first and simplest way to resolve this: Create another method in the Factory to create a Logger object that has the info mode
Let's explore this solution!
public class SimpleLoggerFactory {
@Produces
public SimpleLogger createDebugLogger() { //yes, I renamed it
boolean debugMode = true; //just to be clear that the debug mode is on
LogConfiguration logInDebugMode = new LogConfiguration(false, debugMode, false);
return new SimpleLogger(logInDebugMode);
}
@Produces
public SimpleLogger createInfoLogger() { //new method here :)
boolean infoMode = true;
LogConfiguration logInInfoMode = new LogConfiguration(infoMode, false, false);
return new SimpleLogger(logInInfoMode);
}
}
Yes, we renamed the previous method to be clear that a Debug Logger will be created, and besides that, we created a new method called createInfoLogger().
Notice that both methods are using the @Produces annotation and yes, CDI allows us to have many methods annotated with @Produces annotation in the same class.
Let's run the code?
WELD-001409: Ambiguous dependencies for type SimpleLogger with qualifiers @Default
at injection point [BackedAnnotatedField] @Inject private com.craftcoder.cdi.qualifiers.Checkout.logger
at com.craftcoder.cdi.qualifiers.Checkout.logger(Checkout.java:0)
Possible dependencies:
- Producer Method [SimpleLogger] with qualifiers [@Any @Default] declared as [[BackedAnnotatedMethod] @Produces public com.hackingcode.cdi.qualifiers.SimpleLoggerFactory.createInfoLogger()],
- Producer Method [SimpleLogger] with qualifiers [@Any @Default] declared as [[BackedAnnotatedMethod] @Produces public com.hackingcode.cdi.qualifiers.SimpleLoggerFactory.createDebugLogger()]
Yes, an exception has thrown in our face! What's happened?
CDI can't understand and can't choose which method should be used when the SimpleLogger object is needed.
CDI will say to you:
Hey, I have 2 methods that Produces the object that you are interested in. You should teach me which one I should pick up.
And that really makes sense. CDI doesn't have the capacity to understand your desire when you're injecting a dependency.
Let's teach CDI how to pick up the correct Logger to be injected.
Step 7: Polymorphism With @Qualifiers
There is a great annotation called @Qualifiers since the version 1.0. With this annotation, we can teach CDI which method should be used and CDI will create the correct object.
First of all, we need to mark our Debug method, indicating that this method will use Debug mode.
How can we mark classes or methods in Java? Using annotations! So, let's create a new one!
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD})
public @interface DebugMode {
}
Easy! We created the DebugMode annotation and we will mark the method that creates a Debug logger.
Notice that the ElementType in the annotation indicates where this annotation should be used. In our case, this will be used in methods and fields.
Using this annotation in the createDebugLogger() method:
public class SimpleLoggerFactory {
@Produces @DebugMode
public SimpleLogger createDebugLogger() {
boolean debugMode = true;
LogConfiguration logInDebugMode = new LogConfiguration(false, debugMode, false);
return new SimpleLogger(logInDebugMode);
}
//another method here..
}
Ok! But at any moment, CDI knows that the DebugMode annotation should be used. At this time, we should use its @Qualifier annotation:
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DebugMode {
}
Now CDI knows that the DebugMode annotation is a qualified annotation and will be used to filter which method should be used when a new Logger is requested.
Let's run it?
INFO: WELD-ENV-002003: Weld SE container STATIC_INSTANCE initialized
INFO LOG: Finishing Checkout
Feb 15, 2017 8:09:57 AM org.jboss.weld.environment.se.WeldContainer shutdown
INFO: WELD-ENV-002001: Weld SE container STATIC_INSTANCE shut down
Awesome! Everything is working!
Not so fast! As you can see, we are printing the INFO LOG, right? But we would like to print the DEBUG LOG.
Yes, CDI needs that you specify explicitly which Qualifier should be used. If you don't pass any Qualifier, CDI will use its default Qualifier, called...@Default!
Under the hood, the method createInfoLogger has the @Default qualifier annotation:
public class SimpleLoggerFactory {
@Produces @Default //yes, it is here but you dont't need to use it explicitly!
public SimpleLogger createInfoLogger() {
boolean infoMode = true;
LogConfiguration logInInfoMode = new LogConfiguration(infoMode, false, false);
return new SimpleLogger(logInInfoMode);
}
}
And notice that CDI will configure the @Default Qualifier in the injection point:
public class Checkout {
@Inject @Default //it here again!
private SimpleLogger logger;
public void finishCheckout() {
logger.log("Finishing Checkout");
}
}
So, keep in mind that we always have the @Default annotation when we don't have a specific CDI Qualifier.
Step 8: Choosing Another Qualifier in the Injection Point
Now it's easy to choose the Debug Mode in the Injection Point:
public class Checkout {
@Inject @DebugMode //debug mode now
private SimpleLogger logger;
public void finishCheckout() {
logger.log("Finishing Checkout");
}
}
Let's run it again!
INFO: WELD-ENV-002003: Weld SE container STATIC_INSTANCE initialized
DEBUG LOG: Finishing Checkout
Feb 15, 2017 8:26:41 AM org.jboss.weld.environment.se.WeldContainer shutdown
INFO: WELD-ENV-002001: Weld SE container STATIC_INSTANCE shut down
Everything is working!
Step 9: Creating More Log Modes using Qualifiers
An important note about the approach about CDI Qualifiers:
Qualifiers bring to us the ability to use polymorphism at the Injection Point
So, it's time to create a new Qualifier to the INFO and WARN modes to get the benefits from polymorphism and CDI:
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD})
public @interface InfoMode {
}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD})
public @interface WarnMode {
}
And the entirely SimpleLoggerFactory class should be as follows:
public class SimpleLoggerFactory {
@Produces @DebugMode
public SimpleLogger createDebugLogger() {
boolean debugMode = true;
LogConfiguration logInDebugMode = new LogConfiguration(false, debugMode, false);
return new SimpleLogger(logInDebugMode);
}
@Produces @InfoMode
public SimpleLogger createInfoLogger() {
boolean infoMode = true;
LogConfiguration logInInfoMode = new LogConfiguration(infoMode, false, false);
return new SimpleLogger(logInInfoMode);
}
@Produces @WarnMode
public SimpleLogger createWarnLogger() {
boolean warnMode = true;
LogConfiguration logInWarnMode = new LogConfiguration(false, false, warnMode);
return new SimpleLogger(logInWarnMode);
}
}
Using the WARN mode:
@Inject @WarnMode
private SimpleLogger logger;
If you run the code again, the message will be printed in the WARN mode!
Step 10: @Qualifiers With Arguments
We have 3 Log modes so far. But if we need 5 more modes? Creating 5 more annotations doesn't sound like a good idea
It's time to refactor our code and use Qualifiers with arguments to have less code being written. Yes, Qualifiers accept arguments in its annotation!
So, we'll create a new annotation called @LoggerMode, a more generic annotation:
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD})
public @interface LoggerMode {
}
We can use an Enum to indicate which mode we would like to use in the annotation:
enum Mode {
DEBUG, INFO, WARN
}
The complete Qualifier code will be:
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD})
public @interface LoggerMode {
Mode desiredMode();
}
Pretty cool! We just need to change the methods in the Factory class to use the new annotation with the desiredMode option configured:
public class SimpleLoggerFactory {
@Produces @LoggerMode(desiredMode = Mode.DEBUG)
public SimpleLogger createDebugLogger() {
...
}
@Produces @LoggerMode(desiredMode = Mode.INFO)
public SimpleLogger createInfoLogger() {
...
}
@Produces @LoggerMode(desiredMode = Mode.WARN)
public SimpleLogger createWarnLogger() {
...
}
}
Now we can remove all 3 annotations to use just 1, the @LoggerMode annotation!
If we have 5 more Log Modes, don't worry, you can create 5 more types on Enum and don't need to create more 5 annotations! Excellent! Polymorphism rocks!
That's it! I hope that this article could be helpful to you!
Don't forget to explore all posts of the CDI Series in Java!
Thanks!