Um guia para mapear com o Dozer

Um guia para mapear com o Dozer

1. Visão geral

Dozer é umJava Bean to Java Bean mapper que copia recursivamente dados de um objeto para outro, atributo por atributo.

A biblioteca não apenas suporta mapeamento entre nomes de atributos de Java Beans, mas tambémautomatically converts between types - se eles forem diferentes.

A maioria dos cenários de conversão são suportados fora da caixa, mas Dozer também permitespecify custom conversions via XML.

2. Exemplo Simples

Para o nosso primeiro exemplo, vamos supor que os objetos de dados de origem e destino compartilham os mesmos nomes de atributos comuns.

Este é o mapeamento mais básico que se pode fazer com o Dozer:

public class Source {
    private String name;
    private int age;

    public Source() {}

    public Source(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // standard getters and setters
}

Então nosso arquivo de destino,Dest.java:

public class Dest {
    private String name;
    private int age;

    public Dest() {}

    public Dest(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // standard getters and setters
}

Precisamos ter certeza deinclude the default or zero argument constructors, já que o Dozer usa reflexão sob o capô.

E, para fins de desempenho, vamos tornar nosso mapeador global e criar um único objeto que usaremos em nossos testes:

DozerBeanMapper mapper;

@Before
public void before() throws Exception {
    mapper = new DozerBeanMapper();
}

Agora, vamos executar nosso primeiro teste para confirmar que quando criamos um objetoSource, podemos mapeá-lo diretamente em um objetoDest:

@Test
public void givenSourceObjectAndDestClass_whenMapsSameNameFieldsCorrectly_
  thenCorrect() {
    Source source = new Source("example", 10);
    Dest dest = mapper.map(source, Dest.class);

    assertEquals(dest.getName(), "example");
    assertEquals(dest.getAge(), 10);
}

Como podemos ver, após o mapeamento Dozer, o resultado será uma nova instância do objetoDest que contém valores para todos os campos que possuem o mesmo nome de campo do objetoSource.

Alternativamente, em vez de passarmapper a classeDest, poderíamos apenas ter criado o objetoDest e passadomapper sua referência:

@Test
public void givenSourceObjectAndDestObject_whenMapsSameNameFieldsCorrectly_
  thenCorrect() {
    Source source = new Source("example", 10);
    Dest dest = new Dest();
    mapper.map(source, dest);

    assertEquals(dest.getName(), "example");
    assertEquals(dest.getAge(), 10);
}

3. Configuração do Maven

Agora que temos um entendimento básico de como o Dozer funciona, vamos adicionar a seguinte dependência aopom.xml:


    net.sf.dozer
    dozer
    5.5.1

A versão mais recente está disponívelhere.

4. Exemplo de conversão de dados

Como já sabemos, o Dozer pode mapear um objeto existente para outro, desde que encontre atributos com o mesmo nome nas duas classes.

No entanto, nem sempre é esse o caso; e, portanto, se algum dos atributos mapeados for de tipos de dados diferentes, o mecanismo de mapeamento Dozerautomatically perform a data type conversion.

Vamos ver este novo conceito em ação:

public class Source2 {
    private String id;
    private double points;

    public Source2() {}

    public Source2(String id, double points) {
        this.id = id;
        this.points = points;
    }

    // standard getters and setters
}

E a classe de destino:

public class Dest2 {
    private int id;
    private int points;

    public Dest2() {}

    public Dest2(int id, int points) {
        super();
        this.id = id;
        this.points = points;
    }

