1. Introduction
Orika is a Java Bean mapping library. Essentially it recursively copies data from one object to another using the provided mapping metadata. In this tutorial, we're going to look at how we can create, register, and use Orika bean mappers.
2. Maven Dependency
Let's first add the orika Maven dependency:
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.5.4</version>
</dependency>
3. Sample Application
We'll mainly work with the Person and PersonDto classes:
public class Person {
private String firstName;
private String lastName;
private int age;
// Getters and setters...
}
public class PersonDto {
private String name;
private String surname;
private int age;
// Getters and setters...
}
Note that they contain the same data but some fields have different names: firstName and lastName. Throughout the examples we'll add more fields as required.
4. Basic Usage
We'll now create a mapper for the Person and PersonDto classes.
To create a mapper, we must first construct a MapperFactory instance. MapperFactory is the place where we configure and register our mappers:
final DefaultMapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
Next, we'll define our mapper. For this purpose, we'll use the ClassMapBuilder API that allows us to create field mappings:
final DefaultMapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
mapperFactory.classMap(Person.class, PersonDto.class)
.field("firstName", "name")
.field("lastName", "surname")
.byDefault()
.register();
Here, we're starting the ClassMapBuilder usage with the classMap method. We're mapping Person.firstName to PersonDto.name and Person.lastName to PersonDto.surname. Then we're invoking byDefault which maps the fields that have the same name - e.g. the age field. Lastly, we're registering our mapper invoking register.
At this moment, MapperFactory contains our mapper definition, but we don't have a mapper instance to do the actual work. MapperFacade allows us to do the actual mapping:
final DefaultMapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
mapperFactory.classMap(Person.class, PersonDto.class)
.field("firstName", "name")
.field("lastName", "surname")
.byDefault()
.register();
final MapperFacade mapperFacade = mapperFactory.getMapperFacade();
final Person person = getPerson();
final PersonDto mapped = mapperFacade.map(person, PersonDto.class);
Here, we're acquiring a MapperFacade instance invoking the getMapperFacade method. Then the mapping happens when we invoke the map method.
Alternatively, we can use the BoundedMapperFacade interface:
final BoundMapperFacade<Person, PersonDto> boundMapper = mapperFactory
.getMapperFacade(Person.class, PersonDto.class);
final PersonDto personDto = boundMapper.map(person);
Unlike the MapperFacade, we don't need to specify type information when invoking map. As the name implies, BoundedMapperFacade is bounded to a specific pair of types.
5. Configure Mappings
We've seen that we must configure the mapping process by defining a ClassMap instance. For this purpose, we've used the ClassMapBuilder API. Next, we'll examine how to configure the mappings in a fine-grained manner.
5.1. Map Fields By Default
Orika can map the fields with the same name if we opt-in for it. ClassMapBuilder provides the byDefault method which maps the fields if their names match:
public BoundMapperFacade<Person, PersonDto> mapWithDefaults() {
final DefaultMapperFactory mapperFactory = new Builder().build();
mapperFactory.classMap(Person.class, PersonDto.class)
.byDefault()
.register();
return mapperFactory.getMapperFacade(Person.class, PersonDto.class);
}
With this mapping configuration, Orika will copy only the age field from Person to PersonDto - or vice versa - ignoring other fields.
5.2. Map Fields with Different Names
When the field names don't match, we can define field-level mappings:
public BoundMapperFacade<Person, PersonDto> mapWithFields() {
final DefaultMapperFactory mapperFactory = new Builder().build();
mapperFactory.classMap(Person.class, PersonDto.class)
.field("firstName", "name")
.field("lastName", "surname")
.byDefault()
.register();
return mapperFactory.getMapperFacade(Person.class, PersonDto.class);
}
In this example, we're defining two field mappings by invoking ClassMapBuilder.field. Alternatively, we can use the fieldMap method for more control:
mapperFactory.classMap(Person.class, PersonDto.class)
.fieldMap("firstName", "name").direction(MappingDirection.BIDIRECTIONAL).add()
.fieldMap("lastName", "surname").direction(MappingDirection.BIDIRECTIONAL).add()
.byDefault()
.register();
Here we're explicitly defining the mapping direction as bidirectional - MappingDirection.BIDIRECTIONAL.
5.3. Exclude Fields From Mapping
Next, we'll exclude some fields from the mapping. For this purpose, we'll use the ClassMapBuilder.exclude method:
mapperFactory.classMap(Person.class, PersonDto.class)
.field("firstName", "name")
.field("lastName", "surname")
.exclude("age")
.byDefault()
.register();
Since we're excluding the age field, Orika won't copy it during the mapping.
Keep in mind that the order of invocation matters. If we invoke byDefault before exclude, the age field won't be excluded.
mapperFactory.classMap(Person.class, PersonDto.class)
.field("firstName", "name")
.field("lastName", "surname")
.byDefault()
.exclude("age")
.register();
In this configuration, Orika will copy the age field, although we're trying to exclude it. As a general practice, we must invoke the byDefault method last.
5.4. Map Fields in One Way
By default, the mappings we define work in both ways. In other words, they're bidirectional. Now we'll change this default behavior:
mapperFactory.classMap(Person.class, PersonDto.class)
.fieldAToB("firstName", "name")
.fieldBToA("age", "age")
.byDefault()
.register();
With this configuration, Orika copies firstName to name when we're mapping from Person to PersonDto. On the other way around, name isn't copied to firstName. Similarly, Orika copies the age field only when we're mapping from a PersonDto.
5.5. Map Fields in Nested Objects
Next, we'll use the fields of a nested object in a field mapping.
Let's first add the address details to the Person and PersonDto classes:
public class Address {
private String city;
private String postalCode;
// Getters and setters...
}
public class Person {
// Other fields...
private Address address;
}
public class PersonDto {
// Other fields...
private String city;
}
Here, Person contains an Address field - address - whereas PersonDto contains a String field - city.
In the mapping, we'll navigate to the fields of the Address instance using the dot '.' notation.
mapperFactory.classMap(Person.class, PersonDto.class)
.field("firstName", "name")
.field("lastName", "surname")
.field("address.city", "city")
.byDefault()
.register();
In this configuration, Person.address.city maps to PersonDto.city and vice versa.
5.6. Mapping with Multiple Mappers
When we have multiple classes in our object hierarchy, we need to register multiple mappers with our MapperFactory instance.
Let's add the work address to our classes:
public class Address {
private String city;
private String postalCode;
// Getters and setters...
}
public class AddressDto {
private String city;
private String zipCode;
// Getters and setters...
}
public class Person {
// Other fields...
private Address workAddress;
}
public class PersonDto {
// Other fields...
private AddressDto workAddress;
}
Note that AddressDto has zipCode, whereas the Address class has postalCode.
mapperFactory.classMap(Person.class, PersonDto.class)
.field("firstName", "name")
.field("lastName", "surname")
.byDefault()
.register();
mapperFactory.classMap(Address.class, AddressDto.class)
.field("postalCode","zipCode")
.byDefault()
.register();
final BoundMapperFacade<Person, PersonDto> boundMapper = mapperFactory
.getMapperFacade(Person.class, PersonDto.class);
In this MapperFactory configuration, we're registering two mappers. So when we map from a Person instance, Orika uses both the (Person, PersonDto) mapper and (Address, AddressDto) mapper.
6. Create Custom Mapper
Until now we've configured the mappers in a declarative way. In that we only specify the field names and Orika takes care of the rest including the extraction, conversion, and assignment of the values. But it doesn't always satisfy our needs. For example, we may want to perform a mapping only if some other condition is true. Or we may need to construct the target value after a calculation using the source value. For this purpose Orika provides the CustomMapper interface.
Let's see an example:
mapperFactory.classMap(Person.class, PersonDto.class)
.field("firstName", "name")
.field("lastName", "surname")
.customize(new CustomMapper<Person, PersonDto>() {
@Override
public void mapAtoB(Person person, PersonDto personDto, MappingContext context) {
if (person.getAge() > 21) {
personDto.setAge(person.getAge());
}
}
})
.byDefault()
.register();
Here, we're passing a CustomMapper implementation to the customize method. In the implementation, we're overriding the mapAtoB method, so it'll work only in one direction. Also, it copies the age field only if it's greater than 21.
7. Create Custom Converter
Orika provides some built-in converters to map fields with different types. For example, if class A has a Date field and class B has a long field with the same name, Orika uses Date.getTime when it maps from A to B. Orika encapsulates this conversion logic in the Converter<S, D> interface. It also provides an abstract base class, CustomConverter, for the custom converters to extend.
We'll now modify our Person and PersonDto classes:
public class Person {
// Other fields...
private Date birthDate;
}
public class PersonDto {
// Other fields...
private LocalDateTime birthDate;
}
We're adding a birthDate field to both classes. Note that they have different types: Date and LocalDateTime. Moreover, Orika doesn't provide a built-in Date to LocalDateTime converter. So we must implement one:
public static class DateToLocalDateTimeConverter extends CustomConverter<Date, LocalDateTime> {
@Override
public LocalDateTime convert(Date source, Type<? extends LocalDateTime> destinationType,
MappingContext mappingContext) {
return LocalDateTime.ofInstant(source.toInstant(), ZoneOffset.UTC);
}
}
In the DateToLocalDateTimeConverter, we're implementing the conversion in the convert method.
Next, we must register our converter so the mappers can use it:
ConverterFactory converterFactory = mapperFactory.getConverterFactory();
converterFactory.registerConverter(new DateToLocalDateTimeConverter());
mapperFactory.classMap(Person.class, PersonDto.class)
.field("firstName", "name")
.field("lastName", "surname")
.byDefault()
.register();
final BoundMapperFacade<Person, PersonDto> mapperFacade = mapperFactory
.getMapperFacade(Person.class, PersonDto.class);
As we can see, we're first getting the ConverterFactory and then registering our converter invoking the registerConverter method. As a result, the BoundMapperFacade instances use this custom converter whenever there is a need to convert a Date to a LocalDateTime.
Keep in mind that CustomConverter works only in one direction. To support both directions, we must provide an implementation of BidirectionalConverter<S, D>:
public static class DateAndLocalDateTimeConverter extends BidirectionalConverter<Date, LocalDateTime> {
@Override
public LocalDateTime convertTo(Date source, Type<LocalDateTime> destinationType,
MappingContext mappingContext) {
return LocalDateTime.ofInstant(source.toInstant(), ZoneOffset.UTC);
}
@Override
public Date convertFrom(LocalDateTime source, Type<Date> destinationType, MappingContext mappingContext) {
return Date.from(source.toInstant(ZoneOffset.UTC));
}
}
8. Summary
In this tutorial, we've investigated how we can use the Orika Java Bean mapping library. Firstly, we looked at its basic usage. Then we examined the details of the ClassMapBuilder API. Lastly, we covered the CustomMapper and CustomConverter implementations.
Lastly, the source code for all examples in this tutorial is available on Github.