sexta-feira, 24 de junho de 2011

RMI e sua relação com Padrões de Projeto

Eu percebi que algumas pessoas estão tendo dificuldades com a implementação do trabalho de RMI do prof. Marinaldo. Então, pensei em falar um pouco sobre como funcionam as coisas por trás do RMI.

Como vcs devem ter visto em vários exemplos na internet, pra implementar um serviço servidor-consumidor em RMI é necessário a criação de uma interface que represente o objeto a ser compartilhado e uma classe no servidor que implemente essa interface. Além disso, é necessário criar uma Stub para o objeto em si e que deve estar presente no cliente, junto com a interface.

Pode parecer um pouco complexo mas não é. Para prover a transparência que o RMI se propõe a fazer no código, ele usa um padrão de projeto chamado Proxy Pattern. Este padrão diz o seguinte: "O objetivo do padrão Proxy é criar um objeto que faça a intermediação entre um objeto real e outros objetos que façam uso deste"...



Isso pode ser exemplificado na imagem:



O que acontece no padrão proxy? O cliente irá interagir com um objeto representado pela interface, mas este objeto não é o objeto real que o cliente trabalha. O Proxy pode, por exemplo, ser utilizado para adicionar funcionalidade, adicionar restrições ou mesmo ocultar uma classe já existente, sem que haja a necessidade de se alterar a mesma.

Outro uso para o Pattern Proxy é interceptar as chamadas ao objeto real, para quaisquer motivos, como log de uso dos métodos, ou no caso do RMI, para prover um ambiente remoto de comunicação.

Atente para os seguintes códigos-fonte:

Cliente.java
import java.rmi.*;
import java.rmi.server.*;

public class Cliente {
    public static void main(String[] args) throws Exception {
        Conta conta = (Conta) Naming.lookup("rmi://loacalhost/conta");
        System.out.println(conta.calcula(10));
    }
}

Conta.java
import java.rmi.*;

public interface Conta extends Remote {
    public int calcula(int valor) throws RemoteException;
}

Fatorial.java
import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;
import java.io.*;

public class Fatorial extends UnicastRemoteObject implements Conta {
    public Fatorial() throws Exception {
        super();
    }

    public int calcula(int valor) throws RemoteException {
        return (calculaFatorial(valor));
    }

    protected int calculaFatorial(int valor) {
        if (valor <= 1) {
            return 1;
        } else {
            return (valor * calculaFatorial(valor-1));
        }
    }

    public static void main(String[] args) throws Exception {
        Fatorial conta = new Fatorial();
        Naming.rebind("rmi://localhost/conta", conta);
    }
}

O que acontece no RMI é o seguinte: O cliente, ao utilizar o método Naming.lookup faz duas coisas: se conecta ao servidor RMI e retorna uma instância da classe remota, que é a classe que utilizaremos na nossa aplicação cliente.

Só que, o método Naming.lookup não retorna o objeto real, mas um objeto que faz a intermediação para o objeto real. Ele retorna um objeto Proxy. E o que esse objeto faz, na verdade, é pegar os parâmetros para cada um dos métodos e transformá-los em mensagens, que são encaminhadas para o servidor e que são, então, repassadas ao método da classe implementada, e caso haja um retorno da função, os dados são enviados de volta para a classe Proxy, que então a repassa para o cliente que a está utilizando. E a classe que faz essa tarefa de ser o intermédio é a classe Stub, gerada através do comando rmic . Você pode gerar o código fonte dessa classe usando o parâmetro -keep no comandormic, e ele gerará o fonte do Stub, assim você pode ver como funciona.

Fatorial_Stub.java
// Stub class generated by rmic, do not edit.
// Contents subject to change without notice.

public final class Fatorial_Stub extends java.rmi.server.RemoteStub implements Conta, java.rmi.Remote {
    private static final long serialVersionUID = 2;
    private static java.lang.reflect.Method $method_calcula_0;
    
    static {
        try {
            $method_calcula_0 = Conta.class.getMethod("calcula", new java.lang.Class[] {int.class});
        } catch (java.lang.NoSuchMethodException e) {
            throw new java.lang.NoSuchMethodError("stub class initialization failed");
    }
}
    
    // constructors
    public Fatorial_Stub(java.rmi.server.RemoteRef ref) {
        super(ref);
    }
    
    // methods from remote interfaces
    
    // implementation of calcula(int)
    public int calcula(int $param_int_1) throws java.rmi.RemoteException {
        try {
            Object $result = ref.invoke(this, $method_calcula_0, new java.lang.Object[] {new java.lang.Integer($param_int_1)}, -1377564272400999243L);
            return ((java.lang.Integer) $result).intValue();
        } catch (java.lang.RuntimeException e) {
            throw e;
        } catch (java.rmi.RemoteException e) {
            throw e;
        } catch (java.lang.Exception e) {
            throw new java.rmi.UnexpectedException("undeclared checked exception", e);
        }
    }
}

Em resumo, o que acontece é que a aplicação cliente lida com um objeto falso, cujo único objetivo é enviar o nome do método e os parâmetros a serem processados para o servidor.

Ainda há algo a se considerar, em relação a objetos passados como parâmetros para os métodos da nossa classe remota. Eles devem todos implementar a interface Serializable, pois como foi mostrado, a classe Proxy, no cliente, só pega os parâmetros e os envia através de um protocolo próprio do RMI para o servidor. Em vista disso, deve ser possível enviar estes objetos para o processamento pelo cliente, na outra ponta.

Bom, é isso. Espero ter esclarecido bem as coisas e não confundido ninguém. Os códigos acima não possuem nenhum tratamento de exceções para simplificar o seu entendimento, no entanto numa situação real, é necessário resolver todas as exceções para evitar comportamentos não previstos na aplicação.