A informática está cada dia mais presente na vida das pessoas. A utilização de aplicações para as mais diversas finalidades tem aumentado, e conseqüentemente a exigência de funcionalidades por seus usuários. A necessidade de funcionalidades muito específicas e/ou em maior número, podem tornar o seu desenvolvimento inviável para a empresa responsável pela aplicação. Uma alternativa para viabilizar o desenvolvimento destas funcionalidades é permitir que estas sejam criadas por terceiros e acopladas à aplicação. Muitos aplicativos permitem a extensão de funcionalidades através da utilização de módulos externos, chamados de plug-ins, que são acoplados a uma aplicação-base. Um exemplo muito conhecido é o navegador Firefox. No site deste navegador é possível encontrar diversas funcionalidades implementadas por desenvolvedores do mundo inteiro. Esta extensão pode ser permitida em aplicativos Java através da utilização da reflexão de classes.

O que é reflexão?

Reflexão é um recurso da API Java que possibilita aos aplicativos o acesso e a modificação do comportamento de aplicações que estão rodando na Java Virtual Machine. Uma classe pode acessar outras classes em tempo de execução, sem conhecer sua definição no momento da compilação. Informações relativas à esta definição, como seus construtores, métodos e atributos, podem ser facilmente acessados através de métodos de reflexão da API Java. Classes externas à aplicação, que não foram compiladas junto a mesma, podem ser instanciadas para utilização de seus recursos. Os recursos de reflexão oferecidos pela API Java, na maioria dos casos, são utilizados para prover extensão de funcionalidades a aplicações, desenvolvimento de ferramentas de debug e aplicativos que permitem a navegação no conteúdo de classes compiladas.

Para todo tipo de objeto, a Java Virtual Machine cria uma instância imutável de java.lang.Class, que provê métodos para examinar as propriedades do objeto em tempo de execução. Esta classe é o ponto de partida para trabalhar com reflexão.

Veremos primeiro como obter um objeto Class, e depois como utilizá-lo para instanciar objetos dinamicamente. Leitores que já possuem familiaridade com a API de reflexão podem pular para a seção Desenvolvendo uma aplicação, na qual mostraremos como exemplo uma aplicação que suporta plug-ins através da reflexão de classes.

Utilizando objetos Class

O método getClass() da classe Object retorna a classe de um determinado objeto em tempo de execução. Na expressão a seguir o método getClass() retorna o Class para o tipo String:


   Class c = "Java Magazine".getClass();

Se não há uma instância disponível para obtenção da classe, mas o tipo é conhecido, então pode ser utilizado a sintaxe .class. Esta também permite obter o Class relacionado a tipos primitivos.


   Class c1 = boolean.class;
   Class c2 = java.io.PrintStream.class;

Quando é conhecido o nome da classe e o pacote que está contida, pode ser utilizado o método Class.forName().


   Class c = Class.forName("br.com.devmedia.ReflectSample");

A obtenção da instância do tipo Class da classe desejada é o primeiro passo para operações de reflexão. Esta instância permite a realização de algumas operações interessantes relativas a classe associada ao Class, por exemplo, obter seus construtores, métodos e atributos. A Tabela 1 lista os métodos para acessar cada tipo de membro de uma classe, a patir de seu objeto Class, e informa se este método retorna uma lista com múltiplos membros, membros herdados de classes estendidas e membros privados.

Membro Class Lista dos Membros? Membros herdados? Membros privados?
Field getDeclaredField() Não Não Sim
getField() Não Sim Não
getDeclaredFields() Sim Não Sim
getFields() Sim Sim Não
Method getDeclaredMethod() Não Não Sim
getMethod() Não Sim Não
getDeclaredMethods() Sim Não Sim
getMethods() Sim Sim Não
Constructor getDeclaredConstructor() Não N/A1 Sim
getConstructor() Não N/A¹ Não
getDeclaredConstructors() Sim N/A¹ Sim
getConstructors() Sim N/A¹ Não
Tabela 1. Métodos para acessar atributos, métodos e construtores de um Class

O objetivo deste artigo é apresentar a reflexão em Java como uma alternativa para extensão de funcionalidades de aplicações. Para isto, além do acesso aos objetos Class, precisaremos criar instâncias a partir de um Class para utilizar seus métodos.

Criando uma instância a partir de um Class

Para instanciar um objeto a partir da sua Class, o primeiro passo é obter um construtor:


   Class c = Class.forName("br.com.devmedia.ReflectSample");
   Constructor con = c.getConstructor();

Através do construtor é possível criar um objeto utilizando o método newInstance(). Este retorna um Object, obrigando a realização de um cast para o tipo da classe carregada. Os parâmetros deste método são os argumentos que devem ser passados para o construtor da classe associada ao objeto Class. A classe Constructor possui métodos para descobrir esta informação dentre outras. Considerando que o construtor da classe ReflectSample tem apenas um parâmetro, uma String, a sentença para a criação de uma instância seria:


   ReflectSample  r  = (ReflectSample)con.newInstance(“String parâmetro”);

Desenvolvendo uma aplicação

Para demonstrar a aplicação dos recursos da API de reflexão do Java para extensão de funcionalidades, será apresentado uma aplicação e um plug-in como exemplos. O objetivo desta aplicação é permitir a adição de funcionalidades sem a necessidade de alterar ou recompilar seu código fonte. Estas funcionalidades podem ser desenvolvidas por terceiros e disponibilizadas para os usuários que utilizam a aplicação. O plug-in-exemplo adiciona uma nova funcionalidade à aplicação-exemplo.

A aplicação escolhida como exemplo tem a finalidade de realizar operações em imagens. Entretanto, estas operações não estão implementadas na aplicação e sim nos plug-ins que podem ser acoplados na mesma. A aplicação não especifica qual tipo de operação o plug-in poderá realizar, apenas são fornecidos métodos para acessar e modificar a imagem carregada pela aplicação. Sendo assim, os plug-ins podem realizar qualquer tipo de operação com a imagem. No caso do plug-in-exemplo, a operação realizada é a alteração do tamanho da imagem.

Antes de desenvolver a aplicação é necessário definir como a aplicação receberá as novas funcionalidades. Neste artigo foi demonstrado que através da reflexão de classes, é possível descobrir os métodos de uma classe dinamicamente, porém não é possível descobrir o propósito dos mesmos. A aplicação precisa saber o propósito de cada método implementado no plug-in para que sejam chamados de maneira correta.

Uma possibilidade é definir os métodos que o desenvolvedor deve implementar associados aos eventos da aplicação. Um plug-in para estender as funcionalidades de um navegador implementaria métodos associados aos seus eventos, por exemplo, atualizar página, avançar, voltar, salvar histórico etc. Para o navegador não importa as operações implementadas pelo plug-in, mas sim a quais eventos estão associados, para que sejam chamadas no momento correto. Uma maneira de obrigar o desenvolvedor do plug-in a implementar métodos associados aos eventos da aplicação é definir uma interface contendo a declaração destes métodos. O desenvolvimento de um novo plug-in implica numa nova classe que implementa esta interface.

Outra possibilidade é separar os eventos do plug-in da aplicação. O plug-in acoplado à aplicação tem seus próprios componentes de interface com os seus próprios eventos. A aplicação apenas carrega o plug-in e cria uma interface para que este possa acessar seus recursos. Quando usuário deseja utilizá-lo, uma nova janela ou painel são carregados com os componentes específicos do plug-in. Neste caso, os eventos associados às funcionalidades do plug-in devem ser implementados pelo desenvolvedor do plug-in.

A aplicação, que será utilizada como exemplo, segue a segunda abordagem mencionada. Não há associação entre os eventos da aplicação e o plug-in. A Listagem 1 apresenta a interface que define um plug-in. Como pode ser observado, este receberá um JPanel da aplicação-base através do método setPanel(), no qual deverá deverá adicionar seus próprios componentes de interface.

O método setApplication() será utilizado pela aplicação-base para passar uma referência de si mesma, para que o plug-in possa acessar seus recursos, dentre estes a imagem carregada pelo usuário. O método load() será chamado após os métodos setApplication() e setPanel(). Neste método o desenvolvedor deverá instanciar os componentes de interface do plug-in e adiciona-los ao JPanel. Quando o plug-in for carregado, a aplicação-base irá acopla-lo automaticamente.


import javax.swing.JPanel;
  
 public interface Plugin {
   public void setApplication(AplicacaoExtensivel a_app);
   public void setPanel(JPanel a_panel);
   public void load();
 }
Listagem 1. Plugin

O objetivo da aplicação é carregar uma imagem e um plug-in selecionados pelo usuário. Neste caso, carregar um plug-in significa realizar os seguintes procedimentos: realizar a reflexão da classe que define o plug-in, criar uma instância a partir do Class, passar uma referência da aplicação-base e um JPanel para a instância, chamar o método load() do plug-in e adicionar o JPanel passado para plug-in ao Container da janela da aplicação-base.

A Listagem 2 apresenta a classe que define a aplicação. A aplicação tem apenas dois componentes de interface: um botão que aciona um JFileChooser para a seleção da imagem e outro que aciona um JFileChooser para seleção do plug-in. Uma imagem carregada pelo usuário é associada a um BufferedImage que é renderizado na tela pelo método paint(). Os métodos setImage() e getImage() serão utilizados pelos plug-ins para acessar e modificar a imagem carregada na aplicação.

Para obter a classe do plug-in será utilizado o método Class.forName(). Como mostrado anteriormente, este método utiliza o nome da classe para carregá-la. A classe com a implementação do plug-in deverá estar na mesma pasta que a aplicação, eliminando assim a necessidade de especificar o pacote. O nome da classe é obtido através do arquivo do plug-in selecionado pelo usuário. O método getName(), aplicado ao File associado a este arquivo, retorna o nome do mesmo. É necessário remover a extensão .class deste nome para obter o nome da classe corretamente.

Após obter o Class, é necessário criar um objeto da classe que implementa Plugin. Esta operação é realizada obtendo o construtor da classe carregada através do método getContructor() e instanciando um objeto a partir deste construtor, através do método newInstance(). Este método retorna um Object, necessitando assim da realização de um cast para o tipo Plugin. Em seguida, é passada uma referência da aplicação e de um JPanel para o plug-in, e é chamado o método load(). Enfim, adicionando o JPanel passado para o plug-in ao Container da janela, a interface do plug-in é acoplada à interface da aplicação.


 import java.awt.*;
 import java.awt.event.*;
 import java.awt.image.BufferedImage;
 import javax.imageio.ImageIO;
 import java.io.*;
 import java.lang.reflect.*;
 import javax.swing.*;
  
 public class AplicacaoExtensivel extends JFrame implements ActionListener {
   JPanel panelNorth, panelPlugin;
   JFileChooser fileChooser;
   File file;
   JButton buttonLoadImage, buttonLoadPlugin;
   BufferedImage image;
   Plugin plugin;
  
   public AplicacaoExtensivel() {
     super("Aplicação Extensível");
     buttonLoadImage = new JButton("Carregar Imagem");
     buttonLoadImage.addActionListener(this);
     buttonLoadPlugin = new JButton("Carregar Plugin");
     buttonLoadPlugin.addActionListener(this);
     getContentPane().setLayout(new BorderLayout());
     panelPlugin = new JPanel();
     panelNorth = new JPanel();
     getContentPane().add(panelNorth, BorderLayout.NORTH);
     panelNorth.add(buttonLoadImage);
     panelNorth.add(buttonLoadPlugin);
     setSize(800, 800);
     setVisible(true);
   }
  
   public void setImage(BufferedImage a_image) {
     image = a_image;
   }
  
   public BufferedImage getImage() {
     return image;
   }
  
   public void paint(Graphics g) {
     super.paint(g);
     g.drawImage(image, 10, 100, null);
   }
  
   public static void main(String args[]) {
     AplicacaoExtensivel ae = new AplicacaoExtensivel();
   }
  
   public void actionPerformed(ActionEvent e) {
     fileChooser = new JFileChooser(new File("./"));
     fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
     fileChooser.showOpenDialog(null);
  
     if (e.getSource() == buttonLoadImage) {
       try {
         image = ImageIO.read(fileChooser.getSelectedFile());
       }
       catch (IOException ioexception) { 
         // Tratar 
       }
     }
     else if (e.getSource() == buttonLoadPlugin) {
       try {
         String l_path = fileChooser.getSelectedFile().getName();
         l_path = l_path.substring(0, l_path.indexOf('.'));
         Class l_class = Class.forName(l_path);
         Constructor l_constructor = l_class.getConstructor();
         plugin = (Plugin) l_constructor.newInstance();
         plugin.setApplication(this);
         plugin.setPanel(panelPlugin);
         plugin.load();
         getContentPane().add(panelPlugin, BorderLayout.SOUTH);
         validate();
       }
       catch (Exception exception) { 
         // Tratar
       }
     }
     repaint();
   }
 };
Listagem 2. Código da aplicação

A aplicação apresentada na Listagem 2 está preparada para a extensão de funcionalidades. A Listagem 3 apresenta a implementação de uma nova funcionalidade que permite o redimensionamento da imagem carregada pela aplicação. Esta nova funcionalidade, como já especificamos, deve implementar pluG-in. Como espeficicado na interface pluG-in, os métodos setApplication(), setPanel() e load() devem ser implementados. Os dois primeiros, servem para receber as instâncias da aplicação-base e do JPanel que o plug-in deverá utilizar, respectivamente. O método load() deve ser utilizado pelo plug-in para instanciar os componentes de interface e associar listeners aos mesmos. Neste caso, os componentes de interface serão um JLabel e um JSlider. O JLabel apenas indica a funcionalidade e o JSlider representa a escala do redimensionamento da imagem. Para tratar os eventos do JSlider, é necessário implementar a interface ChangeListener, que especifica a implementação do método stateChanged(), que é chamado toda vez que o usuário altera o JSlider. É este método que, de fato, implementa a nova funcionalidade do plug-in. Basicamente, ele requisita a imagem para a aplicação, calcula o fator de redimensionamento baseado no valor atual do JSlider, cria uma nova imagem vazia com as novas dimensões e desenha a imagem requisitada pela aplicação redimensionada dentro desta imagem.


 import javax.swing.*;
 import javax.swing.event.*;
 import java.awt.*;
 import java.awt.image.BufferedImage;
 import java.awt.event.*;
  
 public class Resize implements Plugin, ChangeListener {
   AplicacaoExtensivel appExtensivel;
   JPanel panel;
   JLabel label;
   JSlider slider;
   BufferedImage sourceImage;
  
   public void setApplication(AplicacaoExtensivel a_app) {
     appExtensivel = a_app;
   }
  
   public void setPanel(JPanel a_panel) {
     panel = a_panel;
   }
  
   public void load() {
     label = new JLabel("Redimensionar:");
     slider = new JSlider(SwingConstants.HORIZONTAL, 1, 401, 100);
     slider.setMajorTickSpacing(5);
     slider.addChangeListener(this);
     panel.add(label);
     panel.add(slider);
     sourceImage = null;
   }
  
   public void stateChanged(ChangeEvent e) {
     double l_factor = slider.getValue() / 100.0;
     if (sourceImage == null) {
       sourceImage = appExtensivel.getImage();
     }
     BufferedImage l_img = new BufferedImage(
         (int) (sourceImage.getWidth() * l_factor), (int) (sourceImage
             .getHeight() * l_factor), BufferedImage.TYPE_INT_RGB);
     Graphics2D l_buf = l_img.createGraphics();
     l_buf.drawImage(sourceImage, 0, 0,
         (int) (sourceImage.getWidth() * l_factor), (int) (sourceImage
             .getHeight() * l_factor), null);
     appExtensivel.setImage(l_img);
     appExtensivel.repaint();
   }
 };
Listagem 3. Plugin para redimensionamento de imagens

A aplicação em execução é apresentada na Figura 1.

Aplicacao Extensível em execução após carregar uma imagem e o plug-in que possibilita o redimensionamento da imagem
Figura 1. Aplicacao Extensível em execução após carregar uma imagem e o plug-in que possibilita o redimensionamento da imagem

Os exemplos utilizados neste artigo usaram como referência o projeto MARVIN. Neste projeto um software para manipulação de imagens está sendo desenvolvido com o intuito de prover uma interface simples para que os usuários possam estudar e desenvolver algoritmos para manipulação de imagens.

Conclusão

Neste artigo, os recursos da API de reflexão do Java e uma das possibilidades de sua utilização, prover extensibilidade a aplicações, foram apresentados. Uma aplicação e um plug-in foram desenvolvidos para exemplificar a implementação de uma aplicação extensível em Java. A API de reflexão se presta para muitas outras finalidades mais avançadas, mas como pudemos ver, com apenas alguns métodos simples podemos implementar um cenário de uso bastante útil e poderoso.

Confira também