Sinced I dived into CDI, I explored classical Java EE uses. Then, I used CDI into a pet project of mine to see how it could be used in Swing applications. This article sums up what lessons I learned from itthis far.
This article assumes you have some familiarity with CDI; if not, please read my previous articles on CDI (CDI an overview part 1 and part 2) or read the documentation. |
In my last attempts at Swing (I’m more of a web developer), I figured it could be nice to configure most of the UI components in Spring rather than coding the parameters by hand. In doing this, I used the traditional XML configuration approach, since using annotations would couple my code to Spring JAR, something I loathe to do. With CDI, however, I can live with having my code tied to a standard API: I can change the implementation. This is akin to developing a servlet where you reference the Servlet API, then run it in any container you want.
I coded and coded this way, looked at things made, refactored, then code some more, just like when it’s a personal project and you have no deadlines to meet. Then, I realized I could learn some lessons I could put to good use for the future. Here they are, in no particular order.
The right JAR
Getting all the right CDI JARs for your Java SE project can be a rather daunting task. In order to ease your pain, Weld provides you with a all-in-one JAR aptly named weld-se. Don’t argue that it’s bloated and repackaged, just use it, you will save yourself a lot of pain. For Maven users, this is the configuration:
<project>
...
<dependencies>
<dependency>
<groupId>org.jboss.weld</groupId>
<artifactId>weld-se</artifactId>
<version>1.0.1-Final</version>
</dependency>
</dependencies>
</project>
She’s an easy logger
All the log frameworks I know of (JDK, Commons Logging, Log4J and SLF4J) let you declare your class logger rather easily:
private static final Logger LOGGER = LoggerFactory.getLogger(MyClass.class);
Anyway, most of the time, in order to use this, you either:
- create a template in your IDE.
Yet configuring templates in each of your IDE can be rather tedious.Note to self
Propose a feature where such templates (and preferences) could be saved on the web and shared across all of my IDE instances
- copy the line from somewhere then paste it where you need it. Then you change the class to be logged. To be frank, and though it does not add to my legend (quite the contrary) this is what I do all the time
And doing the latter all the time, it happens I forget to change the logged class. Stupid, isn’t it? Perhaps, but what’s even more stupid is that I have to write it in the first place: the class is my context, it should be inferred. Guess what, Weld does it, with a little help from the weld-logger JAR (it uses SLF4J so beware or use the code as a template to use another framework):
@Inject
private Logger logger;
I truly don’t think it could be easier! The only drawback is that you lose the constant: for the projects I handle now, I can live with it. To use this feature, just add the following dependency to your POM:
<project>
...
<dependencies>
<dependency>
<groupId>org.jboss.weld</groupId>
<artifactId>weld-logger</artifactId>
<version>1.0.1-Final</version>
</dependency>
</dependencies>
</project>
Remember initialization
CDI can of course inject your dependencies.
But injecting your dependencies does not assemble them on the screen.
Luckily, CDI also provides you with a hook in order to do it:
this is the realm of the @PostConstruct
annotation.
Let’s take a very simple example:
public class MyPanel extend JPanel {
@Inject
private JCheckBox checkbox;
@PostConstruct
public void afterPropertiesSet() {
setLayout(new FlowLayout());
add(checkbox);
}
}
The afterPropertiesSet()
method is called after having injected all dependencies, and plays the role previously done in the constructor.
Yes, the method is named after an old Spring method defined in the InitializingBean interface, dating from version 1.
It can now be replaced by specifying the |
(Don’t) grow in numbers
With CDI, your classes are injected into one another. However, this means that each component should have its own class. Tthis can be rather cumbersome: for buttons, for example, each should be attached its action on a specific basis. With time, even a moderately size application will grow in term of classs to be much bigger than a standard application. IMHO, this is not desirable since:
- it makes your codebase bigger, and thus increase your development complexity
- in Sun’s JVM, loading more classes mean you will use more PermGen space and you’re more likely to run into
OutOfMemoryError
In order to limit the number of classes I produce, I used a component factory filled with producer methods. These methods are identified by CDI as injection providers.
public class ComponentFactory {
@Produces
public JButton getButton() {
return new JButton();
}
}
Use the injection point
This is good but not enough, since action (or text and icon) association will have to be done in the injected class.
I would like to annotate the injected attribute with informations like the text of the button and get that information in my producer method.
This is the goal of the InjectionPoint
optional parameter.
CDI provides it to you free of charge if you reference it in your method as a parameter.
public class ComponentFactory {
@Produces
public JButton getButton(InjectionPoint ip) {
JButton button = new JButton();
// Do something based on annotations on the parameter
...
return button;
}
}
This is exactly the way that Weld loggers (see above) are created.
Respect the Higlander rule
The Highlander rule is: "There can be only one".
Apart from being taken from a movie that young people mostly don’t know, it also enunciate a basic truth.
Since injection must be deterministic, there cannot be two candidates for an injection point.
Using producer methods as previously stated will run you into this problem:
CDI will have both the JButton
class and the producer method and will loudly complains about it in its usual way.
Exception in thread "main" org.jboss.weld.exceptions.DeploymentException: WELD-001409 Injection point has ambiguous dependencies..... at org.jboss.weld.bootstrap.Validator.validateInjectionPoint(Validator.java:280) at org.jboss.weld.bootstrap.Validator.validateBean(Validator.java:122) at org.jboss.weld.bootstrap.Validator.validateRIBean(Validator.java:141) at org.jboss.weld.bootstrap.Validator.validateBeans(Validator.java:331) at org.jboss.weld.bootstrap.Validator.validateDeployment(Validator.java:317) at org.jboss.weld.bootstrap.WeldBootstrap.validateBeans(WeldBootstrap.java:399) at org.jboss.weld.environment.se.Weld.initialize(Weld.java:81) at org.jboss.weld.environment.se.StartMain.go(StartMain.java:45) at org.jboss.weld.environment.se.StartMain.main(StartMain.java:57)
To be compliant with the Highlander rule, you’ll have to discard the JButton
class as a candidate for injection.
In order to do this, I used qualifiers, both on the injection point and on the producer method.
Since I did not want a qualifier per injection point / producer method pair, I made it parameterizable:
@Qualifier
@Target( { FIELD, METHOD })
@Retention(RUNTIME)
public @interface JType {
public Class<? extends JComponent> value();
}
Annotating both the injected attribute and the producer method with the JType
annotation made my code compliant with the Highlander rule!
Spread the word
A good practice I can recommend is to create a special Properties
class tasked with initializing your labels (and another for your preferences, etc.), then inject it in all the client classes you need.
Now, all classes have access to your internationalized labels.
Truly terrific!
Forget Java WebStart
Weld JAR analyze process is incompatible with Java WebStart. You will likely have such nice error messages:
122 [javawsApplicationMain] WARN org.jboss.weld.environment.se.discovery.URLScanner - could not read entries java.io.FileNotFoundException: http:\xxxxxxxxxx\weld-logger-1.0.0-CR2.jar (La syntaxe du nom de fichier, de répertoire ou de volume est incorrecte) at java.util.zip.ZipFile.open(Native Method) at java.util.zip.ZipFile.<init>(Unknown Source) at java.util.zip.ZipFile.<init>(Unknown Source) at org.jboss.weld.environment.se.discovery.URLScanner.handleArchiveByFile(URLScanner.java:142) at org.jboss.weld.environment.se.discovery.URLScanner.handle(URLScanner.java:126) at org.jboss.weld.environment.se.discovery.URLScanner.scanResources(URLScanner.java:107) at org.jboss.weld.environment.se.discovery.SEWeldDiscovery.scan(SEWeldDiscovery.java:71) at org.jboss.weld.environment.se.discovery.SEWeldDiscovery.<init>(SEWeldDiscovery.java:45) at org.jboss.weld.environment.se.discovery.SEBeanDeploymentArchive$1.<init>(SEBeanDeploymentArchive.java:45) at org.jboss.weld.environment.se.discovery.SEBeanDeploymentArchive.<init>(SEBeanDeploymentArchive.java:44) at org.jboss.weld.environment.se.discovery.SEWeldDeployment.<init>(SEWeldDeployment.java:37) ... at com.sun.javaws.Launcher.run(Unknown Source) at java.lang.Thread.run(Unknown Source)
I hope it will be fixed in future releases…
Singletons are NOT evil
Twice already I stumbled upon a strange behaviour: toggle buttons selected when they shouldn’t or state was lost.
After debugging like mad, I saw that @PostConstruct
methods where called well beyond the initial call.
It seems my code called for another injection after that.
In order to remedy to this, annotate your class with @Singleton
in order to share the instance across multiple calls.
I haven’t investigated more than that because:
- I resolved the bug
- I don’t know why I shouldn’t use singletons in a Swing application
Conclusion
I’m still in development, so I don’t think I’ve seen all there’s to see. Yet, the previous points can make a good starting point for any project wanting to use CDI in a Java SE context.
And please pardon the puns, I was feeling jolly because of this fine summer of ours 😅️