Professor J coluna RMI/JNDI - Fundamentos Um exemplo prático do que são e de como funcionam RMI e JNDI Roberto Vezzoni ([email protected]): SCJP, faz Ciência da Computação na Faesa e atua como assessor/instrutor de TI. A API RMI (Remote Method Invocation) é o mecanismo em Java que permite a manipulação de objetos distribuídos. Com RMI, é possível realizar chamadas de métodos em objetos remotos (numa outra VM, talvez no mesmo host). Como é necessário vasculhar outra VM para a tentativa de encontrar um objeto remoto, nós precisamos fornecer meios para que aquele objeto seja, de fato, encontrado. É através da API JNDI (Java Naming and Directory Interface) que conseguimos registrar e encontrar objetos remotos para que estes sejam utilizados por RMI. O diretor de compras de uma empresa, por conta de suas viagens a negócios, solicita ao departamento de TI a possibilidade de acessar e manipular informações sobre pedidos de compras e estoque disponível através de seu smartphone. Como já existe um sistema de gestão para pedidos de compras que foi desenvolvido em duas camadas (cliente/servidor) e sendo este um sistema desktop para janelas mais ricas em recursos de entrada pelo usuário, será necessário quebrar o processo de pedido de compras em três camadas lógicas para evitar a duplicação do código na camada de negócios da nova aplicação. O cenário que possuímos é de que ainda será possível realizar a manipulação dos pedidos de compras no sistema de gestão já implantado e temos um novo sistema que permite acesso através daquele smartphone e demais dispositivos móveis compatíveis. A arquitetura deste cenário está demonstrada na figura 1. O padrão de projeto Façade (ou Fachada, em português) é responsável por fornecer um acesso padronizado e unificado a um subsistema mais complexo. Imagine, por exemplo, o processo de cadastro de nota fiscal de entrada por compra para comercialização. Será necessário além de fazer entrada em estoque das mercadorias envolvidas, fazer um lançamento de contas a pagar ao fornecedor em questão. Assim, o programador se vê em meio a várias regras de negócio. Para resolver essa situação, pode ser fornecido ao programador um objeto responsável por fazer o fechamento daquela nota fiscal, realizando apenas uma chamada, em vez de implementar um código com várias instâncias de objetos diferentes que se relacionam entre si. A esse novo objeto, se dá o nome de Façade. Num paralelo com tecnologias para este fim, além da RMI, temos como exemplos os padrões COM e DCOM (Microsoft) e o padrão CORBA (Common Object Request Broken Architeture) do OMG (Object Management Group). Como os objetos em RMI trafegam entre diferentes VMs, é necessário o uso 22 www.mundoj.com.br 'JHVSB"SRVJUFUVSBVUJMJ[BEBQBSBFYFNQMJmDBÎÍPEFVTPEFTJTUFNBEJTUSJCVÓEPBDSFEJUFP UFMFGPOFWFSNFMIPEBmHVSBÏVNTNBSUQIPOF de protocolos para a comunicação entre as aplicações cliente e servidor, e o protocolo padrão é o JRMP (Java Remote Method Protocol). Para se obter interoperabilidade entre diferentes linguagens de programação orientadas a objetos que seguem o padrão do OMG, o protocolo utilizado é o IIOP (Internet Inter-ORB Protocol) no qual esse ORB (Object Request Broker) atua fazendo o "meio de campo" entre o cliente e o servidor. Quando é citado RMI-IIOP, significa explicitar que os objetos trafegados por RMI estão sobre o protocolo IIOP. EJBs (Enterprise JavaBeans) utilizam RMIIIOP por "trás das cenas". Para que tudo isso funcione, nós precisamos de arquivos auxiliares que serão responsáveis por, de fato, realizar chamadas em métodos de objetos remotos. Com esses arquivos disponíveis em nosso classpath, realizamos chamadas em objetos remotos como se fossem locais, pois eles atuam como proxies. Esses arquivos são conhecidos como skeleton e stub e ficam no servidor e cliente, respectivamente. Mais adiante veremos como gerar tais arquivos. Um proxy é responsável por gerenciar o tráfego de informações entre as aplicações cliente e servidor. 1SPGFTTPS+t3.*+/%*'VOEBNFOUPT você tenha permissão para escrita de seu sistema de arquivos (os arquivos de código-fonte já inclusos nesta árvore serão criados por nós neste arquivo). Figura 2. Árvore de diretórios para nosso exemplo. Passagem de argumentos Quando usamos RMI devemos estar atentos ao tratamento das passagens de argumentos, pois em Java passagens de argumentos se dão por cópia de valor. Mas o que acontece quando passamos objetos como argumentos em RMI? A referência de um determinado objeto local (endereço na memória heap local) pode não existir na VM remota e se existir não significa que seja o que esperamos ser, então caímos numa exceção na passagem de argumentos em Java: passagem de argumentos por referência. Tudo bem, na verdade é uma pseudopassagem por referência, pois o objeto utilizado como argumento é enviado para a outra VM. Isso acontece por meio de serialização, ou seja, devemos fazer com que esse objeto que deve ser trafegado implemente a interface java.io.Serializable. Caso o objeto não deva ser enviado para que o volume de dados trafegados seja menor, basta não implementar aquela interface. Mas como não existe “almoço grátis”, ao não implementarmos a interface java. io.Serializable, deixamos de trafegar um objeto pela rede, porém agora para cada mudança de estado desse objeto deverá ser realizada uma chamada de rede. Servidor Para começar a programação, iremos montar um objeto que contenha uma operação simples e, seguindo uma máxima para encapsulamento e baixo acoplamento na orientação a objetos, vamos programar para interfaces. Não faremos isso apenas por uma boa prática, em RMI não temos escolha, é impossível fazer chamadas diretamente ao objeto que mantém a implementação. Devido a essa regra, precisamos construir uma interface que estenda java.rmi. Remote, com os métodos desejados. Salve o código da Listagem 1 em um arquivo chamado FacadeIntf.java em / rmi_home/server/src/mj/rmiserver/intf. Listagem 1. mj.rmiserver.intf.ServidorIntf.java. package mj.rmiserver.intf; public interface FacadeIntf extends java.io.Serializable, java.rmi.Remote { String JNDI_NAME = “Server”; Objetivo: “Hello World” String sayHello() throws java.rmi.RemoteException; Bom, mãos à obra! Nossa intenção é construir uma aplicação RMI stand alone, ou seja, ela deve executar fora de um container JEE (Java Enterprise Edition). Ela nos mostrará como as coisas funcionam exibindo o nosso já bem conhecido “Hello World”. Configuração do ambiente A primeira coisa a fazer é criar uma estrutura de diretórios em nosso sistema de arquivos para organizar de forma adequada nossos artefatos. Crie a árvore de diretórios conforme a figura 2 em qualquer parte que } Todas as operações declaradas em nossa interface remota devem lançar java. rmi.RemoteException, pois a RMI força-nos a considerar a possibilidade de instabilidades de rede, travamentos em máquinas remotas, etc. Agora devemos implementar nossa interface montando nosso objeto remoto. Para tal, precisamos estender uma classe especial: javax.rmi.PortableRemoteObject. Salve o código da Listagem 2 em um arquivo com o nome FacadeImpl.java em rmi_home/server/src/mj/rmiserver/impl. 23 Professor J 1SPGFTTPS+t3.*+/%*'VOEBNFOUPT Listagem 2. mj.remiserver.impl.FacadeImpl.java. Listagem 3. mj.rmiserver.ServerRMI. package mj.rmiserver.impl; package mj.rmiserver; import mj.rmiserver.intf.FacadeIntf; public class ServerRMI { public class FacadeImpl extends javax.rmi.PortableRemoteObject implements FacadeIntf { public ServerRMI() { try { public FacadeImpl() throws java.rmi.RemoteException { super(); } /* * cria um registro (responsável por aceitar requisições) na VM * local na porta padrão * para distribuição de objetos sobre RMI - 1099. * Assim, sempre que a máquina, em que reside o nosso servidor * em execução, * receber solicitações na porta 1099, * essa solicitação será encaminhada para o nosso servidor. */ java.rmi.registry.LocateRegistry .createRegistry( java.rmi.registry.Registry.REGISTRY_PORT ); public String sayHello() { return “Hello World!”; } } Agora nos resta montar uma classe que seja responsável por levantar nosso servidor (todos os artefatos deste artigo estão disponíveis no site da revista Mundoj). Ela é mostrada na Listagem 3. Agora devemos compilar as classes do nosso servidor. Para isso, basta compilar o arquivo ServerRMI.java que todas as outras classes utilizadas por ele também serão compiladas. Entre no diretório rmi_home/server/src e execute o seguinte comando: mj.rmiserver.impl.FacadeImpl facade = new mj.rmiserver.impl.FacadeImpl(); O utilitário rmic, que vem junto com o JDK, deve ser usado para gerar arquivos auxiliares para cada um dos objetos registrados na VM e deve ser executado sobre o bytecode. Entre no diretório rmi_home/server/bin e execute o seguinte comando: /* * amarra a instância de nosso objeto remoto a um nome * (examinaremos esse “nome” mais adiante) * e armazenamos isso no registro. * A classe java.rmi.Naming é responsável por fornecedor métodos * para armazenar e obter objetos remotos no registro. */ java.rmi.Naming .bind( mj.rmiserver.intf.FacadeIntf.JNDI_NAME, facade ); rmic -classpath . mj.rmiserver.impl.FacadeImpl synchronized (facade) { javac -d ../bin/ -cp . mj/rmiserver/ServerRMI.java Para complementar nosso servidor, precisamos apenas gerar um arquivo auxiliar para que a RMI saiba como lidar com nosso objeto remoto. De posse do arquivo auxiliar FacadeImpl_Stub.class gerado no diretório rmi_ home/server/bin/impl, devemos disponibilizá-lo nos classpaths das aplicações cliente e servidor. try { /* * chamada ao método wait() herdado de java.lang.Object * para que nosso servidor fique esperando por requisições. * E fazemos isso dentro de um bloco sincronizado * pois queremos nosso servidor trabalhando de forma thread-safe. */ System.out.println(“esperando por requisições...”); facade.wait(); } catch (InterruptedException ie) { ie.printStackTrace(); } Assim, podemos executar nosso servidor. Entre no diretório rmi_home/server/ bin e execute o seguinte comando: java -cp . mj.rmiserver.ServerRMI A seguinte saída lhe será apresentada: esperando por requisições... (Uau, 'tá funcionando mesmo! :o) } Resumo } catch (java.rmi.RemoteException re) { re.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } Esse servidor, em execução, criará um registro na VM local esperando por requisições na porta 1099. E também terá uma instância da classe mj.rmiserver.impl. FacadeImpl.java amarrada ao nome “Server” (ainda falaremos desse “nome”, por favor, não me deixe esquecer). } Cliente public static void main(String[] args) { new ServerRMI(); } Agora daremos início à construção de uma aplicação RMI cliente para o servidor que acabamos de desenvolver juntos. Na Listagem 4, é apresentada a classe cliente. 24 www.mundoj.com.br } 1SPGFTTPS+t3.*+/%*'VOEBNFOUPT Copie o arquivo FacadeIntf.class para rmi_home/client/bin/mj/rmiserver/ intf para que possamos compilar a nossa classe cliente. Entre no diretório rmi_home/client/src e execute o seguinte comando: package mj.rmiclient; javac -cp .:../bin/ -d ../bin/ mj/rmiclient/ClientRMI.java public class ClientRMI { Listagem 4: mj.rmiclient.ClientRMI public ClientRMI(String host) { Falta apenas um processo a fazer antes de executar nosso cliente: disponibilizar no classpath da aplicação cliente o stub gerado para o servidor que desenvolvemos há pouco. Ou seja, você deve copiar o artefato FacadeImpl_Stub para rmi_home/client/bin/mj/rmiserver/impl. try { /* * procura por uma instância no servidor, passado na URL, * através de um “nome” (que mistério!) * utilizando o método lookup() da classe java.rmi.Naming. */ Object remoteObject = java.rmi.Naming.lookup( host + mj.rmiserver.intf.FacadeIntf.JNDI_NAME ); Isso tornará a nossa árvore de diretórios para o cliente e servidor nas seguintes estruturas demonstradas nas figuras 3 e 4, respectivamente. /* * fazemos um casting do objeto retornado pela instrução * demonstrada na listagem 11. * Para essa operação precisamos utilizar a classe * javax.rmi.PortableRemoteObject para realizar uma chamada ao * seu método narrow(). * A Classe javax.rmi.PortableRemoteObject é a super classe do * nosso objeto remoto * ou foi utilizada para exportar o objeto para o registro no servidor. * O método narrow() assegura que o objeto retornado pelo lookup() * está de acordo com a interface passada no segundo argumento, * se a conversão não for apropriada, uma exceção será lançada. */ mj.rmiserver.intf.FacadeIntf facade = (mj.rmiserver.intf.FacadeIntf) javax.rmi.PortableRemoteObject .narrow( remoteObject, mj.rmiserver.intf.FacadeIntf.class ); Figura 3. Estrutura de diretórios e arquivos da nossa aplicação cliente. /* * realizamos uma chamada como se tivéssemos uma instância local. */ System.out.println(“Diga olá: “ + facade.sayHello() ); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { if (args.length < 1) { System.out.println(“Uso: java ClientRMI <host>”); System.exit( 0 ); } new ClientRMI( “//” + args[0] + “/” ); } Figura 4. Estrutura de diretórios e arquivos da nossa aplicação servidora. Pronto! Com o servidor em funcionamento, basta entrar no diretório rmi_home/client/bin e executar o seguinte comando:: java -cp . mj/rmiclient/ClientRMI localhost Caso tudo tenha ocorrido como o esperado, você deve ter a seguinte saída: Diga olá: Hello World! (Woohoo! Funcionou! :o) } Melhorando o nosso exemplo Empacotando o servidor Para facilitar a disponibilização de nosso servidor RMI, podemos empacotar os bytecodes gerados em um arquivo JAR. Com esse propósito, vamos criar um arquivo para explicitar qual classe deve ser executada. Crie um arquivo texto de nome MANIFEST.MF dentro de rmi_home/server/ com o seguinte conteúdo: Main-Class: mj.rmiserver.ServerRMI Agora vamos criar o pacote com a aplicação servidora. Entre no diretório 25 Professor J 1SPGFTTPS+t3.*+/%*'VOEBNFOUPT rmi_home/server/bin e execute o seguinte comando: nos mais variados tipos de serviços de nomes e diretórios. jar -cvfm server.jar ../MANIFEST.MF . Serviço de nomes O comando acima criará o arquivo JAR server.jar dentro de rmi_home/server/bin. Resta-nos executar o arquivo JAR que acabamos de criar. Entre no diretório rmi_home/server/bin e execute o seguinte comando: java -jar server.jar Empacotando o cliente Para facilitar a disponibilização de nosso cliente RMI, podemos empacotar os bytecodes gerados em um arquivo JAR. Com esse propósito, vamos criar um arquivo para explicitar qual classe deve ser executada. Crie um arquivo-texto de nome MANIFEST.MF dentro de rmi_home/client/ com o seguinte conteúdo: Main-Class: mj.rmiclient.ClientRMI Agora vamos criar o pacote com a aplicação cliente. Entre no diretório rmi_ home/client/bin e execute o seguinte comando: jar -cvfm client.jar ../MANIFEST.MF . O comando acima criará o arquivo JAR client.jar dentro de rmi_home/client/ bin. Resta-nos executar o arquivo JAR que acabamos de criar. Entre no diretório rmi_home/client/bin e execute o seguinte comando: java -jar client.jar localhost E novamente, caso tudo tenha ocorrido como o esperado, você deve ter a seguinte saída: Um nome se refere a uma entidade como pessoa ou objeto. O fato de se usar um nome é devido a ser mais usual e fácil. Quando você faz uma solicitação, por exemplo, ao website www.mundoj.com. br um Domain Name System (DNS) é responsável por traduzir aquela URL ao endereço IP amarrado àquele nome. Outro bom exemplo é quando você solicita à sua operadora telefônica o número do telefone de uma determinada pessoa que você tem o nome. Então o serviço de nomes (naming service) permite que seja amarrada alguma “coisa” a um nome e também permite que seja feita uma busca por essa “coisa” através de seu nome. Serviço de diretórios Um diretório é um tipo de objeto (coisa) de atenção particular, pois ele pode conter atributos. Você pode procurar por um objeto que seja um diretório, como por exemplo, um usuário de rede que possui como atributos o seu nome de usuário e senha. Exemplos de serviços de diretórios (directory service) são o Microsoft Active Directory e o OpenLDAP. A sua estrutura de trabalho é organizar os diretórios em uma hierarquia conhecida como árvore. Um organograma é um exemplo dessa estrutura. Um serviço de diretórios se parece muito com bancos de dados. Em ambos, podemos armazenar informações e/ou consultar informações já previamente definidas. Muitos dos serviços de diretórios são implementados por bancos de dados por trás das cenas. Conclusão Diga olá: Hello World! Nós poderíamos não adicionar os artefatos do nosso servidor (FacadeIntf e FacadeImpl_Stub) diretamente no pacote cliente (client.jar), disponibilizando-os em um pacote separado para facilitar a manutenção e redistribuição de ambas as aplicações: cliente e servidor. Mas isso nos levaria a um assunto paralelo à intenção inicial deste artigo. Java Naming and Directory Interface A JNDI API é responsável por fornecer um padrão para definição e localização através de um “nome” de recursos como EJBs, conexões JDBC, usuários, entre outros dentro de um determinado ambiente. A JNDI resolve o que deveria ser um problema para programadores Java: fornecedores diferentes com técnicas diferentes para trabalho sobre o serviço de nomes e diretórios. Usando unicamente a JNDI fazemos operações sobre serviços de nomes e diretórios de forma transparente. Isso significa que não nos importa saber se estamos acessando um Lightweight Diretory Access Protocol (LDAP) ou Network Information System (NIS) ou ainda Network Directory System (NDS). Com isso, basta aprender uma única API para acessar informações que estejam 26 www.mundoj.com.br A intenção era apresentar-lhe o uso prático da RMI e para tal nós tínhamos que ter uma noção básica da API JNDI. Com este exemplo, foi mostrado que nada além da JDK, que possuímos, é necessário para construir uma aplicação distribuída em Java. Claro, dessa forma, é dever do programador implementar códigos para tratamento de transações, segurança, requisições simultâneas e balanceamento de carga, entre outros, fugindo um pouco de manter a atenção apenas no negócio do sistema. Por isso, além de outras coisas, existem os containers. Eles permitem que fiquemos focados apenas no negócio do sistema em questão. Agradecimentos A Henrique Winckler ([email protected]) e a equipe da Mundoj pela SFWJTÍPEFTUFBSUJHPt Referências t "QSFOEB+BWBFNEJBT3PHFST$PEFOIFBEF-BVSB-FNBZ t .BTUFSJOH&OUFSQSJTF+BWB#FBOT3JNB1BUFM4SJHBOFTI(FSBME#SPTFF.JDB4JMWFSman.