Mittwoch, 25. Mai 2011

Configuration Parameter Injection with CDi

CDI Based Configuration

I experimented with a simple possibility to inject parameter values, that can easily configured by external configuration providers.
At the injection point it should be possible to provide default values used if the parameters are not set.
All parameters are referenced by names. The default name of a parameter is the simple class name followed by a dot and the name of the injection point.
It should be possible to overwrite the name of the parameter.
The usage of the parameters should look like the following example:

@Named @RequestScoped public class ExampleBean {

 @Inject @StringParameter("Default Title") String title;
 @Inject @IntParameter(42) int value;
 @Inject @IntParameter(4711) int valueWithDefault;
 @Inject @StringParameter(name="customParamName",value="Default Value") String namedParameter;
 
 //... getters omitted
} 
  

The definition of the parameter uses one of the following qualifier annotations:

@Inherited
@Qualifier
@Target({java.lang.annotation.ElementType.METHOD,java.lang.annotation.ElementType.FIELD })
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
public @interface StringParameter {
 @Nonbinding String name() default "";
 @Nonbinding String value() default ""; // parameter default value
}
  

@Inherited
@Qualifier
@Target({java.lang.annotation.ElementType.METHOD,java.lang.annotation.ElementType.FIELD })
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
public @interface IntParameter {
 @Nonbinding String name() default "";
 @Nonbinding int value() default 0; // parameter default value
}
  

Both qualifiers provide an optional default value and an optional custom name.
The producer method for the injected parameters accesses the injection point to
read the default value and the name from the qualifier annotation of the parameter:


  @javax.inject.Singleton public class ParameterProducer {
 
   private Map<String, Integer> paramMapInt;
 private Map<String, String> paramMapString;
 
   @SuppressWarnings("unchecked") private <T extends Annotation> T getQualifier(Set<Annotation> annotations, Class<T> clazz) {
  for (Annotation qualifier : annotations) {
   if (qualifier.annotationType().equals(clazz)) {
    return (T) qualifier;
   }
  }
  return null;
 }

 @Produces @IntParameter public int createIntegerParameter(InjectionPoint ip) {
  IntParameter qualifier = getQualifier(ip.getQualifiers(), IntParameter.class);
  String parameterName = qualifier.name();
  if (parameterName == null || parameterName.isEmpty()) {
   parameterName = ip.getMember().getDeclaringClass().getSimpleName() + "." + ip.getMember().getName();
  }
  Integer configuredValue = paramMapInt.get(parameterName);
  if (configuredValue != null) {
   return configuredValue;
  } else {
   return qualifier.value();
  }
 }

 @Produces @StringParameter public String createStringParameter(InjectionPoint ip) {
  StringParameter qualifier = getQualifier(ip.getQualifiers(), StringParameter.class);
  String parameterName = qualifier.name();
  if (parameterName == null || parameterName.isEmpty()) {
   parameterName = ip.getMember().getDeclaringClass().getSimpleName() + "." + ip.getMember().getName();
  }
  String configuredValue = paramMapString.get(parameterName);
  if (configuredValue != null) {
   return configuredValue;
  } else {
   return qualifier.value();
  }
 }

  }
  

The configured parameters shall be configured by configuration providers implementing the following interface:


public interface ParameterProvider {
 
 public Map<String, Integer> getIntParameters();

 public Map<String, String> getStringParameters();

}
  

An example of a configuration provider using some hard coded values could be:


public class HardCodedParameterProvider implements ParameterProvider {

 
 @Override public Map<String, Integer> getIntParameters() {
  Map<String, Integer> params = new HashMap<String, Integer>();
  params.put("ExampleBean.value", 4711007);
  return params;
 }

 @Override public Map<String, String> getStringParameters() {
  Map<String, String> params = new HashMap<String, String>();
  params.put("ExampleBean.title", "Hello from Configuration Provider: "+this.getClass().getSimpleName());
  params.put("customParamName", "Configured-Parameter-Value");
  return params;
 }

}
  


