Dozer is an open source (Apache 2 license) "Java Bean to Java Bean mapper that recursively copies data from one object to another." As this description from its main web page states, it is used to map two JavaBeans instances for automatic data copying between the instances. Although these can be any of the many types of JavaBeans instances, I will focus this post on using Dozer to map JAXB-generated objects to "business data objects" (sometimes referred to as "domain objects").
In Java applications making use of the Java Architecture for XML Binding (JAXB), it is very common for developers to write specific business or domain objects for use in the application itself and only use the JAXB-generated objects for reading (unmarshalling) and writing (marshalling) XML. Although using the JAXB-generated objects themselves as the business/domain objects has some appeal (DRY), there are disadvantages to this approach. JAXB-generated classes do not have toString(), equals(Object), or hashCode() implementations, making these generated classes unsuitable for use in many types of collections, unsuitable for comparison other than identity comparison, and unsuitable for easily logging their contents. Manually editing these generated classes after their generation is tedious and is not conducive to regeneration of the JAXB classes again when even slight changes might be made to the source XSD.
Although JAXB2 Basics can be used to ensure that JAXB-generated classes have some of the common methods needs for use in collections, use in comparisons, and for logging of their contents, a potentially even bigger issue with using JAXB-generated classes as domain/business objects is the tight coupling of business logic to XSD this entails. A schema change in an XSD (such as for version update) typically leads to a different package structure for classes generated from that XSD via JAXB. The different package structure then forces all code that imports those JAXB-generated classes to change their import statements. Content changes to the XSD can have even more dramatic impacts, affecting get/set methods on the JAXB classes that would be strewn throughout the application if the JAXB classes are used for domain/business objects.
Assuming that one decides to not use JAXB-generated classes as business/domain classes, there are multiple ways to map the generated JAXB classes to the classes defining the business/domain objects via a "mapping layer" described in code or configuration. To demonstrate two code-based mapping layer implementations and to demonstrate a Dozer-based mapping layer, I introduce some simple examples of JAXB-generated classes and custom built business/domain classes.
The first part of the example for this post is the XSD from which JAXB'x xjc will be instructed to general classes for marshalling to XML described by that XSD or unmarshalling from XML described by that XSD. The XSD, which is shown next, defines a Person
element which can have nested MailingAddress
and ResidentialAddress
elements and two String attributes for first and last names. Note also that the main namespace is http://marxsoftware.blogspot.com/, which JAXB will use to determine the Java package hierarchy for classes generated from this XSD.
<?xml version="1.0"?> <xs:schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:marx="http://marxsoftware.blogspot.com/" targetNamespace="http://marxsoftware.blogspot.com/" elementFormDefault="qualified"> <xs:element name="Person" type="marx:PersonType" /> <xs:complexType name="PersonType"> <xs:sequence> <xs:element name="MailingAddress" type="marx:AddressType" /> <xs:element name="ResidentialAddress" type="marx:AddressType" minOccurs="0" /> </xs:sequence> <xs:attribute name="firstName" type="xs:string" /> <xs:attribute name="lastName" type="xs:string" /> </xs:complexType> <xs:complexType name="AddressType"> <xs:attribute name="streetAddress1" type="xs:string" use="required" /> <xs:attribute name="streetAddress2" type="xs:string" use="optional" /> <xs:attribute name="city" type="xs:string" use="required" /> <xs:attribute name="state" type="xs:string" use="required" /> <xs:attribute name="zipcode" type="xs:string" use="required" /> </xs:complexType> </xs:schema>
When xjc (the JAXB compiler delivered with Oracle's JDK) is executed against the above XSD, the following four classes are generated in the directory com/blogspot/marxsoftware (derived from the XSD's namespace): AddressType.java
, PersonType.java
, ObjectFactory.java
, and package-info.java
.
The next two code listings are of the two main classes of interest (PersonType.java
and AddressType.java
) generated by JAXB. The primary purpose of showing them here is as a reminder that they lack methods we often need our business/domain classes to have.
// // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.4-2 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. // Generated on: 2013.12.03 at 11:44:32 PM MST // package com.blogspot.marxsoftware; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlType; /** * <p>Java class for PersonType complex type. * * <p>The following schema fragment specifies the expected content contained within this class. * * <pre> * <complexType name="PersonType"> * <complexContent> * <restriction base="{http://www.w3.org/2001/XMLSchema}anyType"> * <sequence> * <element name="MailingAddress" type="{http://marxsoftware.blogspot.com/}AddressType"/> * <element name="ResidentialAddress" type="{http://marxsoftware.blogspot.com/}AddressType" minOccurs="0"/> * </sequence> * <attribute name="firstName" type="{http://www.w3.org/2001/XMLSchema}string" /> * <attribute name="lastName" type="{http://www.w3.org/2001/XMLSchema}string" /> * </restriction> * </complexContent> * </complexType> * </pre> * * */ @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "PersonType", propOrder = { "mailingAddress", "residentialAddress" }) public class PersonType { @XmlElement(name = "MailingAddress", required = true) protected AddressType mailingAddress; @XmlElement(name = "ResidentialAddress") protected AddressType residentialAddress; @XmlAttribute(name = "firstName") protected String firstName; @XmlAttribute(name = "lastName") protected String lastName; /** * Gets the value of the mailingAddress property. * * @return * possible object is * {@link AddressType } * */ public AddressType getMailingAddress() { return mailingAddress; } /** * Sets the value of the mailingAddress property. * * @param value * allowed object is * {@link AddressType } * */ public void setMailingAddress(AddressType value) { this.mailingAddress = value; } /** * Gets the value of the residentialAddress property. * * @return * possible object is * {@link AddressType } * */ public AddressType getResidentialAddress() { return residentialAddress; } /** * Sets the value of the residentialAddress property. * * @param value * allowed object is * {@link AddressType } * */ public void setResidentialAddress(AddressType value) { this.residentialAddress = value; } /** * Gets the value of the firstName property. * * @return * possible object is * {@link String } * */ public String getFirstName() { return firstName; } /** * Sets the value of the firstName property. * * @param value * allowed object is * {@link String } * */ public void setFirstName(String value) { this.firstName = value; } /** * Gets the value of the lastName property. * * @return * possible object is * {@link String } * */ public String getLastName() { return lastName; } /** * Sets the value of the lastName property. * * @param value * allowed object is * {@link String } * */ public void setLastName(String value) { this.lastName = value; } }JAXB-generated AddressType.java
// // This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.4-2 // See http://java.sun.com/xml/jaxb // Any modifications to this file will be lost upon recompilation of the source schema. // Generated on: 2013.12.03 at 11:44:32 PM MST // package com.blogspot.marxsoftware; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlType; /** * <p>Java class for AddressType complex type. * * <p>The following schema fragment specifies the expected content contained within this class. * * <pre> * <complexType name="AddressType"> * <complexContent> * <restriction base="{http://www.w3.org/2001/XMLSchema}anyType"> * <attribute name="streetAddress1" use="required" type="{http://www.w3.org/2001/XMLSchema}string" /> * <attribute name="streetAddress2" type="{http://www.w3.org/2001/XMLSchema}string" /> * <attribute name="city" use="required" type="{http://www.w3.org/2001/XMLSchema}string" /> * <attribute name="state" use="required" type="{http://www.w3.org/2001/XMLSchema}string" /> * <attribute name="zipcode" use="required" type="{http://www.w3.org/2001/XMLSchema}string" /> * </restriction> * </complexContent> * </complexType> * </pre> * * */ @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "AddressType") public class AddressType { @XmlAttribute(name = "streetAddress1", required = true) protected String streetAddress1; @XmlAttribute(name = "streetAddress2") protected String streetAddress2; @XmlAttribute(name = "city", required = true) protected String city; @XmlAttribute(name = "state", required = true) protected String state; @XmlAttribute(name = "zipcode", required = true) protected String zipcode; /** * Gets the value of the streetAddress1 property. * * @return * possible object is * {@link String } * */ public String getStreetAddress1() { return streetAddress1; } /** * Sets the value of the streetAddress1 property. * * @param value * allowed object is * {@link String } * */ public void setStreetAddress1(String value) { this.streetAddress1 = value; } /** * Gets the value of the streetAddress2 property. * * @return * possible object is * {@link String } * */ public String getStreetAddress2() { return streetAddress2; } /** * Sets the value of the streetAddress2 property. * * @param value * allowed object is * {@link String } * */ public void setStreetAddress2(String value) { this.streetAddress2 = value; } /** * Gets the value of the city property. * * @return * possible object is * {@link String } * */ public String getCity() { return city; } /** * Sets the value of the city property. * * @param value * allowed object is * {@link String } * */ public void setCity(String value) { this.city = value; } /** * Gets the value of the state property. * * @return * possible object is * {@link String } * */ public String getState() { return state; } /** * Sets the value of the state property. * * @param value * allowed object is * {@link String } * */ public void setState(String value) { this.state = value; } /** * Gets the value of the zipcode property. * * @return * possible object is * {@link String } * */ public String getZipcode() { return zipcode; } /** * Sets the value of the zipcode property. * * @param value * allowed object is * {@link String } * */ public void setZipcode(String value) { this.zipcode = value; } }
A common and straightforward tactic for copying data between the JAXB-generated objects and the custom-written business/domain objects is to use the "get" methods of one object and pass its return value to the "set" method of the other object. For example, in the process of unmarshalling/reading XML into the application, the results of "get" methods called on the JAXB-generated objects can be passed to the "set" methods of the business/domain objects. In the opposite direction, marshalling/writing XML can be easily accomplished by passing the result of "get" methods on the domain/business objects to corresponding "set" methods of the JAXB-generated objects. The next code listing is for PersonCoverter.java
and illustrates one implementation of this approach.
package dustin.examples.dozerdemo; import com.blogspot.marxsoftware.AddressType; import com.blogspot.marxsoftware.ObjectFactory; import com.blogspot.marxsoftware.PersonType; import dustin.examples.Address; import dustin.examples.Person; /** * Static functions for converting between JAXB-generated objects and domain * objects. * * @author Dustin */ public class PersonConverter { /** * Extract business object {@link dustin.examples.Person} from the JAXB * generated object {@link com.blogspot.marxsoftware.PersonType}. * * @param personType JAXB-generated {@link com.blogspot.marxsoftware.PersonType} * from which to extract {@link dustin.examples.Person} object. * @return Instance of {@link dustin.examples.Person} based on the provided * {@link com.blogspot.marxsoftware.PersonType}. */ public static Person extractPersonFromPersonType(final PersonType personType) { final String lastName = personType.getLastName(); final String firstName = personType.getFirstName(); final Address residentialAddress = extractAddressFromAddressType(personType.getResidentialAddress()); final Address mailingAddress = extractAddressFromAddressType(personType.getMailingAddress()); return new Person(lastName, firstName, residentialAddress, mailingAddress); } /** * Extract business object {@link dustin.examples.Address} from the JAXB * generated object {@link com.blogspot.marxsoftware.AddressType}. * * @param addressType JAXB-generated {@link com.blogspot.marxsoftware.AddressType} * from which to extract {@link dustin.examples.Address} object. * @return Instance of {@link dustin.examples.Address} based on the provided * {@link com.blogspot.marxsoftware.AddressType}. */ public static Address extractAddressFromAddressType(final AddressType addressType) { return new Address( addressType.getStreetAddress1(), addressType.getStreetAddress2(), addressType.getCity(), addressType.getState(), addressType.getZipcode()); } /** * Extract an instance of {@link com.blogspot.marxsoftware.PersonType} from * an instance of {@link dustin.examples.Person}. * * @param person Instance of {@link dustin.examples.Person} from which * instance of JAXB-generated {@link com.blogspot.marxsoftware.PersonType} * is desired. * @return Instance of {@link com.blogspot.marxsoftware.PersonType} based on * provided instance of {@link dustin.examples.Person}. */ public static PersonType extractPersonTypeFromPerson(final Person person) { final ObjectFactory objectFactory = new ObjectFactory(); final AddressType residentialAddressType = extractAddressTypeFromAddress(person.getResidentialAddress()); final AddressType mailingAddressType = extractAddressTypeFromAddress(person.getMailingAddress()); final PersonType personType = objectFactory.createPersonType(); personType.setLastName(person.getLastName()); personType.setFirstName(person.getFirstName()); personType.setResidentialAddress(residentialAddressType); personType.setMailingAddress(mailingAddressType); return personType; } /** * Extract an instance of {@link com.blogspot.marxsoftware.AddressType} from * an instance of {@link dustin.examples.Address}. * * @param address Instance of {@link dustin.examples.Address} from which * instance of JAXB-generated {@link com.blogspot.marxsoftware.AddressType} * is desired. * @return Instance of {@link com.blogspot.marxsoftware.AddressType} based on * provided instance of {@link dustin.examples.Address}. */ public static AddressType extractAddressTypeFromAddress(final Address address) { final ObjectFactory objectFactory = new ObjectFactory(); final AddressType addressType = objectFactory.createAddressType(); addressType.setStreetAddress1(address.getStreetAddress1()); addressType.setStreetAddress2(address.getStreetAddress2()); addressType.setCity(address.getMunicipality()); addressType.setState(address.getState()); addressType.setZipcode(address.getZipCode()); return addressType; } }
The last code listing demonstrated a common third-party class approach to copying data in both directions between the JAXB-generated objects and the domain/business objects. Another approach is to build this copying capability into the domain/business objects themselves. This is shown in the next two code listings for PersonPlus.java
and AddressPlus.java
which are versions of the previously covered Person.java
and Address.java
with support added for copying data to and from JAXB-generated objects. For convenience, I added the new methods to the bottom of the classes after the toString
implementations.
package dustin.examples; import com.blogspot.marxsoftware.ObjectFactory; import com.blogspot.marxsoftware.PersonType; import java.util.Objects; /** * Person class enhanced to support copying to/from JAXB-generated PersonType. * * @author Dustin */ public class PersonPlus { private String lastName; private String firstName; private AddressPlus mailingAddress; private AddressPlus residentialAddress; public PersonPlus( final String newLastName, final String newFirstName, final AddressPlus newResidentialAddress, final AddressPlus newMailingAddress) { this.lastName = newLastName; this.firstName = newFirstName; this.residentialAddress = newResidentialAddress; this.mailingAddress = newMailingAddress; } public String getLastName() { return this.lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getFirstName() { return this.firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public AddressPlus getMailingAddress() { return this.mailingAddress; } public void setMailingAddress(AddressPlus mailingAddress) { this.mailingAddress = mailingAddress; } public AddressPlus getResidentialAddress() { return this.residentialAddress; } public void setResidentialAddress(AddressPlus residentialAddress) { this.residentialAddress = residentialAddress; } @Override public int hashCode() { int hash = 3; hash = 19 * hash + Objects.hashCode(this.lastName); hash = 19 * hash + Objects.hashCode(this.firstName); hash = 19 * hash + Objects.hashCode(this.mailingAddress); hash = 19 * hash + Objects.hashCode(this.residentialAddress); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final PersonPlus other = (PersonPlus) obj; if (!Objects.equals(this.lastName, other.lastName)) { return false; } if (!Objects.equals(this.firstName, other.firstName)) { return false; } if (!Objects.equals(this.mailingAddress, other.mailingAddress)) { return false; } if (!Objects.equals(this.residentialAddress, other.residentialAddress)) { return false; } return true; } @Override public String toString() { return "PersonPlus{" + "lastName=" + lastName + ", firstName=" + firstName + ", mailingAddress=" + mailingAddress + ", residentialAddress=" + residentialAddress + '}'; } /** * Provide a JAXB-generated instance of {@link com.blogspot.marxsoftware.PersonType} * that corresponds to me. * * @return Instance of {@link com.blogspot.marxsoftware.PersonType} that * corresponds to me. */ public PersonType toPersonType() { final ObjectFactory objectFactory = new ObjectFactory(); final PersonType personType = objectFactory.createPersonType(); personType.setFirstName(this.firstName); personType.setLastName(this.lastName); personType.setResidentialAddress(this.residentialAddress.toAddressType()); personType.setMailingAddress(this.mailingAddress.toAddressType()); return personType; } /** * Provide instance of {@link dustin.examples.PersonPlus} corresponding * to the provided instance of JAXB-generated object * {@link com.blogspot.marxsoftware.PersonType}. * * @param personType Instance of JAXB-generated object * {@link com.blogspot.marxsoftware.PersonType}. * @return Instance of me corresponding to provided JAXB-generated object * {@link com.blogspot.marxsoftware.PersonType}. */ public static PersonPlus fromPersonType(final PersonType personType) { final AddressPlus residentialAddress = AddressPlus.fromAddressType(personType.getResidentialAddress()); final AddressPlus mailingAddress = AddressPlus.fromAddressType(personType.getMailingAddress()); return new PersonPlus(personType.getLastName(), personType.getFirstName(), residentialAddress, mailingAddress); } }AddressPlus.java
package dustin.examples; import com.blogspot.marxsoftware.AddressType; import com.blogspot.marxsoftware.ObjectFactory; import java.util.Objects; /** * Address class with support for copying to/from JAXB-generated class * {@link com.blogspot.marxsoftware.AddressType}. * * @author Dustin */ public class AddressPlus { private String streetAddress1; private String streetAddress2; private String municipality; private String state; private String zipCode; public AddressPlus( final String newStreetAddress1, final String newStreetAddress2, final String newMunicipality, final String newState, final String newZipCode) { this.streetAddress1 = newStreetAddress1; this.streetAddress2 = newStreetAddress2; this.municipality = newMunicipality; this.state = newState; this.zipCode = newZipCode; } public String getStreetAddress1() { return this.streetAddress1; } public void setStreetAddress1(String streetAddress1) { this.streetAddress1 = streetAddress1; } public String getStreetAddress2() { return this.streetAddress2; } public void setStreetAddress2(String streetAddress2) { this.streetAddress2 = streetAddress2; } public String getMunicipality() { return this.municipality; } public void setMunicipality(String municipality) { this.municipality = municipality; } public String getState() { return this.state; } public void setState(String state) { this.state = state; } public String getZipCode() { return this.zipCode; } public void setZipCode(String zipCode) { this.zipCode = zipCode; } @Override public int hashCode() { return Objects.hash( this.streetAddress1, this.streetAddress2, this.municipality, this.state, this.zipCode); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final AddressPlus other = (AddressPlus) obj; if (!Objects.equals(this.streetAddress1, other.streetAddress1)) { return false; } if (!Objects.equals(this.streetAddress2, other.streetAddress2)) { return false; } if (!Objects.equals(this.municipality, other.municipality)) { return false; } if (!Objects.equals(this.state, other.state)) { return false; } if (!Objects.equals(this.zipCode, other.zipCode)) { return false; } return true; } @Override public String toString() { return "Address{" + "streetAddress1=" + streetAddress1 + ", streetAddress2=" + streetAddress2 + ", municipality=" + municipality + ", state=" + state + ", zipCode=" + zipCode + '}'; } /** * Provide a JAXB-generated instance of {@link com.blogspot.marxsoftware.AddressType} * that corresponds to an instance of me. * * @return Instance of JAXB-generated {@link com.blogspot.marxsoftware.AddressType} * that corresponds to me. */ public AddressType toAddressType() { final ObjectFactory objectFactory = new ObjectFactory(); final AddressType addressType = objectFactory.createAddressType(); addressType.setStreetAddress1(this.streetAddress1); addressType.setStreetAddress2(this.streetAddress2); addressType.setCity(this.municipality); addressType.setState(this.state); addressType.setZipcode(this.zipCode); return addressType; } /** * Provide instance of {@link dustin.examples.AddressPlus} corresponding * to the provided instance of JAXB-generated object * {@link com.blogspot.marxsoftware.AddressType}. * * @param addressType Instance of JAXB-generated object * {@link com.blogspot.marxsoftware.AddressType}. * @return Instance of me corresponding to provided JAXB-generated object * {@link com.blogspot.marxsoftware.AddressType}. */ public static AddressPlus fromAddressType(final AddressType addressType) { return new AddressPlus( addressType.getStreetAddress1(), addressType.getStreetAddress2(), addressType.getCity(), addressType.getState(), addressType.getZipcode()); } }
The two approaches demonstrated above for mapping JAXB-generated objects to business/domain objects will definitely work and for my simple example might be considered the best approaches to use (especially given that NetBeans made the generation of the business/domain objects almost trivial). However, for more significant object hierarchies that require mapping, the Dozer configuration-based mapping might be considered preferable.
Dozer is downloaded from the download page (dozer-5.3.2.jar
in this case). The Getting Started page shows that mapping is really easy (minimal configuration) when the attributes of the classes being mapped have the same names. This is not the case in my example in which I intentionally made one attribute "city" and the other "municipality" to make the mapping more interesting. Because these names are different, I need to customize the Dozer mapping and this is done with XML mapping configuration. The necessary mapping file is named with a "default mapping name" of dozerBeanMapping.xml
and is shown next. I only needed to map the two fields with different names (city
and municipality
) because all other fields of the two classes being mapped have the same names and are automatically mapped together without explicit configuration.
<?xml version="1.0" encoding="UTF-8"?> <mappings xmlns="http://dozer.sourceforge.net" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://dozer.sourceforge.net http://dozer.sourceforge.net/schema/beanmapping.xsd"> <configuration> <stop-on-errors>true</stop-on-errors> <date-format>MM/dd/yyyy HH:mm:ss</date-format> <wildcard>true</wildcard> </configuration> <mapping> <class-a>dustin.examples.Address</class-a> <class-b>com.blogspot.marxsoftware.AddressType</class-b> <field> <a>municipality</a> <b>city</b> </field> </mapping> </mappings>
Note that XML is not the only approach that can be used to customize Dozer mapping; annotations and programmatic API are also supported.
The Dozer 3rd Party Object Factories page briefly covers using Dozer with JAXB and using the JAXBBeanFactory. It is also recommended that injection be used with Dozer and an example of Spring integration is provided. For my simple example of applying Dozer, I'm not using those approaches, but use the very straight forward instantiation approach. This is shown in the next code listing.
DozerPersonConverter.javapackage dustin.examples.dozerdemo; import com.blogspot.marxsoftware.PersonType; import dustin.examples.Person; import java.util.ArrayList; import java.util.List; import org.dozer.DozerBeanMapper; /** * Dozer-based converter. * * @author Dustin */ public class DozerPersonConverter { static final DozerBeanMapper mapper = new DozerBeanMapper(); static { final List<String> mappingFilesNames = new ArrayList<>(); mappingFilesNames.add("dozerBeanMapping.xml"); mapper.setMappingFiles(mappingFilesNames); } /** * Provide an instance of {@link com.blogspot.marxsoftware.PersonType} * that corresponds with provided {@link dustin.examples.Person} as * mapped by Dozer Mapper. * * @param person Instance of {@link dustin.examples.Person} from which * {@link com.blogspot.marxsoftware.PersonType} will be extracted. * @return Instance of {@link com.blogspot.marxsoftware.PersonType} that * is based on provided {@link dustin.examples.Person} instance. */ public PersonType copyPersonTypeFromPerson(final Person person) { final PersonType personType = mapper.map(person, PersonType.class); return personType; } /** * Provide an instance of {@link dustin.examples.Person} that corresponds * with the provided {@link com.blogspot.marxsoftware.PersonType} as * mapped by Dozer Mapper. * * @param personType Instance of {@link com.blogspot.marxsoftware.PersonType} * from which {@link dustin.examples.Person} will be extracted. * @return Instance of {@link dustin.examples.Person} that is based on the * provided {@link com.blogspot.marxsoftware.PersonType}. */ public Person copyPersonFromPersonType(final PersonType personType) { final Person person = mapper.map(personType, Person.class); return person; } }
The previous example shows how little code is required to map the JAXB-generated objects to business/domain objects. Of course, there was some XML needed, but only for fields with different names. This implies that the more the names of the fields differ, the more configuration is required. However, as long as fields are mostly mapped one-to-one without any special "conversion" logic between them, Dozer replaces much of the tedious code with configuration mapping.
If fields needed to be converted (such as converting meters in one object to kilometers in another object), then this mapping support may be less appealing when custom converters must be written. Dozer mapping can also become more difficult to apply correctly with deeply nested objects, but my example did nest Address
within Person
as a simple example. Although complex mappings might become less appealing in Dozer, many mappings of JAXB-generated objects to business/domain objects are simple enough mappings to be well served by Dozer.
One last thing I wanted to point out in this post is that Dozer has runtime dependencies on some third-party libraries. Fortunately, these libraries are commonly used in Java projects anyway and are readily available. As the next two images indicate, the required runtime dependencies are SLF4J, Apache Commons Lang, Apache Commons Logging, and Apache BeanUtils.
Dozer Runtime Dependencies Page NetBeans 7.4 Project Libraries for this Post's ExamplesThere is a small amount of effort required to set up Dozer and its dependencies and then to configure mappings, but this effort can be well rewarded with significantly reduced mapping code in many common JAXB-to-business object data copying applications.
3 comments:
The final section of this post on Dozer runtime dependencies was added later in the same day of the original post.
Nice intro to Dozer, thanks.
It's worth noting that some of your reasons for doing mapping can be resolved with the JAXB2 Basics Plugins: http://confluence.highsource.org/display/J2B/JAXB2+Basics+Plugins namely toString, hashCode, and equals. There's even a mapping one, tho I haven't used it.
But I admit I still use business model objects, largely to separate concerns between application layers and avoid tight coupling.
Here's some other reasons tho. Does Dozer help with any of these?
- Immutable business model objects with builders for thread-safety
- Method argument and return preconditions (@Nonnull, @Nullable, etc) for ease of use
- Flat schema, deep hierarchical business model
- Business model objects shared between multiple APIs with different schemas.
Mapping JAXB generated classes with collections is a serious pain in Dozer. Due to the lack of a generated collection setter Dozer seems to throw a NPE when trying to access that missing setter using Reflection.
Post a Comment