Java Reflection
Java Reflection
0 / 8 13min
0 / 7 20min
0 / 9 36min
0 / 9 29min
01 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.
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!
02 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.
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;
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.
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á!
03 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!
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.
Transformator.java:
package br.com.alura.refl;
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;
}
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".
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.
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;
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.
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.
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.
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.
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.
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.
Você está trabalhando em um projeto e precisa criar um método chamado transformToDTO que
deve receber uma classe de qualquer tipo e retornar a sua versão DTO.
Resposta:
Class.forName(input.getClass().getName() + "DTO");
A concatenação do nome da classe com "DTO" e o uso do método forName são consistentes com o
conteúdo do curso e permitem carregar dinamicamente a classe DTO correspondente, seguindo a
nomenclatura padrão estabelecida.
05 Mão na massa: passando a classe Target como parâmetro
Uma maneira diferente de desenvolver o nosso projeto e dar um pouco mais de liberdade para os
clientes seria permitir que o usuário passasse o objeto a ser transformado e qual classe vai ter o
resultado da transformação. Assim, os clientes ficam livres para chamar a classe com o sufixo DTO, se
quiserem, ou com qualquer outro nome que faça sentido. Abaixo na Opinião você pode conferir
como fica o código.
Opinião do instrutor
•
•
Em vez de usarmos o método forName para pegar o source.getName e adicionar o sufixo DTO,
usamos apenas o parâmetro target que já é a class:
U instanceOfTarget = (U)
targetClass.getDeclaredConstructor().newInstance();
Após essa alteração, todo o código será o mesmo de antes, porém, precisamos alterar a forma que
nossos testes fazem a chamada ao método transform. Vamos ver como fica, por exemplo, o teste
shoudTransform.
@Test
public void shouldTransform() throws InvocationTargetException,
IllegalAccessException, NoSuchMethodException, InstantiationException {
Transformator transformator = new Transformator();
PessoaDTO transformated = transformator.transform(pessoa,
PessoaDTO.class);
Assertions.assertInstanceOf(PessoaDTO.class, transformated);
Assertions.assertEquals("João", transformated.getNome());
Assertions.assertEquals(32, transformated.getIdade());
Assertions.assertEquals("04333958210", transformated.getCpf());
}
Como podemos perceber, agora passamos a classe que queremos transformar como segundo
parâmetro do método transform. No nosso caso continua sendo o PessoaDTO, porém, chamando
o .class. Poderíamos passar qualquer classe e isso nos traria mais flexibilidade no projeto.
06 O que aprendemos?