To add the values of all found parameter providers we have to add some lines to the parameter producer:


  @javax.inject.Singleton public class ParameterProducer {
   
     @Inject Instance<ParameterProvider> parameterProviders;
   
     //...
     
 @PostConstruct public void init() {
  initParameters();
 }

 private void initParameters() {
  paramMapInt = new HashMap<String, Integer>();
  paramMapString = new HashMap<String, String>();
  for (ParameterProvider provider : parameterProviders) {
   Map<String, Integer> intParams = provider.getIntParameters();
   if (intParams != null) {
    paramMapInt.putAll(intParams);
   }
   Map<String, String> stringParams = provider.getStringParameters();
   if (stringParams != null) {
    paramMapString.putAll(stringParams);
   }
  }
 }

  }
  

To further play around with the system we add the possibility to maintain the configuration via JMX.
Therefore we make the parameter provider a managed bean by implementing the interface:


public interface ParameterMonitoringMXBean {

 public Map getIntegerParameters();

 public Map getStringParameters();

 public Integer getIntegerParameter(String parameterName);
 
 public String getStringParameter(String parameterName) ;
 
 public void setIntegerParameter(String parameterName, Integer value);
 
 public void setStringParameter(String parameterName, String value);

 public void resetIntegerParameter(String parameterName);
 
 public void resetStringParameter(String parameterName);

}
  

Furthermore we have to register the managed bean at the managed bean server:


@javax.inject.Singleton public class ParameterProducer implements ParameterMonitoringMXBean {

 @PostConstruct public void init() {
  initParameters();
  registerMxBean();
 }

 protected void registerMxBean() {
  try {
   MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
   ObjectName name = new ObjectName("com.jaw.cfg:type=Configuration");
   if (mbs.isRegistered(name)) {
    mbs.unregisterMBean(name);
   }
  } catch (Exception e) {
   throw new RuntimeException(e);
  }
 }

}
  

Of course the managed bean interface has to be implemented. Here we show only two methods as examples:


@javax.inject.Singleton public class ParameterProducer implements ParameterMonitoringMXBean {

 public Map<String, String> getStringParameters() {
  return paramMapString;
 }

 public void setStringParameter(String parameterName, String value) {
  paramMapString.put(parameterName, value);
 }

}
  

Now its possible to view and manipulate the configuration of the parameters e.g. using the MBeans view in the jConsole.
One last step is to provide the possibility to observe an event indicating a parameter changed via JMX:


public class ParameterChanged {

 private String parameterName;
 private Object oldValue;
 private Object newValue;

 public ParameterChanged(String parameterName, Object oldValue, Object newValue) {
  this.parameterName = parameterName;
  this.oldValue = oldValue;
  this.newValue = newValue;
 }

   // ... getters and setters omitted...

}
  

All JMX methods changing an parameter have to fire the event:


@javax.inject.Singleton public class ParameterProducer implements ParameterMonitoringMXBean {

 @Inject Event<ParameterChanged> parameterChangedEvent;

 public void sendParameterChanged(String parameterName, Object oldValue, Object newValue) {
  Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
  parameterChangedEvent.fire(new ParameterChanged(parameterName, oldValue, newValue));
 }


 public void setStringParameter(String parameterName, String value) {
  sendParameterChanged(parameterName, paramMapString.get(parameterName), value);
  paramMapString.put(parameterName, value);
 }

}
  

Note that setting the context class loader before sending the event is needed, because by default JMX uses another class loader than the application.
An example client observing the fired event may look like:


  @Singleton public class ParameterChangeListener {

 @Inject Logger log; // from somewhere...
 
 public void onParameterChange(@Observes ParameterChanged pc) {
  log.info("parameter "+pc.getParameterName()+" changed from "+pc.getOldValue()+" to "+pc.getNewValue());
 }
  

Note that the observer method must not be defined at session or request scope since JMX calls do not create a
context for these scopes.