Orika é uma estrutura de mapeamento Java Bean querecursively copies data from one object to another. Pode ser muito útil ao desenvolver aplicativos de várias camadas.
Ao mover objetos de dados para frente e para trás entre essas camadas, é comum descobrir que precisamos converter objetos de uma instância para outra para acomodar APIs diferentes.
Algumas maneiras de fazer isso são:hard coding the copying logic or to implement bean mappers like Dozer. No entanto, ele pode ser usado para simplificar o processo de mapeamento entre uma camada de objeto e outra.
Orikauses byte code generation to create fast mappers com sobrecarga mínima, tornando-o muito mais rápido do que outros mapeadores baseados em reflexão comoDozer.
2. Exemplo Simples
A pedra angular básica da estrutura de mapeamento é a classeMapperFactory. Esta é a classe que usaremos para configurar os mapeamentos e obter a instânciaMapperFacade que realiza o trabalho de mapeamento real.
Criamos um objetoMapperFactory assim:
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
Então, supondo que temos um objeto de dados de origem,Source.java, com dois campos:
public class Source {
private String name;
private int age;
public Source(String name, int age) {
this.name = name;
this.age = age;
}
// standard getters and setters
}
E um objeto de dados de destino semelhante,Dest.java:
public class Dest {
private String name;
private int age;
public Dest(String name, int age) {
this.name = name;
this.age = age;
}
// standard getters and setters
}
Este é o mais básico de mapeamento de bean usando o Orika:
@Test
public void givenSrcAndDest_whenMaps_thenCorrect() {
mapperFactory.classMap(Source.class, Dest.class);
MapperFacade mapper = mapperFactory.getMapperFacade();
Source src = new Source("example", 10);
Dest dest = mapper.map(src, Dest.class);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), src.getName());
}
Como podemos observar, criamos um objetoDest com campos idênticos aosSource, simplesmente por mapeamento. O mapeamento bidirecional ou reverso também é possível por padrão:
@Test
public void givenSrcAndDest_whenMapsReverse_thenCorrect() {
mapperFactory.classMap(Source.class, Dest.class).byDefault();
MapperFacade mapper = mapperFactory.getMapperFacade();
Dest src = new Dest("example", 10);
Source dest = mapper.map(src, Source.class);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), src.getName());
}
3. Configuração do Maven
Para usar o Orika mapper em nossos projetos maven, precisamos terorika-core dependência empom.xml:
ma.glasnost.orikaorika-core1.4.6
A versão mais recente sempre pode ser encontradahere.
3. Trabalhando comMapperFactory
O padrão geral de mapeamento com Orika envolve a criação de um objetoMapperFactory, configurando-o no caso de precisarmos ajustar o comportamento de mapeamento padrão, obtendo um objetoMapperFacade dele e, finalmente, o mapeamento real.
Estaremos observando esse padrão em todos os nossos exemplos. Mas nosso primeiro exemplo mostrou o comportamento padrão do mapeador sem nenhum ajuste do nosso lado.
3.1. OBoundMapperFacade vsMapperFacade
Uma coisa a notar é que podemos escolher usarBoundMapperFacade em vez doMapperFacade padrão, que é bem lento. São casos em que temos um par específico de tipos para mapear.
Nosso teste inicial se tornaria assim:
@Test
public void givenSrcAndDest_whenMapsUsingBoundMapper_thenCorrect() {
BoundMapperFacade
boundMapper = mapperFactory.getMapperFacade(Source.class, Dest.class);
Source src = new Source("example", 10);
Dest dest = boundMapper.map(src);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), src.getName());
}
No entanto, paraBoundMapperFacade mapear bidirecionalmente, temos que chamar explicitamente o métodomapReverse em vez do método de mapa que examinamos para o caso doMapperFacade padrão:
@Test
public void givenSrcAndDest_whenMapsUsingBoundMapperInReverse_thenCorrect() {
BoundMapperFacade
boundMapper = mapperFactory.getMapperFacade(Source.class, Dest.class);
Dest src = new Dest("example", 10);
Source dest = boundMapper.mapReverse(src);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), src.getName());
}
Caso contrário, o teste falhará.
3.2. Configurar mapeamentos de campo
Os exemplos que vimos até agora envolvem classes de origem e destino com nomes de campos idênticos. Esta subseção aborda o caso em que há uma diferença entre os dois.
Considere um objeto de origem,Person, com três campos, a sabername,nicknameeage:
public class Person {
private String name;
private String nickname;
private int age;
public Person(String name, String nickname, int age) {
this.name = name;
this.nickname = nickname;
this.age = age;
}
// standard getters and setters
}
Em seguida, outra camada do aplicativo tem um objeto semelhante, mas escrito por um programador francês. Digamos que seja chamado dePersonne, com os camposnom,surnomeage, todos correspondendo aos três acima:
public class Personne {
private String nom;
private String surnom;
private int age;
public Personne(String nom, String surnom, int age) {
this.nom = nom;
this.surnom = surnom;
this.age = age;
}
// standard getters and setters
}
Orika não pode resolver automaticamente essas diferenças. Mas podemos usar oClassMapBuilderAPI to register these unique mappings.
Já o usamos antes, mas ainda não utilizamos nenhum de seus poderosos recursos. A primeira linha de cada um de nossos testes anteriores usando oMapperFacade padrão estava usando oClassMapBuilderAPI to register the two classes we wanted to map:
mapperFactory.classMap(Source.class, Dest.class);
Também podemos mapear todos os campos usando a configuração padrão, para deixar mais claro:
Não se esqueça de chamar o método APIregister() para registrar a configuração com oMapperFactory.
Mesmo que apenas um campo seja diferente, seguir essa rota significa que devemos registrar explicitamente os mapeamentos de campoall, incluindoage, que é o mesmo em ambos os objetos, caso contrário, o campo não registrado não será mapeado e o teste seria falhou.
Isso logo se tornará enfadonho,what if we only want to map one field out of 20, precisamos configurar todos os mapeamentos?
Não, não quando dizemos ao mapeador para usar sua configuração de mapeamento padrão nos casos em que não definimos explicitamente um mapeamento:
Aqui, não definimos um mapeamento para o campoage, mas mesmo assim o teste será aprovado.
3.3. Excluir um Campo
Supondo que gostaríamos de excluir o camponom dePersonne do mapeamento - de modo que o objetoPerson receba apenas novos valores para campos que não foram excluídos:
Observe como o excluímos na configuração deMapperFactorye, em seguida, observe também a primeira afirmação onde esperamos que o valor dename no objetoPerson permaneçanull, como resultado de sua exclusão no mapeamento.
4. Mapeamento de coleções
Às vezes, o objeto de destino pode ter atributos exclusivos, enquanto o objeto de origem apenas mantém todas as propriedades em uma coleção.
4.1. Listas e matrizes
Considere um objeto de dados de origem que tem apenas um campo, uma lista dos nomes de uma pessoa:
public class PersonNameList {
private List nameList;
public PersonNameList(List nameList) {
this.nameList = nameList;
}
}
Agora considere nosso objeto de dados de destino que separafirstNameelastName em campos separados:
public class PersonNameParts {
private String firstName;
private String lastName;
public PersonNameParts(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
Vamos supor que temos certeza de que no índice 0 sempre haverá ofirstName da pessoa e no índice 1 sempre haverá seulastName.
A Orika nos permite usar a notação de colchete para acessar membros de uma coleção:
@Test
public void givenSrcWithListAndDestWithPrimitiveAttributes_whenMaps_thenCorrect() {
mapperFactory.classMap(PersonNameList.class, PersonNameParts.class)
.field("nameList[0]", "firstName")
.field("nameList[1]", "lastName").register();
MapperFacade mapper = mapperFactory.getMapperFacade();
List nameList = Arrays.asList(new String[] { "Sylvester", "Stallone" });
PersonNameList src = new PersonNameList(nameList);
PersonNameParts dest = mapper.map(src, PersonNameParts.class);
assertEquals(dest.getFirstName(), "Sylvester");
assertEquals(dest.getLastName(), "Stallone");
}
Mesmo se em vez dePersonNameList, tivéssemosPersonNameArray, o mesmo teste seria aprovado para uma matriz de nomes.
4.2. Maps
Supondo que nosso objeto de origem tenha um mapa de valores. Sabemos que há uma chave nesse mapa,first, cujo valor representafirstName de uma pessoa em nosso objeto de destino.
Da mesma forma, sabemos que existe outra chave,last, no mesmo mapa, cujo valor representalastName de uma pessoa no objeto de destino.
public class PersonNameMap {
private Map nameMap;
public PersonNameMap(Map nameMap) {
this.nameMap = nameMap;
}
}
Semelhante ao caso da seção anterior, usamos a notação entre colchetes, mas, em vez de passar um índice, passamos a chave cujo valor queremos mapear para o campo de destino especificado.
Orika aceita duas maneiras de recuperar a chave, ambas são representadas no seguinte teste:
@Test
public void givenSrcWithMapAndDestWithPrimitiveAttributes_whenMaps_thenCorrect() {
mapperFactory.classMap(PersonNameMap.class, PersonNameParts.class)
.field("nameMap['first']", "firstName")
.field("nameMap[\"last\"]", "lastName")
.register();
MapperFacade mapper = mapperFactory.getMapperFacade();
Map nameMap = new HashMap<>();
nameMap.put("first", "Leornado");
nameMap.put("last", "DiCaprio");
PersonNameMap src = new PersonNameMap(nameMap);
PersonNameParts dest = mapper.map(src, PersonNameParts.class);
assertEquals(dest.getFirstName(), "Leornado");
assertEquals(dest.getLastName(), "DiCaprio");
}
Podemos usar aspas simples ou duplas, mas devemos escapar da última.
5. Mapear campos aninhados
Seguindo os exemplos das coleções anteriores, suponha que dentro do nosso objeto de dados de origem, haja outro DTO (Data Transfer Object) que contenha os valores que queremos mapear.
public class PersonContainer {
private Name name;
public PersonContainer(Name name) {
this.name = name;
}
}
public class Name {
private String firstName;
private String lastName;
public Name(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
Para poder acessar as propriedades do DTO aninhado e mapeá-las em nosso objeto de destino, usamos a notação de ponto, assim:
Em alguns casos, você pode querer controlar se os nulos são mapeados ou ignorados quando são encontrados. Por padrão, o Orika mapeará valores nulos quando encontrados:
@Test
public void givenSrcWithNullField_whenMapsThenCorrect() {
mapperFactory.classMap(Source.class, Dest.class).byDefault();
MapperFacade mapper = mapperFactory.getMapperFacade();
Source src = new Source(null, 10);
Dest dest = mapper.map(src, Dest.class);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), src.getName());
}
Esse comportamento pode ser personalizado em diferentes níveis, dependendo de quão específico gostaríamos de ser.
6.1. Configuração Global
Podemos configurar nosso mapeador para mapear nulos ou ignorá-los no nível global antes de criarMapperFactory globais. Lembra-se de como criamos esse objeto em nosso primeiro exemplo? Desta vez, adicionamos uma chamada extra durante o processo de compilação:
MapperFactory mapperFactory = new DefaultMapperFactory.Builder()
.mapNulls(false).build();
Podemos executar um teste para confirmar que, de fato, os nulos não estão sendo mapeados:
@Test
public void givenSrcWithNullAndGlobalConfigForNoNull_whenFailsToMap_ThenCorrect() {
mapperFactory.classMap(Source.class, Dest.class);
MapperFacade mapper = mapperFactory.getMapperFacade();
Source src = new Source(null, 10);
Dest dest = new Dest("Clinton", 55);
mapper.map(src, dest);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), "Clinton");
}
O que acontece é que, por padrão, os nulos são mapeados. Isso significa que mesmo se um valor de campo no objeto de origem fornulle o valor do campo correspondente no objeto de destino tiver um valor significativo, ele será substituído.
Em nosso caso, o campo de destino não é sobrescrito se seu campo de origem correspondente tiver um valornull.
6.2. Configuração Local
O mapeamento de valores denull pode ser controlado emClassMapBuilder usandomapNulls(true|false) oumapNullsInReverse(true|false) para controlar o mapeamento de nulos na direção reversa.
Ao definir esse valor em uma instânciaClassMapBuilder, todos os mapeamentos de campo criados no mesmoClassMapBuilder, após o valor ser definido, assumirão o mesmo valor.
Vamos ilustrar isso com um exemplo de teste:
@Test
public void givenSrcWithNullAndLocalConfigForNoNull_whenFailsToMap_ThenCorrect() {
mapperFactory.classMap(Source.class, Dest.class).field("age", "age")
.mapNulls(false).field("name", "name").byDefault().register();
MapperFacade mapper = mapperFactory.getMapperFacade();
Source src = new Source(null, 10);
Dest dest = new Dest("Clinton", 55);
mapper.map(src, dest);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), "Clinton");
}
Observe como chamamosmapNulls antes de registrar o camponame, isso fará com que todos os campos após a chamadamapNulls sejam ignorados quando tiverem o valornull.
O mapeamento bidirecional também aceita valores nulos mapeados:
@Test
public void givenDestWithNullReverseMappedToSource_whenMapsByDefault_thenCorrect() {
mapperFactory.classMap(Source.class, Dest.class).byDefault();
MapperFacade mapper = mapperFactory.getMapperFacade();
Dest src = new Dest(null, 10);
Source dest = new Source("Vin", 44);
mapper.map(src, dest);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), src.getName());
}
Também podemos evitar isso chamandomapNullsInReversee passandofalse:
@Test
public void
givenDestWithNullReverseMappedToSourceAndLocalConfigForNoNull_whenFailsToMap_thenCorrect() {
mapperFactory.classMap(Source.class, Dest.class).field("age", "age")
.mapNullsInReverse(false).field("name", "name").byDefault()
.register();
MapperFacade mapper = mapperFactory.getMapperFacade();
Dest src = new Dest(null, 10);
Source dest = new Source("Vin", 44);
mapper.map(src, dest);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), "Vin");
}
6.3. Configuração de Nível de Campo
Podemos configurar isso no nível do campo usandofieldMap, assim:
Neste caso, a configuração afetará apenas o camponame como o chamamos no nível do campo:
@Test
public void givenSrcWithNullAndFieldLevelConfigForNoNull_whenFailsToMap_ThenCorrect() {
mapperFactory.classMap(Source.class, Dest.class).field("age", "age")
.fieldMap("name", "name").mapNulls(false).add().byDefault().register();
MapperFacade mapper = mapperFactory.getMapperFacade();
Source src = new Source(null, 10);
Dest dest = new Dest("Clinton", 55);
mapper.map(src, dest);
assertEquals(dest.getAge(), src.getAge());
assertEquals(dest.getName(), "Clinton");
}
7. Orika Mapeamento Personalizado
Até agora, vimos exemplos de mapeamento personalizado simples usando oClassMapBuilderAPI. We shall still use the same API but customize our mapping using Orika’s CustomMapper class.
Assumindo que temos dois objetos de dados, cada um com um determinado campo chamadodtob, representando a data e hora de nascimento de uma pessoa.
Um objeto de dados representa esse valor comodatetime String no seguinte formato ISO:
2007-06-26T21:22:39Z
e o outro representa o mesmo que um tipolong no seguinte formato de carimbo de data / hora Unix:
1182882159000
Claramente, nenhuma das personalizações que cobrimos até agora é suficiente para converter entre os dois formatos durante o processo de mapeamento, nem mesmo o conversor embutido do Orika pode lidar com o trabalho. É aqui que temos que escrever umCustomMapper para fazer a conversão necessária durante o mapeamento.
Vamos criar nosso primeiro objeto de dados:
public class Person3 {
private String name;
private String dtob;
public Person3(String name, String dtob) {
this.name = name;
this.dtob = dtob;
}
}
então nosso segundo objeto de dados:
public class Personne3 {
private String name;
private long dtob;
public Personne3(String name, long dtob) {
this.name = name;
this.dtob = dtob;
}
}
Não rotularemos qual é a origem e qual é o destino agora, poisCustomMapper nos permite fornecer mapeamento bidirecional.
Aqui está nossa implementação concreta da classe abstrataCustomMapper:
class PersonCustomMapper extends CustomMapper {
@Override
public void mapAtoB(Personne3 a, Person3 b, MappingContext context) {
Date date = new Date(a.getDtob());
DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
String isoDate = format.format(date);
b.setDtob(isoDate);
}
@Override
public void mapBtoA(Person3 b, Personne3 a, MappingContext context) {
DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
Date date = format.parse(b.getDtob());
long timestamp = date.getTime();
a.setDtob(timestamp);
}
};
Observe que implementamos os métodosmapAtoBemapBtoA. A implementação de ambos torna nossa função de mapeamento bidirecional.
Each method exposes the data objects we are mapping and we take care of copying the field values from one to the other.
Lá é onde escrevemos o código personalizado para manipular os dados de origem de acordo com nossos requisitos antes de gravá-los no objeto de destino.
Vamos fazer um teste para confirmar se nosso mapeador personalizado funciona:
@Test
public void givenSrcAndDest_whenCustomMapperWorks_thenCorrect() {
mapperFactory.classMap(Personne3.class, Person3.class)
.customize(customMapper).register();
MapperFacade mapper = mapperFactory.getMapperFacade();
String dateTime = "2007-06-26T21:22:39Z";
long timestamp = new Long("1182882159000");
Personne3 personne3 = new Personne3("Leornardo", timestamp);
Person3 person3 = mapper.map(personne3, Person3.class);
assertEquals(person3.getDtob(), dateTime);
}
Observe que ainda passamos o mapeador personalizado para o mapeador do Orika viaClassMapBuilderAPI, just like all other simple customizations.
Também podemos confirmar que o mapeamento bidirecional funciona:
@Test
public void givenSrcAndDest_whenCustomMapperWorksBidirectionally_thenCorrect() {
mapperFactory.classMap(Personne3.class, Person3.class)
.customize(customMapper).register();
MapperFacade mapper = mapperFactory.getMapperFacade();
String dateTime = "2007-06-26T21:22:39Z";
long timestamp = new Long("1182882159000");
Person3 person3 = new Person3("Leornardo", dateTime);
Personne3 personne3 = mapper.map(person3, Personne3.class);
assertEquals(person3.getDtob(), timestamp);
}
8. Conclusão
Neste artigo, temosexplored the most important features of the Orika mapping framework.
Definitivamente, existem recursos mais avançados que nos dão muito mais controle, mas na maioria dos casos de uso, os abordados aqui serão mais do que suficientes.
O código completo do projeto e todos os exemplos podem ser encontrados em meugithub project. Não se esqueça de verificar nosso tutorial noDozer mapping framework também, já que ambos resolvem mais ou menos o mesmo problema.