    // standard getters and setters
}

Observe que os nomes dos atributos são iguais, mastheir data types are different.

Na classe de origem,id é aStringepoints é adouble, enquanto na classe de destino,idepoints são ambos integers.

Vamos agora ver como Dozer lida corretamente com a conversão:

@Test
public void givenSourceAndDestWithDifferentFieldTypes_
  whenMapsAndAutoConverts_thenCorrect() {
    Source2 source = new Source2("320", 15.2);
    Dest2 dest = mapper.map(source, Dest2.class);

    assertEquals(dest.getId(), 320);
    assertEquals(dest.getPoints(), 15);
}

Passamos“320” e15.2, aString e adouble no objeto de origem e o resultado teve320e15,integer) ss no objeto de destino.

5. Mapeamentos personalizados básicos via XML

Em todos os exemplos anteriores que vimos, os objetos de dados de origem e de destino têm os mesmos nomes de campo, o que permite um mapeamento fácil do nosso lado.

No entanto, em aplicativos do mundo real, haverá inúmeras vezes em que os dois objetos de dados que estamos mapeando não terão campos que compartilham um nome de propriedade comum.

Para resolver isso, Dozer nos dá a opção de criar umcustom mapping configuration in XML.

Nesse arquivo XML, podemos definir entradas de mapeamento de classe que o mecanismo de mapeamento do Dozer usará para decidir qual atributo de origem mapear para qual atributo de destino.

Vamos dar uma olhada em um exemplo e tentar desempacotar objetos de dados de um aplicativo construído por um programador francês para um estilo inglês de nomear nossos objetos.

Temos um objetoPerson com os camposname,nicknameeage:

public class Person {
    private String name;
    private String nickname;
    private int age;

    public Person() {}

    public Person(String name, String nickname, int age) {
        super();
        this.name = name;
        this.nickname = nickname;
        this.age = age;
    }

    // standard getters and setters
}

O objeto que estamos desempacotando é denominadoPersonne e tem os camposnom,surnomeage:

public class Personne {
    private String nom;
    private String surnom;
    private int age;

    public Personne() {}

    public Personne(String nom, String surnom, int age) {
        super();
        this.nom = nom;
        this.surnom = surnom;
        this.age = age;
    }

    // standard getters and setters
}

Esses objetos realmente alcançam o mesmo objetivo, mas temos uma barreira de linguagem. Para ajudar com essa barreira, podemos usar o Dozer para mapear o objeto francêsPersonne para nosso objetoPerson.

Só temos que criar um arquivo de mapeamento personalizado para ajudar Dozer a fazer isso, vamos chamá-lo dedozer_mapping.xml:



    
        com.example.dozer.Personne
        com.example.dozer.Person
        
            nom
            name
        
        
            surnom
            nickname
        
    

Este é o exemplo mais simples de um arquivo de mapeamento XML personalizado que podemos ter.

Por enquanto, é suficiente notar que temos<mappings> como nosso elemento raiz, que tem um filho<mapping>, podemos ter tantos filhos dentro de<mappings> quantas incidências de classe pares que precisam de mapeamento personalizado.

Observe também como especificamos as classes de origem e destino dentro das tags<mapping></mapping>. Isso é seguido por<field></field> para cada par de campos de origem e destino que precisa de mapeamento personalizado.

Finalmente, observe que não incluímos o campoage em nosso arquivo de mapeamento personalizado. A palavra francesa para idade ainda é idade, o que nos leva a outra característica importante de Dozer.

Properties that are of the same name do not need to be specified in the mapping XML file. O Dozer mapeia automaticamente todos os campos com o mesmo nome de propriedade do objeto de origem no objeto de destino.

Em seguida, colocaremos nosso arquivo XML personalizado no caminho de classe, diretamente na pastasrc. No entanto, onde quer que o coloquemos no caminho de classe, o Dozer pesquisará o caminho de classe inteiro procurando o arquivo especificado.

Vamos criar um método auxiliar para adicionar arquivos de mapeamento ao nossomapper:

public void configureMapper(String... mappingFileUrls) {
    mapper.setMappingFiles(Arrays.asList(mappingFileUrls));
}

Vamos agora testar o código:

@Test
public void givenSrcAndDestWithDifferentFieldNamesWithCustomMapper_
  whenMaps_thenCorrect() {
    configureMapper("dozer_mapping.xml");
    Personne frenchAppPerson = new Personne("Sylvester Stallone", "Rambo", 70);
    Person englishAppPerson = mapper.map(frenchAppPerson, Person.class);

    assertEquals(englishAppPerson.getName(), frenchAppPerson.getNom());
    assertEquals(englishAppPerson.getNickname(), frenchAppPerson.getSurnom());
    assertEquals(englishAppPerson.getAge(), frenchAppPerson.getAge());
}

Conforme mostrado no teste,DozerBeanMapper aceita uma lista de arquivos de mapeamento XML personalizados e decide quando usar cada um no tempo de execução.

Supondo que agora começamos a organizar esses objetos de dados para frente e para trás entre nosso aplicativo em inglês e o aplicativo francês. Não precisamos criar outro mapeamento no arquivo XML,Dozer is smart enough to map the objects both ways with only one mapping configuration:

@Test
public void givenSrcAndDestWithDifferentFieldNamesWithCustomMapper_
  whenMapsBidirectionally_thenCorrect() {
    configureMapper("dozer_mapping.xml");
    Person englishAppPerson = new Person("Dwayne Johnson", "The Rock", 44);
    Personne frenchAppPerson = mapper.map(englishAppPerson, Personne.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(),englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), englishAppPerson.getAge());
}

