A common requirement for web and enterprise applications is that they have the capability to configure themselves for each environment without modifying the archive itself -- most commonly this is used only for environment specific attributes such as a test vs. production data store, or for Strings describing a file or directory on the file system which will be different on a developers box vs. a clustered production server, or perhaps it is a URL that points to your test payment gateway vs. your production gateway... This is the sort of thing that might easily be done with 'Alternatives' in CDI, but many shops put a premium on the ability to package the application once (on a Continuous Integration server, for example) and copy that file from development to integration to QA to staging to production, all of which are on very (very!) different platforms, using different databases, different file systems, and must integrate with different third party environments -- configuring this stuff externally means you don't have to deal with the error prone and possibly time consuming process of building for each environment... unfortunately, this doesn't appear to be a scenario that Alternatives can help us with...
One resource that works really well for this sort of configuration is JNDI... configure these items on your servers' JNDI registry independently from your application, and then have your application read the environment configuration settings from here -- and CDI makes it very easy to manage both sides of this scenario!
Reading from JNDI
The easier side of this is reading the data from JNDI, so let's start there... actually, you don't need CDI at all to start doing this -- the easiest way is to use the '@Resources' annotation provided in Java EE 5, like so:
@ApplicationScoped
public class FolderConfig {
@Resource(lookup="java:global/folderToPoll")
private String folderToPoll;
public String folderToPoll() {
return folderToPoll;
}
}
Not much to this -- we have an ApplicationScoped Managed Bean which does a lookup from JNDI, and provides a getter for the result... in this case we're pulling from the new "java:global" context that is provided with Java EE 6 -- there's no reason we couldn't map this to local context, but frankly, I wanted to fiddle with the global context :)
Ok, now on to something more interesting...
Writing to JNDI
Writing to JNDI is pretty easy -- get an InitialContext and call 'bind'... it's basically an overblown HashMap... for some reason, though, configuring JNDI outside of an application always seems to be more difficult than it should be -- several years ago, I actually had to write a JBoss plugin to do it, even though they had quite an advanced configuration mechanism for the time... all I wanted to do was put String 'A' at Key 'B', but no -- not supported out of the box!
That solution was configured by an XML file, which left me dealing with Strings... this solution is better on two accounts: 1) It can bind any Object into JNDI, and 2) it's a Portable Extension, and should therefore work on any platform... whew!
So here's how it works -- this extension would likely be packaged into a .jar library, and deployed with a simple webapp or ear archive that is packaged separately from the main application... the piece that provides the configuration is actually a class or a set of classes that are annotated to bind certain fields and/or methods into JNDI, like this:
@EnvironmentBinding
public class Env {
@Inject @Bind(jndiAddress="adminUser")
private User admin;
@Bind(jndiAddress="test") private String test = "This is a test";
}
Pretty straight forward -- what's going on here? Well, first you'll notice that the class is annotated with @EnvironmentBinding -- this is a Stereotype annotation that extends @ApplicationScoped, and acts as a marker for the class to be processed later on... further down, we have two fields that are annotated with @Bind and provided with a jndiAddress... this pretty much works as you would expect -- the value of that object is injected into JNDI, with the 'java:global/' prefix added to the front...
You'll also notice that one of the elements has its' value injected into the field -- this means that the Objects that are bound into JNDI can be derived from a more complex application if need be, so the support that we have here goes well above and beyond the simple XML file configuration that I dealt with way back when...
So how does this thing work? Well, one implementation that I put together has a two part infrastructure to do the job... remember, the end user should never be exposed to the following two items -- the extent of their exposure into this library will be the two annotations shown above...
First, our Portable Extension class:
public class EnvironmentBindingExtension implements Extension {
private Set envBeans = new HashSet();
private BeanManager beanManager;
public void discoverEnvironmentBindingClasses(@Observes ProcessBean pb, BeanManager bm) throws Exception {
this.beanManager = bm;
Bean bean = pb.getBean();
Class beanClass = bean.getBeanClass();
Set sts = bean.getStereotypes();
for (Class st : sts) {
if (st.equals(EnvironmentBinding.class)) {
log.info("Found class annotated with EnvironmentBinding: " + beanClass.getName());
envBeans.add(bean);
}
}
}
public Set getEnvBeans() {
return Collections.unmodifiableSet(envBeans);
}
public BeanManager getBeanManager() {
return beanManager;
}
}
This Extension class is pretty straight forward -- as with all Portable Extensions, it starts by implementing the 'Extension' interface... in this case, we're also creating an Observer method for the 'ProcessBean' event... this event is fired during the application startup lifecycle for every 'Bean' that is discovered in a Bean archive... this will fire for Managed Beans, EJB's, Interceptors, etc, but here, we're specifically looking for beans that have the EnvironmentBinding Stereotype on them -- that is the trigger to further process this class... in this case, our process simply consists of adding the Bean to our 'envBeans' Set for later use... in addition, we provide accessor methods for the BeanManager (which is injected into our Observer method), and the envBeans Set... Now let's have a look at what we do with these Beans...
The next class is the one that does most of the heavy lifting -- it is a Singleton EJB which is marked as a Startup bean, meaning it will be instantiated upon application startup, after the CDI discovery phases are complete... in this case, we have created a PostConstruct method to do our work for us:
@Singleton
@Startup
@ApplicationScoped
public class BindingsProcessor {
@Inject
private EnvironmentBindingExtension bindingExtension;
@PostConstruct
public void processBindings() throws Exception {
Set envBeans = bindingExtension.getEnvBeans();
log.info("Processing EnvironmentBinding Classes: "+envBeans);
Context appContext = bindingExtension.getBeanManager().getContext(ApplicationScoped.class);
for(Bean bean:envBeans) {
Class beanClass = bean.getBeanClass();
Object beanInstance = appContext.get(bean, bindingExtension.getBeanManager().createCreationalContext(bean));
Field[] fields = beanClass.getDeclaredFields();
for(Field field:fields) {
if(field.isAnnotationPresent(Bind.class)) {
field.setAccessible(true);
String jndi = field.getAnnotation(Bind.class).jndiAddress();
Object val = field.get(beanInstance);
bindValue(jndi, val);
}
}
}
}
Hey, wait a minute -- this is pretty simple, too! Iterate over the set of Beans that we've collected, use reflection to find all of the fields that are annotated with @Bind, and bind the value into the appropriate JNDI location... I've even removed the JNDI api work here, because it's not interesting at all...
This could be expanded in a couple of way, most obviously to allow methods to act as Binders as well... I do want to discuss my choice here of using the Singleton EJB as well, since I've had a few posts recently which talk about doing away with EJB's altogether -- well, initially I was attempting to use the 'AfterBeanDiscovery' or 'AfterDeploymentValidation' events to trigger this loading, but I was having trouble getting an instance of 'Env' that was capable of having its' injection points... er... injected...
The Singleton EJB is somewhat of a last-ditch sanity effort, but after considering it for a few days, I'm actually alright with it... the Startup Singleton EJB's are something that has interested me for a while, and it proves its' usefulness here, but what's more, I'm still able to take the EJB interface out of the end-user's experience here... they simply need to make use of the EnvironmentBinding annotation, and be on their merry way, as long as they are deployed in a container which supports Singletons (which all Java EE 6 containers do)... that being said, I'm hoping that Gavin will show me what the heck I was doing wrong :)
One other thing -- using an @Inject method on an ApplicationScoped bean doesn't appear to do the trick... reading the spec, it appears to be caused by the fact that ApplicationScoped beans are 'active' during Servlet calls, EJB calls, etc -- meaning it doesn't have it's own 'startup' lifecycle, but depends on the lifecycle of other Java EE component models... interesting, to be sure -- adding a more generic Startup capability would be a cinch if done similar to how I've done this...
Wow, that was a lot of words
So what does this all mean? Basically, it just shows another way of skinning that old, damn cat that is environment configuration -- but it also shows that it's pretty darn easy to put together some CDI extensions, and when working with the surrounding Java EE specs and resources, that it can be done in a minimal amount of code... in this case, I was looking at a requirement that I often have to support external configuration -- one that CDI doesn't accommodate out of the box... with a few lines of code, it turned out to be possible to break that box open and stuff some more toys inside :)
Finally, the more complete code samples can be found here -- the EnvironmentBinding project has the core code, the TestEnvironmentConfig project shows a test web application that could be used to create the binding configuration, and the EnvTest project is an application which makes use of the JNDI entries... have fun!
M
2 comments:
One thing to look out for: javax.naming.Context#bind()'s behavior is not guaranteed or specified by Java EE 6; specifically, an EJB may not rely on the ability to make such a binding.
Great and Useful Article.
J2EE Training
Java EE course
Java EE training
J2EE training in chennai
Java J2EE Training Institutes in Chennai
Java J2EE Training in Chennai
Java EE training
Java Interview Questions
Post a Comment