CAPÍTULO 7 JAVA Java é uma linguagem orientada a objeto cujo projeto foi desenvolvido pela Sun Microsystems no início de 1991. Ela foi originalmente concebida para ser utilizada na programação de dispositivos de consumo ( consumer devices ). Por volta de 1995, quando houve a explosão no uso da Internet, notou-se que Java era uma linguagem de programação ideal para este tipo de aplicação, uma vez que ela endereçava muitas questões de softwares distribuídos através de redes, tais como: interoperabilidade, portabilidade e segurança (Tilley, 1999). 7.1 CARACTERÍSTICAS DA LINGUAGEM Java é uma linguagem de programação que apresenta uma sintaxe próxima ao C e C++, sendo porém menos complexa que o C++. Estas características a tornam familiar à maioria dos desenvolvedores. Ela provê garbage collection, isto é, torna disponível um mecanismo que se encarrega de desalocar, automaticamentente, da memória todos os objetos que não estão sendo mais referenciados, suporta herança de classe simples e não usa ponteiros (Deitel e Deitel, 1997). Java provê flexibilidade porque as classes são linkadas na medida em que são necessárias para atender as requisições dos clientes, existindo também a possibilidade de ser feito o download dessas classes através da rede. Tal flexibilidade é uma mudança de paradigma na computação que requer todas as funcionalidades instaladas na plataforma do usuário antes da execução de uma aplicação (Tilley, 1999). 187
Ao contrário da maioria das linguagens de programação, o código escrito em Java não é compilado para uma máquina real, mas sim para uma virtual, chamada máquina virtual Java ( Java Virtual Machine JVM), cujas características são mantidas em qualquer tipo de hardware ou sistema operacional (Lemay e Perkins, 1997). Os aplicativos do Java dividem-se em dois grupos: os applets e as aplicações Java. Um applet é um programa escrito em Java que executa em um browser Web compatível com essa linguagem. Ele é carregado e executado ao mesmo tempo em que aparece, na tela do cliente, a apresentação de uma página de visualização. Agora, uma aplicação Java independente ( standalone ) é um programa escrito em Java que executa fora do browser Web, mas apenas onde existir uma JVM disponível. Os passos necessários para a construção de um aplicativo Java são listados a seguir (Lemay e Perkins, 1997): Escrever o código do programa em Java, mantendo um arquivo por classe. Compilar os arquivos com o programa fonte da aplicação para que possam ser executados na JVM. Como resultado, são gerados os arquivos <.class>, conhecidos como bytecodes, representando arquivos prontos para serem interpretados. Bytecode é um conjunto de instruções bastante parecida com alguns códigos de máquina, mas não é específica a nenhum processador. Este bytecode pode ser interpretado por qualquer compilador que possua uma JVM. Utilizar um software com as características JVM para executar os arquivos bytecodes através da emulação, isto é, interpretação dos comandos virtuais para a máquina ou ambiente real. Os arquivos <.class> podem ser carregados ou transferidos entre estações de trabalho e interpretados por qualquer JVM nelas disponível. O bytecode Java 188
provê portabilidade porque não é específico a nenhuma plataformas em particular nem nativo a qualquer processador. Entretanto, antes de serem executados, os bytecodes passam por uma verificação, onde os seguintes aspectos são observados (Lemay e Perkins, 1997): Se o código está de acordo com as especificações Java. Se o código não viola a integridade do sistema. Se o código não causa overflow ou underflow nas operações com pilhas. Se os tipos de parâmetros para os códigos estão corretos. Se não existem conversões ilegais de dados. Se o código não viola direitos de acesso a objetos. Se o código não força a mudança de tipos de objeto. Após passar por todos os testes, o arquivo <.class> poderá ser executado. Existe ainda outros mecanismos de segurança adicionais que fazem parte do núcleo do sistema Java, são eles (Shoffner e Hughes, 1999): o carregador de classes ( class loader ) e o gerenciador de segurança ( security manager ). O Java, atualmente, define três domínios possíveis: o computador local, a rede local onde o computador encontra-se localizado e a Internet. Cada um desses é tratado diferentemente pelo class loader, onde ele nunca permite que uma classe de um domínio menos protegido substitua uma classe de um outro domínio mais protegido. Classes em um domínio não podem recorrer a métodos de classes em outros domínios, a menos que essas classes tenham declarado explicitamente esses métodos como públicos. O gerenciador de segurança é uma classe adicionada recentemente ao Java reunindo, em um só lugar, todas as decisões sobre a política de segurança, isto é, quais aspectos de segurança um sistema deve suportar em relação a execução dos programas. 189
Assim, o gerenciador de segurança regula o acesso às funções sensíveis e o carregador de classes garante a aderência das mesmas às normas de segurança definidas pelo padrão Java. 7.1.1 INTERFACE Java suporta herança simples, isto é, cada classe Java pode ter somente uma superclasse, embora qualquer superclasse possa ter várias subclasses. Em outras linguagens de programação como C++ e Smalltalk as classes podem ter mais de uma superclasse herdando variáveis e métodos combinados dessas superclasses. A herança simples torna o relacionamento entre as classes e a funcionalidade que elas implementam simples de entender e projetar, mas de certa forma restritivo, por exemplo, comportamentos semelhantes precisam ser duplicados ao longo de diferentes ramificações da hierarquia de classes. O Java utiliza, para sanar as restrições de comportamento compartilhado, o conceito de interfaces. Uma interface, para o Java, é uma coleção de nomes de métodos indicando o comportamento de uma classe (Lemay e Perkins, 1997). Embora uma classe Java possa ter somente uma superclasse (herança simples) ela pode implementar qualquer número de interfaces. Uma classe oferece implementações aos métodos definidos na interface. Duas classes A e B diferentes podem implementar a mesma interface, mas o resultado da execução de um dos métodos definido nessa interface obtido através da classe A pode ser diferente do obtido pela classe B. As interfaces no Java suportam o conceito de herança múltipla de interface, passando para as suas filhas somente as descrições dos métodos e não a implementação desses métodos. As interfaces, como as classes, são declaradas em arquivos, sendo um para cada interface. Assim, como as classes elas são compiladas em arquivos <.class>. As interfaces 190
complementam e estendem o poder das classes e as duas podem ser tratadas praticamente da mesma maneira. A diferença é que uma classe pode ser instanciada enquanto uma interface não. Java não utiliza para definir uma interface uma linguagem de definição de interface (IDL) específica, a definição é escrita na própria linguagem de programação Java. 7.1.2 BIBLIOTECAS JAVA Java contém uma série de classes pré-definidas agrupadas em categorias chamadas pacotes. Juntos esses pacotes são referenciados como APIs Java ( Java Application Programming Interfaces ). A seguir são listados os pacotes Java disponíveis (Deitel e Deitel, 1997): java.applet ( Java Applet Package ), esse pacote contém a classe Applet e várias interfaces que habilitam a criação desses applets, assim como, interação entre applets / browsers e applets / audio. java.awt ( Java Abstract Windowing Toolkit Package ), esse pacote contém todas as classes e interfaces necessárias para um usuário criar e manipular interfaces gráficas (GUI). java.awt.image ( Java Abstract Windowing Toolkit Image Package ), esse pacote contém todas as classes e interfaces que habilitam armazenamento e manipulação de imagens num programa. java.awt.peer ( Java Abstract Windowing Toolkit Peer Package ), esse pacote contém interfaces que habilitam a interação dos componentes gráficos do Java em diferentes plataformas. java.io ( Java Input/Output Package ), esse pacote contém classes que manipulam entrada e saída de dados. 191
java.lang ( Java Language Package ), esse pacote é automaticamente importado em todos os programas. Ele contém classes e interfaces básicas necessárias a muitos programas Java. java.net ( Java Networking Package ), esse pacote contém classes que habilitam a comunicação entre programas via Internet e Intranets corporativas. java.util ( Java Utilities Package ), esse pacote contém classes e interfaces de propósito geral como, manipulação de data e hora, armazenamento e processamento de grande quantidade de dados, etc. E mais recentemente, java.rmi ( Java Remote Method Invocation Package ), esse pacote contém classes e interfaces que habilitam a invocação de métodos remotos entre aplicações Java. Applets e aplicações Java, em diferentes máquinas, comunicam-se diretamente através da invocação de seus métodos. java.server ( Java Server Package ), esse pacote contém classes e interfaces que facilitam a criação de servidores Java. java.security ( Java Security Package ), esse pacote contém classes e interfaces que facilitam ao desenvolvedor estabelecer a política de segurança, como por exemplo, o uso de criptografia e assinatura digital. Assim, a Figura 7.1 mostra o ambiente Java. 192
Java Applets Aplicações Java independentes Web browser Applets Java executando em um browser Aplicações Java independentes interagindo com o Sist.Operacional através de um ambiente de execução Ambiente de execução Java (Java Developer s Kit - JDK) -Carregador de classe -Interpretador de bytecode -Java Virtual Machine (JVM) -Biblioteca de Classes Java Compilador JIT (opcional) Compiladores JIT (Just-in-Time) converte de forma otimizada o bytecode Java em código nativo Windows MacOS/ PowerPC UNIX/RISC Outras Plataformas Fig. 7.1 Ambiente Java FONTE: Adaptada de Tilley (1999, p.2) 193
7.2 RMI Sistemas distribuídos necessitam que aplicações, executando em vários espaços de endereçamento, normalmente em diferentes computadores, sejam capazes de se comunicarem. Para um mecanismo de comunicação básico, a linguagem Java suporta o conceito de socket. A seguir, é descrito, brevemente, este conceito. O endereçamento de equipamentos na Internet é baseado em um identificador que independe da tecnologia de rede envolvida, conhecido como endereço IP ( Internet Protocol ), cujo formato é o do exemplo a seguir: 128.32.96.4. Este formato representa um número de 32 bits que, no modo normal de endereçamento da Internet, está associado a um único equipamento. Ele identifica a rede e o seu equipamento nela. Existe um outro modo de endereçamento conhecido como multicasting, onde um endereço IP está associado a um grupo de computadores. O endereço IP permite apenas a localização de um dado equipamento na Internet; para a localização de uma aplicação qualquer executando nesse equipamento, existe um outro identificador conhecido como port. Esta localização é obtida através da associação do port com um dos protocolos de transporte Transmission Control Protocol (TCP) ou User Datagram Protocol (UDP). Essa tríade composta por endereço IP, número de port e protocolo de transporte é conhecida como socket (Cyclades Brasil, 1996). Apesar do socket ser flexível o suficiente para comunicação em geral, ele requer do cliente e do servidor o conhecimento dos protocolos para troca de mensagens, isto é, a codificação e decodificação destas mensagens. O projeto destes protocolos é sempre difícil e susceptível a erros. Assim, o Java suporta o conceito de invocação de método remoto ( Remote Method Invocation RMI) permitindo uma abstração maior ao desenvolvedor que não precisa se preocupar com o mecanismo de comunicação entre cliente e servidor. 194
RMI permite manipular objetos distribuídos utilizando Java. Exemplificando, assim como o CORBA especificou um padrão de comunicação entre cliente e servidor, onde esses podem ser escritos em qualquer linguagem de programação, o padrão RMI foi projetado para ser especialmente integrado à linguagem Java, isto é, clientes escritos em Java se comunicam com objetos escritos em Java através do RMI. Basicamente o RMI é um RPC, que ao invés de realizar chamadas a procedimento remotos realiza chamadas a métodos de um objeto remoto. Um objeto remoto é aquele que se localiza em uma JVM diferente daquela em que se encontra o cliente, potencialmente em outro computador. Um objeto remoto é descrito por uma ou mais interfaces escritas na própria linguagem de programação Java. Por ser tratar de um objeto remoto estas interfaces são chamadas de interfaces remotas. A invocação de um método de um objeto remoto tem a mesma sintaxe que a invocação de um método de um objeto local. O cliente quando invoca um objeto não sabe se esse objeto é local ou remoto em relação a ele. O RMI providencia a: Localização ( binding ) de um método de um objeto remoto. Comunicação através de uma conexão de rede confiável permitindo a transmissão de requisições e respostas, funcionando como um canal entre cliente e servidor. Interface com usuário convertendo uma chamada de método em um bloco de dados (mensagem) que possa trafegar através da conexão e remontando a mesma chamada de método no outro extremo (remoto) a partir desta mensagem. São necessários códigos nos pontos finais da conexão para deixar transparente o uso do RMI pelo usuário. No lado cliente, um stub tem a mesma assinatura (nome, argumentos e tipo de retorno) que o método remoto e sua função é empacotar os argumentos em 195
uma mensagem para ser enviada através do canal de comunicação. Quando uma mensagem contendo os resultados enviados pelo servidor surgir neste canal, o stub extrai o valor de retorno antes de passá-lo para o código que chamou o método. O complemento do stub, localizado no lado do servidor, é chamado de skeleton. Sua função é converter as mensagens que chegam pelo canal de comunicação em argumentos e chamar as implementações dos métodos requisitados. Quando o método termina a execução, o valor de retorno é convertido em uma mensagem que é remetida ao stub do cliente. A arquitetura em que se baseia o RMI consiste de 3 camadas: a camada stub / skeleton, a camada de referência remota ( remote reference ) e a camada de transporte, mostradas na Figura 7.2. Elas são independentes entre si e podem ser substituídas por diferentes implementações sem que umas afetem as outras. Por exemplo, pode-se substituir a camada de transporte implementada em TCP, usando socket Java, por outra baseada em UDP. 196
Interface do objeto Implementação do objeto Call ( ) Call ( ) return return Stubs Referência Remota Skeleton Referência Remota Transporte Transporte Máquina do cliente Máquina Remota Conexão TCP/IP Fig. 7.2 RMI FONTE: Adaptada de Java (1999, p.2) 7.2.1 CAMADA STUB / SKELETON A camada stub / skeleton é a interface entre a aplicação e as outras camadas que compõem o RMI. Esta camada não trata do transporte em si, mas é responsável por transmitir os dados para a camada de referência remota como um stream de dados. Esse stream chamado de marshal stream emprega o mecanismo de serialização de objeto ( object serialization ). Este mecanismo permite que numa invocação (local ou remota) objetos Java possam ser passados como argumentos ou retornados como resultado da execução de 197
métodos. Os objetos Java, transmitidos através deste sistema de serialização de objetos, são passados por cópia entre espaços de endereçamento diferentes, isto é, máquinas diferentes. Caso já sejam objetos remotos, eles serão repassados por referência, onde referência de um objeto remoto é a referência de uma instância desse objeto. A serialização de objeto é específica da linguagem Java, assim, tanto clientes quanto servidores devem estar escritos em Java (Albuquerque, 1998). Quando um cliente invoca um método de um objeto remoto, os parâmetros do método são convertidos em um bloco de dados (mensagem) pelo stub e esta mensagem é enviada a máquina remota utilizando o protocolo, por exemplo, TCP/IP. Quando a mensagem chega na máquina remota, o skeleton transforma esta mensagem em argumentos e chama a implementação do método convertendo o resultado em uma mensagem que é redirecionada a máquina cliente, cujo stub é responsável pela extração do valor de retorno. O stub que se localiza no lado cliente contém todas as interfaces suportadas pela implementação do objeto remoto, sendo responsável por (Java, 1999): Iniciar a chamada do objeto remoto (acionando a camada de referência remota). Fazer o marshaling dos argumentos, utilizando o mecanismo de serialização de objeto, que será enviado para a camada de referência remota. Informar à camada de referência remota que a chamada pode ser invocada. Fazer o unmarshaling do valor de retorno ou exceção obtido. Informar à camada de referência remota que a chamada foi completada. O skeleton que se localiza no lado servidor contém um método que envia as chamadas recebidas do cliente para a implementação do objeto remoto, sendo responsável por (Java, 1999): 198
Fazer o unmarshaling dos argumentos recebidos. Realizar a chamada da implementação do objeto que executará o serviço requerido pelo cliente remoto. Fazer o marshaling do valor de retorno ou da situação de exceção, caso ocorra, utilizando o mecanismo de serialização de objeto que será enviado para a origem da chamada. As classes dos stubs e skeletons apropriadas são determinadas em tempo de execução e carregadas dinamicamente quando necessárias para atender uma requisiçâo do cliente. 7.2.2 CAMADA DE REFERÊNCIA REMOTA A camada de referência remota lida com a interface de transporte de baixo nível por meio de um protocolo. Esse protocolo é independente do stub do cliente e skeleton do servidor. O stub e skeleton desconhecem os diversos tipos de protocolo que um servidor pode utilizar. Existem vários protocolos que podem ser especificados e executados por esta camada, tais como (Java, 1999): Servidores suportam somente invocação ponto-a-ponto, onde um cliente interage com um único objeto. Servidores suportam replicação, onde um número de objetos remotos replicados precisam se manter sincronizados sempre que uma instância é modificada. Objetos remotos não estão continuamente ativos, sendo somente carregados do disco quando um cliente invoca um de seus métodos. Este protocolo pode ser selecionado pelo usuário que escolhe como a camada de referência será utilizada. Atualmente, a implementação da camada de referência está restrita a invocação ponto-a-ponto, onde um cliente interage 199
com um único objeto (não replicado) cuja referência é somente válida quando o processo servidor está executando. A classe que implementa esse protocolo é a java.rmi.server.unicastremoteserver. 7.2.3 CAMADA DE TRANSPORTE A camada de transporte manipula os detalhes da conexão e providencia um canal de comunicação entre as JVMs executando o código cliente e código servidor. O lado cliente deve possuir a interface do objeto remoto para poder referenciá-lo, enquanto o lado servidor possui a implementação do mesmo. Em geral, esta camada é responsável por (Java, 1999): Estabelecer conexões entre espaços de endereçamentos remotos, isto é, entre JVMs diferentes. Gerenciar conexões. Monitorar conexões (se elas existem ou não ( liveness )). Escutar chamadas que estejam sendo recebidas. Manter uma tabela que contém os objetos remotos residentes num determinado espaço de endereçamento. Estabelecer a conexão para as chamadas que estejam sendo recebidas. Localizar o alvo de uma chamada remota passando a conexão para quem a chamou. 7.3 APLICAÇÕES UTILIZANDO RMI O RMI, utilizando a facilidade de nomeação ( rmiregistry ), implementa um objeto servidor de nomes que mantem registrados todos os objetos localizados num host com os seus respectivos nomes, tornando acessível a referência desses objetos ao cliente. Uma aplicação cliente típica para invocar métodos de um objeto remoto deve obter primeiro a referência desse objeto. Essa referência é passada ao servidor que localizará a implementação do 200
objeto. O RMI trata os detalhes de comunicação entre cliente e servidor, assim, o cliente vê a invocação de um método de um objeto remoto como a de um objeto local. RMI (a) Registro Cliente RMI (c) Protocolo URL RMI (b) Servidor Protocolo URL Servidor Web Protocolo URL Fig. 7.3 Aplicações de objetos distribuídos com RMI FONTE: Adaptada de Java (1999, p.2) Na Figura 7.3 um cliente (RMI (a)) utiliza o sistema de registro para obter a referência de um objeto remoto, após obtida, invoca os seus métodos (RMI (b)). Caso seja necessário o RMI pode, se o cliente autorizar, utilizar o servidor web para localizar e carregar objetos remotos, isto é, classes Java em bytecodes. Para o carregamento pode-se utilizar qualquer protocolo Uniform Resource Locator (URL) suportado pelo sistema Java, por exemplo, HTTP, FTP, file, etc. Essas classes podem ser carregadas para serem executadas no servidor ou no cliente. Em aplicações que utilizam RMI, o método é executado na máquina onde se encontra o objeto sobre o qual ele atua. O objeto pode estar tanto em uma máquina diferente daquela onde se encontra o cliente quanto na mesma máquina. 201
7.4 CARREGAMENTO DINÂMICO DE STUB Em sistemas onde se utiliza o RPC, o código do stub deve ser gerado e linkado junto ao cliente antes de sua execução. Este código pode ser linkado estaticamente ou linkado dinamicamente em tempo de execução, utilizando para isso bibliotecas disponíveis localmente ou na rede. Em ambos os casos, o código que realiza o RPC deve estar disponível, na forma compilada, na máquina do cliente. O RMI generaliza essa técnica de carregar o código do stub em tempo de execução. Essa generalização do carregamento dinâmico do stub ( dynamic stub loading ) utiliza-se da capacidade do Java de realizar downloading de código. Tipicamente, o stub é carregado dinamicamente do sistema de arquivos locais, neste caso, o gerenciador de segurança não apresenta restrições. Entretanto, quando ele é carregado da rede, suas classes passam pelo gerenciador de segurança, prevenindo, assim, problemas potenciais de segurança (Java, 1999). O carregamento do stub, de forma dinâmica, é utilizado somente quando um cliente necessita de um determinado stub não disponível. Neste esquema, o código do stub (lado cliente) suporta o mesmo conjunto de interfaces remotas da implementação da classe que o gerou. Esse código do stub, residente num servidor host ou em qualquer outra localização, será enviado ao cliente sob demanda ( downloaded ). Para uma implementação remota, esse código pode ser gerado em tempo real ( on-the-fly ) no lado remoto e enviado para o cliente ou pode ser gerado no lado cliente através de uma lista de interfaces suportadas pela implementação remota. O carregamento dinâmico do stub emprega o carregador de classes do Java, o gerenciador de segurança e a serialização de objeto (Java, 1999). Quando a referência de um objeto remoto for passada como parâmetro ou como 202
resultado de uma chamada de método, o marshal stream, que transmite essa referência, incluirá também a informação de onde carregar a classe do stub para o objeto remoto (o URL, se ele for conhecido). Assim, quando a referência de objeto remoto for unmarshaled no destino, o marshal stream poderá localizar e carregar o código do stub (via carregador de classe e verificado pelo gerenciador de segurança) de tal forma que o cliente tenha disponibilizado o stub correto. 7.5 GARBAGE COLLECTION DE OBJETOS REMOTOS Num sistema distribuído, assim como, num sistema local, é desejável que todos os objetos remotos, não mais referenciados por qualquer cliente, sejam destruídos. Isso libera o desenvolvedor da necessidade de rastrear e finalizar, de maneira apropriada, os objetos remotos utilizados pelos clientes. Para executar a tarefa de coleta de lixo ( garbage collection ) o RMI utiliza-se de um algoritmo que implementa um contador de referência. Em tempo de execução, o RMI mantém, em cada JVM, o rastreamento de todas as referências de objetos vivos, isto é, que estão sendo referenciados por algum cliente naquela JVM. Quando um cliente referencia um objeto o contador é incrementado e quando deixa de referenciá-lo o contador é decrementado. A primeira vez que um objeto é referenciado é enviada uma mensagem de referenciamento para o servidor. No momento em que não existir mais referências vivas, uma mensagem de não referenciamento é enviada para o servidor. Se um objeto remoto não for mais solicitado pelos clientes, o RMI, em tempo de execução, utiliza uma referência fraca ( weak ) para referenciá-lo, permitindo ao coletor de lixo do JVM descartá-lo, caso não exista qualquer referência local a ele. Notar que, se existir partição de rede entre cliente e objeto remoto, é possível a ocorrência de uma remoção prematura deste objeto. Devido a esta 203
possibilidade, referências remotas não garantem integridade referencial, porque ela pode estar associada a um objeto inexistente e, neste caso, ocorrerá uma situação de falha (Java, 1999). 7.6 CONSIDERAÇÕES FINAIS Apesar do ambiente Java, como descrito neste capítulo, suportar as várias exigências de um ambiente de objetos distribuídos, nota-se que o RMI é um mecanismo caracterizado pelo fato do cliente, assim como o servidor serem, necessariamente, escritos em Java, tornando, por exemplo, a integração de sistemas legados, isto é, sistemas já existentes escritos nas mais diversas linguagens de programação, uma tarefa complexa (Curtis, 1998). Como visto nos capítulos anteriores, tem-se sempre um código, isto é, a implementação de um objeto executando numa máquina servidora acessada por um cliente. No Java, o mecanismo de adicionar uma camada extra de interpretação de bytecodes permite cópias e execução de seus códigos nas diversas máquinas onde as cópias estejam presentes. Esse mecanismo possibilita o ganho muito grande de portabilidade, mas, algumas vezes, causa perda de desempenho (Curtis, 1998). Pode-se também questionar, levando em conta a diferença existente na localização do código, a forma de distribuição implementada pelo Java. Tipicamente, um código é executado num servidor onde um cliente tem acesso a todas as suas funcionalidades. No Java, o código é copiado para o cliente como um arquivo e então executado. A aplicação do cliente acessa o código, em execução, localmente e, assim, o que é distribuído é o código Java e não, necessariamente, o dado e nem o processamento utilizado por ele (Grimes, 1997). De forma a ilustrar a utilização do Java/RMI encontra-se no Apêndice A um exemplo de aplicação. 204