E, portanto, este teste de exemplo usa outro recurso do Dozer - o fato dethe Dozer mapping engine is bi-directional, portanto, se quisermos mapear o objeto de destino para o objeto de origem, não precisamos adicionar outro mapeamento de classe ao arquivo XML.

Também podemos carregar um arquivo de mapeamento personalizado de fora do caminho de classe, se necessário, use o prefixo “file:” no nome do recurso.

Em um ambiente Windows (como o teste abaixo), é claro que usaremos a sintaxe de arquivo específica do Windows.

Em uma máquina Linux, podemos armazenar o arquivo em/homee:

configureMapper("file:/home/dozer_mapping.xml");

E no Mac OS:

configureMapper("file:/Users/me/dozer_mapping.xml");

Se você estiver executando os testes de unidade degithub project (o que deve ser feito), poderá copiar o arquivo de mapeamento para o local apropriado e alterar a entrada para o métodoconfigureMapper.

O arquivo de mapeamento está disponível na pasta test / resources do projeto GitHub:

@Test
public void givenMappingFileOutsideClasspath_whenMaps_thenCorrect() {
    configureMapper("file:E:\\dozer_mapping.xml");
    Person englishAppPerson = new Person("Marshall Bruce Mathers III","Eminem", 43);
    Personne frenchAppPerson = mapper.map(englishAppPerson, Personne.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(),englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), englishAppPerson.getAge());
}

6. Caracteres curinga e personalização de XML adicional

Vamos criar um segundo arquivo de mapeamento personalizado chamadodozer_mapping2.xml:



    
        com.example.dozer.Personne
        com.example.dozer.Person
        
            nom
            name
        
        
            surnom
            nickname
        
    

Observe que adicionamos um atributowildcard ao elemento<mapping></mapping> que não existia antes.

Por padrão,wildcard étrue. Diz ao mecanismo Dozer que queremos que todos os campos no objeto de origem sejam mapeados para seus campos de destino apropriados.

Quando o definimos comofalse,, estamos dizendo ao Dozer para mapear apenas os campos que especificamos explicitamente no XML.

Portanto, na configuração acima, queremos apenas dois campos mapeados, deixando de foraage:

@Test
public void givenSrcAndDest_whenMapsOnlySpecifiedFields_thenCorrect() {
    configureMapper("dozer_mapping2.xml");
    Person englishAppPerson = new Person("Shawn Corey Carter","Jay Z", 46);
    Personne frenchAppPerson = mapper.map(englishAppPerson, Personne.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(),englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), 0);
}

Como podemos ver na última asserção, o campo de destinoage permaneceu0.

7. Mapeamento personalizado por meio de anotações

Para casos simples de mapeamento e casos em que também temos acesso de gravação aos objetos de dados que gostaríamos de mapear, talvez não seja necessário usar o mapeamento XML.

O mapeamento de campos com nomes diferentes por meio de anotações é muito simples e precisamos escrever muito menos código do que no mapeamento XML, mas só pode nos ajudar em casos simples.

Vamos replicar nossos objetos de dados emPerson2.javaePersonne2.java sem alterar os campos.

Para implementar isso, precisamos apenas adicionar a anotação @mapper(“destinationFieldName”) nos métodosgetter no objeto de origem. Igual a:

@Mapping("name")
public String getNom() {
    return nom;
}

@Mapping("nickname")
public String getSurnom() {
    return surnom;
}

