Spring, JAXB oraz Maven – czyli łatwe i przyjemne budowanie oraz konfigurowanie aplikacji
Jakiś czas temu stanąłem przed koniecznością napisania aplikacji, która miała mieć możliwość konfigurowania z zewnętrznego pliku XML. Wybór formatu XML był podyktowany złożoną strukturą informacji, które należało wczytać. Aplikacja była oparta o szkielet Spring a zależnościami i budowaniem zajmował się Maven. Pozostał wybór narzędzia, które przeniesie mi w wygodny sposób dane z pliku XML do aplikacji. Wybór padł na JAXB – Java Architecture for XML Binding. JAXB to technologia pozwalająca wygenerować klasy języka Java, które pomogą nam odwzorować plik XML na obiekty.
Nie będę się skupiał nadto na opisie każdego z wymienionych narzędzi lecz skupię się raczej na pokazaniu jak zaprząc je razem do działania.
Przykładowa aplikacja
Zacznę od przygotowania niewielkiej aplikacji testowej. W tym celu w konsoli wpisujemy:
mvn archetype:generate
Powyższe polecenie zainicjuje kreator, w którym wybieramy 15 (maven-archetype-quickstart) a później podajemy szczegóły. Ja podałem:
Define value for groupId: : pl.michalmech Define value for artifactId: : jaxb-example Define value for version: 1.0-SNAPSHOT: : Define value for package: pl.michalmech: :
Aplikacja testowa jest gotowa a po wykonaniu polecenia:
mvn eclipse:eclipse
można wygodnie zaimportować ją do Eclipse‘a.
Kolejnym krokiem jest konfiguracja zależności aplikacji w pliku POM. Jedyne czego potrzebujemy to Spring dlatego wyrzuciłem zależność od JUnit‘a oraz folder src/test.
<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.michalmech</groupId>
<artifactId>jaxb-example</artifactId>
<packaging>jar</packaging>
<name>jaxb-example</name>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
<version>2.5.6.SEC01</version>
</dependency>
</dependencies>
</project>
Teraz czas na skonfigurowanie i wczytanie kontekstu Springa. Na razie pustego ale niebawem wypełnimy go ziarnami. Najpierw przygotowujemy plik src/main/resources/context.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
</beans>
Następnie zmieniamy klasę App:
public class App {
public static void main(String[] args) {
@SuppressWarnings("unused")
ApplicationContext context = new ClassPathXmlApplicationContext("context.xml");
}
}
Należy pamiętać o
mvn eclipse:eclipse
żeby Eclipse odświeżył biblioteki po zmianie pliku pom.xml. Próba uruchomiania (
) aplikacji nie powinna sprawić problemu.
Schemat XML
Założyłem, że aplikacja ma korzystać z klas wygenerowanych za pomocą JAXB‘a. Potrzebujemy więc schematu XML, na podstawie którego klasy zostaną wygenerowane. Dla przykładu skorzystamy z takiego schematu:
<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://michalmech.pl/jaxb-example"
xmlns:tns="http://michalmech.pl/jaxb-example"
elementFormDefault="qualified">
<element name="config">
<complexType>
<sequence>
<element name="parameters">
<complexType>
<sequence minOccurs="0" maxOccurs="unbounded">
<element name="parameter" type="tns:parameter" />
</sequence>
</complexType>
</element>
<element name="cities">
<complexType>
<sequence minOccurs="0" maxOccurs="unbounded">
<element name="city" type="tns:city" />
</sequence>
</complexType>
</element>
</sequence>
</complexType>
</element>
<complexType name="parameter">
<attribute name="name" use="required" type="string" />
<attribute name="value" use="required" type="string" />
</complexType>
<complexType name="city">
<attribute name="name" use="required" type="string" />
<attribute name="postCode" use="required" type="tns:postCode" />
</complexType>
<simpleType name="postCode">
<restriction base="string">
<pattern value="[0-9]{2}-[0-9]{3}" />
</restriction>
</simpleType>
</schema>
Zapisujemy plik pod nazwą config.xsd w src/main/resources. W dużym skrócie powyższy schemat opisuje plik XML, w którym spodziewamy się węzła zawierającego parametry oraz węzła zawierającego miasta. Parametr to element posiadający atrybut name oraz value natomiast miasto to element posiadający atrybut name oraz postCode. Atrybut postCode jest dodatkowo ograniczony wzorcem wyrażenia regularnego.
Chcielibyśmy aby oczekiwane klasy, opisujące schemat config.xsd zostały wygenerowane w locie podczas budowania aplikacji. Skorzystamy w tym celu z pluginu do Mavena. Zmieniamy plik pom.xml dodając do niego poniższą konfigurację budowania projektu:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxb2-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>xjc</goal>
</goals>
</execution>
</executions>
<configuration>
<packageName>${project.groupId}.config</packageName>
<schemaDirectory>${basedir}/src/main/resources</schemaDirectory>
<outputDirectory>${basedir}/src/main/generated-resources</outputDirectory>
</configuration>
</plugin>
</plugins>
</build>
jaxb2-maven-plugin wyszuka wszystkie schematy plików XML znajdujące się w lokalizacji wskazanej przez schemaDirectory a następnie wygeneruje klasy umieszczając je w paczce pl.michalmech.config oraz zapisze w lokalizacji wskazanej przez outputDirectory.
Użycie pluginu maven-compiler-plugin jest wymagane ponieważ JAXB korzysta z adnotacji więc kompilacja musi odbyć się w Javie, w wersji nie niższej niż 1.5.
No to do dzieła! Wykonujemy polecenie:
mvn clean compile eclipse:clean eclipse:eclipse
oraz odświeżamy projekt w IDE. Zawartość projektu powinna prezentować się teraz jak na załączonym poniżej obrazku.
Konfiguracja kontekstu
Nasza aplikacja kompiluje się i uruchamia, nadal jednak żadnej konfiguracji nie wczytujemy. Do tego przede wszystkim potrzebujemy pliku XML zgodnego z utworzonym wcześniej schematem. Takiego jak ten:
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://michalmech.pl/jaxb-example"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://michalmech.pl/jaxb-example config.xsd">
<parameters>
<parameter name="jakiś" value="parametr" />
<parameter name="inna" value="opcja" />
</parameters>
<cities>
<city name="Warszawa" postCode="03-481" />
<city name="Łęczna" postCode="21-010" />
</cities>
</config>
Umieszczamy go, tak jak pozostałe zasoby, w src/main/resources i nadajemy nazwę config.xml. Aby mieć pewność, że podczas budowania aplikacji dostarczany plik konfiguracyjny jest poprawny dołożymy jeszcze jeden plugin do POM‘a:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>xml-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>validate</goal>
</goals>
<phase>validate</phase>
</execution>
</executions>
<configuration>
<validationSets>
<validationSet>
<dir>${basedir}/src/main/resources</dir>
<includes>
<include>config.xml</include>
</includes>
<systemId>${basedir}/src/main/resources/config.xsd</systemId>
<schemaLanguage>http://www.w3.org/2001/XMLSchema</schemaLanguage>
<validating>true</validating>
</validationSet>
</validationSets>
</configuration>
</plugin>
Plugin ten zweryfikuje czy dostarczany plik XML jest zgodny ze schematem.
Do wczytania konfiguracji pozostało już bardzo niewiele. Posłużymy się narzędziami znajdującymi się w pakiecie org.springframework.oxm. W tym celu poprosimy Mavena o dostarczenie nam wymaganych źródeł dopisując odpowiednie zależności w pliku pom.xml:
<dependency>
<groupId>org.springframework.ws</groupId>
<artifactId>spring-ws</artifactId>
<version>1.5.2</version>
</dependency>
Teraz zajmiemy się kontekstem aplikacji. Do tej pory wszystko za mnie robiło jakieś narzędzie czy biblioteka. Nie będę burzył tego porządku i inicjacją odpowiednich obiektów oraz wczytaniem zawartości pliku config.xml do aplikacji zajmie się Spring. Aby to zrobił, konieczna jest ingerencja w deskryptor kontekstu context.xml. Zmieniamy jego zawartość na:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
<bean id="jaxb2Marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
<property name="schema" value="classpath:config.xsd" />
<property name="classesToBeBound">
<list>
<value>pl.michalmech.config.Config</value>
</list>
</property>
</bean>
<bean id="config" class="pl.michalmech.config.Config" factory-bean="jaxb2Marshaller" factory-method="unmarshal">
<constructor-arg>
<bean class="javax.xml.transform.stream.StreamSource">
<constructor-arg>
<bean class="java.io.FileInputStream" factory-bean="configClasspathResource" factory-method="getInputStream" />
</constructor-arg>
</bean>
</constructor-arg>
</bean>
<bean id="configClasspathResource" class="org.springframework.core.io.ClassPathResource">
<constructor-arg value="config.xml" />
</bean>
</beans>
Obiektem klasy ClassPathResource posłużymy się jak fabryką po to by wydobyć strumień zawierający zawartość pliku config.xml. Strumień tenże zostanie użyty w obiekcie fabrycznym Jaxb2Marshaller do naniesienia zawartości pliku config.xml na obiekt klasy Config.
Ostatnią czynnością jest pobranie obiektu Config z kontekstu i wyświetlenie przykładowych danych dostarczonych za pomocą pliku XML. Zmodyfikujmy więc aplikację do postaci:
public class App {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("context.xml");
Config config = (Config) context.getBean("config");
for (Parameter parameter : config.getParameters().getParameter()) {
System.out.println(parameter.getName() + ":" + parameter.getValue());
}
for (City city : config.getCities().getCity()) {
System.out.println(city.getName() + " - " + city.getPostCode());
}
}
}
Uruchomienie aplikacji (
) da następujący wynik w konsoli:
jakiś:parametr inna:opcja Warszawa - 03-481 Łęczna - 21-010
Podsumowanie
To wszystko co chciałem przekazać. Nie zagłębiałem się w szczegóły każdego z użytych narzędzi bo wpis musiałby być dużo dłuższy. Zależało mi na tym by podać gotowy przepis na użycie JAXB‘a w projekcie opartym na szkielecie Spring. Poniżej zamieszczam źródła projektu, które przydadzą się do samodzielnego uruchomienia aplikacji.
Do pobrania: jaxb-example.zip
Po pobraniu najwygodniej jest wykonać:
mvn compile eclipse:eclipse
oraz zaimportować projekt do Eclipse‘a.
