Serialisering med JAXB
JAXB betyder Java Architecture for XML Binding och används för att konvertera POJOs till XML och vice versa. Metoden för att konvertera en POJO till XML kallas för marshalling eller serialisering. Det motsatta, det vill säga att konvertera från XML till en POJO, kallas för unmarshalling eller deserialisering.
I denna artikel visar jag hur man kan använda JAXB för serialisering/deserialisering på ett enkelt sätt med hjälp av ett enhetstest. Exemplet finns att hämta på GitHub – https://github.com/cygni-stacktrace/jaxb-sample.
Ett sätt att arbeta med JAXB är att utgå från ett XML-schema (XSD, DTD eller liknande) och sedan generera POJOs utifrån schemat. Generering av POJOs kan exempelvis åstadkommas genom att använda verktyget xjc som följer med SDK:n.
xjc -d <output_folder> -p <package_name> <schema(s)>
Ett annat sätt är att använda Maven. Nedanstående POM innehåller den extrakonfiguration som krävs för att JAXB-klasserna ska genereras under generate-sources
-fasen. Jag har även lagt till ett beroende till JUnit eftersom det kommer att krävas senare i artikeln.
<!-- POM -->
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>se.cygni.stacktrace</groupId>
<artifactId>jaxb-sample</artifactId>
<version>1.0-SNAPSHOT</version>
<name>jaxb-sample</name>
<description>JAXB-sample for stacktrace</description>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.5</source>
<target>1.5</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.4.2</version>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.jvnet.jaxb2.maven2</groupId>
<artifactId>maven-jaxb2-plugin</artifactId>
<version>0.7.4</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<generatePackage>
se.cygni.stacktrace.jaxb
</generatePackage>
</configuration>
</plugin>
</plugins>
</build>
</project>
Tidigare har Jon Edvardsson specificerat ett Protobuf-schema för UserAccounts
. Jag tänkte använda mig av samma modell men istället för Protobuf så använder jag ett XML-schema. Det ser ut så här:
<!-- XML Schema -->
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://cygni.se/stacktrace/UserAccount"
xmlns:ua="http://cygni.se/stacktrace/UserAccount"
elementFormDefault="qualified">
<xs:element name="UserAccounts">
<xs:complexType>
<xs:sequence>
<xs:element
name="UserAccount"
type="ua:UserAccount"
minOccurs="0"
maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:complexType name="UserAccount">
<xs:sequence>
<xs:element name="id" type="xs:int" />
<xs:element name="username" type="xs:string" />
<xs:element name="email" type="xs:string" minOccurs="0" />
<xs:element name="AccountState">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="ACTIVE" />
<xs:enumeration value="PENDING_TANDC" />
<xs:enumeration value="SUSPENDED" />
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="services">
<xs:complexType>
<xs:sequence>
<xs:element
name="service"
type="xs:string"
maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:schema>
För att generera POJOs kan jag då antingen bygga via Maven eller använda xjc
direkt på detta sätt:
jaxb-sample$ xjc -d src/main/java -p se.cygni.stacktrace.jaxb src/main/resources/UserAccount.xsd
Output i min terminal ser ut så här:
parsing a schema...
compiling a schema...
se/cygni/stacktrace/jaxb/ObjectFactory.java
se/cygni/stacktrace/jaxb/UserAccount.java
se/cygni/stacktrace/jaxb/UserAccounts.java
se/cygni/stacktrace/jaxb/package-info.java
Men... jag skulle rekommendera att ni använder Maven genom att ex skriva
jaxb-sample$ mvn generate-sources
Det skapas alltså fyra stycken filer och dessa placeras i katalogen target/generated-sources/xjc
. Källkodsfilerna, som beskrivs nedan, kommer med Mavens hjälp att ingå i kompileringen om ni ex kör mvn compile
.
ObjectFactory.java
är en klass med fabriksmetoder som kan användas för att skapa de POJOs vi är intresserade av. Den genereras pga bakåtkompatibilitet och kan ignoreras i detta exempel (se även en diskussion kring detta på stackoverflow.package-info.java
innehåller den namespace-informationen som finns specificerad i XML-schemats toppelement.UserAccounts.java
är POJOn för rotelementet UserAccounts och innehåller i stort sett bara en getter för att hämta en lista av UserAccount-objektUserAccount.java
specificerar ett konto med e-post, användarnamn osv
De två klasser som vi faktiskt kommer att arbeta med är alltså UserAccounts
och UserAccount
, de ser ut enligt figurerna nedan och är alltså ganska dumma databärare med JAXB-annotationer som mappar mot XML-schemat. Obs att jag tagit bort kommentarer för läsbarhetens skull.
// UserAccounts.java
package se.cygni.stacktrace.jaxb;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
"userAccount"
})
@XmlRootElement(name = "UserAccounts")
public class UserAccounts {
@XmlElement(name = "UserAccount")
protected List<UserAccount> userAccount;
public List<UserAccount> getUserAccount() {
if (userAccount == null) {
userAccount = new ArrayList<UserAccount>();
}
return this.userAccount;
}
}
Nedan ser vi UserAccount.java som faktiskt innehåller de element som vi specificerade i XML-schemat.
// UserAccount.java
package se.cygni.stacktrace.jaxb;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "UserAccount", propOrder = {
"id",
"username",
"email",
"accountState",
"services"
})
public class UserAccount {
protected int id;
@XmlElement(required = true)
protected String username;
protected String email;
@XmlElement(name = "AccountState", required = true)
protected String accountState;
@XmlElement(required = true)
protected UserAccount.Services services;
public int getId() {
return id;
}
public void setId(int value) {
this.id = value;
}
public String getUsername() {
return username;
}
public void setUsername(String value) {
this.username = value;
}
public String getEmail() {
return email;
}
public void setEmail(String value) {
this.email = value;
}
public String getAccountState() {
return accountState;
}
public void setAccountState(String value) {
this.accountState = value;
}
public UserAccount.Services getServices() {
return services;
}
public void setServices(UserAccount.Services value) {
this.services = value;
}
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
"service"
})
public static class Services {
@XmlElement(required = true)
protected List<String> service;
public List<String> getService() {
if (service == null) {
service = new ArrayList<String>();
}
return this.service;
}
}
}
För att åskådliggöra konverteringen mellan POJOs och XML har jag skapat ett enhetstest (JUnit). Detta test skapar först testdata genom att instansiera två stycken UserAccount
-objekt. Dessa konverteras sedan till XML och sedan tillbaka till POJOs. Därefter kontrolleras att allt data konverterats korrekt. Konvertering sker med hjälp av gränssnitten Marshaller
och Unmarshaller
. Enhetstestet exekveras mha mvn test
och ser ut så här:
// JaxbTest.java
package se.cygni.stacktrace.jaxb;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Arrays;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import junit.framework.Assert;
import org.junit.Test;
import se.cygni.stacktrace.jaxb.UserAccount.Services;
/** Test case for the JAXB-sample (stacktrace). */
public class JaxbTest {
/** Creates a UserAccount-object. */
private UserAccount createUserAccount(
final int id, final String username,
final String email, final String... serviceStrings) {
final UserAccount account = new UserAccount();
account.setId(id);
account.setUsername(username);
account.setEmail(email);
final Services services = new Services();
services.service = Arrays.asList(serviceStrings);
account.setServices(services);
return account;
}
/** Test marshal/unmarshal via JAXB. */
@Test
public void testMarshalUnmarshal() throws JAXBException {
// First, setup test data - two user accounts
final UserAccount account1 =
createUserAccount(1, "account1", "account1@localhost", "news", "music");
final UserAccount account2 =
createUserAccount(
2, "account2", "account2@localhost",
"news", "sports");
final UserAccounts accounts = new UserAccounts();
accounts.getUserAccount().add(account1);
accounts.getUserAccount().add(account2);
// Marshal the test data to XML
final StringWriter writer = new StringWriter();
final JAXBContext context = JAXBContext.newInstance(
UserAccount.class.getPackage().getName());
final Marshaller marshaller =
context.createMarshaller();
marshaller.marshal(accounts, writer);
// Unmarshal the XML to classes
final Unmarshaller unmarshaller =
context.createUnmarshaller();
final UserAccounts accountsAfterRoundtrip =
(UserAccounts) unmarshaller.unmarshal(
new StringReader(writer.toString()));
// Verify that the newly created objects are equal to the
// original test data
Assert.assertNotNull(accountsAfterRoundtrip);
Assert.assertFalse(
accountsAfterRoundtrip.getUserAccount().isEmpty());
Assert.assertEquals(
accounts.getUserAccount().size(),
accountsAfterRoundtrip.getUserAccount().size());
final UserAccount accountAfterRoundtrip1 =
accountsAfterRoundtrip.getUserAccount().get(0);
Assert.assertEquals(
account1.getId(),
accountAfterRoundtrip1.getId());
Assert.assertEquals(
account1.getEmail(),
accountAfterRoundtrip1.getEmail());
Assert.assertEquals(
account1.getServices().getService().size(),
accountAfterRoundtrip1.getServices().getService()
.size());
Assert.assertTrue(
accountAfterRoundtrip1.getServices()
.getService().contains("music"));
}
}
Som ni ser är det relativt enkelt att konvertera mellan POJOs och XML. De steg som krävs är alltså:
- Skapa ett schema (XSD)
- Generera JAXB-klasser via xjc eller Maven
- Använd Marshaller och Unmarshaller för exekvera själva konverteringen