Alura > Cursos de Programação > Cursos de Java > Conteúdos de Java > Primeiras aulas do curso Java Reflection: simplifique a conversão de objetos

Java Reflection: simplifique a conversão de objetos

A classe Class - Apresentação

Olá! Meu nome é João Victor Martins e serei seu instrutor neste curso de Java Reflection! Se você ainda não tem familiaridade com a API de Reflection, esta é uma ótima oportunidade para descobrir esse poderoso recurso do Java.

Audiodescrição: João Victor é um homem branco, de cabelo liso curto preto, barba preta, sobrancelhas pretas e olhos pretos. Ele veste uma camisa preta e está sentado em frente a uma parede clara iluminada em gradiente azul.

Pré-requisitos

É recomendável que você tenha conhecimento prévio sobre o núcleo do Java, como orientação a objetos, classes e interfaces, pois isso facilitará seu aprendizado durante o curso.

O que vamos aprender?

Se você já possui esses conhecimentos e deseja aprofundar-se em Reflection, agregando habilidades específicas a seu repertório, este é o lugar certo. Neste curso, vamos simular um código que realiza a transformação de objetos, como, por exemplo, um objeto Pessoa, com suas características e métodos, para um objeto DTO. Lembrando que seguiremos boas práticas visando a proteção de dados, e é vantajoso utilizar DTOs para o tráfego entre camadas da aplicação.

Nosso código será abstrato. Vamos capturar um input, nesse caso, um objeto Pessoa, e transformá-lo em PessoaDTO. Utilizaremos um código genérico, empregando genéricos e a API de Reflection, evitando a instanciação manual de objetos e suas transformações para cada caso. Evitaremos a necessidade de instanciar um EnderecoDTO e atribuir todos os seus campos manualmente, por exemplo. Com isso, teremos um código reutilizável.

Para verificar o funcionamento, realizaremos diversos cenários de teste com o JUnit, uma biblioteca para testes de unidade em Java. Exploraremos situações onde a transformação deve acontecer, quando não deve, ou quando um campo está nulo.

Além disso, teremos uma aula adicional sobre a conversão de objetos Java para JSON, uma demanda comum atualmente. Utilizaremos uma biblioteca externa, mas ainda aproveitando a API de Reflection, para testar a transformação completa e verificar o resultado em formato JSON.

Conclusão

Se você tem interesse em enriquecer seus conhecimentos sobre Reflection e APIs Java, nos acompanhe neste curso. Aguardamos você no próximo vídeo!

A classe Class - Falando sobre Reflection

A grande maioria das pessoas desenvolvedoras de software já tiveram que trabalhar com sistemas que possuem múltiplas camadas. Quando falamos de múltiplas camadas, nos referimos a camadas como controllers, services e repositories.

Estas camadas servem para separar e atribuir responsabilidades únicas às nossas classes, tornando o código mais legível. Portanto, é muito comum realizarmos essa separação. Existem vários tipos de arquitetura que promovem essa divisão, mas, ao trabalharmos com código, sempre lidamos com camadas.

Um aspecto comum é o objeto que trafegamos entre essas camadas. Vamos considerar uma arquitetura mais simples, que inclui o controller, o service e o repository. A camada controller é a que recebe a requisição da pessoa usuária, que pode ser um insert ou a solicitação de uma lista, por exemplo. Essa requisição chega ao Controller.

Temos uma camada intermediária, geralmente chamada de classe de serviço, onde se encontram as regras de negócio. Por fim, temos o repository, onde recebemos informações e salvamos detalhes no banco de dados. Assim, o repository é utilizado para armazenar essas informações.

Falando sobre Reflection

Na IDE, começaremos com a classe PessoaRepository aberta, que simula o comportamento da pessoa. Há um método list() que retorna uma Pessoa que instanciamos manualmente.

A classe Pessoa possui atributos privados, como id, nome e cpf, para promover o encapsulamento. Usamos um construtor para criar a classe Pessoa, e métodos getters são disponibilizados para acessar as informações da pessoa.

Quando trafegamos esses dados entre camadas, por exemplo, a partir de PessoaService, chamamos PessoaRepository. O método list() retorna uma instância de Pessoa, que seria encaminhada para o controller e, em seguida, retornada para a pessoa usuária. No entanto, não é uma boa prática retornar objetos diretos do banco de dados, que podem conter informações sensíveis como o ID.

Para proteger nosso domínio e os dados, criamos classes DTO (Data Transfer Object, ou Objeto de Transferência de Dados), que contêm apenas os campos necessários para o cliente. Assim, protegemos nosso objeto, no caso, a pessoa.

Geralmente, fazemos uma transformação de forma explícita em alguns projetos. Por exemplo: na classe PessoaService, teríamos um método list() que retorna PessoaDTO. Este método chama o PessoaRepository(), obtém uma Pessoa e a transforma em PessoaDTO.

Para isso, usamos o construtor PessoaDTO(), passando os campos necessários, e retornamos esta nova instância sem o ID, que é uma informação do banco de dados irrelevante para a pessoa usuária.

PessoaService.java:

package br.com.alura;

