Migrating From JAXB XML Processing to XStream
Java 9's modules have made JAXB less desirable for some projects. So let's refactor some XML processing code to use XStream instead.
Join the DZone community and get the full member experience.
Join For FreeI have a small REST API application that uses Spark and GSON and JAXB. I haven’t released this to GitHub yet, but I did release some of the example's externally executed integration verification code for it.
When trying to package this for Java 1.9, I encountered the, now standard, missing JAXB, libraries. So I thought I’d investigate another XML library.
JAXB
I used JAXB because I didn’t need to add any additional dependencies in Maven and it was bundled by default in < Java 1.8. Now that the new module system is in place, JAXB has to be included via dependencies, and this ballooned my deployable JAR file from 2.9 meg to 4.3 meg.
That might not seem like much, but I grew up with 48K and that privation leaves a permanent ‘this seems too big’ mentality.
JAXB is a big powerful set of code, and I’m only using it to serialize and deserialize a few objects.
So in reality, when I have to bundle it with my app, it is too big.
I had to add the following dependencies to my pom.xml to have JAXB functioning with Java 1.9:
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
XStream
After a quick look around and I thought I’d try XStream.
- XStream does not require any annotations — which is good because I have a mild aversion to annotations and this might help me remove the annotations in my payload objects.
- XStream seems smaller as a packaged
.jar
- XStream requires a single import, to continue with JAXB I had to add four.
Not Ready Yet
My application is one that I use for training and teaching REST API testing.
Therefore:
- It sometimes has some hacky code
- It hasn’t been fully refactored
- JAXB was used in quite a few places
The first thing I had to do was refactor the code to isolate the XML processing as much as possible.
I can’t do much about the JAXB annotations, since they are on the payload classes. But I can refactor out the marshalling and unmarshalling code into a single “Payload XML Parser” object.
Fortunately, I do have quite a lot of tests — far more than in the external integration test pack.
Part of the reason I use Spark is that I find it easy to spin up an instance of the app which will listen on an HTTP port so I can have HTTP tests running as part of the main project very easily.
Isolate the JAXB code
I want to isolate code like this:
try {
JAXBContext context = JAXBContext.newInstance(ListOfListicatorListsPayload.class);
Unmarshaller m = context.createUnmarshaller();
ListOfListicatorListsPayload lists = (ListOfListicatorListsPayload)
m.unmarshal(new StringReader(payload));
newLists = new PayloadConvertor().transformFromPayLoadObject(lists);
} catch (Exception e) {
e.printStackTrace();
}
Ignore the throwing away of exceptions — this is a ‘training’ app. I’m allowed to take shortcuts and have verbose console output.
And push it into something like MyXMLProcessor so that I’m creating an abstraction layer on top of the XML Payload processing, which will make it easier to test out different XML libraries if I don’t like XStream.
And so this was a simple ‘refactor to method’ approach. Which led to code like this:
try {
newLists = new MyXmlProcessor().getListOfListicatorLists(payload);
} catch (Exception e) {
e.printStackTrace();
}
The code behind getListOfListicatorLists at this point is exactly the same as the code that was in my PayloadConvertor.
I simply used the Find in Path to find any of the following imports, and moved the code that required the import into MyXmlProcessor as a method:
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import java.io.StringReader;
import javax.xml.bind.Marshaller;
Migrate Method by Method
I then migrated each method in turn from JAXB to XStream.
I had to do the following in response to failing tests and exceptions during the conversion.
Add an XML Header When Marshalling
When XStream marshalls an Object to XML, it doesn’t return an XML header.
So I added a quick hack rather than see if there was a configuration option in XStream. It seemed easier.
String header = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";
return header + xstream.toXML(payload);
Configure in Code Rather Than Annotate
I much prefer configuring in code rather than XML files or annotations.
With XStream, this meant:
public MyXmlProcessor(){
xstream = new XStream();
xstream.alias("lists", ListOfListicatorListsPayload.class);
xstream.addImplicitCollection(ListOfListicatorListsPayload.class, "lists");
xstream.alias("list", ListicatorListPayload.class);
}
The Alias is to take the place of the JAXB annotations.
.alias
When a class name is different from the XML name, I need an alias of an annotation.
The following is required to map a UserPayLoad to a user element.
@XmlRootElement(name = "list")
public class ListicatorListPayload {
public String guid;
public String title;
}
In XStream, I don’t annotate the class, I configure the XStream processor:
xstream.alias("user", UserPayload.class);
I prefer this second approach, since it should allow me to handle any special cases more easily, and it is a single place to configure everything.
The addImplicitCollection is to handle element to collection mapping.
@XmlRootElement(name = "lists")
public class ListOfListicatorListsPayload {
@XmlElement(name="list")
public List<ListicatorListPayload> lists = new ArrayList<>();
}
For the above, in addition to the .alias calls to map Classes to elements, I also had to map the collection to the element:
xstream.addImplicitCollection(ListOfListicatorListsPayload.class, "lists");
This happened by default in JAXB, but it seems a simple enough change for XStream.
Once all the fields were aliased, and I was using XStream to convert to and from XML, I could remove all the JAXB annotations.
XML Formatting
XStream provides nicely formatted XML. JAXB provides everything on a single line with no new lines.
This has a side-effect that it makes the API easier to work with as a training tool because the messages are formatted when viewed in a proxy without any additional parsing.
But it meant I had to change some of my assertions.
To assert on the XML generated by XStream I de-prettified it by removing all white space in the response String:
String bodyWithNoWhiteSpace = response.body.replaceAll("\\s", "");
Assert.assertTrue(
bodyWithNoWhiteSpace.contains(
"<user><username>user</username></user>"
)
);
XStream vs. JAXB
I consider the code for XStream much smaller and easier to read.
UserPayload user = (UserPayload) xstream.fromXML(payload);
vs.
try{
JAXBContext context = JAXBContext.newInstance(UserPayload.class);
Unmarshaller m = context.createUnmarshaller();
return (UserPayload)m.unmarshal(new StringReader(payload));
} catch (JAXBException e) {
e.printStackTrace();
}
And the more general form:
public <T> T simplePayloadStringConvertorConvert(String body, Class<T> theClass) {
try {
return (T)xstream.fromXML(body);
}catch(Exception e){
e.printStackTrace();
}
return null;
}
vs.
public <T> T simplePayloadStringConvertorConvert(String body, Class<T> theClass) {
try {
JAXBContext context = JAXBContext.newInstance(theClass);
Unmarshaller m = context.createUnmarshaller();
return (T)m.unmarshal(new StringReader(body));
} catch (JAXBException e) {
e.printStackTrace();
}
return null;
}
TODO
I still have to configure the XStream security before I release. And I will eventually have to remove some reflection warnings.
Why
- This forced me to refactor my code — which is always a good idea — and allowed me to isolate the XML conversion.
- The packaged JAR was 4.3 meg with JAXB and dropped to 3.5 meg with XStream
- Originally it was 2.9 meg when packaged against Java 1.8.
- XStream also seems faster.
Summary of Process
- Have tests in place to support refactoring.
- Isolate existing XML code.
- Read XStream 2 minute tutorial.
- Add XStream maven dependency.
- Convert code step by step — while running the tests
- Done
Final Notes
I have a very small app, with minor usage of XML.
I will be converting my other XML processing apps, which also use minimal XML processing to use XStream because it was so much easier to understand and use.
Your mileage may vary.
Published at DZone with permission of Alan Richardson, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments