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.

Osobiste