public class PessoaService {

    public PessoaDTO list() {
        Pessoa pessoa = new PessoaRepository().list();
        PessoaDTO pessoaDTO = new PessoaDTO(pessoa.getNome(), pessoa.getCpf());
        return pessoaDTO;
    }
}

Embora seja possível realizar essa transformação no arquivo PessoaRepository.java, preferimos deixar essa responsabilidade para o serviço, pois consideramos que o repositório deve se concentrar mais no banco de dados.

O processo de transformar objetos em DTOs pode se tornar trabalhoso, principalmente quando temos vários domínios e precisamos realizar essas conversões frequentemente. Seria ideal ter uma função ou biblioteca capaz de realizar a conversão de uma classe para uma classe DTO de maneira abstrata.

Essa lógica de um código mais abstrato conseguiria identificar que recebemos, por exemplo, uma classe Pessoa. Existe uma classe PessoaDTO para transformar? Quais são os campos entre eles que precisamos usar de Pessoa para PessoaDTO? A lógica faz isso: compara campos, tudo isso sem a necessidade do trabalho manual de instanciar.

É nesse momento que entra a API Java Reflection. Com a documentação Java Reflection API aberta, temos a informação de que a Reflection, basicamente, habilita o código Java a descobrir informações sobre campos, métodos e construtores de classes carregadas, permitindo que um código Java manipule objetos de uma maneira mais flexível e abstrata.

A documentação da Oracle nos mostra o que é a Java Reflection API e como ela permite que um código descubra informações sobre as classes, sem necessidade de conhecer os detalhes previamente. Com isso, podemos transformar classes em DTOs de forma mais abstrata e eficiente.

Pode parecer confuso a princípio, mas conseguiremos usar a Reflection para fazer o trabalho que antes fazíamos manualmente, passaremos por todos os detalhes e você vai perceber que não é tão trivial, mas conseguiremos fazer um acompanhamento interessante e deixar nosso código muito elegante.

Ainda na documentação, temos a categoria "API Specification", que são as especificações da API Java Reflection, onde encontramos o link java.lang.reflect, que é de fato o pacote com as classes e interfaces da Reflection que usaremos no nosso código.

Acessando este link, encontramos um sumário de interfaces do pacote java.lang.reflect que iremos usar para fazer a transformação das classes em classes DTO de forma muito mais abstrata.

Conclusão

No próximo vídeo, vamos começar a melhorar o nosso código e explorar mais a fundo o desenvolvimento com a API de Reflection. Te encontramos lá!

A classe Class - Usando a classe Class

Falamos anteriormente que vamos criar um código bem abstrato e elegante, que vai fazer a transformação da classe em uma classe DTO usando generics. Este será nosso objetivo a partir de agora!

Usando a classe Class

Começaremos na IDE sem nada aberto, para começar agora de fato o desenvolvimento da nova estrutura. Com a aba do projeto aberta na lateral esquerda da IDE, vamos criar uma nova classe que será responsável por fazer essa transformação genérica.

Temos uma estrutura com as pastas "src", "main", "java", e dentro de "java" temos o pacote "br.com.alura", onde estão as classes usamos para simular o comportamento da transformação: Pessoa, PessoaDTO, PessoaRepository, e PessoaService.

Não precisamos nos preocupar com essas classes agora, então basta criar um novo pacote, clicando no botão direito em "br.com.alura". Esse pacote se chamará "br.com.alura.refl", referente a reflexão.

Não usaremos "reflection", porque já temos a API Reflection e podemos confundir o pacote "reflection" com o pacote Reflection do Java.

Criando a classe Transformator

Uma vez criado o novo pacote, criaremos uma nova classe dentro dele, chamada Transformator.

Transformator.java:

package br.com.alura.refl;

public class Transformator {

}

A ideia é ter uma função que vamos poder chamar. Essa função vai receber qualquer tipo e vai retornar o tipo dessa entrada DTO. Mas como fazemos para informar qualquer tipo para a classe?

Antes, estávamos trabalhando com Pessoa, transformando em PessoaDTO; agora, falamos de algo que não sabemos e temos que transformar. Como passamos esse parâmetro?

Vamos começar criando na classe Transformator o primeiro método, que será público (public). Por enquanto, deixaremos ele como void, e definiremos o nome do método como transform().

package br.com.alura.refl;

public class Transformator {

    public void transform() {
    
    }

}

Temos uma função básica. Agora precisamos receber qualquer tipo. Para receber qualquer tipo, temos outra API, outro recurso no Java, que são os generics.

Usando Generics

Os generics nos permitem deixar o código genérico para que possamos receber qualquer coisa. Então, em vez de trabalhar com tipos específicos, trabalhamos com os generics, que podem receber qualquer coisa. Na documentação, encontramos a seção "Generic Types".

Basicamente, podemos trabalhar com as convenções presentes na documentação, como o "The Diamond", que vamos criar na sequência, que seria o sinal de menor e maior, onde vamos passar letras e valores que vão representar o parâmetro que será informado ainda no momento em execução.

