EJB: Server vs. Applikation
Suns J2EE Blueprints stellen die wohl bekannteste Architekturempfehlung für J2EE-Applikationen dar. Der Kern dieser Architektur ist striktes, potenziell physisches Layering: Die Web Tier (Servlet, JSP) ist nur eine dünne Fassade für die Middle Tier (EJB); jegliche Business-Logik soll als EJB-Komponente implementiert werden. Ein besonderer Fokus von EJB und den Blueprints liegt auf verteilten Komponenten: In EJB 1.x mussten alle Komponenten mit Remote Interfaces geschrieben werden, auch wenn sie letztendlich oft in derselben Virtual Machine wie ihre Clients liefen. EJB 2.0 führte Local Interfaces ein, um Komponenten mit lokaler Aufrufsemantik zu ermöglichen. Eine interessanter Punkt ist hierbei die Trennung der Verantwortung zwischen Server und Applikation. J2EE ist naturgemäß nicht nur eine Plattform mit serverseitigem Fokus, sondern eine rein serverorientierte Plattform, die Business-Komponenten vollständig in die Verantwortung des Servers überführt. Applikationen bestehen aus Komponenten, von denen jede einzelne dem Server über Deployment Descriptors bekannt gegeben wird; der Server verwaltet deren Konfiguration und bestimmt ihren Lebenszyklus.Über gewisse Komponenten muss der J2EE-Server Bescheid wissen, nämlich über die Einstiegspunkte der Applikation: Der Server verwaltet sowohl die HTTP Sockets für den Webbereich als auch die RMI Sockets für Remote EJBs; für beide besitzt der Server Thread Pools. Bei einer konkreten Anfrage muss der Server die entsprechende Applikation und darin wiederum die entsprechende Komponente aufrufen können. Dazu benötigt er passende Informationen in Form von Deployment Descriptors.
Local EJBs hingegen bezeichnen keine Einstiegspunkte in die Applikation. Methoden von lokalen EJBs nehmen immer am Thread des Aufrufers teil; es ist hier kein eigener Thread Pool des Servers notwendig. Der wichtigste Dienst, den der J2EE-Server dennoch für lokale EJBs anbietet, ist deklaratives Transaktionsmanagement. Es ist aber keine Form von Ressourcenmanagement involviert, die ein zentraler Server übernehmen müsste. Daher gibt es hier interessante Alternativen, wie wir sehen werden.
Komponenten, die keinen Einstiegspunkt in die Applikation darstellen, können problemlos als POJOs (Plain Old Java Objects) modelliert werden, z.B. für eine lokales Middle Tier im Rahmen einer Webapplikation. Anstatt Local EJBs werden hier einfach POJOs verwendet, die dem J2EE-Server nicht bekannt gegeben, sondern innerhalb der Applikation konfiguriert werden. Diesen Architekturansatz werden wir nun genauer betrachten.
Wiederverwendbare Business-Logik
Business-Logik für die klassische J2EE - implementiert als EJBs - ist inhärent an einen J2EE-Server gebunden: Sie ist ohne Server nicht lauffähig. Während dies für Webkomponenten - implementiert als Servlets oder JSPs - völlig einsichtig ist, liegen die Kriterien bei der Business-Logik anders: Warum sollte die Implementierung einer Produktsuche oder ein Preisbestimmungsdienst nur innerhalb eines J2EE-Servers laufen können?Nicht nur die Implementierung als EJB, auch das Auffinden von Ressourcen über JNDI bindet eine Komponente an den J2EE-Server, da andere Laufzeitumgebungen üblicherweise nicht über einen JNDI-Dienst verfügen. Wenn eine Komponente also ihre JDBC Data Source an der JNDI Location java:comp/env/jdbc/myds erwartet, kann diese Bedingung nur in J2EE-Umgebungen auf natürliche Weise erfüllt werden.
Zum Beispiel könnte ein klassisches J2EE-Datenzugriffsobjekt folgendermaßen aussehen. Es gelangt durch einen JNDI-Aufruf an die JDBC Data Source und ist als Singleton geschrieben. Typischerweise wird es von einem EJB verwendet (Listing 1).
Listing 1
public class MyDaoImpl implements MyDao {private static MyDao instance;static {instance = new MyDaoImpl();}public static MyDao getInstance() {return instance;}private DataSource dataSource;private void MyDaoImpl() {try {InitialContext ic = new InitialContext();this.dataSource = (DataSource) ic.lookup("java:comp/env/jdbc/myds");}catch (NamingException ex) {throw new MyInitException("Could not fetch DataSource from JNDI", ex);}}public void deleteAllImages() throws SQLException {Connection con = this.dataSource...try {...}finally {con.close();}}}
Das MyDao-Interface definiert hierbei die verfügbaren Datenzugriffsoperationen. Aufrufer sind zwar durch die getInstance()-Methode des Singletons an die Implementierungsklasse gebunden, arbeiten aber beim Aufruf von Operationen mit dem Interface.
public interface MyDao {void deleteAllImages() throws SQLException;}
Natürlich kann man einwerfen, dass gewisse Komponenten von vornherein ausschließlich in einer J2EE-Umgebung laufen werden und sich daher ohne Bedenken auf JNDI verlassen können. Dies trifft allerdings für zwei wichtige Bereiche nicht zu: JUnit-Testsuites und Standalone-Applikationen (z.B. auf Swing-Basis). Preisbestimmungsregeln sollten in einer Testsuite in Isolation getestet werden können und ein Standalone-Bestellerfassungsprogramm sollte offline dieselbe Implementierung des Preisbestimmungsdienstes verwenden können wie das entsprechende Serversystem.
Idealerweise sollte die Business-Logik also in einer Form implementiert werden, die Wiederverwendung in jeglicher Umgebung erlaubt. Eine Voraussetzung hierfür ist, dass die Komponente keine unnötigen Annahmen über ihre Umgebung trifft, also insbesondere weder mit EJB noch mit JNDI-API arbeitet.
Das Dependency-Injection-Prinzip
Um Springs Architekturprinzipien zu illustrieren, werden wir nun grundsätzliche Modellierungsaspekte im Detail betrachten. Wie sollen Applikationsobjekte aufgebaut sein, konfiguriert und verwaltet werden, um einen möglichst hohen Grad an Wiederverwendbarkeit zu erreichen? Um eine Komponente von ihrer Umgebung zu entkoppeln, können Abhängigkeiten (Dependencies) von außen in sie injiziert werden, anstatt sie von der Komponente selbst auffinden zu lassen. Die Konfiguration und der Lebenszyklus der Komponente werden also von außen verwaltet - typischerweise, aber nicht notwendigerweise durch einen Lightweight Container.Wenn das Datenzugriffsobjekt aus dem vorherigen Abschnitt nach dem Dependency-Injection-Prinzip modelliert wird, erwartet es, die Data Source übergeben zu bekommen. Der typische Weg hierzu ist ein JavaBean Property namens dataSource, also eine Setter Method namens setDataSource. Diese Variante heißt Setter Injection (Listing 2). Alternativ kann auch ein Konstruktor-Argument benutzt werden, nach dem so genannten Constructor-Injection-Prinzip (Listing 3).
Listing 2
public class MyDaoImpl implements MyDao {private DataSource dataSource;public void setDataSource(DataSource dataSource) {this.dataSource = dataSource;}public void deleteAllImages() throws SQLException {Connection con = this.dataSource...try {...}finally {con.close();}}}
Listing 3
public class MyDaoImpl implements MyDao {private final DataSource dataSource;public MyDaoImpl(DataSource dataSource) {this.dataSource = dataSource;}public void deleteAllImages() {Connection con = this.dataSource...try {...}finally {con.close();}}}
Das allgemeinere Prinzip hinter Dependency Injection heißt Inversion of Control (IoC), auch genannt das Hollywood Principle: Don't call us, we'll call you. Applikationsobjekte sollen nicht - dem klassischem Kontrollfluss gemäß - sich selbst aktiv konfigurieren, sondern sich auf ordnungsgemäße Konfiguration von außen verlassen. Das MyDao-Interface definiert immer noch nur die deleteAllImages()-Methode. Konfigurationsmethoden wie setDataSource sind nur auf der Implementierungsklasse definiert; sie können sich abhängig von der Implementierungsstrategie ändern. Aufrufer verwenden nur das MyDao-Interface, ohne sich etwaiger Konfigurationsmethoden bewusst zu sein. Die obige Klasse kann nun in vielerlei Weise verwendet werden, da sie keinerlei unnötige Annahmen über die Umgebung trifft. Als einfachsten Fall kann sie über direkte Instanziierung verwendet werden, z.B. mit einer Jakarta Commons DBCP Data Source:
public static void main(String[] args) {BasicDataSource dataSource = new BasicDataSource();dataSource.setUrl("jdbc:mysql://localhost:3306/mydb");...MyDaoImpl myDao = new MyDaoImpl();myDao.setDataSource(dataSource);myDao.deleteAllImages();}
Beziehungsweise im Falle eines Konstruktorargumentes:
public static void main(String[] args) {BasicDataSource dataSource = new BasicDataSource();dataSource.setUrl("jdbc:mysql://localhost:3306/mydb");...MyDao myDao = new MyDaoImpl(dataSource);myDao.deleteAllImages();}
Auf demselben Weg kann auch eine Data Source über JNDI aufgefunden und dem DAO übergeben werden. Dies ist externalisiert; es ist nicht mehr die Aufgabe des DAO selbst, einen JNDI Lookup durchzuführen.
public static void main(String[] args) throws NamingException {InitialContext ic = new InitialContext();DataSource dataSource = (DataSource) ic.lookup("java:comp/env/jdbc/myds");...MyDaoImpl myDao = new MyDaoImpl();myDao.setDataSource(dataSource);myDao.deleteAllImages();}
Ein- und dieselbe DAO-Klasse kann also sowohl mit einer lokalen Data Source arbeiten als auch mit einer über JNDI aufgefundenen. In einer Testumgebung könnte sie auch mit einer Mock Data Source arbeiten, also einer Dummy-Implementierung, die nicht auf eine echte Datenbank zugreift. Alles, was die DAO-Klasse erwartet, ist, eine Implementierung des Data-Source-Interfaces zur Initialisierungszeit zu erhalten.
Obwohl der obige Beispielcode ein zentrales Konzept in Spring illustriert und im Prinzip lauffähig ist, wurde noch kein einziges Spring-API verwendet - weder in der Implementierung der MyDaoImpl-Klasse noch in dem Beispielcode zur Konfiguration eines MyDao-Objektes! Dies illustriert eine wichtige Eigenschaft von Dependency Injection: Dieses Prinzip arbeitet so weit wie möglich mit Plain-Java-Konstrukten anstatt mit frameworkspezifischen Callback-Interfaces.
Spring als Lightweight Container
Wie wir gesehen haben, können Objekte, die nach dem Dependency-Injection-Prinzip modelliert wurden, problemlos als normale Java-Objekte - POJOs - verwendet werden. Es ist kein Server oder Container notwendig, um sie auszuführen. Dies ist für einige Szenarien völlig ausreichend, vor allem für Testumgebungen. Einige Probleme werden allerdings schnell offensichtlich: Wo wird der Initialisierungscode registriert? Wo werden Referenzen zu den Objekten gehalten, nachdem sie initialisiert wurden? Woher wird die Konfiguration bezogen? All diese Bereiche werden durch einen Lightweight Container abgedeckt, also durch Infrastruktur, die solche Komponenten in jeder Art von Umgebung verwalten kann. Ein solcher Container ist typischerweise in wenigen Zeilen Code erzeugbar; wir sprechen hier nicht von einem Server, sondern einfach von einer Library. Das obige Beispiel könnte in Spring wie in Listing 4 aussehen: Die Implementierung der Klasse bleibt, wie sie ist, mit einem Bean Property oder einem Konstruktorargument. Die Instanzen werden jedoch in einem XML-File konfiguriert. Falls die MyDaoImpl-Klasse ein Konstruktorargument erwartet, muss nur die Bean-Definition myDao entsprechend angepasst werden (Listing 5).Listing 4
<beans><bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource"><property name="url"><value>jdbc:mysql://localhost:3306/mydb</value></property>...</bean><bean id="myDao" class="mypackage.MyDaoImpl"><property name="dataSource"><ref bean="myDataSource"/></property></bean></beans>
Listing 5
<beans><bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource"><property name="url"><value>jdbc:mysql://localhost:3306/mydb</value></property>...</bean><bean id="myDao" class="mypackage.MyDaoImpl"><constructor-arg index="0"><ref bean="myDataSource"/></constructor-arg></bean></beans>
Um die Data Source via JNDI zu beziehen, kann die Definition myDataSource durch Springs JndiObjectFactoryBean ersetzt werden, welche das angegebene Objekt über JNDI aufsucht und es für Referenzen anbietet (anstatt einer Referenz auf ein lokales Objekt) (Listing 6).
Listing 6
<beans><bean id="myDataSource"class="org.springframework.jndi.JndiObjectFactoryBean"><property name="jndiName"><value>java:comp/env/jdbc/myds</value></property></bean><bean id="myDao" class="mypackage.MyDaoImpl"><property name="dataSource"><ref bean="myDataSource"/></property></bean></beans>
XML-Bean-Definitionen sind nicht der einzige Weg, wie Beans in Spring definiert werden können, aber der mit Abstand beliebteste. Im einfachsten Fall kann ein derartiges Definitions-File folgendermaßen geladen werden, z.B. wenn es als applicationContext.xml im Classpath liegt. Wir nehmen an, dass darin eine lokale Data Source definiert ist - womit keine JNDI-Umgebung notwendig ist.
public static void main(String[] args) {BeanFactory beanFactory =new XmlBeanFactory(new ClassPathResource("/applicationContext.xml"));MyDao myDao = (MyDao) beanFactory.getBean("myDao");myDao.deleteAllImages();}
Das MyDao-Objekt, das hier zurückgeliefert wird, ist fertig vorkonfiguriert, hat also bereits eine Referenz zu seiner Data Source erhalten. Der aufrufende Code muss sich nicht um die Konfiguration des Objektes kümmern, sondern kann es einfach über das MyDao-Interface verwenden, ohne die Implementierungsklasse zu kennen. Falls nicht anders angegeben, verwaltet die BeanFactory eine gegebene Bean als Singleton; wiederholte Aufrufe der getBean-Methode liefern also dieselbe Instanz zurück.
Eine Spring XmlBeanFactory kann in jeder Umgebung verwendet werden, da sie beliebige Arten von File-Ressourcen laden kann und ansonsten keine Anforderungen stellt. Durch ihren kleinen Footprint und ihre kurze Startup-Zeit ist sie auch ideal für Testsuites geeignet.
Spring als Web Application Context
Spring bietet speziellen Support für typische Laufzeitumgebungen an. Eine der wichtigsten Umgebungen ist eine J2EE-Webapplikation: Spring kann hier eine lokale Middle Tier verwalten, als Ersatz für lokale EJBs - eines der häufigsten Einsatzgebiete. Obwohl für typische Webapplikationen nicht relevant, enthält Spring auch EJB-Zugriffsklassen, ähnlich der oben gezeigten JndiObjectFactoryBean, und Support-Klassen für die Implementierung von EJBs. In letzterem Szenario arbeitet eine Spring BeanFactory hinter einer EJB-Fassade, z.B. im Falle einer Remote EJB.Die Komponenten einer lokalen Middle Tier in einer Webapplikation werden in einem oder mehreren XML-Bean-Definitions-Files konfiguriert und mittels Springs ContextLoaderListener (oder ContextLoaderServlet) geladen. Der entsprechende web.xml-Eintrag sieht folgendermaßen aus:
<web-app><listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener>...</web-app>
Wenn kein spezieller Parameter angegeben ist, wird das XML-Bean-Definitions-File /WEB-INF/applicationContext.xml geladen. Unter der Annahme, dass dort das oben verwendete applicationContext.xml-File mit den Bean-Definitionen myDataSource und myDao liegt, kann auf die definierten Beans wie folgt zugegriffen werden:
WebApplicationContext context =WebApplicationContextUtils.getWebApplicationContext(servletContext);MyDao myDao = (MyDao) context.getBean("myDao");
Alles, was hierfür notwendig ist, ist eine Referenz auf den ServletContext: Dieser ist z.B. in jedem Servlet (getServletContext()), jeder JSP (application) und jeder Struts Action (getActionServlet().getServletContext()) verfügbar. Die von Spring verwalteten Middle-Tier-Komponenten können demnach in jeder Art von Webressource problemlos angesprochen werden. Insbesondere ist die Verwendung eines beliebigen Web MVC Frameworks möglich.
Das WebApplicationContext-Interface stellt eine Erweiterung des oben vorgestellten BeanFactory-Interfaces dar, mit zusätzlicher abstrahierter Funktionalität wie einer MessageSource und einem ResourceLoader, auf die wir im Rahmen dieses Artikels allerdings nicht eingehen werden.
Der Spring WebApplicationContext kann in gewisser Hinsicht als Ersatz für JNDI betrachtet werden: Er erlaubt einfachen Zugriff auf benannte Middle-Tier-Ressourcen. Der Unterschied ist, dass ein WebApplicationContext kein systemweiter Service ist, sondern eine leichtgewichtige Factory mit Singleton Cache innerhalb der Applikation, deren Definitionen in jeder Art von Umgebung geladen werden können. Ein WebApplicationContext kann jedoch problemlos JNDI-Objekte über den oben vorgestellten JndiObjectFactoryBean referenzieren.
Web MVC Frameworks
Da kein Web MVC Framework vergleichbare Funktionalität anbietet, ist die Kombination eines gängigen Web-Frameworks mit einer lokalen, Spring-verwalteten Middle Tier sehr beliebt: z.B. mit Struts, WebWork1, WebWork2, Tapestry, JSF (Java Server Faces). WebWork2 und JSF bieten zwar selbst IoC-Funktionalität für Webobjekte, jedoch wesentlich limitierter und ohne Middle-Tier-Funktionalität. Daher stellt Spring auch für diese beiden eine gute Middle-Tier-Lösung dar. Spring bietet für Struts zusätzlich einfache Helper-Klassen, die den Zugriff auf von Spring verwaltete Beans einfacher machen als der gezeigte manuelle Weg über den ServletContext. Derartige Support-Klassen sind auch für JSF verfügbar; WebWork2 enthält ähnliche Klassen im Rahmen der XWork Extensions.Spring bietet darüber hinaus ein eigenes, sehr mächtiges Web MVC Framework, das mit denselben Konfigurationsmitteln wie die oben gezeigten Beispiele arbeitet. Dieses Web-Framework ist Teil der Spring-Core-Distribution und sehr populär, jedoch völlig unabhängig von Springs Middle-Tier-Funktionalität. Wie wir gesehen haben, kann auch jede andere Webressource auf Spring-verwaltete Middle-Tier-Komponenten zugreifen.
Da das Thema dieses Artikels lokale Middle-Tiers sind, werden wir nicht weiter auf Springs Web MVC Framework eingehen. Die Spring-Distribution enthält mehrere lauffähige Webapplikationen als Beispiele, wobei die meisten - wie Petclinic - Springs eigenes Web MVC Framework verwenden. JPetStore bietet hingegen alternative Web Tiers für Spring Web MVC und Struts 1.1; es eignet sich daher gut zum Studium der Web-MVC-Besonderheiten.
AOP mit Spring
Über den Core-Container hinaus bietet Spring ein breites Sortiment an Middle-Tier-Funktionalität. Das Ziel ist hierbei nicht, Alternativen zu J2EE-Diensten wie JTA zu bieten, sondern hauptsächlich, vereinfachte Zugriffsmodelle auf diese Dienste zur Verfügung zu stellen. Durch die von Spring angebotene Entkopplung werden J2EE-Dienste jedoch so weit optional, dass sie nur verfügbar sein müssen, wenn sie auch tatsächlich gebraucht werden: z.B. im Falle von JTA, wenn verteilte Transaktionen durchgeführt werden.Eines der beliebtesten Middle-Tier-Features ist deklaratives Transaktionsmanagement für POJOs. Dies baut auf zwei anderen Teilen von Spring auf, nämlich der Transaktionsabstraktion (die auch programmatisch genutzt werden kann) auf der einen Seite und dem AOP-Framework auf der anderen Seite. Aspect-Oriented Programming (AOP) ist einer der großen Hypes der Jahre 2003 und 2004 - zurecht! Das Grundprinzip ist, so genannte cross-cutting concerns zu identifizieren und als Aspekte zu modellieren, die auf beliebige Zielobjekte angewendet werden können. Es gibt einige AOP-Frameworks auf dem Markt, mit zum Teil sehr unterschiedlichem Fokus: AspectJ (IBM), AspectWerkz (BEA), JBoss AOP, Nanning, DynaOp.
Wir werden in diesem Artikel nur auf ein Beispiel für Proxy-basiertes AOP eingehen, wobei Aspekte mittels Dynamic Proxies auf Zielobjekte angewendet werden. Dies ist der Weg, den Nanning, DynaOp und eben auch Spring AOP gewählt haben, um einfache Integration in bestehende Umgebungen zu gewährleisten. Zur Erzeugung von Proxies werden entweder JDK 1.3 Proxies (java.lang.reflect.Proxy, nur für Interfaces) oder CGLIB Proxies (auch für Klassen, die vollständig ansprechbar sein sollen) verwendet.
Andere Lösungen wie JBoss AOP arbeiten hingegen auf der Classloader-Ebene, um Aspekte automatisch anwenden zu können. Dies bedingt Controller über den Classloader, was in J2EE-Applikationen nur durch den J2EE-Server möglich ist. Daher sind diese Lösungen in einem J2EE-Umfeld problematisch zu integrieren bzw. an einen bestimmten J2EE-Server gebunden. Der Proxy-basierte Ansatz kennt hingegen keine derartigen Probleme.
Deklarative Transaktionen via AOP
Viele Spring-Applikationen verwenden keine eigenen Aspekte; als Applikationsentwickler muss man sich in diesem Fall nie mit den Details von AOP beschäftigen. Spring liefert hingegen einige vorgefertigte Aspekte mit (z.B. einen TransactionInterceptor), die durch bloße Konfiguration angewendet werden können. Zur Illustration werden wir deklarative Transaktionen zu unserem obigen Beispiel hinzufügen. Wir definieren hierzu zwei zusätzliche Beans: einen Spring PlatformTransactionManager und ein transaktionales Proxy für das MyDao-Objekt, das jeden Methodenaufruf im Rahmen einer Transaktion ausführt (Listing 7).Listing 7
<beans><bean id="myDataSource"class="org.springframework.jndi.JndiObjectFactoryBean"><property name="jndiName"><value>java:comp/env/jdbc/myds</value></property></bean><bean id="myTransactionManager"class="org.springframework.transaction.jta.JtaTransactionManager"/><bean id="myDaoTarget" class="mypackage.MyDaoImpl"><property name="dataSource"><ref bean="myDataSource"/></property></bean><bean id="myDao" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"><property name="transactionManager"><ref bean="transactionManager"/></property><property name="target"><ref bean="myDaoTarget"/></property><property name="proxyInterfaces"><value>mypackage.MyDao</value></property><property name="transactionAttributes"><props><prop key="*">PROPAGATION_REQUIRED</prop></props></property></bean></beans>
Weder die MyDaoImpl-Klasse noch der zugreifende Web-Tier-Code müssen geändert werden: Der Transaktionsaspekt wird durch bloße Konfiguration auf das MyDao-Objekt angewendet. Das eigentliche Zielobjekt heißt nun myDaoTarget (eine reine Namenskonvention); unter dem Namen myDao ist ein Proxy verfügbar, das dieselbe Schnittstelle anbietet. Jeder Methodenaufruf auf dem Proxy wird an myDaoTarget delegiert, jedoch innerhalb einer aktiven Transaktion.
Da die MyDaoImpl-Klasse das MyDao-Interface implementiert, wird Springs TransactionProxyFactoryBean automatisch ein JDK 1.3 Dynamic Proxy erzeugen, das ebenfalls die MyDao-Schnittstelle anbietet. Falls das Zielobjekt kein derartiges Interface implementiert, erzeugt Springs AOP-Framework ein CGLIB-Proxy, das die vollständige Schnittstelle der Implementierungsklasse zur Verfügung stellt. Es ist allerdings generell empfehlenswert, mit Interfaces zu arbeiten.
Das Grundprinzip hinter dieser Art von Transaktionsmanagement ist im Wesentlichen identisch mit deklarativen Transaktionen bei lokalen EJBs. Die Bezeichnungen für unterschiedliches Transaktionsverhalten (PROPAGATION_REQUIRED, PROPAGATION_REQUIRES_NEW etc.) sind an EJB angelehnt. Spring unterstützt alle sechs Transaktionsattribute, die EJB definiert, plus zusätzlich PROPAGATION_NESTED für Nested Transactions.
Der größte Unterschied zu EJB ist, dass Spring deklarative Transaktionen auf beliebige POJOs anwenden kann, ohne die Komponenten explizit in einen EJB-fähigen J2EE-Server einspielen zu müssen. Die obige Konfiguration kann problemlos in jedem J2EE Web Application Server mit JTA-Unterstützung - aber ohne EJB! - ausgeführt werden, z.B. in Resin oder Weblogic Express. Außerdem kann beim Entwickeln einfach neu kompiliert und neu gestartet werden; es ist kein Redeployment notwendig.
Verschiedene Transaktionsstrategien
Spring bietet auch Alternativen zur im Beispiel verwendeten JtaTransactionManager-Strategie: Implementierungen von Springs PlatformTransactionManager-Interface stehen für eine einzelne JDBC Data Source, Hibernate Session Factory, JDO Persistence Manager Factory bzw. einen einzelnen OJB Persistence Broker Descriptor zur Verfügung. Diese so genannten lokalen Transaktionsstrategien können nur Transaktionen für eine einzige Datenbank bearbeiten (im Gegensatz zu globalen JTA-Transaktionen), was aber in vielen Fällen ausreichend ist.Der große Vorteil von lokalen Transaktionsstrategien ist, dass sie ohne JTA-Implementierung arbeiten können, z.B. in Tomcat, in einer Testsuite oder in einer Standalone-Applikation - solange nur eine einzige Datenbank angesprochen wird. Spring erlaubt hierbei, durch Konfiguration zwischen JTA und einer lokalen Strategie zu wechseln: Es kann derselbe DAO-Implementierungs-Code und aufrufende Code verwendet werden. Die Konfiguration kann also einfach an die tatsächliche Umgebung angepasst werden.
Lokale Transaktionsstrategien bieten darüber hinaus mehr spezielle Funktionalität als ein verteilter Transaktionsmanager mit JTA-API, was dadurch möglich ist, dass genauer auf die einzelne Zielressource eingegangen werden kann. Hierzu gehören transaktionsspezifische JDBC Isolation Levels und Support für Nested Transactions (via JDBC 3.0 Savepoints), die beide vom Standard JTA nicht angeboten werden.
Es ist auch erwähnenswert, dass JTA nicht unbedingt an einen J2EE-Server gebunden ist. Es gibt Standalone-JTA-Implementierungen, die relativ problemlos in Nicht-J2EE-Umgebungen verwendet werden können: z.B. ObjectWebs JOTM (in Kombination mit XAPool). Springs JtaTransactionManager kann reibungslos mit JOTM verwendet werden; Spring bietet für JOTM und XAPool darüber hinaus spezielle Support-Klassen.
Springs Unterstützung für DAOs
Ein weiteres Middle-Tier-Feature von Spring ist die Unterstützung für diverse Datenzugriffsstrategien bzw. für die Abstraktion von DAOs. Spring bietet eine generische DataAccessException-Hierarchie, die von RuntimeException abgeleitet ist und dadurch nicht deklariert werden muss (aber trotzdem deklariert werden kann!). Datenzugriffsfehler sind üblicherweise fatal, können also nur in Ausnahmefällen behandelt werden. Diese generische Exception-Hierarchie vermeidet es, spezifische Exceptions wie SQLException in DAO-Interfaces deklarieren zu müssen, was ja die Interfaces an die Implementierungsstrategie binden würde.Für JDBC, Hibernate, JDO, Apache OJB und iBATIS SQL Maps liefert Spring vorgefertigte DAO-Support-Klassen mit, die Ressourcenverwaltung, Transaktionsteilnahme und Exception-Konvertierung (in Springs DataAccessException-Hierarchie) übernehmen. Natürlich können DAOs auch manuell geschrieben werden; die Support-Klassen erleichtern nur die Arbeit und verhindern Fehler durch Copying-and-Pasting von repetitivem Code.
Für unser MyDao-Beispiel könnte dies folgendermaßen aussehen. Die deleteAllImages()-Methode deklariert nun DataAccessException statt, wie zuvor, SQLException:
public interface MyDao {public void deleteAllImages() throws DataAccessException;}
public class MyDaoImpl implements MyDao {private JdbcTemplate jdbcTemplate;public void setDataSource(DataSource dataSource) {this.jdbcTemplate = new JdbcTemplate(dataSource);}public void deleteAllImages() throws DataAccessException {this.jdbcTemplate.update("DELETE * FROM imagedb");}}
Trotz der einzeiligen Datenzugriffsoperation werden hier alle benötigten Ressourcen korrekt geöffnet und geschlossen, es wird korrekt an Transaktionen teilgenommen automatisch in Springs DataAccessException-Hierarchie übersetzt. Die Template-Klassen für Hibernate, JDO, OJB und iBATIS SQL Maps erlauben ebenfalls in vielen Fällen einzeilige Datenzugriffsoperationen. Die in der Spring-Distribution enthaltenen Beispielapplikationen zeigen einige der Strategien in action: Petclinic hat z.B. alternative Data Access Tiers für Hibernate, OJB und JDBC, während JPetStore iBATIS SQL Maps verwendet.
Fazit
Der Kern des Spring-Frameworks ist ein Komponentenmodell für lokale Middle Tiers, das auf POJOs aufbaut und explizites Deployment à la EJB von vornherein vermeidet. Da Spring auch deklarative Transaktionen und frei definierbare Aspekte auf POJOs anwenden kann, bietet es eine vollständige Alternative zu lokalen EJBs, die wesentlich einfachere Entwicklung und flexiblere Wiederverwendung ermöglichen. Ein und dasselbe Komponentenmodell kann hierbei für grob granulare Fassadenkomponenten wie auch für fein granulare Applikationsobjekte eingesetzt werden.Dieser Artikel hat nur einen Teil der Funktionalität, die Spring bietet, vorgestellt: das Komponentenmodell, den Web Application Context, deklarative Transaktionen und DAO-Unterstützung. Für weitere Informationen über das Web MVC Framework, den EJB-Support, Lightweight Remoting, MessageSources, die Mail-Abstraktion, die Quartz-Integration etc. stehen das Spring-Referenzhandbuch und die Beispielapplikationen zur Verfügung.
Jürgen Höller ist Mitbegründer und Co-Lead des Spring Framework Project und Co-Autor des Buches J2EE Development without EJB (Wiley, 2004). Er befasst sich seit 1999 mit J2EE und ist hauptberuflich als Softwarearchitekt bei werk3AT in Linz beschäftigt. Derzeit schreibt er als Co-Autor an Professional Spring Development für Wiley.
Links und Literatur
- [1] Rod Johnson, Jürgen Höller: J2EE Development without EJB, Wiley, 2004
- [2] Rod Johnson, Jürgen Höller, Thomas Risberg, Alef Arendsen: Professional Spring Development, Wiley, 2004/2005
- [3] Bruce Tate, Justin Gehtland: Better, Faster, Lighter Java, O'Reilly, 2004
- [4] Matt Raible: Spring Live (2004): www.sourcebeat.com/
- [5] Introducing the Spring Framework: www.theserverside.com/articles/article.tss?l=SpringFramework
- [6] Data Access with the Spring Framework: www.hibernate.org/110.html
- [7] Spring Framework Website: www.springframework.org/
- [8] Hibernate Website: www.hibernate.org/
- [9] Apache OJB Website: db.apache.org/ojb/
- [10] iBATIS Website: www.ibatis.com/














