Wenn man einmal die Vorteile einer BeanFactory ala Spring kennengelernt hat, will man deren Vorteile nicht mehr missen. Es ist einfach viel angenehmer – insbesondere auch im Hinblick auf die Testbarkeit – wenn man Klassen als POJOs entwirft, die Eigenschaften besitzen, die von aussen gesetzt werden (Dependency Injection). Nun will man aber nicht immer einen "Container" wie Spring bemühen, selbst wenn Spring modular aufgebaut ist und leicht die Verwendung nur von Teilen erlaubt. Konkret wollte ich Dependency Injection sogar gerne in einem Appet nutzen, wofür mir Spring zu gross erschien. Ergebnis war eine eigene kleine "BeanFactory" ala Spring …
Die Features welche ich in typischen Anwendungen brauche:
- Konfiguration über Config-Datei (vorzugeweise XML).
- Zuweisen von Properties
- Zuweisen von Referenzen auf andere Beans
- Beans als Singleton
- Init-Methode nach dem Setzen der Properties
- Die Factory selbst als Singleton
Alle diese Features sind in der einfachen Klasse "SimpleBeanFactory" realisiert:
package com.rinke.solutions.beans;
import java.beans.PropertyDescriptor;
import java.beans.PropertyEditor;
import java.beans.PropertyEditorManager;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;/**
* simple bean factory.
* @author sr
*/
public class SimpleBeanFactory extends DefaultHandler {/**
* stores singletons
*/
private Map<String, Object> singletons = new HashMap<String, Object>();
/**
* stores bean definitions
*/
private Map<String, BeanDefinition> beanDefs = new HashMap<String, BeanDefinition>();/**
* state of parser
*/
private String aktbean;
/**
* part of bean definition: a setter call for init.
* @author sr
*/
private class SetterCall {
/**
* setter to call.
*/
Method setter;
/**
* immediate value to set.
*/
Object value;
/**
* alternatively: bean ref to set.
*/
String ref;
/**
* ctor using fields
* @param setter setter method to call
* @param value value to set
* @param ref or bean ref to set
*/
public SetterCall(Method setter, Object value, String ref) {
this.setter = setter;
this.value = value;
this.ref = ref;
}
}
/**
* bean definition consists of singleton flag, class, optional init method
* and a list of setter calls for initialization.
* @author sr
*/
private class BeanDefinition {
boolean isSingleton;
Class clazz;
Method initMethod;
List<SetterCall> inits = new ArrayList<SetterCall>();
public BeanDefinition(boolean isSingleton, Class clazz, Method initMethod) {
this.isSingleton = isSingleton;
this.clazz = clazz;
this.initMethod = initMethod;
}
}/**
* the factory singleton
*/
private static SimpleBeanFactory theInstance;
/**
* accessor for the factory
* @return the factory singleton
*/
public static SimpleBeanFactory getInstance() {
if( theInstance == null ) {
theInstance = new SimpleBeanFactory(SimpleBeanFactory.class.getResourceAsStream("/context.xml"));
}
return theInstance;
}
/**
* ctor using input stream
* @param is
*/
public SimpleBeanFactory(InputStream is) {
parse(is);
}/**
* parser handler
* @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
*/
@Override
public void startElement(String uri, String localName, String qName,
Attributes attributes) throws SAXException {
try {
if (qName.equals("bean")) {
aktbean = attributes.getValue("id");
if( beanDefs.get(aktbean) != null ) {
throw new IllegalArgumentException("duplicate bean name " + aktbean);
}
String classname = attributes.getValue("class");
String initMethodName = attributes.getValue("init-method");
Method initMethod = null;
if( initMethodName != null ) {
initMethod = Class.forName(classname).getMethod(initMethodName,new Class[]{});
}
Class clazz = Class.forName(classname);
beanDefs.put(aktbean, new BeanDefinition("true".equals(attributes.getValue("singleton")),
clazz,initMethod));
} else if (qName.equals("property")) {
String name = attributes.getValue("name");
PropertyDescriptor pd = new PropertyDescriptor(name, beanDefs.get(aktbean).clazz);
// has it a value
String value = attributes.getValue("value");
if( value != null ) {
PropertyEditor pe = PropertyEditorManager.findEditor(pd.getPropertyType());
pe.setAsText(value);
beanDefs.get(aktbean).inits.add(new SetterCall(pd.getWriteMethod(),pe.getValue(),null));
}
// has it a reference (must already defined).
String ref = attributes.getValue("ref");
if( ref != null ) {
beanDefs.get(aktbean).inits.add(new SetterCall(pd.getWriteMethod(),null,ref));
}
}
} catch (Exception e) {
throw new IllegalArgumentException("exception parsing bean " + aktbean, e);
}
}// merge get / create
public Object getBean(String id) {
if( singletons.get(id) != null ) {
return singletons.get(id);
} else {
BeanDefinition def = beanDefs.get(id);
Object bean = null;
try {
bean = def.clazz.newInstance();
for (SetterCall call : def.inits) {
Object v = call.value != null ? call.value : getBean( call.ref );
call.setter.invoke(bean,new Object[] {v});
}
if( def.initMethod != null ) {
def.initMethod.invoke(bean,new Object[]{});
}
} catch (Exception e) {
throw new IllegalArgumentException("error creating bean "+ id,e);
}
if( def.isSingleton ) {
singletons.put(id,bean);
}
return bean;
}
}/**
* parse config and init factory instance.
* @param is
*/
private void parse(InputStream is) {
try {
SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
parser.parse(is, this);
} catch (Exception e) {
throw new IllegalArgumentException("parser error",e);
}}
}
Die Syntax der Konfiguration ist einfach und entspricht weitgehend der von Spring. Ein Bespiel erläutert alle wesentlichen Elemente:
<beans>
<bean id="bean1" class="test.MyMessageBean" singleton="true" init-method="init">
<property name="message" value="Hello World" />
</bean>
<bean id="bean2" class="test.Sender">
<property name="message" ref="bean1" />
</bean>
</beans>
Das Beispiel ist für Spring Kenner selbsterklärend. Es werden genauso wie bei Spring PropertyEditors benutzt, um vom String zum Object zu kommen, d.h. wann immer ein PropertyEditor registriert ist, kann der Wert im Value-Attribut verarbeitet werden.