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-objekt
  • UserAccount.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