Статьи

Orika: сопоставление объектов JAXB с объектами бизнеса / домена

В этом посте рассматривается сопоставление объектов JAXB с объектами бизнес-доменов с помощью  Orika . Ранее в этом месяце  я описал тот же вариант  использования картографирования, используя  основанный на  отражении Dozer . В этом посте я буду предполагать, что те же примеры классов должны быть отображены, но они будут  отображаться  с использованием  Orika  вместо  Dozer .

Dozer и  Orika  предназначены для решения одной и той же проблемы: автоматическое сопоставление двух объектов «данных», которые не имеют общего наследования, но представляют одинаковые поля данных. Dozer  использует отражение  для достижения этой цели, в то время как  Orika использует  отражение и   манипулирование байт-кодом для достижения этой цели. Девиз Orika — «проще, легче и быстрее отображать Java-бин».

Orika  имеет  лицензию Apache версии 2 и может быть загружена по адресу  https://github.com/orika-mapper/orika/archive/master.zip  (sources) или по адресу  http://search.maven.org/#search. | га | 1 | орика  (двоичные файлы). У Orika есть  зависимости  от  Javassist  (для манипулирования байт-кодом),  SLF4J и  paranamer  (для доступа к именам параметров метода / конструктора во время выполнения). Две из этих трех зависимостей (JavaAssist и paranamer, но не SLF4J) связаны между собой  orika-core-1.4.4-deps-included.jar. Если зависимости уже доступны, orika-core-1.4.4.jar вместо них можно использовать более тонкий  . Как следует из названий этих JAR-файлов, я использую Orika 1.4.4 для своих примеров в этом посте.

В своем посте «  Dozer: отображение объектов JAXB на объекты бизнеса / домена» я рассмотрел причины, по которым использование экземпляров классов, генерируемых JAXB, в качестве объектов бизнеса или домена часто нежелательно. Затем я показал «традиционные» способы отображения между классами, сгенерированными JAXB, и пользовательскими классами данных, чтобы данные могли передаваться по всему приложению в объектах данных бизнес-области. В этом посте я буду использовать тот же подход, но Орика будет делать сопоставление, а не настраивать сопоставление или использовать Dozer для сопоставления. Для удобства, я включил списки стоимости здесь для JAXB сгенерированных классов  com.blogspot.marxsoftware.AddressTypeи  com.blogspot.marxsoftware.PersonType а также переименованных классы пользовательских данных dustin.examples.orikademo.Address и  dustin.examples.orikademo.Person.

Генерируемый JAXB AddressType.java

//
// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.4-2 
// See <a href="http://java.sun.com/xml/jaxb">http://java.sun.com/xml/jaxb</a> 
// 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;
    }

}

Генерируемый JAXB PersonType.java

//
// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, v2.2.4-2 
// See <a href="http://java.sun.com/xml/jaxb">http://java.sun.com/xml/jaxb</a> 
// 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;
    }

}

Домен / Бизнес класс Address.java

package dustin.examples.orikademo;

import java.util.Objects;

/**
 * Address class.
 * 
 * @author Dustin
 */
public class Address
{
   private String streetAddress1;
   private String streetAddress2;
   private String municipality;
   private String state;
   private String zipCode;

   public Address() {}

   public Address(
      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 Address other = (Address) 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 + '}';
   }
   
}

Домен / Бизнес класс Person.java

package dustin.examples.orikademo;

import java.util.Objects;

/**
 * Person class.
 * 
 * @author Dustin
 */
public class Person
{
   private String lastName;
   private String firstName;
   private Address mailingAddress;
   private Address residentialAddress;

   public Person() {}

   public Person(
      final String newLastName,
      final String newFirstName,
      final Address newResidentialAddress,
      final Address 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 Address getMailingAddress()
   {
      return this.mailingAddress;
   }

   public void setMailingAddress(Address mailingAddress)
   {
      this.mailingAddress = mailingAddress;
   }

   public Address getResidentialAddress()
   {
      return this.residentialAddress;
   }

   public void setResidentialAddress(Address 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 Person other = (Person) 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  "Person{" + "lastName=" + lastName + ", firstName=" + firstName
            + ", mailingAddress=" + mailingAddress + ", residentialAddress="
            + residentialAddress + '}';
   }

}

As was the case with Dozer, the classes being mapped need to have no-arguments constructors and «set» and «get» methods to support conversion in both directions without any special additional configuration. Also, as was the case with Dozer, Orika maps same-named fields automatically and makes it easy to configure the mapping of the exceptions (fields whose names don’t match). The next code listing, for a class I callOrikaPersonConverter, demonstrates the instantiation and configuration of an Orika MapperFactory to map most fields by default and to map the fields with different names than each other («municipality» and «city») through explicit mapping configuration. Once the MapperFactory is configured, copying from one object to another is easy and both directions are depicted in the methods copyPersonTypeFromPerson andcopyPersonFromPersonType.

OrikaPersonConverter

package dustin.examples.orikademo;

import com.blogspot.marxsoftware.AddressType;
import com.blogspot.marxsoftware.PersonType;
import ma.glasnost.orika.MapperFacade;
import ma.glasnost.orika.MapperFactory;
import ma.glasnost.orika.impl.DefaultMapperFactory;

/**
 * Convert between instances of {@link com.blogspot.marxsoftware.PersonType}
 * and {@link dustin.examples.orikademo.Person}.
 * 
 * @author Dustin
 */
public class OrikaPersonConverter
{
   /** Orika Mapper Facade. */
   private final static MapperFacade mapper;

   static
   {
      final MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
      mapperFactory.classMap(Address.class, AddressType.class)
                   .field("municipality", "city")
                   .byDefault()
                   .register();
      mapper = mapperFactory.getMapperFacade();
   }

   /** No-arguments constructor. */
   public OrikaPersonConverter() {}

   /**
    * Provide an instance of {@link com.blogspot.marxsoftware.PersonType}
    * that corresponds with provided {@link dustin.examples.orikademo.Person} as
    * mapped by Dozer Mapper.
    * 
    * @param person Instance of {@link dustin.examples.orikademo.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.orikademo.Person} instance.
    */
   public PersonType copyPersonTypeFromPerson(final Person person)
   {
      PersonType personType = mapper.map(person, PersonType.class);
      return personType;
   }

   /**
    * Provide an instance of {@link dustin.examples.orikademo.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.orikademo.Person} will be extracted.
    * @return Instance of {@link dustin.examples.orikademo.Person} that is based on the
    *    provided {@link com.blogspot.marxsoftware.PersonType}.
    */
   public Person copyPersonFromPersonType(final PersonType personType)
   {
      Person person = mapper.map(personType, Person.class);
      return person;
   }
}

As is the case with Dozer, the mapping between two classes is bidirectional and so only needs to be made once and will apply in copying from either object to the other.

Conclusion

Like Dozer, Orika offers much more customizability and flexibility than demonstrated in this post. However, for relatively simple mappings (which are very common with applications using JAXB-generated objects), Orika is very easy to use out of the box. A good resource for learning more about Orika is the Orika User Guide.