Primeiras aulas do curso Java Reflection parte 1: entendendo a metaprogramação

Java Reflection parte 1: entendendo a metaprogramação

Começando com Reflection - Introdução

Sejam bem vindo ao curso de Java Reflection da Alura! Eu sou o instrutor Gabriel Leite, e serei o responsável por guiá-lo pelos pormenores do que vem a ser reflexão no mundo Java.

Esse curso será dividido em duas partes. Nelas, de modo geral, iremos estudar e nos aprofundar nos recursos disponibilizados pelo Java para trabalharmos em situações nas quais não temos garantia, em tempo de desenvolvimento, sobre o tipo de informação que o usuário nos passará quando nossa aplicação estiver sendo executada.

Ou seja, estamos em um cenário altamente dinâmico, no qual os recursos estáticos da linguagem Java já não nos atendem. Aqui entra o primeiro grande aprendizado que você precisa guardar: não usaremos reflexão para toda e qualquer situaçaõ. A reflexão é um artifício muito poderoso e que só deve ser aplicado nessas situações altamente dinâmicas.

Durante esse processo de aprendizado, nós entenderemos os mecanismos dos grandes frameworks e ferramentas que os desenvolvedores Java utilizam no dia-a-dia de trabalho.

Pré-requisitos da carreira Java

Existem alguns pré-requisitos para ter um melhor aproveitamento desse curso. É necessário ter todos os cursos de Java aqui da Alura bem fundamentados - ou seja, você precisa saber lidar bem e ter uma boa prática com a linguagem Java, e, além disso, saber quais recursos surgiram nessa linguagem a partir do JDK 8 (e ter uma boa familiaridade com eles).

Os cursos mais indicados são:

Cenário de atuação

Nesse curso, criaremos uma aplicação que simulará a interação entre o navegador e uma aplicação Java, na qual o usuário (no caso, um navegador) passa uma URL pedindo por um determinado recurso (por exemplo, listagem de produtos), e essa aplicação devolve esse recurso em formato XML ou JSON.

Na primeira parte do curso, iremos focar na requisição do usuário - ou seja, iremos descobrir, a partir de uma string enviada na URL, qual recurso esse usuário quer acessar, e devolveremos esse recurso a ele.

Para isso, precisaremos lançar mão de metaprogramação, o que consiste em criar um código que consegue avaliar outro código já existente.

Na segunda parte desse curso, veremos como os metadados (que no mundo Java são chamados de anotações, e que são informações sobre os dados já existentes) conseguem nos ajudar nesse problema.

Também focaremos em transformar a informação que foi acessada em, por exemplo, um XML. Além disso, também nos aprofundaremos em recursos um pouco mais avançados da API de reflexão do Java, além de estudar coisas mais interessantes que só conseguimos implementar pela existência dessa API.

Embora esse cenário pareça simplista, existem várias bibliotecas e frameworks (como o VRaptor, o XStream e o Spring) que são utilizadas para essas situações. Ou seja, estaremos desenvolvendo soluções que já são disponibilizadas por essas ferramentas, mas que usamos sem nos dar conta de como elas funcionam.

Ao final do projeto, se entrarmos na nossa aplicação com uma URL como /produto/lista, teremos como resposta um XML da listagem de produtos que temos no sistema.

Ansioso? Então até a próxima atividade!

Começando com Reflection - Visão geral do problema

É hora de termos uma visão mais geral e aprofundada do problema que iremos atacar durante esse curso. Como cenário de atuação, nós iremos simular a interação entre o navegador e uma aplicação Java, de maneira que o navegador passaria uma requisição para a URL /produto/1 e a nossa aplicação Java responderia com as informações relativas ao produto 1 em formato XML ou JSON.

No entanto, repare que iremos somente simular, pois não queremos ter como pré-requisito que você, aluno, já entenda como funcionam os mecanismos da web para realizar esse curso. Portanto, de modo a eliminar esse pré-requisito, apresentaremos um cenário mais simples e focaremos na aplicação Java em si e em como essa linguagem trabalha com reflexão.

Embora simples, esse cenário irá nos disponibilizar algumas dificuldades ou problemas bastante interessantes. Tanto que, no mundo Java, bibliotecas como o VRaptor, o XStream e o Spring existem basicamente para solucionar situações relativas ao tipo de problema que enfrentaremos no curso.

Determinando classe e método

Nosso primeiro problema seria determinar a classe e o método que deverá ser executado a cada requisição (por exemplo, a URL /produto/1). Uma solução bastante simples seria quebrar a string que estamos recebendo como entrada e fazer vários IFs para definir o que precisa ser executado.

Vamos observar um exemplo de código para essa solução:

String path = "/produto/1";
String[] subPaths = path.replaceFirst("/", "")
                    .split("/");

if (subPaths[0].equals("produto")) { // /produto
    ProdutoController pc = new ProdutoController();

    if (subPaths[1].equals("filtra")) {
        pc.filtra();
    } else if (subPaths[1].equals("lista")) {
        pc.lista();
    } //outro else-if
} else if (subPaths[0].equals("cliente")) { // /cliente
    // ifs aninhados para descobrir o método
} // outros else-if

Nesse caso, primeiramente estaríamos substituindo a barra inicial por vazio, e depois quebrando a string pelo separador / - ou seha, teríamos um vetor com duas posições/strings: na primeira posição, produto, e na segunda posição número 1.

Com a string quebrada, criaríamos um if() verificando se a posição [0] do nosso subPaths (nosso vetor de strings) é igual a produto. Em caso afirmativo, seria criada uma instância da classe ProdutoController, que, no nosso projeto, seria a responsável por lidar com as informações do produto.

A partir do momento em que tivéssemos a instância, precisaríamos verificar qual método deveria ser executado. Para isso, pegaríamos a posição 1 do vetor (a segunda posição) e verificaríamos se ela é igual a filtra. Se sim, seria executado o método filtra() do ProdutoController. Do contrário, verificaríamos se a segunda posição é igual a lista - novamente, se sim, executaríamos o método lista().

Seriam necessários diversos outros IFs para verificar os outros métodos da nossa classe. Além disso, não necessariamente estaríamos lidando com produto - poderíamos, por exemplo, estar lidando com usuario ou outra classe qualquer do nosso sistema.

Portanto, precisaríamos de outros IFs para verificar se a posição 0 do nosso vetor é igual a cliente, e, em caso positivo, instanciar o ClienteController e verificar qual método deveríamos executar dentro da classe, de maneira bastante similiar ao que foi feito para a classeproduto.

O problema dessa implementação é que teriamos uma quantidade de IFs. Se tivéssemos 5 classes de controle, por exemplo, precisaríamos de 5 IFs externos e, dentro deles, outros IFs para verificar qual método deveria ser executado.

Ou seja, quanto maior a quantidade de classes e métodos, maior a quantidade de IFs. Em computação, isso é chamado de complexidade ciclomática, que é basicamente a quantidade de caminhos que o programa tem para chegar até o final. Quanto mais caminhos diferentes dentro de uma aplicação, mais complexo é o código e mais difícil será mantê-lo.

Portanto, embora esse código seja uma solução possível, ele é bem ingênuo e vai gerar muitos problemas para ser mantido no futuro.

Determinando informações da resposta

Nosso segundo problema é determinar as informações que irão compor o XML ou o JSON de resposta que é enviado ao usuário. Ou seja, uma vez que tenhamos encontrado a classe e o método que devem ser executados, ainda é necessário saber como será montado o XML ou o JSON que será enviado na resposta.'

Novamente, uma solução seria criar vários IFs verificando o tipo da instância em questão. Vamos observar esse código:

Object objeto = // obtenho o objeto
String xml = "";

if (objeto instanceof Produto)
    Produto produto = (Produto)objeto;
    String nome = produto.getNome();
    double valor = produto.getValor();
    String marca = produto.getMarca();

    xml = "<produto>" +
            "<nome>" + nome + "</nome>" +
            "<valor>" + valor + "</valor>" +
            "<marca>" + marca + "</marca>" +
        "</produto>";
}else if (objeto instanceof Cliente) {
    // lógica para gerar o XML
} //outros else-if

Dessa vez, pegaríamos o objeto que recebemos como resposta no problema anterior e criaríamos uma variável do tipo string chamada xml, que inicialmente estaria vazia.

Verificaríamos se o tipo de objeto é uma instãncia de Produto. Se sim, seria feito um cast para produto, pegaríamos as informações relativas a produto e geraríamos o xml. No entanto, precisaríamos fazer a mesma coisa para o caso do objeto ser um Cliente ou um Usuario. Ou seja, novamente teríamos um código difícil de manter e com uma complexidade ciclomática muito alta.

De maneira geral, nosso problema é que temos uma entrada de informação dinâmica:

Nosso objetivo é ter uma caixa preta com a qual seja possível gerar um retorno do método a ser executado dependendo da entrada. Essa caixa preta precisará descobrir as informações sobre as classes - por exemplo, qual classe precisa ser instanciada e qual método dessa classe precisa ser executado - e pode precisar pegar informações relativas aos atributos dessa classe, como no caso da serialização para JSON ou XML.

A boa notícia é que, para essas situações, o Java já nos disponibiliza a famosa API de Reflection, o objeto de estudo desse curso. Na próxima aula começaremos a entender como essa API consegue nos ajudar com esses problemas. Até lá!

Começando com Reflection - Criação de objetos

Estamos de volta com o curso de Java Reflection da Alura. O objetivo agora é começarmos a trabalhar com código efetivamente. Nosso primeiro desafio é determinar a classe e o método que precisaremos executar a cada requisição que o usuário nos fizer.

Na prática, teremos a entrada provida pelo usuário (uma URL no padrão /produto/1) e a API de Reflection que irá processar essa string de alguma maneira. Como resultado, nesse primeiro passo, teremos a instância de um objetoda classe em questão.

Por exemplo, se estamos passando um objeto /produto/1, queremos executar algum método dentro da classe ProdutoController ou algum controlador referente à classe Produto.

Mas como instanciar um objeto de acordo com uma entrada dinâmica do usuário? É aí que entra a classe Class<T>, que é parametrizada - ou seja, ela irá sempre representar outra classe da qual queremos obter informações, como string ou integer. Com ela, poderemos:

Nesse caso, o que queremos é justamente instanciar um objeto da nossa lógica a partir de uma string fornecida pelo usuário.

Obtendo objeto da classe Class<T>

Antes de tudo, precisamos obter um objeto do tipo class. Para resolvermos essa situação, temos três opções.

A classe Object é comumente chamada de "mãe de todas as classes", já que todas as classes (se não se estendem de nenhuma outra classe) implicitamente se estendem da classe Object.

Como primeira opçao, a classe Object já provê o método getClass(), que retornará um objeto do tipo Class<T> definido (parametrizado) de acordo com o tipo do objeto em questão. Por exemplo, se temos um objeto do tipo string e chamamos o método getClass(), ele retornará um objeto tipo Class<String>. Já se temos um objeto do tipo produto e chamamos esse mesmo método, ele retornartá um Class<Produto> (parametrizado para produto) - ou seja, é um objeto que consegue inferir todas as informações relativas a classe Produto.

A segunda opção é trabalharmos com a Sintaxe.class, que no mundo Java é chamada de class literal. Com esse tipo de sintaxe, é possível, por exemplo, pegar diretamente a classe string e utilizar a Sintaxe.class de maneira a obter, como resposta, um objeto Class<String>.

A terceira e última opção é utilizarmos um método estático de Class chamado forName(). Porém, a sobrecarga mais simples desse método irá esperar uma string representando o fully qualified name da classe que queremos representar - ou seja, se queremos um objeto do tipo class representando uma string(que consiga inferir informações da classe string), teremos que passar para esse método o fully qualified name da string, que é java.lang.string.

Agora que vimos essa parte teórica, podemos começar a praticar no Eclipse. Para isso, você pode fazer o download dos projetos que serão utilizados durante o curso nos links abaixo:

Na próxima atividade você encontrará a descrição do que são cada um desses projetos!

No alurator-playgound (o projeto em que faremos nossos testes relativos à reflexão) já temos, dentro do pacote reflexao, uma classe chamada TesteIntanciaObjeto, na qual faremos o teste relativo à criação de objetos como vimos na parte teórica.

Trabalhando com class literal

Dentro do pacote controle, temos uma classe chamada Controle. Nós queremos criar um objeto da classe Class<Controle> (parametrizado para a classe Controle) utilizando a sintaxe class literal que vimos anteriormente.

Para isso, podemos utilizar o nome da classe Controle e o .class. Isso nos retornará uma Class<Controle, que chamaremos de controleClasse1:

public class TesteInstanciaObjeto {

    public static void main(String[] args) {
        Class<Controle> controleClasse1 = Controle.class;
    }

}

O objeto controleClasse1 representa um objeto que conseguirá inferir todas as informações relativas a nossa classe controle (atributos, métodos, construtores, etc.).

Método getClass()

Outra forma de criarmos um objeto do tipo Class<T> é por meio de um objeto já criado da classe em questão. Por exemplo, vamos criar um objeto controle da classe Controle:

Controle controle = new Controle();

Para criarmos um objeto da classe Class<T> a partir desse objeto, basta invocarmos o método getClass(), certo? Esse método retornará um novo objeto do tipo Class, que chamaremos de controleClasse2:

Controle controle = new Controle();
Class<? extends Controle> controleClasse2 = controle.getClass();

Repare que nossa declaração em generics ficou um pouco estranha, no formato ? extends Controle. Não precisamos nos preocupar com essa sintage agora, pois, na segunda parte do curso, teremos um capítulo somente sobre generics.

Por alto, essa declaração está indicando que esse objeto controleClasse2 do tipo Class estará parametrizado para qualquer classe que extenda Controle - ou seja, essa declaração não valerá para classes que não sejam filhas de Controle.

No projeto, temos uma classe SubControle, que extende de Controle. Na classe TesteInstanciaObjeto, vamos alterar o tipo Controle para SubControle e importar essa classe com "Ctrl + Shift + O":

public static void main(String[] args) {
    Class<Controle> controleClasse1 = Controle.class;

    SubControle controle = new SubControle();
    Class<? extends Controle> controleClasse2 = controle.getClass();

}

Nossa declaração continua valendo. No entanto, se removermos extends da classe SubControle, teremos um erro, já que SubControle não será mais uma subclasse de Controle.

Nesse ponto, podemos alterar o tipo SubControle para Controle novamamente.

Método forName()

A última forma de criar um objeto da classe Class<T> é pelo método estático forName(). Como vimos anteriormente, a sobrecarga mais simples desse método recebe uma string representando o fully qualified name da classe que queremos representar através da classe Class<T>. Portanto, precisaremos passar o fully qualified name' da classe Controle, que é o nome do pacote concatenado com o nome da classe:

Class.forName("br.com.alura.alurator.playground.controle.Controle");

Porém, o Eclipse retornará um erro. Isso acontece porque o método forName() lança uma exception do tipo Checked chamada ClassNotFoundException.

Por enquanto, não precisaremos entender essas exceções. A API de Reflection lançará várias delas - nesse caso, essa exception é lançada porque o Java pode não encontrar a classe referente ao fully qualified name que informamos. No entanto,teremos um capítulo inteiro dedicado somente a entender e destrinchar os mecanismos por trás dessas exceptions.

Nesse momento, iremos apenas resolver o problema (com o auxílio do Eclipse), adicionando a sintaxe throws no nosso método main():

public static void main(String[] args) throws ClassNotFoundException {
    Class<Controle> controleClasse1 = Controle.class;

    Controle controle = new Controle();
    Class<? extends Controle> controleClasse2 = controle.getClass();

    Class.forName("br.com.alura.alurator.playground.controle.Controle");

}

Isso pode ser feito facilmente usando o atalho "Ctrl + 1" (ou "Command + 1" no Mac) e selecionando "Add throws declaration". Pressionando "Ctrl + 1" novamente, também podemos pedir para o Eclipse criar uma variável local que receberá o retorno do método class.forName(), que chamaremos de controleClasse3.

Porém, diferentemente das outras situações, o Eclipse irá preencher a parametrização de Class<> como ? (ou seja, Class<?>). Isso significa que o objeto do tipo Class pode inferir qualquer classe.

Mas por que isso aconteceu? Porque nós passamos para o class.forName() uma string representando o fully qualified name, e o Eclipse não tem como determinar a classe que essa string representa.

Se salvarmos o código e o executarmos, não ocorrerá nenhum problema. Inclusive, se trocarmos algum caractere do fully qualified name da classe Controle (por exemplo, br.co.alura.alurator.playground.controle.Controle) e rodarmos o programa novamente, teremos uma exception ClassNotFoundException, que é lançada sempre que o método class.forName() não encontra a classe que indicamos.

O próximo passo nesse código é justamente criarmos um objeto da classe representada pelo objeto Class. Nesse caso, nós temos três objetos class que representam a classe Controle.

Nós podemos utilizar qualquer um desses objetos, por exemplo controleClasse1, e chamar o método newInstance():

controleClasse1.newInstance(); 

Porém, imediatamente teremos um sublinhado vermelho (indicando um erro), e o método newInstance() será riscado. Isso acontece porque o newInstance() foi depreciado a partir do JDK 9 do Java. Mais tarde iremos explicar isso melhor, pois no momento queremos focar na criação de objetos a partir de um objeto da classe Class.

Já o erro indicado no Eclipse acontece porque, novamente, esse método vai lançar diversas outras exceções. A exemplo do que fizemos no class.forName(), vamos simplesmente pedir para o eclipse adicionar a sintaxe de throws para essas exceções:

public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    Class<Controle> controleClasse1 = Controle.class;

    Controle controle = new Controle();
    Class<? extends Controle> controleClasse2 = controle.getClass();

    Class<?> controleClasse3 = 
            Class.forName("br.com.alura.alurator.playground.controle.Controle");

    controleClasse1.newInstance(); 

}

O newInstance() deve retornar um objeto da classe Controle, correto? Não! Como vimos na parte teórica, o newInstance() vai retornar um Object (que chamaremos de objetoControle), já que o Eclipse não sabe qual a classe que ele está representando.

Vamos escrever um System.out.println() e pedir para o Eclipse imprimir objetoControle instanceof Controle - ou seja, se esse objeto é do tipo Controle.

public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    Class<Controle> controleClasse1 = Controle.class;

    Controle controle = new Controle();
    Class<? extends Controle> controleClasse2 = controle.getClass();

    Class<?> controleClasse3 = 
            Class.forName("br.com.alura.alurator.playground.controle.Controle");

    Object objetoControle = controleClasse1.newInstance();

    System.out.println(objetoControle instanceof Controle);
}

Como essa afirmação é verdadeira, quando o código for executado o console imprimirá true. Porém, um detalhe: por que estamos utilizando esses genéricos?

Nosso objeto controleClasse1 é do tipo Class parametrizado para Controle, certo? Então, internamente, o Eclipse já sabe que o método newInstance() retornará um objeto do tipo Controle. Portanto, podemos substituir Object por Controle, e o código continuará funcionando.

No entanto, não é possível fazer a mesma coisa para controleClasse3, pois temos um objeto do tipo Class parametrizado para qualquer classe, e o programa não conseguirá garantir que teremos Controle como retorno.

Dessa forma, se utilizarmos Controle outroObjetoControle = controleClasse3.newInstance(), o Eclipse acusará um erro. Nesse caso, o correto seria utilizarmos Object no lugar de Controle.

Esse outroObjetoControle também é uma instância de Controle, e podemos provar isso chamando um System.out.println():

Controle objetoControle = controleClasse1.newInstance();

Object outroobjetoControle = controleClasse3.newInstance();

System.out.println(objetoControle instanceof Controle);

System.out.println(outroobjetoControle instanceof Controle);

Executando novamente a aplicação, teremos como retorno dois valores verdadeiros (true).

Fizemos todo esse processo porque o nosso usuário irá passar uma URL do tipo /controle/lista, por exemplo, e queremos instanciar um objeto relativo ao tipo controle (mas que também poderia ser produto ou qualquer outra classe).

O desafio é, dada a URL passada pelo usuário para o projeto alurator, instanciarmos um objeto do relativo a essa URL. Como esse é um curso avançado de Java, tire um tempo para pensar em qual solução você aplicaria nessa situação.

Não se esqueça de ler os textos explicando os projetos e de fazer os exercícios, e no próximo vídeo traremos a solução desse desafio. Até lá!

Sobre o curso Java Reflection parte 1: entendendo a metaprogramação

O curso Java Reflection parte 1: entendendo a metaprogramação possui 199 minutos de vídeos, em um total de 50 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!

Plus

  • Acesso a TODOS os cursos da plataforma

    Mais de 1200 cursos completamente atualizados, com novos lançamentos todas as semanas, em Programação, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.

  • Alura Challenges

    Desafios temáticos para você turbinar seu portfólio. Você aprende na prática, com exercícios e projetos que simulam o dia a dia profissional.

  • Alura Cases

    Webséries exclusivas com discussões avançadas sobre arquitetura de sistemas com profissionais de grandes corporações e startups.

  • Certificado

    Emitimos certificados para atestar que você finalizou nossos cursos e formações.

  • Alura Língua (incluindo curso Inglês para Devs)

    Estude a língua inglesa com um curso 100% focado em tecnologia e expanda seus horizontes profissionais.

12X
R$85
à vista R$1.020
Matricule-se

Pro

  • Acesso a TODOS os cursos da plataforma

    Mais de 1200 cursos completamente atualizados, com novos lançamentos todas as semanas, em Programação, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.

  • Alura Challenges

    Desafios temáticos para você turbinar seu portfólio. Você aprende na prática, com exercícios e projetos que simulam o dia a dia profissional.

  • Alura Cases

    Webséries exclusivas com discussões avançadas sobre arquitetura de sistemas com profissionais de grandes corporações e startups.

  • Certificado

    Emitimos certificados para atestar que você finalizou nossos cursos e formações.

  • Alura Língua (incluindo curso Inglês para Devs)

    Estude a língua inglesa com um curso 100% focado em tecnologia e expanda seus horizontes profissionais.

12X
R$120
à vista R$1.440
Matricule-se
Conheça os Planos para Empresas

Acesso completo
durante 1 ano

Estude 24h/dia
onde e quando quiser

Novos cursos
todas as semanas