DSL dla konfiguracji Spring Framework
Z Jacek Laskowski - Wiki Projektanta Java EE
Nie pamiętam, co dokładnie sprawiło, że zacząłem poszukiwania znaczenia plików META-INF/spring.handlers oraz META-INF/spring.schemas w Spring Framework, ale pamiętam, że jednym z powodów było z pewnością znalezienie ich w źródłach Spring Dynamic Modules (moduł spring-osgi-core). A może to była lektura OSGi at LinkedIn: Integrating Spring DM (Part 1)? Postanowiłem samodzielnie spróbować się z tematem i po lekturze Appendix B. Extensible XML authoring sprawdzić w działaniu.
Rozpocząłem od utworzenia projektu z pomocą Apache Maven 2 poleceniem mvn archetype:create:
mvn archetype:create -DgroupId=pl.jaceklaskowski.spring -DartifactId=spring-custom-tags
i modyfikacji utworzonego pom.xml, tak że ostatecznie wyglądał następująco (korzystam ze wsparcia wtyczki m2eclipse do edycji pom.xml, dla którego ostatnia wersja 0.9.6 udostępnia liczne uproszczenia, np. uzupełnianie, więc poszło szybko i sprawnie):
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>pl.jaceklaskowski.spring</groupId>
<artifactId>spring-custom-tags</artifactId>
<packaging>jar</packaging>
<version>1.0</version>
<name>spring-custom-tags</name>
<url>http://www.jaceklaskowski.pl</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>2.5.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>2.5.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>2.5.5</version>
</dependency>
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derby</artifactId>
<version>10.4.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.openjpa</groupId>
<artifactId>openjpa</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies>
</project>
Projekt gotowy do działania. Dla zainteresowanych jego poprawnością można wykonać polecenie mvn clean install. Wybrałem akurat takie zależności, aby można było stworzyć aplikację korzystającą z JPA. Dlaczego? Zaraz się wyjaśni.
Teraz pora na konfigurację springową - src/main/resources/beans.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<context:annotation-config />
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="derbyDS" />
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.SimpleLoadTimeWeaver" />
</property>
</bean>
<bean id="derbyDS" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.apache.derby.jdbc.EmbeddedDriver" />
<property name="url" value="jdbc:derby:spring1;create=true" />
<property name="username" value="sa" />
<property name="password" value="" />
</bean>
</beans>
I jak ona się przedstawia? Nie przydługa czasem? To jest bodajże najbardziej wytykana wada Spring Framework - przerośnięty XML. To jest powód, dla którego wybrałem konfigurację aplikacji z JPA na pokładzie. Wprost przeogromna (i tutaj kłania się prostota użycia serwera aplikacyjnego Java EE 5). Szczęśliwie z wersją 2.0+ mamy do dyspozycji mechanizm definiowania własnych rozszerzeń do konfiguracji XML - Extensible XML. Kilka chwil i mamy taki oto znacząco skrócony plik konfiguracyjny, który semantycznie odpowiada poprzedniemu:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jpa="http://www.jaceklaskowski.pl/schema/jpa"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.jaceklaskowski.pl/schema/jpa http://www.jaceklaskowski.pl/schema/jpa/jpa.xsd">
<context:annotation-config />
<jpa:emf id="entityMF" driver="org.apache.derby.jdbc.EmbeddedDriver" url="jdbc:derby:spring2;create=true" username="sa" password="" />
</beans>
Prostszy, nieprawdaż? Po wprowadzeniu jpa:emf skróciłem plik o połowę, a czytelność wzrosła. Przypomina to rodzaj DSL (ang. domain specific language), gdzie definiujemy własny "język" konfiguracji Springa korzystając z jego własnych mechanizmów (to bodajże nazywamy wewnętrznym DSL).
Zacznijmy rozpoznanie tematu od końca. Najpierw określamy nowe znaczniki - w tym przypadku jest to emf (skrót od EntityManagerFactory) oraz jego atrybuty. Następnie deklarujemy przestrzeń nazewniczą, z którą jest związany - jpa, którą związujemy z plikiem XML Schema (plik xsd) poprzez xmlns:jpa oraz xsi:schemaLocation. Jak do tej pory nic nadzwyczajnego, nie wykraczającego poza ramy XML. Gdybym w tej chwili uruchomił parser XML, weryfikujący jego zgodność ze schematem (plikami xsd) podjęta byłaby próba pobrania pliku spod adresu http://www.jaceklaskowski.pl/schema/jpa/jpa.xsd. Oczywiście pliku tam nie ma, więc przetwarzanie pliku zakończyłoby się błędem. Spring Framework udostępnia mechanizm mapowania plików xsd na pliki lokalne dostępne na ścieżce klas poprzez wspomniany na początku META-INF/spring.schemas:
http\://www.jaceklaskowski.pl/schema/jpa/jpa.xsd=pl/jaceklaskowski/spring/jpa.xsd
W nim deklaruję mapowanie adresu URL do pliku xsd na plik lokalny - src/main/resources/pl/jaceklaskowski/spring/jpa.xsd:
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.jaceklaskowski.pl/schema/jpa"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:beans="http://www.springframework.org/schema/beans"
targetNamespace="http://www.jaceklaskowski.pl/schema/jpa"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:import namespace="http://www.springframework.org/schema/beans" />
<xsd:element name="emf">
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="beans:identifiedType">
<xsd:attribute name="driver" type="xsd:string" use="required" />
<xsd:attribute name="url" type="xsd:string" use="required" />
<xsd:attribute name="username" type="xsd:string" use="required" />
<xsd:attribute name="password" type="xsd:string" use="required" />
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
</xsd:schema>
W nim definiuję znacznik emf zgodnie z zasadami XML Schema. Strona zgodności z regułami XML jest obsłużona. A w jaki sposób o tym uproszczeniu miałby dowiedzieć się Spring, podczas konfiguracji ziaren springowych?! Poprzez kolejny plik - META-INF/spring.handlers:
http\://www.jaceklaskowski.pl/schema/jpa=pl.jaceklaskowski.spring.JpaNamespaceHandler
Wskazana klasa pl.jaceklaskowski.spring.JpaNamespaceHandler odpowiada za kolejne mapowanie - nazwy znacznika z jego parserem:
package pl.jaceklaskowski.spring;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
public class JpaNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("emf", new JpaBeanDefinitionParser());
}
}
Klasa pl.jaceklaskowski.spring.JpaBeanDefinitionParser prezentuje się następująco:
package pl.jaceklaskowski.spring;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.w3c.dom.Element;
public class JpaBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
protected void doParse(Element element, BeanDefinitionBuilder builder) {
String driver = element.getAttribute("driver");
String url = element.getAttribute("url");
String username = element.getAttribute("username");
String password = element.getAttribute("password");
String driverBeanClassName = "org.springframework.jdbc.datasource.DriverManagerDataSource";
BeanDefinitionBuilder ds = BeanDefinitionBuilder.genericBeanDefinition(driverBeanClassName);
ds.addPropertyValue("driverClassName", driver);
ds.addPropertyValue("url", url);
ds.addPropertyValue("username", username);
ds.addPropertyValue("password", password);
String timeWeaverClassName = "org.springframework.instrument.classloading.SimpleLoadTimeWeaver";
BeanDefinitionBuilder timeWeaver = BeanDefinitionBuilder.genericBeanDefinition(timeWeaverClassName);
builder.addPropertyValue("dataSource", ds.getBeanDefinition());
builder.addPropertyValue("loadTimeWeaver", timeWeaver.getBeanDefinition());
}
protected Class getBeanClass(Element element) {
return LocalContainerEntityManagerFactoryBean.class;
}
}
W ten sposób uruchomienie skróconej konfiguracji będzie odpowiadało tej początkowej, bardziej rozbudowanej.
Przykładowa aplikacja testująca mogłaby wyglądać tak:
package pl.jaceklaskowski.spring;
import javax.persistence.EntityManagerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext(new String[] { "beans.xml" });
EntityManagerFactory emf = (EntityManagerFactory) context.getBean("entityManagerFactory");
System.out.println(emf);
EntityManagerFactory entityMF = (EntityManagerFactory) context.getBean("entityMF");
System.out.println(entityMF);
}
}
Nie ma w niej nic, co wiązałoby się z wprowadzeniem uproszczenia. Wszystko wykrywane jest przez Spring Framework, podczas wczytywania pliku konfiguracyjnego z nieznanymi konstrukcjami konfiguracyjnymi.
Zabawne, bo właśnie dzisiaj ktoś zadał pytanie w tym temacie na grupie Spring-DM, na które mogłem już udzielić odpowiedzi - The matching wildcard is strict, but no declaration can be found for element 'osgi:reference'.
Kompletny projekt spring-custom-tags do uruchomienia dostępny jest do pobrania jako spring-custom-tags.zip.
