The aim of this post is to show you how, using CDI, you can inject Java properties entries (those in *.properties
files) directly into your Java class. This is somewhat similar to the Spring’s @Value
annotation I needed in my plain
Java EE project.
The whole code can be found on GitHub so feel free to clone it, use it or make any changes you like.
Context
Ok, so basically in a Java EE environment we already have a way to define some properties that are deployment-specific
– it’s the <env-entry>
element in the descriptor.
So, if you use EJB you have ejb-jar.xml
where you can define the <env-entry>
element to define key, type and a
default value (for more information take a look at David Blevins’ resource.)
You can then refer to these values using @Resource
annotation in your EJBs.
If you can, you can also define property using web.xml
and its <env-entry>
element.
Nevertheless, we want something simpler – with Java EE 6 you don’t have to have web.xml
or ejb-jar.xml
.
Moreover, we want to inject resources not only in the EJBs but in any managed Java class.
We want some CDI magic.
Overall description
We want to use code something like this in our classes:
@Inject
@ConfigurationValue
String myProp;
this dependency should inject a property with key myProp
into our annotated class field.
The @Inject
annotation is the official javax.inject.Inject
one and the @ConfigurationValue
is our own custom one
we’ll use to distinct classes fields that should have values injected from the properties.
Qualifier
The @ConfigurationValue
is pretty straightforward. It is a CDI Qualifier, so it helps us to define a type of injection
to apply to it:
package com.piotrnowicki.cdiconfig;
import static java.lang.annotation.RetentionPolicy.*;
import static java.lang.annotation.ElementType.*;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.enterprise.util.Nonbinding;
import javax.inject.Qualifier;
/**
* Defines method / variable that should be injected with value read from some arbitrary resource (e.g. from
* <code>properties</code> file.)
*
* @author Piotr Nowicki
*
*/
@Qualifier
@Retention(RUNTIME)
@Target({ TYPE, METHOD, FIELD, PARAMETER })
public @interface ConfigurationValue {
/**
* Key that will be searched when injecting the value.
*/
@Nonbinding
String value() default "";
/**
* Defines if value for the given key must be defined.
*/
@Nonbinding
boolean required() default true;
}
Note the @Nonbinding
annotations on the attributes. By default, attributes of CDI Qualifier are used to further filter
the injected candidate. In this case, we want just to set additional information to the producer method and not to
use the binding functionality of the attributes.
Ok, so we have qualifier and a client that uses the value. But who will actually provide us with the appropriate value?
Producer Methods
Now we need to define a producer who will produce the objects we want to inject. In other words, it should look in the properties files and grab the appropriate entry for us.
package com.piotrnowicki.cdiconfig;
import javax.enterprise.inject.Produces;
import javax.enterprise.inject.spi.InjectionPoint;
import javax.inject.Inject;
/**
* Produces {@link ConfigurationValue} annotated fields. It's responsible for supporting conversion between
* types (mainly from String to any other type required by the user.)
*
* <p>
* These producers should not be interested where the fields are read from. It's the {@link PropertyResolver}
* who is responsible for the configuration loading.
* </p>
*
* @see PropertyResolver
*
* @author Piotr Nowicki
*
*/
public class ConfigurationValueProducer {
@Inject
PropertyResolver resolver;
/**
* Main producer method - tries to find a property value using following keys:
*
* <ol>
* <li><code>key</code> property of the {@link ConfigurationValue} annotation (if defined but no key is
* found - returns null),</li>
* <li>fully qualified field class name, e.g. <code>eu.awaketech.MyBean.myField</code> (if value is null,
* go along with the last resort),</li>
* <li>field name, e.g. <code>myField</code> for the example above (if the value is null, no can do -
* return null)</li>
* </ol>
*
* @param ip
* @return value of the injected property or null if no value could be found.
*/
@Produces
@ConfigurationValue
public String getStringConfigValue(InjectionPoint ip) {
String fqn = ip.getMember().getDeclaringClass().getName() + "." + ip.getMember().getName();
// Trying with explicit key defined on the field
String key = ip.getAnnotated().getAnnotation(ConfigurationValue.class).value();
boolean isKeyDefined = !key.trim().isEmpty();
boolean valueRequired = ip.getAnnotated().getAnnotation(ConfigurationValue.class).required();
if (isKeyDefined) {
return resolver.getValue(key);
}
// Falling back to fully-qualified field name resolving.
key = fqn;
String value = resolver.getValue(fqn);
// No luck... so perhaps just the field name?
if (value == null) {
key = ip.getMember().getName();
value = resolver.getValue(key);
}
// No can do - no value found but you've said it's required.
if (value == null && valueRequired) {
throw new IllegalStateException("No value defined for field: " + fqn
+ " but field was marked as required.");
}
return value;
}
/**
* Produces {@link Double} type of property from {@link String} type.
*
* <p>
* Will throw {@link NumberFormatException} if the value cannot be parsed into a {@link Double}
* </p>
*
* @param ip
* @return value of the injected property or null if no value could be found.
*
* @see ConfigurationValueProducer#getStringConfigValue(InjectionPoint)
*/
@Produces
@ConfigurationValue
public Double getDoubleConfigValue(InjectionPoint ip) {
String value = getStringConfigValue(ip);
return (value != null) ? Double.valueOf(value) : null;
}
/**
* Produces {@link Integer} type of property from {@link String} type.
*
* <p>
* Will throw {@link NumberFormatException} if the value cannot be parsed into an {@link Integer}
* </p>
*
* @param ip
* @return value of the injected property or null if no value could be found.
*
* @see ConfigurationValueProducer#getStringConfigValue(InjectionPoint)
*/
@Produces
@ConfigurationValue
public Integer getIntegerConfigValue(InjectionPoint ip) {
String value = getStringConfigValue(ip);
return (value != null) ? Integer.valueOf(value) : null;
}
}
The code and comments should be enough to understand what it’s doing.
Notice the PropertyResolver
that is injected in the above code. This class is a CDI singleton bean that is
responsible for actually accessing the properties files.
After its initialization, it allows you to fetch a property with given key (you can think of this class as a
Map<String, Object>
wrapper.) Its actual implementation can be found below:
Property Files Resolver
package com.piotrnowicki.cdiconfig;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.annotation.PostConstruct;
import javax.inject.Singleton;
/**
* <p>
* Reads all valid property files within the classpath and prepare them to be fetched.
* </p>
*
* <p>
* This class <strong>can</strong> be accessed concurrently by multiple clients. The inner representation of
* properties <strong>should not</strong> be leaked out; if this is absolutely required, use unmodifiable
* collection.
* </p>
*
* <p>
* This resolver <strong>doesn't pay attention</strong> to multiple properties defined with the same name in
* different files. It's impossible to determine which one will take precedence, so the responsibility for
* name-clash is a deployer concern.
* </p>
*
* @author Piotr Nowicki
*
*/
@Singleton
public class PropertyResolver {
// TODO: Change it to some hierarchical structure if required.
Map<String, Object> properties = new HashMap<>();
/**
* Initializes the properties by reading and uniforming them.
*
* This method is called by the container only. It's not supposed to be invoked by the client directly.
*
* @throws IOException
* in case of any property file access problem
* @throws URISyntaxException
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@PostConstruct
private void init() throws IOException {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
List<File> propertyFiles = getPropertyFiles(cl);
for (File file : propertyFiles) {
Properties p = new Properties();
p.load(new FileInputStream(file));
// TODO: If required - notify if added key was already present in the map
properties.putAll(new HashMap<String, Object>((Map) p));
}
}
/**
* Gets flat-file properties files accessible from the root of the given classloader.
*
* @param cl
* classpath to be used when scanning for files.
*
* @return found property files.
*
* @throws IOException
* if there was a problem while accessing resources using the <code>cl</code>.
*/
List<File> getPropertyFiles(ClassLoader cl) throws IOException {
List<File> result = new ArrayList<>();
Enumeration<URL> resources = cl.getResources("");
while (resources.hasMoreElements()) {
File resource = getFileFromURL(resources.nextElement());
File[] files = resource.listFiles(new PropertyFileFilter());
result.addAll(Arrays.asList(files));
}
return result;
}
/**
* Converts URL resource to a File. Makes sure that invalid URL characters (e.g. whitespaces) won't
* prevent us from accessing the valid file location.
*
* @param url
* URL to be transformed
*
* @return File pointing to the given <code>url</code>.
*/
File getFileFromURL(URL url) {
File result;
try {
result = new File(url.toURI());
} catch (URISyntaxException e) {
result = new File(url.getPath());
}
return result;
}
/**
* Returns property held under specified <code>key</code>. If the value is supposed to be of any other
* type than {@link String}, it's up to the client to do appropriate casting.
*
* @param key
* @return value for specified <code>key</code> or null if not defined.
*/
public String getValue(String key) {
Object value = properties.get(key);
return (value != null) ? String.valueOf(value) : null;
}
}
It scans all *.properties
in your classpath root, reads them and combines together. The properties file can have
any name.
Property Files Filter
Now the last part that is quite useful in our case is the FileFilter
that allows us to list only *.properties
files
within the root of the classpath. I’ve come up with the following implementation:
package com.piotrnowicki.cdiconfig;
import java.io.File;
import java.io.FileFilter;
/**
* Selects only files that are within <code>WEB-INF</code> directory and have <code>properties</code>
* extension.
*
* @author Piotr Nowicki
*/
public class PropertyFileFilter implements FileFilter {
@Override
public boolean accept(File pathname) {
// TODO: Is this even possible in real-life?
if (pathname == null) {
return false;
}
// We're not investigating sub-directories
boolean isDirectory = pathname.isDirectory();
/*
* FIXME: Change to something more appropriate - user can have a dir named "classes" on the regular
* path - should it also be scanned for 'properties'?
*/
boolean isWebInfResource = pathname.getAbsolutePath().contains("classes/");
if (isDirectory || !isWebInfResource) {
return false;
}
String extension = getExtension(pathname.getName());
if (extension.equals("properties")) {
return true;
} else {
return false;
}
}
/**
* <p>
* Returns filename extension. Returns empty String if no extension is defined. E.g.:
* <ul>
* <li><code>myFile.dat</code>, returns <code>dat</code></li>
* <li><code>myFile.with.dots.properties</code>, returns <code>properties</code></li>
* </ul>
* </p>
*
* <p>
* This method never returns null and is null-argument safe.
* </p>
*
* @param filename
* @return extension of the <code>filename</code> without the trailing dot.
*/
protected String getExtension(String filename) {
if (filename == null) {
return "";
}
int lastDotIdx = filename.lastIndexOf(".");
if (lastDotIdx == -1) {
return "";
}
return filename.substring(lastDotIdx + 1);
}
}
This class has nothing to do with the CDI itself – it’s just a helper class for listing only interesting to us files.
Unit and Integration Tests
I decided to go with TestNG instead of JUnit because I very like the Data Provider feature of the TestNG. For those of you who are not familiar with this feature, take a look at the exemplary test:
package com.piotrnowicki.cdiconfig;
import static org.fest.assertions.Assertions.assertThat;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.testng.Arquillian;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import com.piotrnowicki.cdiconfig.PropertyResolver;
public class PropertyResolverTest extends Arquillian {
@Inject
PropertyResolver cut;
@Deployment
public static Archive<?> createDeployment() throws IOException {
JavaArchive archive = ShrinkWrap.create(JavaArchive.class).addClass(PropertyResolver.class);
return archive;
}
@Test(dataProvider="getFileFromURLProvider")
public void getFileFromURL(URL url, File expected) {
File actual = cut.getFileFromURL(url);
assertThat(actual).isEqualTo(expected);
}
@DataProvider(name = "getFileFromURLProvider")
Object[][] getFileFromURLProvider() throws MalformedURLException {
List<Object[]> data = new ArrayList<>();
data.add(new Object[] { new URL("file:///testMyFile"), new File("/testMyFile") });
data.add(new Object[] { new URL("file:///myDir/file with whitespaces"), new File("/myDir/file with whitespaces") });
data.add(new Object[] { new URL("file:///file%20with%20whitespaces"), new File("/file with whitespaces") });
return data.toArray(new Object[0][0]);
}
@Test(dataProvider = "keyProvider")
public void getValue(String key, String expected) {
String actual = cut.getValue(key);
assertThat(actual).isEqualTo(expected);
}
@DataProvider(name = "keyProvider")
Object[][] dataProvider() {
List<Object[]> data = new ArrayList<>();
data.add(new Object[] { "myProp", "myVal" });
data.add(new Object[] { "myProp2", "myVal2" });
data.add(new Object[] { "myProp3", null });
return data.toArray(new Object[0][0]);
}
}
You can define your Data Provider
method and use it to generate test data (and expected result in my case) for your
test methods.
It allows me to cleanly test method behaviour for different arguments. At the same time, TestNG will save information
about each of the test methods invocation – so for Data Provider
with 3 different data sets, you’ll get information
about 3 executions of a test method in the report (each with different arguments.) It’s quite useful to retain this
kind of information.
Second thing that I use very often are fluent assertions. I don’t care if its Hamcrest or FEST. I just love the fluent API and the way you can easily read the test assertions and know exactly what it does.
Last thing about this snippet is that if you want to use Arquillian to conduct your integration tests, you should
extend the org.jboss.arquillian.testng.Arquillian
class. It’s an equivalent of the @RunWith(Arquillian.class)
in
the JUnit’s world.
It’s worth mentioning, as it took me a few minutes to dig this information.
Nice thing about using Arquillian in your tests is that you can easily simulate a client code of your component just as done here.
Side notes
Below you can find few remarks about problems I’ve stumbled upon when writing this example.
slf4j Hell
I’ve got some weird SLF4J errors when executing the Arquillian test:
java.lang.NoSuchMethodError:
org.slf4j.spi.LocationAwareLogger.log(Lorg/slf4j/Marker;Ljava/lang/String;ILjava/lang/String;Ljava/lang/Throwable;)V
If you’ve ever used SLF4J with log4j and got this error – you probably know it’s because of the messed up versions
of slf4j on your classpath. The point is, I’ve inspected the pom.xml
file and there was no conflicting versions
(well, there was but between versions 1.6.1 and 1.6.4, however the error I’ve posted above is related with differences
between versions 1.5.x and 1.6.x.)
It seemed that my Eclipse project had a Glassfish 3.1.2 facet set and apparently it added slf4j 1.5.x libraries on the classpath. This resulted in those nasty exceptions.
Removing the facet solved the problem.
Java EE API in Maven
Arquillian User Guide is very right about using org.jboss.spec:jboss-javaee-6.0
artifact in your pom.xml
instead of the javax:javaee-api
.
Don’t think twice – if you plan to use Arquillian – just use the coordinates they posted in the user guide instead of Java EE API with empty method bodies.
Accessing Files in Java EE
I’ve seen discussions about allowance of accessing filesystem from within the Java EE applications. I do believe that this restriction is talking about accessing plain filesystem rather than fetching the resources from the classpath, so I don’t believe it breaks the Java EE specification.
Test it
This code is mainly for exemplary purposes, but if instead of checking the sources and fiddling with how it’s implemented, you just want to see it running, you can define the following Maven dependency:
<dependency>
<groupId>com.piotrnowicki</groupId>
<artifactId>cdiconfig</artifactId>
<version>0.1</version>
</dependency>
And set my website as a repository for artifacts:
<repository>
<id>piotrnowicki.com</id>
<url>http://maven.piotrnowicki.com</url>
</repository>
Either way, remember to make your web application CDI managed by putting beans.xml
in your WEB-INF
directory!