Desta vez, estamos tratandoPersonne2 como a fonte, mas isso não importa devido aobi-directional nature do Motor dozer.

Agora, com todo o código relacionado ao XML eliminado, nosso código de teste é mais curto:

@Test
public void givenAnnotatedSrcFields_whenMapsToRightDestField_thenCorrect() {
    Person2 englishAppPerson = new Person2("Jean-Claude Van Damme", "JCVD", 55);
    Personne2 frenchAppPerson = mapper.map(englishAppPerson, Personne2.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(), englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), englishAppPerson.getAge());
}

Também podemos testar a bidirecionalidade:

@Test
public void givenAnnotatedSrcFields_whenMapsToRightDestFieldBidirectionally_
  thenCorrect() {
    Personne2 frenchAppPerson = new Personne2("Jason Statham", "transporter", 49);
    Person2 englishAppPerson = mapper.map(frenchAppPerson, Person2.class);

    assertEquals(englishAppPerson.getName(), frenchAppPerson.getNom());
    assertEquals(englishAppPerson.getNickname(), frenchAppPerson.getSurnom());
    assertEquals(englishAppPerson.getAge(), frenchAppPerson.getAge());
}

8. Mapeamento de API personalizado

Nos exemplos anteriores, em que estamos retirando objetos de dados de um aplicativo em francês, usamos XML e anotações para personalizar nosso mapeamento.

Outra alternativa disponível no Dozer, semelhante ao mapeamento de anotação, é o mapeamento de API. Eles são semelhantes porque eliminamos a configuração XML e usamos estritamente o código Java.

Nesse caso, usamos a classeBeanMappingBuilder, definida em nosso caso mais simples da seguinte forma:

BeanMappingBuilder builder = new BeanMappingBuilder() {
    @Override
    protected void configure() {
        mapping(Person.class, Personne.class)
          .fields("name", "nom")
            .fields("nickname", "surnom");
    }
};

Como podemos ver, temos um método abstrato,configure(), que devemos sobrescrever para definir nossas configurações. Então, assim como nossas tags<mapping></mapping> em XML, definimos tantosTypeMappingBuilders quantos forem necessários.

Esses construtores dizem ao Dozer qual a origem dos campos de destino que estamos mapeando. Em seguida, passamosBeanMappingBuilder paraDozerBeanMapper como faríamos, o arquivo de mapeamento XML, apenas com uma API diferente:

@Test
public void givenApiMapper_whenMaps_thenCorrect() {
    mapper.addMapping(builder);

    Personne frenchAppPerson = new Personne("Sylvester Stallone", "Rambo", 70);
    Person englishAppPerson = mapper.map(frenchAppPerson, Person.class);

    assertEquals(englishAppPerson.getName(), frenchAppPerson.getNom());
    assertEquals(englishAppPerson.getNickname(), frenchAppPerson.getSurnom());
    assertEquals(englishAppPerson.getAge(), frenchAppPerson.getAge());
}

A API de mapeamento também é bidirecional:

@Test
public void givenApiMapper_whenMapsBidirectionally_thenCorrect() {
    mapper.addMapping(builder);

    Person englishAppPerson = new Person("Sylvester Stallone", "Rambo", 70);
    Personne frenchAppPerson = mapper.map(englishAppPerson, Personne.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(), englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), englishAppPerson.getAge());
}

Ou podemos optar por mapear apenas os campos especificados explicitamente com esta configuração do construtor:

BeanMappingBuilder builderMinusAge = new BeanMappingBuilder() {
    @Override
    protected void configure() {
        mapping(Person.class, Personne.class)
          .fields("name", "nom")
            .fields("nickname", "surnom")
              .exclude("age");
    }
};

e nosso testeage==0 está de volta:

@Test
public void givenApiMapper_whenMapsOnlySpecifiedFields_thenCorrect() {
    mapper.addMapping(builderMinusAge);
    Person englishAppPerson = new Person("Sylvester Stallone", "Rambo", 70);
    Personne frenchAppPerson = mapper.map(englishAppPerson, Personne.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(), englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), 0);
}

9. Conversores personalizados

Outro cenário que podemos enfrentar no mapeamento é onde gostaríamos deperform custom mapping between two objects.

Vimos cenários onde os nomes dos campos de origem e destino são diferentes, como no objeto francêsPersonne. Esta seção resolve um problema diferente.

E se um objeto de dados que estamos desempacotando representa um campo de data e hora, comolong ou hora Unix, assim:

1182882159000

Mas nosso próprio objeto de dados equivalente representa o mesmo campo de data e hora e valor neste formato ISO, comoString:

2007-06-26T21:22:39Z

O conversor padrão simplesmente mapearia o valor longo para umString assim:

"1182882159000"

Isso definitivamente incomodaria nosso aplicativo. Então, como resolvemos isso? Resolvemos isso poradding a configuration block no arquivo XML de mapeamento especifying our own converter.

Primeiro, vamos replicar o DTOPerson do aplicativo remoto com umname,, em seguida, data e hora de nascimento, campodtob:

public class Personne3 {
    private String name;
    private long dtob;

    public Personne3(String name, long dtob) {
        super();
        this.name = name;
        this.dtob = dtob;
    }

    // standard getters and setters
}

e aqui é o nosso:

public class Person3 {
    private String name;
    private String dtob;

    public Person3(String name, String dtob) {
        super();
        this.name = name;
        this.dtob = dtob;
    }

    // standard getters and setters
}

Observe a diferença de tipo dedtob nos DTOs de origem e destino.

Vamos também criar nosso próprioCustomConverter para passar para Dozer no XML de mapeamento:

public class MyCustomConvertor implements CustomConverter {
    @Override
    public Object convert(Object dest, Object source, Class arg2, Class arg3) {
        if (source == null)
            return null;

        if (source instanceof Personne3) {
            Personne3 person = (Personne3) source;
            Date date = new Date(person.getDtob());
            DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
            String isoDate = format.format(date);
            return new Person3(person.getName(), isoDate);

        } else if (source instanceof Person3) {
            Person3 person = (Person3) source;
            DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
            Date date = format.parse(person.getDtob());
            long timestamp = date.getTime();
            return new Personne3(person.getName(), timestamp);
        }
    }
}

Nós só temos que substituir o métodoconvert() e retornar o que quisermos. Nós somos beneficiados com os objetos de origem e destino e seus tipos de classe.

Observe como cuidamos da bidirecionalidade assumindo que a fonte pode ser uma das duas classes que estamos mapeando.

Criaremos um novo arquivo de mapeamento para maior clareza,dozer_custom_convertor.xml:



    
        
            
                com.example.dozer.Personne3
                com.example.dozer.Person3
            
        
    

Este é o arquivo de mapeamento normal que vimos nas seções anteriores, adicionamos apenas um bloco<configuration></configuration> dentro do qual podemos definir quantos conversores personalizados forem necessários com suas respectivas classes de dados de origem e destino.

Vamos testar nosso novo códigoCustomConverter:

@Test
public void givenSrcAndDestWithDifferentFieldTypes_whenAbleToCustomConvert_
  thenCorrect() {

    configureMapper("dozer_custom_convertor.xml");
    String dateTime = "2007-06-26T21:22:39Z";
    long timestamp = new Long("1182882159000");
    Person3 person = new Person3("Rich", dateTime);
    Personne3 person0 = mapper.map(person, Personne3.class);

    assertEquals(timestamp, person0.getDtob());
}

Também podemos testar para garantir que seja bidirecional:

@Test
public void givenSrcAndDestWithDifferentFieldTypes_
  whenAbleToCustomConvertBidirectionally_thenCorrect() {
    configureMapper("dozer_custom_convertor.xml");
    String dateTime = "2007-06-26T21:22:39Z";
    long timestamp = new Long("1182882159000");
    Personne3 person = new Personne3("Rich", timestamp);
    Person3 person0 = mapper.map(person, Person3.class);

    assertEquals(dateTime, person0.getDtob());
}

10. Conclusão

Neste tutorial, temosintroduced most of the basics of the Dozer Mapping librarye como usá-lo em nossos aplicativos.

A implementação completa de todos esses exemplos e trechos de código pode ser encontrada em Dozergithub project.