No nosso caso, analisando a interface de exemplo da documentação, temos uma interface Pair que tem o K e o V dentro do "Diamond". Com isso, falamos que o K e o V são retornos das funções getKey() e getValue(), respectivamente.

public interface Pair<K, V> {
    public K getKey();
    public V getValue();
}

Faremos algo semelhante no nosso código para conseguirmos receber qualquer parâmetro.

De volta à IDE, em vez de o método transform() ser um void, ele será um <I, O>, sendo o I referente à entrada (input), e o O referente à saída (output).

Transformator.java:

package br.com.alura.refl;

public class Transformator {

    public <I, O> transform() {
    
    }

}

Dessa forma, já usamos a convenção do "Diamond" do generics. Agora, o método transform() vai receber esse parâmetro qualquer, representado pelo I.

Quando digitamos I entre os parênteses, o IntelliJ sugere a opção "type parameter of transform". Esse é o tipo do parâmetro do método transform(). Chamaremos o I de input.

public <I, O> transform(I input) {

}

Temos um método que pode receber qualquer tipo que será representado pelo input declarado no generics. Ainda não vamos trabalhar com o O, porque nossa intenção é, primeiro, pegar a classe de entrada e fazer o tratamento para depois transformar para o output, que seria o data class, mas isso será feito mais adiante. O que queremos agora é pegar os detalhes da classe que recebemos no input.

Para isso, chamamos a variável input no escopo do método e digitamos um ponto (.). Como sugestão, temos o getClass(). Com o getClass(), conseguimos ter acesso a algumas informações da classe.

Se analisarmos, o retorno fornecido é um tipo Class<?> que chamaremos de source. Com a interrogação em Class<?>, falamos para o código que pegamos do input o getClass(), retornamos uma Class<?>, mas não sabemos o tipo ainda. Para nós, ainda está muito genérico e abstrato.

public <I, O> transform(I input) {
    Class<?> source = input.getClass();
}

Nosso código agora está mais funcional. O método transform() ainda reclama um erro, porque não há um return type e precisamos retornar algo, mas trabalharemos isso ao longo do desenvolvimento.

Se abrirmos o input.getClass(), observaremos que o Class<?> é do objeto Object.java, que é o pai de todos os objetos, o objeto de mais alta hierarquia no modelo do Java.

Por outro lado, se abrirmos Class<?>, ele será uma classe Java como qualquer outra, no arquivo Class.java. Porém, agora começamos a encontrar algumas coisas referentes à reflexão.

Temos várias implementações do que podemos fazer com a classe Class<?>. É dela que vamos pegar, por exemplo, os campos, os construtores, tudo através da classe Class<?>, para conseguirmos instanciar o objeto e transformar.

Já estamos usando a reflexão da Class<?>, e agora a ideia é realmente conseguir usar esses recursos a nosso favor. Temos uma source, e agora precisamos transformar essa classe que é o nosso input, na classe input com o final DTO. O padrão da nomenclatura será, por exemplo, ao entrar com a classe Pessoa, ter um PessoaDTO para conseguir fazer essa transformação.

Nesse caso, podemos usar source.forName. Entre os parênteses de forName(), vamos chamar source.getClass() novamente, mas agora vamos concatenar o nome DTO.

public <I, O> transform(I input) {
    Class<?> source = input.getClass();
    Class<?> target = sourceforName(source.getClass() + "DTO")
}

Com isso, falamos que o nosso objetivo, isto é, o nosso target será ter uma classe que será o nome da source, que pegamos com getClass(), mais o final DTO.

Se o input recebe a classe Pessoa, pegamos com getClass() essa classe, e agora o objetivo é instanciar uma classe PessoaDTO, que pegamos com source.getClass() + "DTO", para então podermos fazer a transformação.

O código é um pouco complicado, mas conforme desenvolvemos, encontramos os detalhes e as coisas começam a fazer sentido.

Você pode estar se perguntando: por que temos um erro de compilação no forName()? Porque precisamos adicionar na assinatura do método transform() a exceção ClassNotFoundException.

public <I, O> transform(I input) throws ClassNotFoundException {
    Class<?> source = input.getClass();
    Class<?> target = source.forName(source.getClass() + "DTO");
}

Em forName(), se, por exemplo, ele tentar buscar uma classe PessoaDTO e ela não existir, teremos ClassNotFoundException, que é uma exceção checada. Precisamos deixar explícito que o método transform() pode dar esse tipo de exceção.

Feito isso, o nosso código passa a compilar, exceto o método transform(), devido ao retorno que ainda não declaramos.

Conclusão

O nosso objetivo era pegar a classe de alguma forma, para começarmos a trabalhar com os seus parâmetros e construtores. O resto de pegar campos, construtores e instanciar, deixaremos para os próximos vídeos. Nossa ideia é realmente dar um passo de cada vez para a concepção da nossa função.

Vamos continuar o trabalho? Para isso, te esperamos no próximo vídeo!

Sobre o curso Java Reflection: simplifique a conversão de objetos

O curso Java Reflection: simplifique a conversão de objetos possui 119 minutos de vídeos, em um total de 39 atividades. Gostou? Conheça nossos outros cursos de Java em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Java acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas