UNIVERSIDADE FEDERAL DE VIÇOSA DEPARTAMENTO DE INFORMÁTICA JAVA NA PRÁTICA Volume II Alcione de Paiva Oliveira Vinícius Valente Maciel 2002 Java na Prática – Volume II 1 Sumário Capítulo I - Concorrência ....................................... 3 CRIANDO THREADS EM JAVA ..................................................................................................... 5 Criando threads por meio da interface Runnable ............................................................ 8 A CLASSE THREAD ..................................................................................................................... 9 Hierarquia ............................................................................................................................ 9 Construtores ......................................................................................................................... 9 Métodos .............................................................................................................................. 10 Variáveis públicas .............................................................................................................. 11 CICLO DE VIDA DOS THREADS ................................................................................................. 12 sleep(), yield(), join(), destroy(), stop(), suspend() e resume(). ................ 13 DAEMON THREADS .................................................................................................................. 17 INFLUÊNCIA DO SISTEMA OPERACIONAL NO COMPORTAMENTO DOS THREADS....................... 18 Forma de escalonamento de threads.................................................................................. 19 Relacionamento entre os níveis de prioridades definidas na linguagem Java e os níveis de prioridades definidas nos Sistemas Operacionais.............................................................. 20 COMPARTILHAMENTO DE MEMÓRIA E SINCRONIZAÇÃO .......................................................... 22 Atomicidade de Instruções e Sincronização do Acesso à Sessões Críticas ........................ 25 Comunicação entre Threads: wait() e notify() ................................................................... 31 Capítulo II - Animação ......................................... 46 Capítulo III - Programação em rede ........................... 50 CONCEITOS SOBRE PROTOCOLOS USADOS NA INTERNET......................................................... 50 TCP..................................................................................................................................... 52 UDP.................................................................................................................................... 52 IDENTIFICAÇÃO DE HOSTS (Número IP)...................................................................... 53 Identificação de Processos (Portas)................................................................................... 54 PROGRAMAÇÃO EM REDE COM JAVA ....................................................................................... 54 Comunicação Básica Entre Aplicações.............................................................................. 55 Comunicação Sem Conexão (UDP) ................................................................................... 60 Comunicação por meio de URL ......................................................................................... 63 Capítulo IV – Computação Distribuída (RMI) ................... 69 CRIANDO NOSSA AGENDA DISTRIBUÍDA ................................................................................... 69 Implementar interface do objeto remoto ............................................................................ 69 Capítulo V - Acesso a Banco de Dados ......................... 71 MODELOS DE ACESSO A SERVIDORES ...................................................................................... 71 TIPOS DE DRIVERS JDBC......................................................................................................... 72 Obtendo os Drivers JDBC.................................................................................................. 74 PREPARANDO UM BANCO DE DADOS ....................................................................................... 74 Configurando o ODBC....................................................................................................... 77 EXEMPLO INICIAL .................................................................................................................... 78 Carregando o Driver.......................................................................................................... 79 Estabelecendo a conexão ................................................................................................... 79 Criando e Executando Comandos ...................................................................................... 81 RECUPERANDO VALORES......................................................................................................... 82 TRANSAÇÕES E NÍVEL DE ISOLAMENTO ................................................................................... 83 Transação........................................................................................................................... 83 Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 2 Níveis de isolamento........................................................................................................... 85 PREPARED STATEMENTS .......................................................................................................... 87 PROCEDIMENTOS ARMAZENADOS (STORED PROCEDURES)...................................................... 88 AGENDA ELETRÔNICA VERSÃO JDBC ..................................................................................... 90 Capítulo VI Servlets e JSP ................................... 97 SERVLETS ................................................................................................................................ 97 Applets X Servlets............................................................................................................... 98 CGI X Servlets .................................................................................................................... 99 A API SERVLET ....................................................................................................................... 99 Exemplo de Servlet ........................................................................................................... 101 COMPILANDO O SERVLET....................................................................................................... 102 Instalando o Tomcat......................................................................................................... 103 PREPARANDO PARA EXECUTAR O SERVLET ............................................................................ 106 Compilando o Servlet ....................................................................................................... 106 Criando uma aplicação no Tomcat .................................................................................. 107 EXECUTANDO O SERVLET ...................................................................................................... 108 Invocando diretamente pelo Navegador........................................................................... 108 Invocando em uma página HTML .................................................................................... 109 Diferenças entre as requisições GET e POST .................................................................. 109 CONCORRÊNCIA ..................................................................................................................... 110 OBTENDO INFORMAÇÕES SOBRE A REQUISIÇÃO .................................................................... 112 LIDANDO COM FORMULÁRIOS................................................................................................ 114 LIDANDO COM COOKIES......................................................................................................... 115 LIDANDO COM SESSÕES ......................................................................................................... 118 JSP......................................................................................................................................... 122 PHP X JSP ....................................................................................................................... 123 ASP X JSP ........................................................................................................................ 124 Primeiro exemplo em JSP ................................................................................................ 124 Executando o arquivo JSP................................................................................................ 125 Objetos implícitos............................................................................................................. 126 Tags JSP........................................................................................................................... 127 Comentários ..................................................................................................................... 130 Diretivas ........................................................................................................................... 131 Extraindo Valores de Formulários................................................................................... 133 Criando e Modificando Cookies....................................................................................... 134 Lidando com sessões ........................................................................................................ 136 O Uso de JavaBeans......................................................................................................... 138 REENCAMINHANDO OU REDIRECIONANDO REQUISIÇÕES ....................................................... 146 UMA ARQUITETURA PARA COMÉRCIO ELETRÔNICO ............................................................... 148 Tipos de aplicações na WEB ............................................................................................ 148 Arquitetura MVC para a Web .......................................................................................... 148 Agenda Web: Um Exemplo de uma aplicação Web usando a arquitetura MVC.............. 151 Capítulo VII Perguntas Frequentes ........................... 171 Bibliografia ................................................ 172 Links ....................................................... 173 Índice ...................................................... 175 Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 3 Capítulo I - Concorrência Um sistema operacional é dito concorrente se permite que mais que uma tarefa seja executada ao mesmo tempo. Na prática a concorrência real ou paralelismo só é possível se o hardware subjacente possui mais de um processador. No entanto, mesmo em computadores com apenas um processador é possível obter um certo tipo de concorrência fazendo com o processador central execute um pouco de cada tarefa por vez, dando a impressão de que as tarefas estão sendo executadas simultaneamente. Dentro da nomenclatura empregada, uma instância de um programa em execução é chamada de processo. Um processo ocupa um espaço em memória principal para o código e para as variáveis transientes (variáveis que são eliminadas ao término do processo). Cada processo possui pelo menos uma linha de execução (Thread). Para ilustrarmos o que é uma linha de execução suponha um determinado programa prog1. Ao ser posto em execução é criado um processo, digamos A, com uma área de código e uma área de dados e é iniciada a execução do processo a partir do ponto de entrada. A instrução inicial assim como as instruções subsequentes formam uma linha de execução do processo A. Portanto, um thread nada mais é que uma sequência de instruções que está em execução de acordo com que foi determinado pelo programa. O estado corrente da linha de execução é representada pela instrução que está sendo executada. A figura IX.1 mostra a relação entre estes elementos. arquivo prog1 101001101 010110010 010101100 100011101 Área de código Memória Principal 101001101 010110010 010101100 100011101 Linha de execução (thread) Processo Área de dados 100101010 101001010 Figura IX.1 – Relação entre Programa, Processo e Thread. É possível existir mais de uma linha de execução em um único processo. Cada linha de execução pode também ser vista como um processo, com a Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 4 diferença que enquanto cada processo possui sua área de código e dados separada de outros processos, os threads em um mesmo processo compartilham o código e a área de dados. O que distingue um thread de outro em um mesmo processo é a instrução corrente e uma área de pilha usada para armazenar o contexto da sequência de chamadas de cada thread. Por isso os threads também são chamados de processos leves (light process). A figura IX.2 mostra esquematicamente a diferença entre processos e threads. Memória Processo A 10100 01001 11001 01010 1010 0111 C 10100 01001 11001 01010 1010 0111 B 10100 01001 11001 01010 1010 0111 Código Dados 1010001001110 0101010101010 1010000101101 1010100010101 0101011011001 0101010100101 0101010010101 0101001000000 1010101010101 Thread 1 Thread 2 Área de pilha do thread2 10100111001 01010101010 10101010101 01010101000 11110101010 Área de pilha do thread1 Figura IX.2 – (a) Processos; (b) Threads. Sistemas monotarefas e monothreads como o DOS possuem apenas um processo em execução em um determinado instante e apenas um thread no processo. Sistemas multitarefas e monothreads como o Windows 3.1 permitem vários processos em execução e apenas um thread por processo. Sistemas multitarefas e multithread como o Solaris, OS/2, Linux e Windows 95/98/NT permitem vários processos em execução e vários threads por processo. Como os threads em um mesmo processo possuem uma área de dados em comum, surge a necessidade de controlar o acesso a essa área de dados, de modo que thread não leia ou altere dados no momento que estão sendo alterados por outro thread. A inclusão de instruções para controlar o acesso a áreas compartilhadas torna o código mais complexo do que o código de processos monothreads. Uma pergunta pode surgir na mente do leitor: se a inclusão de mais de um thread torna o código mais complexo porque razão eu deveria projetar código multithread. Processos com vários threads podem realizar mais de uma Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 5 tarefa simultaneamente. São úteis na criação de processos servidores, criação de animações e no projeto de interfaces com o usuário que não ficam travadas durante a execução de alguma função. Por exemplo, imagine um processo servidor a espera de requisições de serviços, podemos projetá-lo de modo que ao surgir uma solicitação de um serviço por um processo cliente ele crie um thread para atender a solicitação enquanto volta a esperar a requisição de novos serviços. Com isto os processos clientes não precisam esperar o término do atendimento de alguma solicitação para ter sua requisição atendida. O mesmo pode ser dito em relação ao projeto de interfaces com o usuário. O processo pode criar threads para executar as funções solicitadas pelo usuário, enquanto aguarda novas interações. Caso contrário, a interface ficaria impedida de receber novas solicitações enquanto processa a solicitação corrente, o que poderia causar uma sensação de travamento ao usuário. Outra aplicação para processos multithread é a animação de interfaces. Nesse caso cria-se um ou mais threads para gerenciar as animações enquanto outros threads cuidam das outras tarefas como por exemplo entrada de dados. A rigor todas as aplicações acima como outras aplicações de processos multithread podem ser executados por meio de processos monothreads. No entanto, o tempo gasto na mudança de contexto entre processos na maioria dos sistemas operacionais é muito mais lenta que a simples alternância entre threads, uma vez que a maior parte das informações contextuais são compartilhadas pelos threads de um mesmo processo. Mudança de Contexto (task switch) É o conjunto de operações necessárias para gravar o estado atual do processo corrente e recuperar o estado de outro processo de modo a torná-lo o processo corrente. Mesmo que você não crie mais de um thread todo processo Java possui vários threads: thread para garbage collection, thread para monitoramento de eventos, thread para carga de imagens, etc. Criando threads em Java Processos Multithread não é uma invenção da linguagem Java. É possível criar processos multithread com quase todas as linguagens do mercado, como C++, e Object Pascal. No entanto Java incorporou threads ao núcleo básico da linguagem tornado desta forma mais natural o seu uso. Na verdade o uso de Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 6 threads está tão intimamente ligado a Java que é quase impossível escrever um programa útil que não seja multithread. A classe Thread agrupa os recursos necessários para a criação de um thread. A forma mais simples de se criar um thread é criar uma classe derivada da classe Thread. Por exemplo: class MeuThread extends Thread { ... } É preciso também sobrescrever o método run() da classe Thread. O método run() é o ponto de entrada do thread, da mesma forma que o método main() é ponto de entrada de uma aplicação. O exemplo IX.1 mostra uma classe completa. public class MeuThread extends Thread { String s; public MeuThread (String as) { super(); s = new String(as); } public void run() { for (int i = 0; i < 5; i++) System.out.println(i+” “+s); System.out.println("FIM! "+s); } } Exemplo IX.1 – Subclasse da classe Thread. No exemplo IX.1foi inserido um atributo para identificar o thread, apesar de existir formas melhores de se nomear um thread como veremos mais adiante. O método run() contém o código que será executado pelo thread. No exemplo IX.1 o thread imprime cinco vezes o atributo String. Para iniciar a execução de um thread cria-se um objeto da classe e invoca-se o método start() do objeto. O método start() cria o thread e inicia sua execução pelo método run(). Se o método run() for chamado diretamente nenhum thread novo será criado e o método run() será executado no thread corrente. O exemplo IX.2 mostra uma forma de se criar um thread usando a classe definida no exemplo IX.1. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 7 public class TesteThread1 { public static void main (String[] args) { new MeuThread("Linha1").start(); } } Exemplo IX.2 – Criação de um Thread. No exemplo acima apenas um thread, além do principal é criado. Nada impede que sejam criados mais objetos da mesma classe para disparar um número maior de threads. O exemplo IX.3 mostra a execução de dois threads sobre dois objetos de uma mesma classe. public class TesteThread2 { public static void main (String[] args) { new MeuThread("Linha1").start(); new MeuThread("Linha2").start(); } } Exemplo IX.3 – Criação de dois Threads. Cada thread é executado sobre uma instância da classe e, por consequência, sobre uma instância do método run(). A saída gerada pela execução do exemplo IX.3 depende do sistema operacional subjacente. Uma saída possível é a seguinte: 0 Linha2 0 Linha1 1 Linha2 1 Linha1 2 Linha2 2 Linha1 3 Linha2 3 Linha1 4 Linha2 4 Linha1 FIM! Linha2 FIM! Linha1 A saída acima mostra que os threads executam intercaladamente. No entanto, em alguns sistemas operacionais os threads do exemplo IX.3 executariam um após o outro. A relação entre a sequência de execução e o sistema operacional e dicas de como escrever programas multithread com Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 8 sequência de execução independente de plataforma operacional serão em uma seção mais adiante neste mesmo capítulo. Criando threads por meio da interface Runnable Algumas vezes não é possível criar uma subclasse da classe Thread porque a classe já deriva outra classe, por exemplo a classe Applet. Outras vezes, por questões de pureza de projeto o projetista não deseja derivar a classe Thread simplesmente para poder criar um thread uma vez que isto viola o significado da relação de classe-subclasse. Para esses casos existe a interface Runnable. A interface Runnable possui apenas um método para ser implementado: o método run(). Para criar um thread usando a interface Runnable é preciso criar um objeto da classe Thread, passando para o construtor uma instância da classe que implementa a interface. Ao invocar o método start() do objeto da classe Thread, o thread criado, inicia sua execução no método run() da instância da classe que implementou a interface. O exemplo IX.4 mostra a criação de um thread usando a interface Runnable. public class TesteThread2 implements Runnable { private String men; public static void main(String args[]) { TesteThread2 ob1 = new TesteThread2 (“ola”); Thread t1 = new Thread(ob1); t1.start(); } public TesteThread2 (String men) {this.men=men;} public void run() { for(;;) System.out.println(men); } } Exemplo IX.4 – Criação de um thread por meio da interface Runnable. Note que agora ao invocarmos o método start() o thread criado iniciará a execução sobre o método run() do objeto passado como parâmetro, e não sobre o método run() do objeto Thread. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 9 Nada impede que seja criado mais de um thread executando sobre o mesmo objeto: Thread t1 = new Thread(ob1); Thread t2 = new Thread(ob1); Neste caso alguns cuidados devem ser tomados, uma vez que existe o compartilhamento das variáveis do objeto por dois threads. Os problemas que podem advir de uma situação como esta serão tratados mais adiante. A classe Thread A classe Thread é extensa, possuindo vários construtores, métodos e variáveis públicas. Aqui mostraremos apenas os mais usados. Hierarquia A classe Thread deriva diretamente da classe Object. java.lang.Object java.lang.Thread Construtores Construtor Thread(ThreadGroup g, String nome) Thread(Runnable ob, String nome) Thread(ThreadGroup g, Runnable ob, String nome) Thread(String nome) Thread() Thread(Runnable ob) Descrição Cria um novo thread com o nome especificado dentro do grupo g. Cria um novo thread para executar sobre o objeto ob, com o nome especificado. Cria um novo thread para executar sobre o objeto ob, dentro do grupo g, com o nome especificado. Cria um novo thread com o nome especificado. Cria um novo thread com o nome default. Cria um novo thread para executar Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II Thread(ThreadGroup g, Runnable ob) 10 sobre o objeto ob. Cria um novo thread para executar sobre o objeto ob, dentro do grupo g. Tabela IX.1 – Construtores da classe Thread. A tabela X.1 mostra os principais construtores da classe Thread. Podemos notar que é possível nomear os threads e agrupá-los. Isto é útil para obter a referência de threads por meio do seu nome. Métodos Método currentThread() Descrição Retorna uma referência para o thread corrente em execução. destroy() Destroi o thread sem liberar os recursos. dumpStack() Imprime a pilha de chamadas do thread corrente. enumerate(Thread[] v) Copia para o array todos os thread ativos no grupo do thread. getName() Obtém o nome do thread. getPriority() Obtém a prioridade do thread. getThreadGroup() Retorna o grupo do thread. resume() Reassume a execução de um thread previamente suspenso. run() Se o thread foi construído usando um objeto Runnable separado então o método do objeto Runnable é chamado. Caso contrário nada ocorre. setName(String name) Muda o nome do thread. setPriority(int newPriority) Muda a prioridade do thread. sleep(long millis) Suspende o thread em execução o número de milisegundos especificados. sleep(long millis, int Suspende o thread em execução o número de nanos) milisegundos mais o número de nanosegundos especificados. start() Inicia a execução do thread. A máquina virtual chama o método run() do thread. stop() Força o encerramento do thread. suspend() Suspende a execução de um thread. yield() Faz com que o thread corrente interrompa permitindo que outro thread seja executado. Tabela IX.2 – Métodos da classe Thread. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 11 A tabela X.2 apresenta os principais métodos do classe Thread. Alguns métodos muito usados nas versões anteriores do SDK1.2 foram depreciados na versão atual por serem considerados inseguros ou com tendência a causarem deadlock . Os métodos depreciados são: stop(), suspend(), resume() e destroy(). Deadlock Travamento causado pela espera circular de recursos em um conjunto de threads. O travamento por deadlock mais simples é o abraço mortal onde um thread A espera que um thread B libere um recurso, enquanto que o thread B só libera o recurso esperado por A se obter um recurso mantido por A. Desta forma os dois threads são impedidos indefinidamente de prosseguir. Existem alguns métodos da classe Object que são importantes para o controle dos threads. O leitor pode estar se perguntando porque métodos relacionados threads estão na superclasse Object que é “mãe” de todas as classe em Java. A razão disso é que esses métodos lidam com um elemento associado a todo objeto e que é usado para promover o acesso exclusivo aos objetos. Esse elemento é chamado de monitor. Na seção que aborda a sincronização os monitores serão discutidos mais detalhadamente. Os métodos herdados relacionados com controle dos threads estão descritos na tabela IX.3. Método notify() notifyAll() wait() wait(long timeout, int nanos) wait(long timeout) Descrição Notifica um thread que está esperando sobre um objeto. Notifica todos os threads que está esperando sobre um objeto. Espera para ser notificado por outro thread. Espera para ser notificado por outro thread. Espera para ser notificado por outro thread. Tabela IX.3 – Métodos da classe Object relacionados com threads. Variáveis públicas As variáveis públicas da classe Thread definem valores máximo, mínimo e default para a prioridade de execução dos threads. Java estabelece dez valores de prioridade. Como essas prioridades são relacionadas com as prioridades do Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 12 ambiente operacional depende da implementação máquina virtual e pode influenciar no resultado final da execução do programa. Mais adiante abordaremos a influência do ambiente operacional na execução de programas multithread. Variável static final int MAX_PRIORITY static final int MIN_PRIORITY static final int NORM_PRIORITY Descrição A prioridade máxima que um thread pode ter. A prioridade mínima que um thread pode ter. A prioridade default associado a um thread. Tabela IX.4 – Variáveis públicas. Ciclo de Vida dos Threads Um thread pode possuir quatro estados conforme mostra a figura IX.3. Podemos observar que uma vez ativo o thread alterna os estados em execução e suspenso até que passe para o estado morto. A transição de um estado para outro pode ser determinada por uma chamada explícita a um método ou devida a ocorrência de algum evento a nível de ambiente operacional ou de programa. Estados Ativos thread em Execução thread novo thread morto thread suspenso Figura IX.3 – Estados de um thread. A transição de um thread do estado novo para algum estado ativo é sempre realizada pela invocação do método start() do objeto Thread. Já as transições do estado em execução para o estado suspenso e vice-versa e desses para o estado morto podem ser disparadas tanto pela invocação de variados métodos como pela ocorrência de eventos. O exemplo IX.5 mostra as ocorrência de transição em um código. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 13 public class TesteThread3 extends Thread { public TesteThread3 (String str) {super(str);} public void run() { for (int i = 0; i < 10; i++) { System.out.println(i + " " + getName()); try { // Comando para suspender o thread por // 1000 milisegundos (1 segundo) // Transição do estado em execução para o // estado suspenso sleep(1000); } catch (InterruptedException e) {} // Evento: fim do tempo de suspensão // Transição do estado em suspenso para o // estado em execução } System.out.println("FIM! " + getName()); // Evento: fim da execução do thread // Transição do estado ativo suspenso para o // estado morto } public static void main(String args[]) { TesteThread3 t1 = new TesteThread3(args[0]); t1.start(); // Transição para um estado ativo } } Exemplo IX.5 – Alguns comandos e eventos que acarretam transição de estados. sleep(), yield(), join(), destroy(), stop(), suspend() e resume(). Agora que vimos os estados que podem ser assumidos por um thread em seu ciclo de vida vamos examinar mais detalhadamente alguns dos métodos responsáveis pela mudança de estado de um thread. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 14 sleep() O método sleep()é um método estático e possui as seguintes interfaces: static void sleep(long ms) throws InterruptedException ou static void sleep(long ms, int ns) throws InterruptedException Onde ms é um valor em milisegundos e ns é um valor em nanosegundos. O método sleep() faz com que o thread seja suspenso por um determinado tempo, permitindo que outros threads sejam executados. Como o método pode lançar a exceção InterruptedException, é preciso envolver a chamada em um bloco try/catch ou propagar a exceção. O exemplo IX.6 define uma espera mínima de 100 milisegundos entre cada volta do loop. Note que o tempo de suspensão do thread pode ser maior que o especificado, uma vez que outros threads de maior ou mesmo de igual prioridade podem estar sendo executados no momento em que expira o tempo de suspensão solicitado. public class ThreadComYield extends Thread { String s; public ThreadComYield(String as) { super(); s = new String(as); } public void run() { for (int i = 0; i < 5; i++) { System.out.println(i+” “+s); try{ Thread.sleep(100); catch(InterruptedException e){} } System.out.println("FIM! "+s); } } Exemplo IX.6 – Uso do método sleep(). Outro problema com o sleep() é que a maioria dos Sistemas Operacionais não suportam resolução de nanosegundos. Mesmo a resolução a nível de unidade de milisegundo não é suportada pela maioria dos SOs. No caso do SO não suportar a resolução de tempo solicitada, o tempo será arredondado para a nível de resolução suportado pela plataforma operacional. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 15 yield() O método yield() é um método estático com a seguinte interface: static void yield() Uma chamada ao método yield() faz com que o thread corrente libere automaticamente a CPU para outro thread de mesma prioridade. Se não houver nenhum outro thread de mesma prioridade aguardando, então o thread corrente mantém a posse da CPU. O exemplo IX.7 altera o exemplo IX.1 de modo a permitir que outros threads de mesma prioridade sejam executados a cada volta do loop. Public class ThreadComYield extends Thread { String s; public ThreadComYield(String as) { super(); s = new String(as); } public void run() { for (int i = 0; i < 5; i++) { System.out.println(i+” “+s); Thread.yield(); } System.out.println("FIM! "+s); } } Exemplo IX.7 – Uso do método yield(). join() O método join() é um método de instância da classe Thread e é utilizado quando existe a necessidade do thread corrente esperar pela término da execução de outro thread. As versões do método join() são as seguintes: public final void join(); public final void join(long millisecond); public final void join(long millisecond, int nanosecond); Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 16 Na primeira versão o thread corrente espera indefinidamente pelo encerramento da execução do segundo thread. Na segunda e terceira versão o thread corrente espera pelo término da execução do segundo thread até no máximo um período de tempo prefixado. O exemplo IX.8 mostra o como usar o método join(). class ThreadComJoin extends Thread { String s; public ThreadComJoin(String as) { super(); s = new String(as); } public void run() { for (int i = 0; i < 10; i++) System.out.println(i+” “+s); System.out.println("Fim do thread!"); } } public class TestaJoin { public static void main(String args[]) { ThreadComJoin t1 = new ThreadComJoin(args[0]); t1.start(); // Transição para um estado ativo t1.join(); // Espera pelo término do thread System.out.println("Fim do programa!"); } } Exemplo IX.8 – Uso do método join(). stop(), suspend(), resume() e destroy() A partir da versão 1.2 do SDK os métodos stop(), suspend(), and resume() tornaram-se deprecated uma vez que a utilização desses métodos tendia a gerar erros. No entanto, devido a grande quantidade de código que ainda utiliza estes método, acreditamos que seja importante mencioná-los. O método stop() é um método de instância que encerra a execução do thread ao qual pertence. Os recursos alocados ao thread são liberados. É recomendável substituir o método stop() pelo simples retorno do método run(). Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 17 O método suspend() é um método de instância que suspende a execução do thread ao qual pertence. Nenhum recurso é liberado, inclusive os monitores que possuir no momento da suspensão (os monitores serão vistos mais adiante e servem para controlar o acesso à variáveis compartilhadas). Isto faz com que o método suspend() tenda a ocasionar deadlocks. O método resume() é um método de instância que reassume a execução do thread ao qual pertence. Os métodos suspend() e resume() devem ser substituídos respectivamente pelos métodos wait() e notify(), como veremos mais adiante. O método destroy() é um método de instância que encerra a execução do thread ao qual pertence. Os recursos alocados ao thread não são liberados. Não é um método deprecated mas é recomendável substituí-lo pelo simples retorno do método run(). Daemon Threads Daemon threads são threads que rodam em background com a função de prover algum serviço mas não fazem parte do propósito principal do programa. Quando só existem threads do tipo daemon o programa é encerrado. Um exemplo de daemon é o thread para coleta de lixo. Um thread é definido como daemon por meio do método de instância setDaemon(). Para verificar se um thread é um daemon é usado o método de instância isDaemon(). O exemplo IX.9 mostra o como usar esses métodos. import java.io.*; class ThreadDaemon extends Thread { public ThreadDaemon() { setDaemon(true); start(); } public void run() { for(;;) yield(); } } public class TestaDaemon { Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 18 public static void main(String[] args) { Thread d = new ThreadDaemon(); System.out.println("d.isDaemon() = " + d.isDaemon()); BufferedReader stdin = new BufferedReader( new InputStreamReader(System.in)); System.out.println("Digite qualquer coisa"); try { stdin.readLine(); } catch(IOException e) {} } } Exemplo IX.9 – Uso dos métodos relacionados com daemons. No exemplo IX.9 o método main() da classe TestaDaemon cria um objeto da classe ThreadDaemon. O construtor da classe ThreadDaemon define o thread como daemon por meio do método setDaemon() e inicia a execução do thread. Como é apenas um thread de demonstração o método run() da classe ThreadDaemon não faz nada, apenas liberando a posse da CPU toda vez que a adquire. Após a criação da instância da classe ThreadDaemon no método main() é testado se o thread criado é um daemon, utilizando para esse fim o método isDaemon(). Depois disso o programa simplesmente espera o usuário pressionar a tecla <enter>. O programa termina logo em seguida ao acionamento da tecla, mostrando dessa forma que o programa permanece ativo apenas enquanto existem threads não daemons ativos. Influência do Sistema Operacional no Comportamento dos Threads Apesar da linguagem Java prometer a construção de programas independentes de plataforma operacional, o comportamento dos threads pode ser fortemente influenciado pelo sistema operacional subjacente. Portanto, o programador deve tomar alguns cuidados se deseja construir programas que funcionem da mesma forma, independente do ambiente onde está sendo executado. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 19 Alguns sistemas operacionais não oferecem suporte a execução de threads. Neste caso, cada processo possui apenas um thread. Mesmo em sistemas operacionais que oferecem suporte a execução de múltiplos threads por processo o projetista da máquina virtual pode optar por não usar o suporte nativo a threads. Deste modo, é responsabilidade da máquina virtual criar um ambiente multithread. Threads implementados desta forma, a nível de usuário, são chamados de green-threads. As influências da plataforma operacional podem agrupadas em dois tipos: 1) Forma de escalonamento de threads. O ambiente pode adotar um escalonamento não preemptivo ou preemptivo. No escalonamento não preemptivo (também chamado de cooperativo) um thread em execução só perde o controle da CPU (Central Processing Unit) se a liberar voluntariamente ou se necessitar de algum recurso que ainda não está disponível. Já no escalonamento preemptivo, além das formas acima um thread pode perde o controle da CPU por eventos externos, como o fim do tempo máximo definido pelo ambiente para a execução contínua de um thread (fatia de tempo) ou porque um thread de mais alta prioridade está pronto para ser executado. Exemplos de sistemas operacionais não preemptivos são Windows 3.1 e IBM OS/2. Exemplos de sistemas operacionais preemptivos são Windows 95/98/NT e Linux, QNX, e muitos outros. Alguns sistemas operacionais adotam uma abordagem híbrida, suportando tanto o modelo cooperativo como o preemptivo, como o Solaris da Sun. 2) Relacionamento entre os níveis de prioridades definidas na linguagem Java e os níveis de prioridades definidas nos Sistemas Operacionais. Em um SO preemptivo um thread de uma determinada prioridade perde a posse da CPU para um thread de prioridade mais alta que esteja pronto para ser executado. A linguagem Java prevê dez níveis de prioridades que podem ser atribuídas aos threads. No entanto, cada SO possui um número de prioridades diferente e o mapeamento das prioridades da linguagem Java para as prioridades do SO subjacente pode influenciar o comportamento do programa. Forma de escalonamento de threads Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 20 A especificação da máquina virtual Java determina que a forma de escalonamento de threads seja preemptiva. Portanto, mesmo em ambiente operacionais cooperativos a máquina virtual deve garantir um escalonamento preemptivo. No entanto, um escalonamento preemptivo não obriga a preempção por fim de fatia de tempo. Podemos ter um escalonamento preemptivo onde um thread de mais alta prioridade interrompe o thread que tem a posse da CPU mas não existe preempção por fim de fatia de tempo. Um escalonamento onde threads de mesma prioridade intercalam a posse da CPU por força do fim da fatia de tempo é chamado de escalonamento Round-Robin. A especificação da máquina virtual Java não prevê o escalonamento Round-Robin, mas também não o descarta, abrindo uma possibilidade de implementações distintas de máquina virtual e introduzindo o não determinismo na execução de programas multithread. Por exemplo, o exemplo IX.3 poderia ter uma saída distinta da apresentada anteriormente caso seja executado por uma máquina virtual que não implementa o escalonamento Round-Robin. Nesse caso a saída seria a seguinte: 0 Linha2 1 Linha2 2 Linha2 3 Linha2 4 Linha2 FIM! Linha2 0 Linha1 1 Linha1 2 Linha1 3 Linha1 4 Linha1 FIM! Linha1 Neste caso, se o programador deseja que a execução de threads se processe de forma alternada, independentemente da implementação da máquina virtual, então é necessário que ele insira código para a liberação voluntária da CPU. Isso pode ser feito com o método yield() ou com o método sleep(). Relacionamento entre os níveis de prioridades definidas na linguagem Java e os níveis de prioridades definidas nos Sistemas Operacionais. Como já dissemos a linguagem Java prevê dez níveis de prioridades que podem ser atribuídas aos threads. Na verdade são onze prioridades, mas a prioridade nível 0 é reservada para threads internos. As prioridades atribuídas Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 21 aos threads são estáticas, ou seja não se alteram ao longo da vida do thread, a não ser que por meio de chamadas a métodos definidos para esse propósito. A classe thread possui variáveis públicas finais com valores de prioridade predefinidos, como mostrado na tabela IX.4. No entanto, os sistemas operacionais podem possuir um número maior ou menor de níveis de prioridades. Vamos citar um exemplo: o MSWindows 95/98/NT. Este sistema possui apenas sete níveis de prioridades e estes sete níveis devem ser mapeados para os onze níveis de prioridades especificados em Java. Cada máquina virtual fará este mapeamento de modo diferente, porém a implementação comum é mostrada na tabela IX.5. Prioridades Java 0 1(Thread.MIN_PRIORITY) 2 3 4 5(Thread.NORM_PRIORITY) 6 7 8 9 10(Thread.MAX_PRIORITY) Prioridades MSWindows THREAD_PRIORITY_IDLE THREAD_PRIORITY_LOWEST THREAD_PRIORITY_LOWEST THREAD_PRIORITY_BELOW_NORMAL THREAD_PRIORITY_BELOW_NORMAL THREAD_PRIORITY_NORMAL THREAD_PRIORITY_ABOVE_NORMAL THREAD_PRIORITY_ABOVE_NORMAL THREAD_PRIORITY_HIGHEST THREAD_PRIORITY_HIGHEST THREAD_PRIORITY_TIME_CRITICAL Tabela IX.5 –Mapeamento das prioridades de Java para MSWindows. Note que nesta implementação níveis de prioridades diferentes em Java serão mapeados para um mesmo nível de prioridade em MSWindows. Isto pode levar a resultados inesperados caso o programador projete uma aplicação esperando, por exemplo, que um thread de prioridade 4 irá interromper um thread de prioridade 3. Para evitar este tipo de problema o programador pode adotar dois tipos de abordagem: 1) utilizar, se for possível, apenas as prioridades Thread.MIN_PRIORITY, Thread.NORM_PRIORITY e Thread.MAX_PRIORITY para atribuir prioridades aos threads; ou 2) não se basear em níveis de prioridades para definir o escalonamento de threads, utilizando, alternativamente, primitivas de sincronização que serão abordadas na próxima seção. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 22 Compartilhamento de Memória e Sincronização Como já foi dito, mais de um thread pode ser criado sobre um mesmo objeto. Neste caso cuidado especiais devem ser tomados, uma vez que os threads compartilham as mesmas variáveis e problemas podem surgir se um thread está atualizando uma variável enquanto outro thread está lendo ou atualizando a mesma variável. Este problema pode ocorrer mesmo em threads que executam sobre objetos distintos, já que os objetos podem possuir referências para um mesmo objeto. O exemplo IX.8 mostra a execução de dois threads sobre um mesmo objeto. O nome do thread é usado para que o thread decida que ação tomar. O thread de nome “um” ontem um número de 0 a 1000 gerado aleatoriamente e o coloca na posição inicial de um array de dez posições. As outras posições do array são preenchidas com os nove números inteiros seguintes ao número inicial. O thread de nome “dois” imprime o conteúdo do vetor. Obviamente o programa é apenas ilustrativo, não possuindo aplicação prática. A intenção inicial do projetista é obter na tela sequências de dez números inteiros consecutivos iniciados aleatoriamente. No entanto, como os dois threads compartilham o mesmo objeto e não existe qualquer sincronismo entre sí, é pouco provável que o projetista obtenha o resultado esperado. public class CalcDez implements Runnable { private int vetInt[]; public CalcDez () {vetInt=new int[10]; } public void run() { if (Thread.currentThread().getName().equals(”um”)) for (;;) { vetInt[0] = (int)(Math.random() * 1000); for (int i=1;i<10;i++) vetInt[i]= vetInt[0]+i; } else for (;;) { System.out.println(“Serie iniciada por”+ vetInt[0]); for (int i=1;i<10;i++) System.out.println(vetInt[i]+ “ “); } } public static void main(String args[]) { Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 23 CalcDez ob = new CalcDez(); Thread t1 = new Thread(ob,”um”); Thread t2 = new Thread(ob,”dois”); t1.start(); t2.start(); } } Exemplo IX.8 – Dois threads executando sobre o mesmo objeto. Se a máquina virtual não implementar um escalonamento Round-Robin apenas um thread será executado, visto que os dois threads possuem a mesma prioridade. Já no caso da máquina virtual implementar um escalonamento RoundRobin a alternância da execução dos threads produzirá resultados imprevisíveis. Um trecho de uma das saídas possíveis pode ser visto na figura IX.4. Ele foi obtido em Pentium 100MHz executando a máquina virtual da Sun, versão 1.2, sob o sistema operacional MSWindows 95. 258 259 Serie iniciada por573 574 575 576 577 578 579 580 581 582 Serie iniciada por80 81 82 Figura IX.4 – Saída do exemplo IX.8. Podemos notar as sequências estão misturadas, mostrando que cada thread interrompe o outro no meio da execução da tarefa especificada. O mesmo problema pode mesmo em threads que executam sobre objetos diferentes, bastando que cada thread possua referência para um mesmo objeto. O exemplo IX.9 mostra a execução de dois threads sobre objetos distintos. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II class Compartilhada { private int vetInt[]; public Compartilhada() {vetInt=new int[10];} public void setVal() { for (;;) { vetInt[0] = (int)(Math.random() * 1000); for (int i=1;i<10;i++) vetInt[i]= vetInt[0]+i; } } public int getVal(int i) {return vetInt [i];} } public class CalcDez2 extends Thread { private Compartilhada obj; private int tipo; public CalcDez2 (Compartilhada aObj, int aTipo) { obj = aObj; tipo = aTipo;} public void run() { for (;;) if (tipo==1) obj.setVal(); else { System.out.println(“Serie iniciada por”+ obj.getVal(0)); for (int i=1;i<10;i++) System.out.println(obj.getVal(i)+ “ “); } } public static void main(String args[]) { Compartilhada obj = new Compartilhada(); CalcDez2 t1 = new CalcDez2(obj,1); CalcDez2 t2 = new CalcDez2(obj,2); t1.start(); t2.start(); } } Exemplo IX.9 – Dois threads executando sobre objetos distintos. Alcione de P. Oliveira, Vinícius V. Maciel - UFV 24 Java na Prática – Volume II 25 É importante que o leitor não confunda o exemplo IX.9 com o exemplo IX.8 achando que nos dois exemplos os dois threads executam sobre o mesmo objeto, uma vez que a etapa da criação dos threads é bem parecida. No entanto, no exemplo IX.9 foi declarada uma subclasse da classe Thread e não uma classe que implementa a interface Runnable. Apesar de parecer que no exemplo IX.9 ambos os threads executaram sobre um mesmo objeto da classe Compartilhada que é passado como argumento, na verdade cada thread executará sobre sua própria instância da classe CalcDez2, sendo que o objeto da classe Compartilhada é referenciado pelos dois threads. O comportamento do código do exemplo IX.9 é semelhante ao do exemplo IX.8, com a diferença que no primeiro a sequência de inteiros é encapsulado pelo objeto da classe Compartilhada. Este tipo de situação, onde o resultado de uma computação depende da forma como os threads são escalonados, é chamada de condições de corrida (Race Conditions). É um problema a ser evitado uma vez que o programa passa a ter um comportamento não determinístico. Atomicidade de Instruções e Sincronização do Acesso à Sessões Críticas A condição de corrida ocorre porque os acesso à áreas de memória compartilhada não é feita de forma atômica, e nem de forma exclusiva. Por forma atômica queremos dizer que o acesso é feito por meio de várias instruções e pode ser interrompido por outro thread antes que toda as instruções que compõem o acesso sejam executadas. Por forma exclusiva queremos dizer que um thread podem consultar/atualizar um objeto durante a consulta/atualização do mesmo objeto por outros threads. Poucas operações são atômicas em Java. Em geral, as atribuições simples, com exceção dos tipos long e double, são atômicas, de forma que o programador não precisa se preocupar em ser interrompido no meio de uma operação de atribuição. No entanto, no caso de operações mais complexas sobre variáveis compartilhadas é preciso que o programador garanta o acesso exclusivo a essas variáveis. Os trechos de código onde é feito o acesso às variáveis compartilhadas são chamados de Seções Críticas ou Regiões Críticas. Uma vez determinada uma região crítica como garantir o acesso exclusivo? A linguagem Java permite que o programador garanta o acesso exclusivo por meio utilizando o conceito de monitor. O conceito de monitor foi proposto por C. A. R. Hoare em 1974 e pode ser encarado como um objeto que Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 26 garante a exclusão mútua na execução dos procedimentos a ele associados. Ou seja, apenas um procedimento associado ao monitor pode ser executado em um determinado momento. Por exemplo, suponha que dois procedimentos A e B estão associados a um monitor. Se no momento da invocação do procedimento A algum o procedimento B estiver sendo executando o processo ou thread que invocou o procedimento A fica suspenso até o término da execução do procedimento B. Ao término do procedimento B o processo que invocou o procedimento A é “acordado” e sua execução retomada. O uso de monitores em Java é uma variação do proposto por Hoare. na linguagem Java todo objeto possui um monitor associado. Para facilitar o entendimento podemos encarar o monitor como um detentor de um “passe”. Todo thread pode pedir “emprestado” o passe ao monitor de um objeto antes de realizar alguma computação. Como o monitor possui apenas um passe, apenas um thread pode adquirir o passe em um determinado instante. O passe tem que ser devolvido para o monitor para possibilitar o empréstimo do passe a outro thread. A figura IX.5 ilustra essa analogia. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II Monitor de x Monitor de x Objeto x Objeto x ! passe 27 ! thread t1 thread t2 Instante 1: o thread t1 solicita o passe ao monitor do objeto x. Instante 2: o thread t2 solicita o passe ao monitor do objeto x e é bloqueado. Monitor de x Monitor de x Objeto x Objeto x ! ! Instante 3: o thread t1 libera o passe. Instante 4: o thread t2 recebe o passe do monitor do objeto x. Figura IX.5 – Uma possível sequência na disputa de dois threads pela autorização de um monitor. Nos resta saber como solicitar o passe ao monitor. Isto é feito por meio da palavra chave synchronized. Existem duas formas de se usar a palavra chave synchronized: na declaração de métodos e no início de blocos. O exemplo IX.10 mostra duas versões da classe FilaCirc que implementa uma fila circular de valores inteiros: uma com métodos synchronized e outra com blocos synchronized. Um objeto desta classe pode ser compartilhado por dois ou mais threads para implementar o exemplo clássico de concorrência do tipo produtor/consumidor. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II a) Versão com métodos synchronized b) Versão com blocos synchronized class FilaCirc { private final int TAM = 10; private int vetInt[]; private int inicio, total; class FilaCirc { private final int TAM = 10; private int vetInt[]; private int inicio, total; public FilaCirc() { vetInt=new int[TAM]; inicio=0; total =0; } public synchronized void addElement(int v) throws Exception { if (total == TAM) throw new Exception("Fila cheia!"); vetInt[(inicio+total)%TAM] = v; total++; } public synchronized int getElement() throws Exception { if (total == 0 ) throw new Exception("Fila vazia!"); int temp = vetInt[inicio]; inicio = (++inicio)%TAM; total--; return temp; } 28 public FilaCirc() { vetInt=new int[TAM]; inicio=0; total =0; } public void addElement(int v) throws Exception { synchronized(this) { if (total == TAM) throw new Exception("Fila cheia!"); vetInt[(inicio+total)%TAM] = v; total++; } } public int getElement() throws Exception { synchronized(this) { if (total == 0 ) throw new Exception("Fila vazia!"); int temp = vetInt[inicio]; inicio = (++inicio)%TAM; total--; } return temp; } } } Exemplo IX.10 – Duas versões de uma classe que implementa uma fila circular de inteiros. A palavra chave synchronized na frente dos métodos de instância significa que o método será executado se puder adquirir o monitor do objeto a quem pertence o método1. Caso contrário o thread que invocou o método será suspenso até que possa adquirir o monitor. Este forma de sincronização é abordada no exemplo IX.10.a. Portanto, se algum thread chamar algum método de um objeto da classe FilaCirc nenhum outro thread que compartilha o mesmo objeto poderá executar um método do objeto até que o método chamado 1 Não usaremos mais a analogia com a aquisição do passe do monitor. Ela foi usada apenas para facilitar o entendimento do leitor. Quando se trata de monitores os termos mais usados são: “adquirir o monitor” e “liberar o monitor”. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 29 pelo primeiro thread termine. Caso outro thread invoque um método do mesmo objeto ficará bloqueado até que possa adquirir o monitor. O leitor pode estar se perguntando sobre a necessidade de sincronizar os métodos da classe FilaCirc uma vez que ocorrem apenas atribuições simples a elementos individuais de um vetor e as atribuições de inteiros são atômicas. De fato o problema ocorre não na atribuição dos elementos e sim na indexação do array. Por exemplo, a instrução inicio = (++inicio)%TAM; do método getElement() não é atômica. Suponha que a os métodos da classe FilaCirc não são sincronizados e que as variáveis inicio e total possuem os valores 9 e 1 respectivamente. Suponha também que thread invocou o método getElement() e foi interrompido na linha de código mostrada acima após o incremento da variável inicio mas antes da conclusão da linha de código. Nesse caso o valor de inicio é 10. Se neste instante outro thread executar o método getElement() do mesmo objeto ocorrerá uma exceção IndexOutOfBoundsException ao atingir a linha de código int temp = vetInt[inicio]; Se alterarmos a linha de código para inicio = (inicio+1)%TAM; evitaremos a exceção, mas não evitaremos o problema de retornar mais de uma vez o mesmo elemento. Por exemplo, se um thread for interrompido no mesmo local do caso anterior, outro thread pode obter o mesmo elemento, uma vez que os valores de inicio e total não foram alterados. Na verdade o número de situações problemáticas, mesmo para esse exemplo pequeno, é enorme e perderíamos muito tempo se tentássemos descreve-las em sua totalidade. Em alguns casos pode ser indesejável sincronizar todo um método, ou pode-se desejar adquirir o monitor de outro objeto, diferente daquele a quem pertence o método. Isto pode ser feito usando a palavra chave synchronized na frente de blocos. Este forma de sincronização é mostrada no exemplo IX.10.b. Neste modo de usar a palavra-chave synchronized é necessário indicar o objeto do qual tentara-se adquirir o monitor. Caso o monitor seja adquirido o bloco é executado, caso contrário o thread é suspenso até que possa adquirir o monitor. O monitor é liberado no final do bloco. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 30 No exemplo IX.10.b o monitor usado na sincronização é o do próprio objeto do método, indicado pela palavra chave this. Qualquer outro objeto referenciável no contexto poderia ser usado. O que importa que os grupos de threads que possuem áreas de código que necessitam de exclusão mútua usem o mesmo objeto. No exemplo IX.10 não existe vantagem da forma de implementação a) sobre a forma de implementação b) ou vice-versa. Isso ocorre principalmente quando os métodos são muito pequenos ou não realizam computações muito complexas. No entanto, se o método for muito longo ou levar muito tempo para ser executado, sincronizar todo o método pode “travar” em demasia a execução da aplicação. Nesses casos, a sincronização somente das seções críticas é mais indicada. Outra vantagem da segunda forma de sincronização é a liberdade no uso de monitores qualquer objeto referenciável. Isto permite a implementação sincronizações mais complexas como veremos mais adiante. O exemplo IX.11 mostra como pode ser usado um objeto da classe FilaCirc. public class TestaFilaCirc extends Thread { private FilaCirc obj; private int tipo; public TestaFilaCirc (FilaCirc aObj, int aTipo) { obj = aObj; tipo = aTipo;} public void run() { for (;;) try { if (tipo==1){ int i = (int)(Math.random() * 1000); System.out.println("Elemento gerado:"+i); obj.addElement(i); } else System.out.println("Elemento obtido:"+obj.getElement()); } catch(Exception e) {System.out.println(e.getMessage());} } public static void main(String args[]) { FilaCirc obj = new FilaCirc(); TestaFilaCirc t1 = new TestaFilaCirc(obj,1); TestaFilaCirc t2 = new TestaFilaCirc(obj,2); t1.start(); t2.start(); } } Exemplo IX.11 – Uso da fila circular de inteiros. Um trecho possível da saída obtida na execução do programa do exemplo IX.11 seria o seguinte: ... Elemento obtido:154 Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 31 Elemento gerado:725 Fila vazia! Elemento gerado:801 Elemento obtido:725 Elemento gerado:204 Elemento obtido:801 ... É importante observar que o monitor em Java por si só não implementa a exclusão mútua. Ele é apenas um recurso que pode ser usado pelo programador para implementar o acesso exclusivo à variáveis compartilhadas. Cabe ao programador a responsabilidade pela uso adequado deste recurso. Por exemplo se o programador esquecer de sincronizar um bloco ou método que necessita de exclusão mútua, de nada adianta ter sincronizado os outros métodos ou blocos. O thread que executar o trecho não sincronizado não tentará adquirir o monitor, e portanto de nada adianta os outros threads terem o adquirido. Outro ponto que é importante chamar a atenção é ter o cuidado de usar a palavra chave synchronized com muito cuidado. A sincronização custa muito caro em se tratando de ciclos de CPU. A chamada de um método sincronizado é por volta de 10 vezes mais lenta do que a chamada de um método não sincronizado. Por essa razão use sempre a seguinte regra: não sincronize o que não for preciso. Comunicação entre Threads: wait() e notify() O exemplo IX.10 não é um modelo de uma boa implementação de programa. O thread que adiciona elementos à fila tenta adicionar um elemento à cada volta do laço de iteração mesmo que a fila esteja cheia. Por outro lado, o thread que retira os elementos da fila tenta obter um elemento a cada volta do laço de iteração mesmo que a fila esteja vazia. Isto é um desperdício de tempo de processador e pode tornar o programa bastante ineficiente. Alguém poderia pensar em uma solução onde o thread testaria se a condição desejada para o processamento ocorre. Caso a condição não ocorra o thread poderia executar o método sleep() para ficar suspenso por algum tempo para depois testar novamente a condição. O thread procederia desta forma até que a condição fosse satisfeita. Este tipo de procedimento economizaria alguns ciclos de CPU, evitando que a tentativa incessante de executar o procedimento mesmo quando não há condições. O nome desta forma de ação, Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 32 onde o procedimento a cada intervalo de tempo pré-determinado testa se uma condição é satisfeita é chamado de espera ocupada (pooling ou busy wait). No entanto, existem alguns problemas com este tipo de abordagem. Primeiramente, apesar da economia de ciclos de CPU ainda existe a possibilidade de ineficiência, principalmente se o tempo não for bem ajustado. Se o tempo for muito curto ocorrerá vários testes inúteis. Se for muito longo, o thread ficará suspenso além do tempo necessário. Porém, mais grave que isto é que o método sleep() faz com que o thread libere o monitor. Portanto, se o trecho de código for uma região sincronizada, como é o caso do exemplo IX.10, de nada adiantará o thread ser suspenso. O thread que é capaz de realizar a computação que satisfaz a condição esperada pelo primeiro thread ficará impedido de entrar na região crítica, ocorrendo assim um deadlock: o thread que detém o monitor espera que a condição seja satisfeita e o thread que pode satisfazer a condição não pode prossegui porque não pode adquirir o monitor. O que precisamos é um tipo de comunicação entre threads que comunique que certas condições foram satisfeitas. Além disso, é preciso que, ao esperar por determinada condição, o thread libere o monitor. Esta forma de interação entre threads é obtido em Java com o uso dos métodos de instância wait(), notify() e notifyAll(). Como vimos anteriormente, esses métodos pertencem à classe Object e não à classe Thread. Isto ocorre porque esses métodos atuam sobre os monitores, que são objetos relacionados a cada instância de uma classe Java e não sobre os threads. Ao invocar o método wait() de um objeto o thread é suspenso e inserido em uma fila do monitor do objeto, permanecendo na fila até receber uma notificação. Cada monitor possui sua própria fila. Ao invocar o método notify() de um objeto, um thread que está na fila do monitor do objeto é notificado. Ao invocar o método notifyAll() de um objeto, todos os threads que estão na fila do monitor do objeto são notificados. A única exigência é que esses métodos sejam invocados em um thread que detenham a posse do monitor do objeto a que pertencem. Essa exigência faz sentido uma vez que eles sinalizam a threads que esperam na fila desses monitores. Devido a essa exigência a invocação desses métodos ocorre em métodos ou blocos sincronizados. O exemplo IX.12 mostra as formas mais comuns de chamadas desses métodos. Note que o thread deve possuir o monitor do objeto ao qual pertence o método. Por isso, nos exemplo IX.12 b e c, o objeto sincronizado no bloco é o mesmo que invoca os métodos notify() e notifyAll(). a) class X b) class Y Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 33 { { X ob; ... public int mb() { ... synchronized (ob) { // Notifica algum thread ob.notify(); ... } ... ... public synchronized int ma() { ... // Espera uma condição while(!cond) wait(); // Prossegue com a // condição satisfeita ... } ... } } c) class Z { X ob; ... public int mc() { ... synchronized (ob) { // Notifica todos os threads que esperam na fila // do monitor de ob ob.notifyAll(); ... } ... } Exemplo IX.12 – Exemplos de chamadas dos métodos wait(), notify() e notifyAll(). Outra observação importante é que o thread que invoca o método wait() o faz dentro de um laço sobre a condição de espera. Isto ocorre porque apesar de ter sido notificado isto não assegura que a condição está satisfeita. O thread pode ter sido notificado por outra razão ou entre a notificação e a retomada da execução do thread a condição pode ter sido novamente alterada. Uma vez notificado o thread não retoma imediatamente a execução. É preciso primeiro retomar a posse do monitor que no momento da notificação pertence ao thread que notificou. Mesmo após a liberação do monitor nada Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 34 garante que o thread notificado ganhe a posse do monitor. Outros threads podem ter solicitado a posse do monitor e terem preferência na sua obtenção. O exemplo IX.12 mostra apenas um esquema para uso dos métodos para notificação. O exemplo IX.13 é uma versão do exemplo IX.10a que usa os métodos de notificação para evitar problemas como a espera ocupada. O exemplo IX.11 pode ser usado sem modificações para testar essa versão. class FilaCirc { private final int TAM = 10; private int vetInt[]; private int inicio, total; public FilaCirc() { vetInt=new int[TAM]; inicio=0; total =0; } public synchronized void addElement(int v) throws Exception { while (total == TAM) wait(); vetInt[(inicio+total)%TAM] = v; total++; notify(); } public synchronized int getElement() throws Exception { while (total == 0 ) wait(); int temp = vetInt[inicio]; inicio = (++inicio)%TAM; total--; notify(); return temp; } } Exemplo IX.13 – Classe que implementa uma fila circular de inteiros com notificação. A necessidade de se testar a condição em loop pode ser observada na figura IX.6 que mostra a evolução da execução de três threads sobre objetos que compartilham uma instância da classe FilaCirc. O thread 3 executa o método addElement(), no entanto, em virtude da condição total==TAM é obrigado a invocar o método wait() e esperar uma notificação. O próximo Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 35 thread a assumir a CPU é o thread 1 que executa o método getElement(), que estabelece a condição total<TAM e executa um notify(). No entanto, o próximo thread a assumir a CPU é o thread 2 e não o thread 3. O thread 2 executa o método addElement(), o qual estabelece novamente a condição total==TAM. Quando o thread 3 assumi novamente a CPU, uma vez que foi notificado, testa a condição e invoca novamente o método wait() para esperar a condição favorável à execução. Caso não testasse a condição em um loop o thread 3 tentaria inserir um elemento em uma fila cheia. O método notify() não indica que evento ocorreu. No caso do exemplo IX.13 existem dois tipos de eventos (a fila não está cheia e a fila não está vazia), no entanto, podemos observar que não existe a possibilidade de um thread ser notificado em decorrência de um evento diferente do que está aguardando. Método: addElement() condição: total == TAM thread 3 thread 2 Método: addElement() condição: total < TAM thread 1 Método: getElement() Tempo Executando Esperando CPU Esperando notificação Figura IX.6 – Uma possível sequência na execução de três threads. Porém, existem alguns casos mais complexos onde podem existir vários threads aguardando em um mesmo monitor mas esperando por evento diferentes. Neste caso podemos usar o notifyAll() para notificar todos os threads que esperam em um único monitor que um evento ocorreu. Cada thread, a medida que fosse escalado, testaria se ocorreu condição para a execução e em caso positivo prosseguiria na execução e em caso contrário voltaria a aguardar no monitor. O exemplo IX.14 mostra o código de um gerenciador de mensagens. Ele é responsável por receber mensagens destinadas à vários threads. As mensagens Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 36 de cada thread são colocadas em uma fila implementada por um objeto da classe Vector. Cada fila é por sua vez colocada em uma tabela hash onde a chave é um nome associado ao thread a que as mensagens se destinam. As filas são criadas na primeira tentativa de acesso, tanto na leitura quanto no armazenamento. Não existe bloqueio devido à fila cheia, uma vez que as filas são implementadas por objetos da classe Vector que crescem conforme a necessidade. Portanto, o único evento que necessita ser notificado é a chegada de alguma mensagem. Como todos os threads aguardam sobre o mesmo monitor é usado o método notifyAll() para notificar todos os threads. import java.util.*; class GerenteMen { private Hashtable tamMen; public GerenteMen() {tamMen=new Hashtable(); } // Método para adicionar uma mensagem à fila de // um destinatário public synchronized void addMen(String dest, String men){ if (dest==null || men==null) return; Vector listaMen = (Vector) tamMen.get(dest); if (listaMen==null) listaMen = new Vector(); listaMen.addElement(men); tamMen.put(dest, listaMen); notifyAll(); } // Método para obtenção da mensagem public synchronized String getMen(String dest) throws Exception { if (dest==null) return null; Vector listaMen = (Vector) tamMen.get(dest); // Se não existe a fila para esse thread cria uma vazia if (listaMen==null) { listaMen = new Vector(); tamMen.put(dest, listaMen); } // A fila está vazia, portanto thread deve esperar // a chegada de mensagens while(listaMen.size()==0) wait(); String temp = (String) listaMen.firstElement(); Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 37 // A mensagem é removida da fila listaMen.removeElementAt(0); return temp; } } Exemplo IX.14 – Gerenciador de mensagens. O exemplo IX.15 mostra como pode ser usado o gerente de filas do exemplo IX.14. Devido o uso da classe ThreadGroup assim como vários de seus métodos, resolvemos numerar as linhas de código do exemplo IX.15 para melhor podermos explicar o seu funcionamento. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class Receptor extends Thread { private GerenteMen ger; public Receptor(ThreadGroup tg, String nome, GerenteMen aGer) { super(tg,nome); ger = aGer; } public void run() { String nome = Thread.currentThread().getName(); for (;;) try { String men = ger.getMen(nome); if (men.equals("fim")) return; System.out.println(nome+">Mensagem recebida:"+men); } catch(Exception e) {System.out.println(e.getMessage());} } } class Gerador extends Thread { private GerenteMen ger; public Gerador(ThreadGroup tg, String nome, GerenteMen aGer) { super(tg,nome); ger = aGer; } public void run() { String nome = Thread.currentThread().getName(); ThreadGroup tg = Thread.currentThread().getThreadGroup(); Thread[] tl=null; for (int i=0;i<100;i++) { if (tl==null || tl.length!=tg.activeCount()) tl= new Thread[tg.activeCount()]; tg.enumerate(tl); int n = (int)(Math.random() * 1000)%tl.length; if (tl[n]!= Thread.currentThread()) { System.out.println(nome+">Mensagem enviada para "+ tl[n].getName()+":mensagem "+i); ger.addMen(tl[n].getName(),"mensagem "+i); } Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 38 } tl= new Thread[tg.activeCount()]; tg.enumerate(tl); for (int i=0;i<tl.length;i++) if (tl[i]!= Thread.currentThread()) ger.addMen(tl[i].getName(),"fim"); } } public class TestaGerenteMen { public static void main(String args[])throws Exception { GerenteMen ger = new GerenteMen(); ThreadGroup tg = new ThreadGroup("tg"); Receptor r1 = new Receptor(tg,"r_um",ger); Receptor r2 = new Receptor(tg,"r_dois",ger); Gerador g = new Gerador(tg,"g",ger); r1.start(); r2.start(); g.start(); } } Exemplo IX.15 – Uso do gerenciador de filas. Um objeto da classe ThreadGroup agrupa um conjunto de threads. Um ThreadGroup pode possuir como membros outros objetos da ThreadGroup formando assim uma árvore onde todos os grupos, exceto o primeiro possui um grupo pai. O objetivo de se agrupar os threads em conjuntos é facilitar a sua manipulação. No caso do exemplo IX.15 usaremos esse agrupamento para poder acessar cada thread. As linhas 1 a 18 definem a classe que será usada para criação de objetos receptores de mensagens. Na linha 3 é declarada a variável que irá referenciar um objeto do tipo GerenteMen. As linhas 4 a 8 contém o código do único construtor da classe. Ele recebe uma referência para o grupo de threads ao qual deve se associar, o nome que deve ser atribuído ao thread e a referência ao gerente de filas. Na linha 6 os primeiros dois parâmetros são passados ao construtor da superclasse. Na linha 7 a referência ao gerente de filas é atribuída à variável da instância. As linhas 9 a 17 contém o código do método run() que é o método de entrada do thread. Na linha 10 é invocado o método Thread.currentThread().getName(); para se obter o nome do thread corrente. O nome do thread é usado para referenciar a fila de mensagens do thread. Entre as linhas 11 e 16 é executado um laço infinito onde o thread recebe e imprime as mensagens recebidas. Na linha 14 o thread testa se a mensagem recebida é igual a “fim”. Neste caso o thread encerra sua execução. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 39 As linhas 20 a 51 definem a classe que será usada para criação do objeto gerador de mensagens. Este exemplo foi projetado para lidar com apenas um thread gerador de mensagem. Modificações devem ser realizadas para tratar de aplicações com mais de um thread gerador de mensagens. Na linha 22 é declarada a variável que irá referenciar um objeto do tipo GerenteMen. As linhas 23 a 27 contém o código do único construtor da classe. Ele recebe uma referência para o grupo de threads ao qual deve se associar, o nome que deve ser atribuído ao thread e a referência ao gerente de filas. Na linha 25 os primeiros dois parâmetros são passados ao construtor da superclasse. Na linha 26 a referência ao gerente de filas é atribuída à variável da instância. As linhas 28 a 50 contém o código do método run() que é o método de entrada do thread. Na linha 29 é obtido o nome do thread corrente que será usado na impressão de mensagens. Na linha 30 o método Thread.currentThread().getThreadGroup(); obtém uma referência para o grupo de threads ao qual pertence o thread corrente. Na linha 31 é declarada uma variável que irá referenciar um vetor contendo referências a todos os threads ativos do grupo. Entre as linhas 32 e 44 é executado um laço com 100 iterações que produz e armazena as mensagens. Na linha 34 é realizado um teste para a verificação da necessidade de criar o vetor que irá conter as referências para os threads ativos. Ele deve ser criado a primeira vez e toda vez que a capacidade do vetor for diferente do número de threads ativos do grupo. O tamanho do vetor é determinado pelo método de instância activeCount() da classe ThreadGroup. A linha 36 contém o código tg.enumerate(tl); que atribui as referencias aos threads no vetor. O comando int n = (int)(Math.random()*1000)%tl.length; da linha 37 calcula um número que será usado para acessar o thread dentro do vetor de referências. O teste da linha 38 impede que seja enviada uma mensagem para o próprio gerador. Essas mensagens são descartadas. As linhas 40 a 42 tratam da impressão e envio da mensagem construída. Já fora da iteração, as linhas 45 a 49 tratam da do envio da mensagem “fim” para todos os threads receptores, o que fará com que encerrem sua execução. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 40 As linhas 53 a 64 definem a classe que será usada como ponto de entrada da aplicação. Ela é responsável pela criação dos objetos e disparos dos threads. No exemplo, além do thread gerador apenas dois threads receptores são criados. É interessante notar que não é preciso indicara para o thread gerador as referências para os threads receptores. Elas são obtidas dinamicamente por meio do grupo de threads. Um trecho possível da saída obtida na execução do programa do exemplo IX.15 seria o seguinte: ... g>Mensagem enviada para r_dois:mensagem 88 r_um>Mensagem recebida:mensagem 87 g>Mensagem enviada para r_dois:mensagem 90 r_dois>Mensagem recebida:mensagem 88 g>Mensagem enviada para r_um:mensagem 91 r_dois>Mensagem recebida:mensagem 90 g>Mensagem enviada para r_um:mensagem 93 r_um>Mensagem recebida:mensagem 91 g>Mensagem enviada para r_um:mensagem 95 r_um>Mensagem recebida:mensagem 93 g>Mensagem enviada para r_um:mensagem 96 r_um>Mensagem recebida:mensagem 95 g>Mensagem enviada para r_um:mensagem 97 r_um>Mensagem recebida:mensagem 96 g>Mensagem enviada para r_dois:mensagem 99 r_um>Mensagem recebida:mensagem 97 r_dois>Mensagem recebida:mensagem 99 Pressione qualquer tecla para continuar . . . Otimizando a Programação Multithread Existe um problema óbvio com a abordagem do exemplo IX.14: a mensagem é dirigida a apenas um thread mas todos serão notificados, sobrecarregando o sistema, uma vez que todos os threads precisaram testar se a mensagem é destinada a eles. Para contornar esses problema é necessário vislumbrar uma forma de notificar apenas o thread destinatário. Essa solução pode ser obtida se cada thread esperar em um monitor de um objeto diferente. Não importa o tipo do objeto desde que seja referenciável pelo thread receptor e pelo thread que irá armazenar a mensagem. Um candidato natural é a fila de mensagem de cada thread. Existe uma fila para cada thread e o Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 41 thread que armazena a mensagem tem acesso a todas a filas por meio da tabela hash. O exemplo IX.16 mostra uma versão do exemplo IX.14 que utiliza esta técnica para criar uma aplicação multi-thread mais otimizada. import java.util.*; class GerenteMen { private Hashtable tamMen; public GerenteMen() {tamMen=new Hashtable(); } // Método para adicionar uma mensagem à fila de // um destinatário public void addMen(String dest, String men){ if (dest==null || men==null) return; Vector listaMen = (Vector) tamMen.get(dest); if (listaMen==null) listaMen = new Vector(); synchronized (listaMen) { listaMen.addElement(men); tamMen.put(dest, listaMen); listaMen.notify(); }; } // Método para obtenção da mensagem public String getMen(String dest) throws Exception { if (dest==null) return null; Vector listaMen = (Vector) tamMen.get(dest); // Se não existe a fila para esse thread cria uma vazia if (listaMen==null) { listaMen = new Vector(); tamMen.put(dest, listaMen); } // A fila está vazia, portanto thread deve esperar while(listaMen.size()==0) synchronized (listaMen) {listaMen.wait();} String temp = (String) listaMen.firstElement(); // A mensagem é removida da fila listaMen.removeElementAt(0); return temp; } } Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 42 Exemplo IX.16 – Gerenciador de mensagens otimizado. Note que os métodos wait() e notify() invocados pertencem à fila relacionada com cada thread. O exemplo IX.15 pode ser usado sem modificações para testar essa versão. Criando outros mecanismos de sincronização Existem várias propostas de primitivas de sincronização. Dentre as mais comuns podemos citar os semáforos, mutex, variáveis condicionais, monitores e encontros (rendevouz). Cada uma dessas primitivas é mais adequada a um determinado propósito. A implementação de monitores na linguagem Java, juntamente com os métodos wait() e notify() que formam um tipo de variáveis condicionais podem ser combinadas para implementar muitas dessas outras primitivas, de modo a atender objetivos específicos. Para exemplificar essa possibilidade mostraremos como implementar um semáforo usando as primitivas de sincronização da linguagem Java. Um semáforo é uma variável inteira sobre a qual pode-se realizar as seguintes operações: Operação inicializar P V Descrição Um valor inteiro maior ou igual a zero é atribuído ao semáforo. Se o semáforo é maior que zero, o semáforo é decrementado. Caso contrário, o thread é suspenso até que o semáforo contenha um valor maior que zero. Incrementa o semáforo e acorda os threads que estiverem bloqueados na fila de espera do semáforo. Tabela IX.6 –Operações sobre um semáforo. Semáforo é um mecanismo de sincronização muito utilizado quando existe a necessidade de comunicação entre dois ou mais processos, como no caso de sistemas do tipo produtor/consumidor. Por exemplo, suponha dois processos onde um coloca mensagens em buffer e outro retira as mensagens. Os processos podem usar dois semáforos para sincronizar o acesso ao buffer de mensagens: um para controlar a entrada na região crítica e outro para contar o número de mensagens. A figura IX.xx mostra os esquemas dos processos. A implementação dos semáforos em Java pode ser visto no exemplo IX.xx e o uso dos semáforos Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 43 em uma situação como a ilustrada pela figura IX.xx pode ser visto no exemplo IX.xx. Produtor Consumidor s=1; n=0; início loop início loop Produz mensagem P(n) // Verifica se existe mensagens P(s) // Verifica se pode entrar na região P(s) // Verifica se pode entrar na região // crítica //crítica Coloca mensagem no buffer Retira mensagem V(n) // incrementa no, de mensagens V(s) // sai da região crítica V(s) // sai da região crítica Consome Mensagem fim loop fim loop Figura IX.xx – Comunicação entre processos usando semáforos. public class Semaforo { private int cont; public Semaforo(){cont =0;} public Semaforo(int i){cont =i;} public synchronized void P() throws InterruptedException { while(cont <=0) this.wait(); cont--; } public synchronized void V() { cont++; notifyAll(); } } Exemplo IX.XX – Implementação de um Semáforo. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II import java.util.Vector; class Consumidor extends Thread { private Vector buff; private Semaforo s,n; public Consumidor(Vector aBuff, Semaforo as, Semaforo an) { super(); buff = aBuff; s = as; n = an; } public void run() { for (;;) try { n.p(); // Verifica se existe mensagens s.p(); // Verifica se pode entrar na região crítica String men = (String)buff.firstElement(); buff.removeElementAt(0); s.v(); if (men.equals("fim")) return; System.out.println("Mensagem recebida:"+men); } catch(Exception e) {System.out.println(e.getMessage());} } } class Produtor extends Thread { private Vector buff; private Semaforo s,n; public Produtor(Vector aBuff, Semaforo as, Semaforo an) { super(); buff = aBuff; s = as; n = an; } public void run() { for (int i=0;i<11;i++) { try { s.p();// Verifica se pode entrar na região crítica if (i<10) { buff.addElement(""+i); Alcione de P. Oliveira, Vinícius V. Maciel - UFV 44 Java na Prática – Volume II System.out.println("Mensagem enviada: "+ i); } else buff.addElement("fim"); n.v(); // incrementa o número de mensagens s.v(); // abandona a região crítica Thread.yield(); } catch(Exception e) {System.out.println(e.getMessage());} } } } public class TestaSemaforo { public static void main(String args[])throws Exception { Vector buff = new Vector(); Semaforo s = new Semaforo(1); Semaforo n = new Semaforo(0); Produtor t1 = new Produtor(buff,s,n); Consumidor t2 = new Consumidor (buff,s,n); t1.start(); t2.start(); } } Exemplo IX.XX – Uso de semáforos por dois threads. Alcione de P. Oliveira, Vinícius V. Maciel - UFV 45 Java na Prática – Volume II 46 Capítulo II - Animação Animação é exibir uma figura que muda com o tempo. No momento o suporte a animação da API central do Java é limitado. Espera-se para o final de 1997 uma API que dê suporte avançado para animação. A animação pode ser controlado por um thread que é executado em um certo intervalo pré-definido. Exemplo básico de animação “in-place”. import java.awt.*; import java.applet.Applet; public class exemplo10 extends Applet implements { Image imgs[]; int ind=0; Thread t1; Runnable public void init() {imgs = initImgs(); t1=new Thread(this); t1.start();} public void paint(Graphics g) {g.draw.Image(imgs[ind],0,0,this);} public void start() { if (t1 == null) { t1 = new Thread(this); t1.start();} } public void stop() { if (t1 != null) {t1.stop();t1 = null;} } public void run() { while (true){ try {Thread.sleep(100};} catch(InterruptedException ex){} repaint(); ind=++ind % imgs.length; } } } Problemas com o exemplo Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 47 Ele não permite interromper a animação. O repaint() chama o update() default que repinta todo o fundo, o que causa “flicker” na animação. Existe um problema relacionado com a integridade da variável ind. A variável é atualizada pelo Thread t1 (run) e lida pelo Thread update (paint). permitir interromper a animação. boolean pause = false; public boolean mouseDown(Event e, int x, int y) { if (pause) {t1.resume();} {t1.suspend();} else pause = !pause; return true; } Eliminar o “flicker” Default public void update(Graphics g) { g.setColor(getBackground()); g.fillRect(0,0,width, height); g.setColor(getForeground()); paint(g); } Mudança public void update(Graphics g) {paint(g);} Eliminando conflitos public synchronized void paint(Graphics g) { g.draw.Image(imgs[ind],0,0,this); } public synchronized void mudaInd() Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 48 { ind = ++ind % imgs.length; } Copiando a figura public void drawStickFigure (Graphics g, int nX, { g.drawOval (nX + 10, nY + 20, 20, 40); g.drawLine (nX + 20, nY + 60, nX + 20, nY g.drawLine (nX + 10, nY + 70, nX + 30, nY g.drawLine (nX + 10, nY + 150, nX + 20, nY g.drawLine (nX + 20, nY + 100, nX + 30, nY } public void paint (Graphics g, Applet Parent) { if (bFirstTime) { bFirstTime = false; drawStickFigure (g, nX, nY); } else { g.copyArea (nX, nY, 35, 155, 5, 0);} } int nY) + 100); + 70); + 100); + 150); Double-buffer offScreenImage = createImage (nWidth, nHeight); offScreenGraphic = offScreenImage.getGraphics(); ... offScreenGraphic.setColor (Color.lightGray); offScreenGraphic.fillRect (0, 0,nWidth, nHeight); offScreenGraphic.setColor (Color.black); ... offScreenGraphic. drawOval(10,10,20,20); ... g.drawImage (offScreenImage, 0, 0, this); Ticker-Tape class TextScrolling extends AnimationObject { String pcMessage; // The message int nXPos; // The location of the message int nYPos; // The location of the message int nAppletWidth; // The width of the applet int nMessageWidth; // The width of the message public TextScrolling (String pcMsg, int nWide) Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 49 { pcMessage = pcMsg; nAppletWidth = nWide; nMessageWidth = -1; nYPos = -1; nXPos = 0; } public void paint (Graphics g, Applet parent) { if (nYPos < 0) { nYPos = (g.getFontMetrics ()).getHeight (); char pcChars []; pcChars = new char [pcMessage.length() + 2]; pcMessage.getChars(0, pcMessage.length()- 1, pcChars, 0); nMessageWidth = (g.getFontMetrics ()).charsWidth (pcChars, 0, pcMessage.length()); } g.drawString (pcMessage, nXPos, nYPos); } public void clockTick () { if (nMessageWidth < 0) return; // Move Right nXPos -= 10; if (nXPos < -nMessageWidth) nXPos = nAppletWidth - 10; } public void run() { int ndx = 0; Thread.currentThread().setPriority (Thread.MIN_PRIORITY); while (size().width > 0 && size().height > 0 && kicker != null) { AnimatedObjects[0].clockTick (); repaint(); try {Thread.sleep(nSpeed);} catch (InterruptedException e){} } } Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 50 Capítulo III - Programação em rede Diferentemente das linguagens mais populares atualmente, Java foi projetada na era da Internet, e por isso mesmo ferramentas para comunicação dentro da Grande Rede foram incorporadas à linguagem desde a sua concepção. Classes para manipulação de URLs e dos protocolos que constituem a Internet fazem parte do núcleo básico da linguagem. Isto facilita muito a tarefa de desenvolver aplicações que para a Internet ou outras redes que fazem uso do mesmo conjunto de protocolos. Esta é uma das principais forças da linguagem Java. De modo a entendermos como desenvolver aplicações em rede com Java é importante o compreensão de alguns conceitos básicos sobre protocolos de comunicação. Conceitos Sobre Protocolos Usados na Internet Um protocolo de comunicação é um conjunto de formatos e regras usadas para transmitir informação. Computadores distintos devem obedecer estas regras e formatos de modo que se possam comunicar. Podemos encarar o protocolo como a definição de uma linguagem comum de modo a possibilitar a comunicação entre diferentes entidades. Visando diminuir a complexidade de implementação e uso do protocolo, ele é divido e organizado em forma de camadas de protocolos, onde a camada relativamente inferior na pilha a outra estabelece as regras para a camada superior sobre a utilização de seus serviços. As camadas inferiores fornecem serviços mais básicos de transmissão de dados, enquanto que as camadas superiores oferecem serviços de mais alto nível. Esta forma de organização hierárquica de protocolos é também chamada de pilha de protocolos. A principal pilha de protocolo sobre o qual a Internet se organiza é o TCP/IP. Por simplicidade chamaremos a pilha de protocolos TCP/IP apenas como protocolo TCP/IP ou TCP/IP. A figura XI.1 mostra como se organizam alguns dos protocolos que fazem parte do TCP/IP. A camada física é a responsável pela transporte efetivo dos dados sobre o meio físico. A camada de rede é responsável pela interface lógica entre os computadores. A camada de transporte provê transferência de dados a nível de Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 51 serviço, e a camada de aplicação provê comunicação a nível de processos ou aplicações. Exemplos de protocolos a nível de aplicação são: FTP, usado para transferência de arquivos; HTTP, usado para transmissão de páginas Web; TELNET provê capacidade de log-on remoto; SMTP provê serviços básicos de correio eletrônico; SNMP usado para gerência da rede; e MIME que é uma extensão do SMTP para lidar com mensagens com conteúdos diversos. MIME Camada de Aplicação FTP HTTP Camada de Transporte SMTP TCP Camada de rede SNMP UDP IP Camada física FTP HTTP IP MIME TELNET Ethernet, X.25, Token Ring - File Transfer Protocol - Hypertext Transfer Protocol - Internet Protocol - Multi-purpose Internet Mail Extensions UDP SMTP - Simple Mail Transfer Protocol SNMP - Simple Network Management Protocol TCP - Transmission Control Protocol - User Datagram Protocol Figura XI.1 –Alguns protocolos da pilha TCP/IP. No processo de transmissão de dados sobre uma rede TCP/IP os dados são divididos em grupos chamados de pacotes. Cada camada adiciona um alguns dados a mais no início de cada pacote para permitir que o pacote chegue ao destino. Os dados adicionados são chamados de headers. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II Dados 52 Aplicação Transporte TCP Header Dados IP Header TCP Header Dados Ethernet Header IP Header TCP Header Rede Dados Física Figura XI.2 –Headers adicionados a cada camada de protocolo. Na camada de transporte existem dois protocolos que fazem uso do protocolo IP: o protocolo TCP/IP e o UDP. TCP O protocolo TCP é um protocolo orientado a conexão que provê um fluxo confiável de dados entre dois computadores. Por protocolo orientado a conexão queremos dizer que é estabelecido um canal de comunicação ponto-aponto onde os dados podem trafegar em ambas as direções. O TCP garante que os dados enviados em uma ponta cheguem ao destino, na mesma ordem que foram enviados. Caso contrário, um erro é reportado. Protocolos como HTTP, FTP e TELNET exigem um canal de comunicação confiável e a ordem de recebimento dos dados é fundamental para o sucesso dessas aplicações. UDP No entanto, nem todas as aplicações necessitam destas características do protocolo TCP e o processamento adicional exigido para garantir a confiabilidade e a ordenação dos dados podem inviabilizá-las. Para esses casos existe o protocolo de transporte UDP. UDP é um protocolo para envio de pacotes independentes de dados, chamados de datagramas, de um computador a outro, sem garantias sobre a chegada dos pacotes. O protocolo UDP não é orientado a conexão. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 53 IDENTIFICAÇÃO DE HOSTS (Número IP) Cada computador conectado a uma rede TCP/IP é chamado de Host e é identificado por um único número de 32 bits, denominado de número IP. O número IP é representado por quatro grupos de 8 bits, limitando desta forma o valor numérico de cada grupo ao valor máximo de 255. Um exemplo de número IP é 200.65.18.70. Uma vez que é muito difícil lembrar e atribuir significado a números existe uma forma alternativa de identificar os computadores da rede por meio de nomes. Um ou mais computadores da rede fazem o papel de resolvedores de nomes, mantendo bases de dados que associam o nomes do Hosts à seus números IP. Desta forma é possível um computador comunicar com outro computador por meio do nome lógico e não por meio do número IP. A figura XI.3 ilustra a comunicação entre dois computadores. A Internet é representada como uma nuvem devido a complexidade da rede. Host meucomp.com.br IP: 200.18.46.12 Host outrocomp.edu IP: 205.50.30.75 Figura XI.3 –Representação da comunicação entre dois computadores. O representação dos nomes dos computadores na Internet é feita por substrings separadas por ‘.’ e obedecem uma regra de nomeação que define que o primeiro substring representa o nome da máquina, e os restantes representa o domínio onde está inserida a máquina. Um domínio é um agrupamento de computadores que pertencem a uma instituição, órgão, empresa, ou uma organização qualquer. Assim, no exemplo da figura XI.3 o computador meucomp pertence ao domínio com.br. No Brasil a FAPESP (Fundação de Amparo à Pesquisa do Estado de São Paulo) responsável pela gerência dos nomes dos domínios na Internet. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 54 Identificação de Processos (Portas) A comunicação entre dois processos em uma rede TCP/IP é assimétrica, no sentido um processo faz o papel de servidor, oferecendo um serviço e outro faz o papel de cliente do serviço. Em um único Host vários processos podem estar fazendo o papel de servidor, oferecendo serviços através de um único meio físico. Portanto, é preciso uma forma de identificar as servidores em um mesmo Host. Isto é feito por meio da associação de um número inteiro, chamado de porta, ao processo servidor. Essa associação é feita pelo processo assim que é carregado, por meio de uma chamada ao sistema. O número da porta pode variar de 1 a 65535, no entanto os números de 1 a 1023 são reservados para serviços conhecidos como FTP e HTTP. O programador não deve usar estas portas a não ser que esteja implementando algum desses serviços. Nos ambientes Unix as portas que vão de 6000 a 6999 são usadas pelo gerenciador de Interfaces X Windows e 2000 a 2999 por outros serviços, como o NFS. Nestes ambientes, estas faixas de números de portas também devem ser evitadas. A tabela XI.1 mostra o número da porta de alguns dos serviços mais conhecidos. Protocolo HTTP echo FTP SMTP Finger Daytime pop3 Porta 80 7 20,21 25 79 13 110 Tabela XI.1 – Número das portas dos principais serviços. Uma vez associado a uma porta o serviço pode ser acessado por uma aplicação cliente, bastando para isso que ela indique o nome do Host e o número da porta ao se comunicar. Programação em Rede com Java O pacote java.net contém as classes e interfaces usadas para programação de sistemas em rede com Java. As classes podem ser enquadradas em três categorias: Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 55 1. Classes para comunicação básica em rede. Tratam da comunicação em baixo nível entre aplicações. Outros protocolos podem ser implementados usando como base esta comunicação básica. 2. Classes para comunicação dentro da Web. Estas classes provêem facilidades para acessar conteúdos por meio de URLs. 3. Classes para tratamento dos formatos estendidos da Web. Utilizadas para tratar novos protocolos e tipos MIME. Comunicação Básica Entre Aplicações As classes Socket, ServerSocket, DatagramSocket, DatagramPacket e InetAddress, fornecem os métodos necessários para a comunicação básica entre dois processos. A tabela XI.2 descreve sucintamente cada uma das classes. Classe Socket Descrição Provê um socket cliente para comunicação orientada à conexão via protocolo TCP. ServerSocket Provê um socket servidor para comunicação orientada à conexão via protocolo TCP. DatagramSocket Provê um socket UDP para comunicação não orientada à conexão. DatagramPacket Representa um datagrama que pode ser enviado usando DatagramSocket. InetAddress Representa os dados de um Host (Nome e endereço IP) Tabela XI.2 – Classes para comunicação básica. As classes Socket e ServerSocket são utilizadas para comunicação orientada à conexão, enquanto que as classes DatagramSocket e DatagramPacket são utilizadas para comunicação não orientada à conexão. Comunicação orientada à conexão (cliente) Para comunicar via protocolo TCP é preciso que a aplicação cliente crie um objeto Socket. É preciso passar o nome ou número IP do Host e o número da porta onde o servidor está esperando as solicitações de serviço. A Classe Socket getInputStream() possui os métodos e Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 56 getOutputStream(), que são usados para obter Streams associados ao Socket. Deste modo, a transmissão de dados via Socket é idêntica à leitura e escrita em arquivos via Streams. O exemplo XI.1 mostra o código de um cliente que acessa um servidor de Daytime. O serviço de Daytime é disponibilizado nas plataformas UNIX e é acessado via porta 13. Sua função enviar, aos processos clientes, uma linha de texto contendo a data e a hora corrente . import java.io.*; import java.net.*; public class ClienteData { public static void main(String[] args) throws IOException { Socket socket = null; BufferedReader in = null; try { socket = new Socket(args[0], 13); in = new BufferedReader(new InputStreamReader( socket.getInputStream())); }catch (UnknownHostException e) {System.err.println("Não achou o host:"+args[0]); System.exit(1);} catch (IOException e) {System.err.println("Erro de I/O."+e.getMessage()); System.exit(1);} System.out.println("Data: " + in.readLine()); } in.close(); socket.close(); } } Exemplo XI.1 – Cliente para o serviço de Daytime. No programa do exemplo XI.1 o usuário precisa passar o nome do Host servidor pela linha de comando. Você pode testar este programa mesmo que seu computador não esteja conectado em uma rede, desde que o protocolo TCP/IP esteja instalado. É que você pode passar como parâmetro o nome do seu computador ou, alternativamente, o nome localhost, ainda o número IP 127.0.0.0. O nome localhost e o número IP127.0.0.0 sempre identificam o computador local. Note que após a criação do objeto Socket, o método getInputStream() é chamado e o objeto retornado é envolvido por um Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 57 objeto BufferedReader de modo a comunicar dados via rede da mesma forma que é realizada uma operação de E/S. Ao se terminar a operação é preciso fechar tanto a instância BufferedReader quanto o objeto Socket. A instância da Classe de E/S sempre deve ser fechada primeiro. Os Hosts que utilizam as variações do sistema operacional UNIX possuem o serviço de Daytime, no entanto, outros sistemas operacionais podem não implementar este serviço. O programador pode resolver este problema implentando ele mesmo um servidor de Daytime. A próxima seção mostrará como isto pode ser feito. Comunicação orientada à conexão (servidor) Para criar um processo servidor é preciso associá-lo à uma porta. Isto é feito ao se criar uma instância da classe ServerSocket. Se estamos criando um novo serviço é preciso associar a uma porta com valor maior que 1023. Se estamos implementando um serviço já estabelecido é preciso obedecer as especificações definidas para o serviço. O exemplo XI.2 mostra o código de um servidor do serviço Daytime. Como não queremos substituir o serviço padrão de Daytime utilizaremos o número de porta 5013 no lugar do número 13. Para se testar este servidor com o programa cliente do exemplo X.1 é preciso alterar o número da porta no código do cliente. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 58 import java.util.*; import java.io.*; import java.net.*; public class ServerData { public static void main(String[] args) throws IOException { SeverSocket ssocket = null; Socket socket = null; BufferedWriter out = null; ssocket = new SeverSocket (5013,5); for(;;) { socket = ssocket.accept(); out = new BufferedWriter (new OuputStreamWriter ( socket.getOuputStream())); out.write((new Date()).toString()+”\n”); } out.close(); socket.close(); } } Exemplo XI.2 – Servidor de Daytime. Ao criar uma instância da classe ServerSocket o programador pode indicar o tamanho da fila de solicitações de conexão. As conexões são colocadas na fila até que o servidor possa atende-las. Se chegar alguma conexão e não houver espaço na fila a conexão será recusada. No nosso exemplo passamos como parâmetro o valor 5. Após criar o objeto ServerSocket o servidor deve indicar que está disposto a receber conexões. Isto é feito por meio da execução do método accept() do objeto ServerSocket. Ao executar este método o processo passa para o estado bloqueado até que alguma conexão seja solicitada. Quando a conexão é solicitada o método accept() retorna um objeto Socket igual ao do processo cliente, que será usado para obter os Streams onde será efetuada a comunicação. O Stream obtido do objeto Socket é encapsulado em objeto BufferedWriter que será encarregado de enviar a cadeia de caracteres contendo a data e a hora para o cliente. O Servidor em implementa um laço infinito, recebendo e tratando solicitações de serviços. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 59 O servidor implementado acima trata uma solicitação de serviço por vez. Isto pode ser problemático quando existem vários clientes solicitando serviços ao mesmo tempo e o servidor leva um longo tempo tratando cada solicitação. Nesta situação o cliente pode ficar um longo tempo a espera de atendimento. Isto pode ser remediado por meio da implementação de servidores Multithreaded. Apesar do serviço implementado pelo exemplo XI.2 não exigir um servidor Multithreaded, uma vez que o cliente é atendido rapidamente, o servidor Daytime foi alterado para exemplificar a implementação de um servidor Multithreaded. O exemplo XI.3 mostra o código da versão Multithreaded do servidor. import java.util.*; import java.net.*; import java.io.*; public class ServerData { public static void main(String args[]) { ServerSocket ssocket=null; try { ssocket = new ServerSocket(pt); } catch(Exception e) {System.err.println(e); System.exit(1);} while(true) { try { Socket socket = ssocket.accept(); (new serversec(socket)).start(); } catch(Exception e) {System.err.println(e);} } } } class serversec extends Thread { Socket socket; public serversec(Socket aSocket) {socket = aSocket;} public void run() { Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 60 try { BufferedWriter out = new BufferedWriter (new OuputStreamWriter ( socket.getOuputStream())); out.write((new Date()).toString()+”\n”); out.flush(); out.close(); socket.close(); } catch(Exception e) {System.err.println(e);} } } Exemplo XI.3 – Servidor de Daytime Multithreaded. No exemplo XI.3, ao receber uma conexão o servidor cria um Thread para atender o cliente e fica disponível para receber novas solicitações. Esse exemplo pode ser usado como esqueleto para desenvolvimento de servidores mais complexos, porém, neste caso é necessário limitar o número de Threads que podem ser criados. Comunicação Sem Conexão (UDP) Como já dissemos na seção anterior, nem sempre é necessário um canal de comunicação confiável entre duas aplicações. Para estes casos existe o protocolo UDP, que provê uma forma de comunicação onde a aplicação envia pacotes de dados, chamados de datagramas, para outra aplicação, sem garantias se e quando a mensagem vai chegar, nem se o conteúdo está preservado. As portas do protocolo UDP obedecem a mesma distribuição das TCP porém são distintas uma da outra, de modo que o programador pode associar uma porta TCP de um determinado número à uma aplicação em um host e o mesmo número de porta UDP a outra aplicação no mesmo host. As classes DatagramPacket e DatagramSocket contém os métodos necessários para realizar este tipo de comunicação. Para ilustrar o uso destas classes modificaremos os exemplos XI.1 e XI.2 para implementarmos uma aplicação Cliente/Servidor Daytime que usa o protocolo UDP. O Exemplo XI.4 mostra o código fonte do cliente e o Exemplo XI.5 mostra o código do servidor. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 61 Analisando o código da aplicação cliente podemos notar que é necessário criar um objeto da classe DatagramPacket para representar os Datagrama onde os dados são armazenados. No nosso exemplo colocamos também os dados do Host e a porta, porém estes dados poderiam ser omitidos na construção do Datagrama e serem passados somente no envio/recepção do pacote ou na construção do DatagramSocket. O Datagrama deverá armazenar os dados enviados pelo servidor e foi dimensionado para conter 64 bytes. O método receive() do objeto DatagramSocket aguarda o recebimento do pacote, tendo como argumento o objeto da classe DatagramPacket. Após o recebimento do pacote os dados são convertidos para String e exibidos na saída padrão. O servidor, diferentemente do servidor TCP, precisa saber a quem deve enviar os pacotes, uma vez que não é estabelecida uma conexão. Podíamos simplesmente passar o nome do host pela linha de comando, mas resolvemos adotar uma estratégia de Broadcasting. Nesta abordagem os datagramas são enviados a vários computadores e não apenas um. Isto é feito passando-se como argumento para o método InetAddress.getByName() o endereço de Broadcast da rede. import java.io.*; import java.net.*; public class DataClienteUDP { public static void main(String args[]) throws Exception { if (args.length != 1) { System.err.println("Uso: java DataClienteUDP host"); System.exit(1); } byte [] buff = new byte[64]; DatagramSocket ds = new DatagramSocket(); DatagramPacket dp = new DatagramPacket(buff, buff.length, InetAddress.getByName(args[0]),5013); ds.receive(dp); String s = new String(dp.getData()); System.out.println("Data e hora recebida de”+dp.getAddress()+ " : "+s); } } Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 62 Exemplo XI.4 – Cliente Daytime UDP. import java.io.*; import java.net.*; public class DataServerUDP { public static void main(String args[]) throws Exception { DatagramSocket ds; DatagramPacket dp; InetAddress addr = InetAddress.getByName(“255.255.255.0”); ds = new DatagramSocket(); byte [] buff; for (;;) { Thread.sleep(1000); String s = (new Date()).toString(); buff = s.getBytes(); dp = new DatagramPacket(buff, buff.length, addr, 5013); ds.send(dp); } } } Exemplo XI.5 – Servidor Daytime UDP. O tipo de endereço de Broadcast depende da classe de endereçamento IP da rede. O endereço usado no exemplo IX.5 funciona para a classe de endereçamento C. Para descobrir que tipo de classe pertence a rede onde está seu computador olhe o primeiro byte do endereço IP de sua máquina e verifique junto a tabela XI.3. Primeiro byte do endereço IP 0 a 126 128 a 191 192 a 223 Classe A B C Endereço de Broadcast 255.0.0.0 255.255.0.0 255.255.255.0 Tabela XI.3 – Classes para comunicação básica. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 63 O envio dos dados é feito pelo método send() do objeto DatagramSocket, tendo como argumento um objeto da classe DatagramPacket. Note que não é necessário um “Socket servidor” uma vez que o servidor envia os pacotes independentemente de existir clientes solicitando-os. Comunicação por meio de URL URLs Um URL (Uniform Resource Locator) é uma referência (um endereço) a um recurso na Internet. O URL é dividido em partes, sendo que apenas a primeira parte é obrigatória. A maioria das URLs é dividida em três partes: Informação sobre o Host 678 123 http://dpi.ufv.br/professores.html 123 Protocolo endereço do recurso no Host A parte do protocolo define o protocolo que deve ser usado para acessar o recurso. Os protocolos mais comuns são FTP, HTTP e file, este último indicando que o recurso se encontra no sistema de arquivos local. O protocolo é seguido do caractere “:”. A parte com informação sobre o Host fornece a informação necessária para acessar o Host onde está localizado o recurso. Esta parte é omitida caso o recurso esteja no sistema de arquivos local. A informação sobre o Host é precedida por duas barras (“//”), no caso de aplicação na Internet e apenas por uma barra (“/”), caso contrário. A informação sobre o Host também pode ser dividida em três partes: a) o nome do domínio do Host; b) o nome e senha do usuário para login; e c) o número da porta caso necessário, após o nome do host, precedida pelo caractere “:”. Exemplos: http://java.sun.com:80/doc/tutorial.html http://infax.com.br."claudio"."1234"/base/vendas Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 64 A última parte de uma URL representa o caminho até o recurso no sistema de arquivos do Host. Esta seção é separada da seção anterior por uma barra simples (“/”). Manipulando URLs em Java A linguagem Java fornece as seguintes classes para manipulação de URLs: Classe URL URLConnection URLEncoder Descrição Representa um URL Classe abstrata que representa uma conexão entre uma aplicação e um URL. Instâncias desta classe podem ser usadas para ler e escrever no recurso referenciado pela URL. Usada para lidar com o formato MIME. Tabela XI.3 – Classes para manipulação de URLs. Um objeto URL é criado passando como parâmetro para o construtor o URL na forma de String: URL dpi = new URL("http://www.dpi.ufv.br"); Um objeto URL pode ser construído passando como parâmetro outro URL para servir de endereço base. Por exemplo URL profs = new URL (dpi,”professores.html”); Isto tem o mesmo efeito que gerar uma instância da classe URL com primeiro construtor, passando como parâmetro o endereço: http://www.dpi.ufv.br/professores.html Os construtores geram a exceção MalformedURLException, se o URL é inválido. Portanto, o programador deve providenciar o código para a captura e tratamento desta exceção. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 65 De posse de um objeto URL é possível obter um objeto InputStream para ler os dados endereçados pelo URL, utilizando o método openStream() do objeto URL. O Exemplo XI.4 mostra o código de um programa que pode ser usado para listar na saída padrão o conteúdo de um URL passado pela linha de comando. import java.net.*; import java.io.*; public class LeURL { public static void main(String[] args) throws Exception { if (args.length < 1) { System.err.println("uso: java LeURL <URL>..."); System.exit(1); } URL url = new URL(args[0]); BufferedReader in = new BufferedReader( new InputStreamReader(url.openStream())); String linha; while ((linha = in.readLine()) != null) System.out.println(linha); in.close(); } } Exemplo XI.4 – Leitor de URL. Comunicando por meio de URLConnection O programador pode utilizar o método openConnection() do objeto URL para obter uma conexão entre a aplicação é o recurso referenciado pelo o URL. O método openConnection() retorna um objeto URLConnection, que permite que a aplicação escreva e leia através da conexão. Alguns URLs, como os conectados à scripts CGI2 (Common-Gateway Interface), permitem que 2 Common-Gateway Interface (CGI) é um mecanismo para gerar páginas Web dinamicamente. Os dados são obtidos de fórmulários HTML e submetidos a um programa binário no servidor Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 66 aplicação cliente escreva informação no URL. A saída do programa CGI pode ser interceptada pelo programa Java de modo que possa ser exibida para o usuário. Esta forma de comunicação com scripts CGI é melhor do que por meio de formulários HTML, uma vez que o usuário não precisa navegar entre páginas para visualizar os formulários e a resposta retornada. O Applet Java se encarrega de enviar os dados e exibir o resultado na mesma página. Para ilustrar esta forma de comunicação mostraremos um programa que submete uma cadeia de caracteres a um URL para que seja invertido e enviado de volta. O script CGI utilizado, escrito em Perl é exibido no exemplo XI.5 e foi escrito por Hassan Schroeder, um membro da equipe de desenvolvimento da linguagem Java. Este CGI pode ser acessado no URL http://java.sun.com/cgibin/backwards. #!/opt/internet/bin/perl read(STDIN, $buffer, $ENV{'CONTENT_LENGTH'}); @pairs = split(/&/, $buffer); foreach $pair (@pairs) { ($name, $value) = split(/=/, $pair); $value =~ tr/+/ /; $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg; # Stop people from using subshells to execute commands $value =~ s/~!/ ~!/g; $FORM{$name} = $value; } print "Content-type: text/plain\n\n"; print "$FORM{'string'} reversed is: "; $foo=reverse($FORM{'string'}); print "$foo\n"; exit 0; Exemplo XI.5 – Script backwards para inverter uma cadeia de caracteres. O exemplo XI.6 contém o programa que envia uma cadeia de caracteres ao URL e recebe de volta outra cadeia de caracteres que é a inversão da primeira. A ação necessária é codificar a cadeia de caracteres por meio do método estático encode() da classe URLEncoder: que gera a resposta na forma de uma página Web. O programa pode ser escrito em uma variadade delinguagens, como Perl e C. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 67 String string = URLEncoder.encode(args[0]); Isto é necessário porque a string enviada a um URL necessita de uma codificação particular, como por exemplo, os espaços em branco são substituídos pelo caractere “+”, os campos são separados pelo caractere “&”e valor do campo é separado do nome do campo pelo caracteres “=”. Em seguida o programa cria um objeto URL relacionado com o endereço onde se encontra o script, abre uma conexão e define que será usada para entrada e saída. URL url = new URL("http://java.sun.com/cgibin/backwards"); URLConnection c = url.openConnection(); c.setDoOutput(true); Neste momento o programa está preparado para trabalhar com o URL como se fosse um Stream. O Stream para escrever no URL é obtido do objeto URLConnection por meio do método getOutputStream() e o Stream para ler do URL é obtido do objeto URLConnection por meio do método getInputStream(). Primeiro o programa submete a cadeia de caracteres a ser invertida precedida pelo nome do campo e pelo caractere “=”: out.println("string=" + string); import java.io.*; import java.net.*; public class Inverte { public static void main(String[] args) throws Exception { if (args.length != 1) { System.err.println("Uso: java Inverte string"); System.exit(1); } String string = URLEncoder.encode(args[0]); URL url = new URL("http://java.sun.com/cgi- Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 68 bin/backwards"); URLConnection c = url.openConnection(); c.setDoOutput(true); PrintWriter out = new PrintWriter(c.getOutputStream()); out.println("string=" + string); out.close(); BufferedReader in = new BufferedReader(new InputStreamReader( c.getInputStream())); String retorno; while ((retorno = in.readLine()) != null) System.out.println(retorno); in.close(); } } Exemplo XI.6 – Programa que escreve em um URL. Após isso o Stream de saída é fechado e o Stream de entrada é aberto. A cadeia de caracteres invertida é lida e exibida no dispositivo de saída padrão. No exemplo apresentado o script CGI usa o POST METHOD para ler dados enviados pelo cliente. Alguns scripts CGI usam o GET METHOD para ler dados do cliente, no entanto, este último está ficando rapidamente obsoleto devido a maior versatilidade do primeiro. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 69 Capítulo IV – Computaç ã o Distribuída (RMI) A RMI (Invocação de métodos remotos) é uma tecnologia que coloca a programação com rede em um nível mais alto. RMI torna possível que objetos distribuídos em uma rede se comuniquem de forma transparente para o programador utilizando chamadas de procedimentos remotos. O principal objetivo da tecnologia RMI é permitir que programadores desenvolvam programas distribuídos utilizando a mesma sintaxe e semântica de programas Java convencionais. Antes da introdução da RMI no mundo Java, no JDK 1.1, para fazer com que dois objetos em máquinas diferentes se comunicassem o programador deveria definir um protocolo de comunicação e escrever código utilizando socket para implementar este protocolo. Com RMI a maior parte do trabalho quem realiza é a máquina virtual Java. Existem outras tecnologias, como CORBA (Common Object Request Brocker Architecture), que também tem como objetivo fazer com que objetos distribuídos em uma rede se comuniquem. Java também tem suporte a CORBA, mas para projetos em um ambiente Java puro, a RMI é consideravelmente mais simples que a CORBA. Criando nossa agenda distribuída De modo exemplificar o uso de RMI modificaremos a agenda distribuída fazendo uso desta tecnologia. Os passos a serem seguidos são: 1. Escrever e compilar a interface que descreve a como serão as chamadas do cliente ao servidor; 2. Escrever e compilar a classe que implementa a interface do passo 1 (objeto servidor); 3. Gerar Stubs e Skeleton do objeto distribuído; 4. Desenvolver o código que disponibiliza o objeto; 5. Escrever e compilar o código para o cliente RMI; e 6. Testar. Implementar interface do objeto remoto Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 70 O primeiro passo quando se deseja criar um objeto Remoto com RMI é implementar uma interface para este Objeto. Essa interface deve herdar a interface Remote. É através dessa interface Remote, que não tem métodos, que a máquina virtual Java sabe qual objeto pode ser disponibilizado para acesso remoto. Abaixo temos o exemplo da interface do nosso objeto: 1 public interface Agenda extends java.rmi.Remote{ public void inserir(Pessoa p) 2 throws java.rmi.RemoteException; 3 public Pessoa getPessoa(String nome) 4 throws java.rmi.RemoteException; 5 public java.util.Enumeration getPessoas() 6 throws java.rmi.RemoteException; 7 8 } Exemplo XX.XX – Interface do objeto remoto. A interface Remote deve ser herdada pela nossa interface Agenda. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 71 Capítulo V - Acesso a B a nco de Dados No dias de hoje, uma linguagem sem recursos para acesso a sistemas de Banco de Dados está fadada ao fracasso. Pensando nisso a Sun incluiu como parte do núcleo de bibliotecas de classes da linguagem Java uma API com o objetivo de preencher esta função, chamada de JDBC (segundo a Sun JDBC é apenas um acronismo, no entanto, muitas pessoas acreditam que é uma sigla para Java Database Connectivity). JDBC é uma API baseada no X/Open SQL Call Level Interface, tendo sido desenvolvida originalmente como um pacote separado, porém a partir do JDK1.1 passou a fazer parte do núcleo básico de pacotes. Utilizando a API JDBC é possível conectar um programa Java com servidores de Banco de Dados e executar comandos SQL (Structure Query Language). Sendo uma API independente do Sistema Gerenciador de Banco de Dados, não é necessário escrever uma aplicação para acessar um Banco de Dados Oracle, outra para uma Base de Dados Sybase, outra para o DB2, e assim por diante. A idéia de se usar uma camada intermediária entre o Banco de Dados e a aplicação, com o objetivo de isolá-la das particularidades do SGBD, não é nova. O exemplo mais popular deste enfoque é a API ODBC (Open DataBase Connectivity), proposta pela Microsoft. O leitor pode estar se perguntando porque a Sun resolveu propor mais uma API em vez de adotar a ODBC. Existem vários motivos, porém o principal é que a API ODBC simplesmente não é adequada para a linguagem Java. Isto ocorre porque ODBC foi desenvolvida para ser usada na linguagem C e é baseada fortemente no uso de ponteiros, estrutura que não existe em Java. Modelos de Acesso a Servidores O modelo mais simples de aplicação Cliente/Servidor é o chamado de modelo de duas camadas, onde a aplicação acessa diretamente o Banco de Dados. A figura XIII.1 mostra o esquema para uma aplicação que acessa um Banco de Dados usando o modelo de duas camadas. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 72 Aplicação Java DBMS Figura XIII.1 – Modelo de acesso a Banco de Dados em duas camadas. Aplicação Java ou Applet HTTP, RMI, CORBA Aplicação Servidora Java JDBC DBMS Figura XIII.2 – Modelo de acesso a Banco de Dados em três camadas. Tipos de Drivers JDBC Os drivers JDBC devem suportar o nível de entrada do padrão ANSI SQL-2. No momento, os drivers JDBC existentes se encaixam em um dos quatro tipos abaixo: 1. Ponte JDBC-ODBC com driver ODBC – o driver JDBC acessa o banco de dados via drivers ODBC. Como ODBC é um código binário e, em alguns casos, compõe o código do cliente, é necessário instalar em cada máquina cliente que usa o driver. Essa é uma solução adequada somente para uma Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 73 interna corporativa, ou em aplicações que adotam o modelo de três camadas sendo a camada intermediária um servidor Java. SGBD1 Cliente Java Ponte JDBC-ODBC SGBD2 SGBD3 2. Driver Java parcial e Api Nativa – neste caso as chamadas JDBC são convertidas para as chamadas às APIs nativas do SGBD. Como o driver possui uma parte em código binário é necessário instalar algum código na máquina cliente, como é feito nos drivers do tipo 1. Protocolo do SGBD Cliente Java JDBC Java & Código Binário SGBD 3. Driver puro Java e protocolo de rede – neste caso as chamadas JDBC são convertidas para um protocolo de rede independente do SGBD que é depois traduzido para as chamadas às APIs nativas do SGBD por um servidor. Esta é uma arquitetura em três camadas, onde o servidor middleware é capaz de conectar seus clientes Java puros com vários SGBDs. Esta solução permite o desenvolvimento de clientes 100% Java, tendo como consequência a não necessidade de instalação de qualquer código na máquina cliente. JDBC DRIVER (100% Java) Cliente Java Servidor de acesso SGBD1 SGBD2 SGBD3 4. Driver Java Puro e protocolo nativo - neste caso as chamadas JDBC são convertidas para as chamadas às APIs nativas do SGBD pelo driver, que foi escrito totalmente em Java. Protocolo do SGBD Cliente Java JDBC Java (100% Java) Alcione de P. Oliveira, Vinícius V. Maciel - UFV SGBD Java na Prática – Volume II 74 Atualmente existe uma maior disponibilidade dos drivers tipo 1 e 2, mas a tendência é que estes desapareçam, sendo substituídos pelos drivers do tipo 3 e 4. Obtendo os Drivers JDBC Informações sobre como obter drivers JDBC podem ser obtidas no site http://www.javasoft.com/products/jdbc. Outra alternativa é acessar as páginas dos fabricantes de SGBD, para verificar se existe driver disponível. Preparando um Banco de Dados Os exemplos deste livro usam a ponte JDBC-ODBC para conectar com o Banco de Dados. Isto facilita para os usuários que possuem um gerenciador Banco de Dados pessoal como o Access, Paradox, e outros semelhantes. Além disso, como driver JDBC-ODBC está incorporado ao SDK, o usuário não necessita procurar um driver para testar os exemplos. O lado negativo desta abordagem está na necessidade de configurar o ODBC e no fato de que as aplicações remotas deverão ser desenvolvidas em três camadas. No entanto, nada impede que o leitor use outro driver para rodar os exemplos, bastando para isso alterar a chamada da carga do driver. Primeiramente é necessário criar uma base de dados em algum SGBD. Nos exemplos deste livro será usada uma base contendo dados sobre livros, alunos e empréstimos de livros aos alunos. Não trataremos neste livro dos conceitos relacionados com banco de dados relacionais nem sobre a linguagem de consulta SQL. Existem vários textos sobre o assunto onde o leitor pode buscar informação. As figuras XIII.3 a XII.5 mostram as tabelas que formam a base de dados usada nos exemplos. O banco de dados é formado por três tabelas. Uma para armazenar os dados dos alunos, outra para receber os dados dos livros e uma terceira para conter os dados dos empréstimos. alunos matricula nome 1 Railer Costa Freire 2 Alexandre Altoé Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 3 4 5 6 75 André M. A. Landro Ana Maria Freitas Claudia Maria Alexandra Moreira Figura XIII.3 – Tabela de alunos livros codlivro 1 2 3 4 5 6 7 8 9 10 titulo volume Curso Pratico de Java Curso Pratico de Java Introdução a Compiladores Fundamentos de Banco de Dados Redes de Computadores Redes de Computadores Fácil Lógica matemática Engenharia de Software para Leigos Aprenda Computação Gráfica em duas Aprenda Inteligência Artificial em 5 1 2 1 1 1 2 1 1 1 1 Figura XIII.4 – Tabela de livros. codlivr 1 7 9 1 4 10 emprestimos matricu data_empresti data_devoluc 1 01/01/99 10/01/99 3 03/01/99 13/01/99 6 12/01/99 22/01/99 3 20/01/99 30/01/99 2 03/02/99 13/02/99 2 12/02/99 22/02/99 Figura XIII.5 – Tabela de empréstimos. Em um Banco de Dados Relacional cada tabela representa um conjunto de entidades ou relacionamentos entre entidades, e cada linha da tabela representa uma entidade particular ou um relacionamento entre entidades. Assim, cada linha das tabelas alunos e livros representa um aluno e um livro respectivamente. Já na tabela empréstimos cada linha representa o relacionamento por empréstimo de um livro a um aluno. Para estabelecer este Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 76 tipo de relacionamento em um Banco de Dados Relacional é preciso colocar na tabela que representa o relacionamento os atributos chaves de cada entidade que participa da relação. Atributos chaves são os atributos que identificam cada entidade. No caso dos alunos é seu número de matrícula, uma vez que pode existir dois alunos com o mesmo nome. Já no caso de um livro o seu atributo chave é o código do livro. Um Banco de Dados Relacional pode ser representado pelo diagrama de classes da UML, como mostrado pela figura XIII.6, onde cada tabela é vista como uma classe. Alunos livros matricula nome codlivro titulo volume emprestimos data_emprestimo data_devolução Figura XIII.6 – Diagrama de Classes do Banco de Dados. Para criação das tabelas em banco de dados relacional deve-se usar comandos DDL (Data Definition Language). O exemplo XX.XX mostra os comandos em DDL para a criação das tabelas do exemplo: CREATE TABLE ALUNOS (MATRICULA INT PRIMARY KEY, NOME VARCHAR(50) NOT NULL); CREATE TABLE LIVROS (CODLIVRO INT PRIMARY KEY, TITULO VARCHAR(50) NOT NULL, VOLUME INT NOT NULL); CREATE TABLE EMPRESTIMOS ( CODLIVRO INT NOT NULL, MATRICULA INT NOT NULL, DATAEMP DATE NOT NULL, DATADEV DATE NOT NULL, CONSTRAINT PK_EMP PRIMARY KEY (CODLIVRO, MATRICULA, DATAEMP), CONSTRAINT FK_EMP1 FOREIGN KEY (CODLIVRO ) REFERENCES LIVROS (CODLIVRO ), Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 77 CONSTRAINT FK_EMP2 FOREIGN KEY (MATRICULA) REFERENCES ALUNOS (MATRICULA )); Exemplo XX.XX – Comandos em DDL para criação das tabelas em um SGBD relacional. Configurando o ODBC Como utilizamos nos exemplos a ponte JDBC-ODBC é necessário configurar o ODBC para acessar a base de dados acima. Na plataforma Windows 98 isso é feito da seguinte forma: 1. Execute o programa de configuração do ODBC por meio do ícone “ODBC de 32bits” do painel de controle. 2. Clique na pasta “NFD do sistema” e em seguida no botão de “adicionar...”. O NFD do sistema é escolhido no lugar da opção “NFD do usuário” porque permite o compartilhado à base de dados. 3. Selecione o driver do banco de dados e pressione o botão de concluir. Por exemplo, se você estiver usando o Access selecione a opção “Driver para o Microsoft Access (*.mdb)”. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 78 Figura XIII.7 – Seleção do driver ODBC no Windows. 4. Ao surgir a tela para a entrada seleção do Banco de Dados, utilize a tecla “Selecionar...” localizar o arquivo onde está a base de dados e preencha a janela de texto “Nome da fonte de dados” com o nome que será usado para referenciar a base. 5. Feche o programa de configuração. A configuração de uma base de dados para ser acessada via ODBC possui vários outros detalhes que consideramos não ser relevantes para os propósitos deste livro. A configuração como apresentada acima é a suficiente para se executar os exemplos deste livro. Exemplo Inicial O pacote java.sql fornece as classes e interfaces necessárias para a conexão com uma base de dados e a posterior manipulação dos dados. As etapas para se criar uma aplicação cliente de um SGBD em Java são as seguintes: 1. 2. 3. 4. 5. Carregar o driver JDBC. Estabelecer a conexão. Criar um objeto Statement. Executar o comando SQL por meio de um método do objeto Statement. Receber o resultado, se for o caso. Para comentar cada uma destas etapas utilizaremos o exemplo XII.1 que mostra o código de uma aplicação que lista no dispositivo de saída padrão o nome de todos os alunos. import java.sql.*; import java.net.URL; class jdbc { public static void main(String a[]) Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 79 { try { Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); Connection con = DriverManager.getConnection("jdbc:odbc:biblioteca"); Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery("SELECT NOME FROM alunos"); System.out.println("Nome"); while(rs.next()) System.out.println(rs.getString("nome")); stmt.close(); con.close(); } catch(Exception e) {System.out.println(e.getMessage()); e.printStackTrace();} } } Exemplo XX.XX – Código para listar o nome dos alunos. Carregando o Driver A primeira etapa é carregar o driver JDBC. Para isso é usado o método estático forName() da classe Class. Em caso de erro este método lança a exceção ClassNotFoundException. O método cria uma instância do driver e o registra junto ao DriverManager. No exemplo XIII.1 é carregado o driver JDBC-ODBC que vem junto com o SDK. Estabelecendo a conexão A segunda etapa é realizada por meio do método estático getConnection() da classe DriverManager. Este método, na sua forma mais simples, recebe como parâmetro um URL que faz referência a base de dados e retorna um objeto da classe Connection, que representa a conexão com a base de dados. Já discutimos sobre URLs no capítulo XI. No entanto, existem algumas particularidades no que refere a URLs que fazem referência à Banco de Dados. O formato padrão deste tipo de URL é o seguinte: jdbc:<subprotocolo>:<identificador> onde: Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 80 1. jdbc representa o protocolo; 2. <subprotocolo> se refere ao driver ou ao mecanismo de conexão com o Banco de Dados, que pode ser suportado por um ou mais drivers. No exemplo XII.1 o nome é utilizada a ponte JDBC-ODBC, representada pela palavra odbc no subprotocolo. 3. <identificador> é a parte onde se identifica o Banco de Dados. A forma de identificação varia de acordo com o subprotocolo. No nosso exemplo é colocado o mesmo nome usado para identificar a fonte de dados na configuração do ODBC. A sintaxe para o subprotocolo odbc é a seguinte: jdbc:odbc:<fonte de dados>[;<atributo>=<valor>]* onde <atributo> e <valor> representam parâmetros a serem passados para o gerente de conexão do Banco de Dados. Um banco de dados acessado remotamente requer maiores informações, como o nome do host e a porta. Tanto o uso local como remoto pode requerer a identificação do usuário, assim como uma senha. Estes dados podem ser passados como parâmetro no método getConnection(): getConnection("jdbc:odbc:contas",”ana”,”sght”); ou como parte do URL. Alguns exemplos de URLs estão descritos na tabela XIII.1 URL Descrição jdbc:odbc:biblioteca Referencia fonte de dados biblioteca via ponte JDBC-ODBC. Referencia fonte de dados bd1 via ponte jdbc:odbc:bd1;CacheSize=20 JDBC-ODBC. É definido o tamanho do cache. Referencia fonte de dados contas via jdbc:odbc:contas;UID=ana;PWD=sght ponte JDBC-ODBC. É passado também o nome do usuário e a senha. jdbc:oracle:thin:@sap.dpi.ufv.br:1521:agenda Referencia fonte de dados agenda no host remoto sap.dpi.ufv.br via subprotocolo oracle. É passado também o número da porta usada no acesso. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 81 Tabela.1 – Exemplos de URLs JDBC. Criando e Executando Comandos É necessário cria um ou mais objetos da classe Statement, que possui os métodos necessários para manipular a base de dados. Este objeto é criado por meio do método createStatement() do objeto da classe Connection. Statement stmt = con.createStatement(); Podemos então usar o objeto Statement para executar comandos de manipulação do Banco de Dados. No exemplo XIII.1 o objetivo é recuperar s nomes dos alunos. Este objetivo é atingido por meio da execução do comando SQL SELECT nome FROM alunos passado como parâmetro para o método executeQuery() do objeto Statement. Este método retorna um objeto que implementa a interface ResultSet, e que fornece os meios de acesso ao resultado da consulta. Muitas vezes, como no exemplo, o resultado de uma consulta é uma tabela com várias linhas. O programador pode utilizar o objeto ResultSet para acessar cada linha da tabela resultante em sequência. Para isso o objeto mantém um apontador para a linha corrente, chamado de cursor. Inicialmente o cursor é posicionado antes da primeira linha, movimentado para próxima linha por meio de chamadas ao método next() do objeto ResultSet. O método executeQuery() é usado apenas para consultas. Além desse método, a classe Statement possui o método execute() que retorna múltiplos ResultSets e o método executeUpdate(), para atualização (comandos INSERT, DELETE e UPDATE da linguagem SQL), criação de tabelas (comandos CREATE TABLE) e remoção (DROP TABLE). O valor de retorno do método executeUpdate() é um valor inteiro indicando o número de linhas afetadas ou zero no caso do DROP TABLE. Um exemplo de um comando para inserir um uma novo aluno na tabela seria: stmt.executeUpdate("INSERT INTO alunos VALUES(7, 'Ana mia')"); Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 82 Recuperando Valores O objeto ResultSet possui os métodos necessários para recuperar os valores de cada coluna da tabela, bastando passar o nome da coluna como parâmetro. No exemplo XIII.1 é utilizado método getString() para recuperar o valor da coluna “nome”. Os métodos para recuperação possuem o formato geral getXXX(), onde XXX é o nome de um tipo. A tabela XIII.2 mostra qual o método mais indicado para cada tipo SQL. getByte getShort getInt getLong getFloat getDouble getBigDecimal getBoolean getString getBytes getDate getTime getTimestamp getAsciiStream getUnicodeStre am T I N Y I N T S M A L L I N T I N T E G E R B I G I N T R E A L F L O A T D O U B L E D E C I M A L X x x x x x x x x x X x x x x x x x x x X x x x x x x x x x X x x x x x x x x x X x x x x x x x x x X x x x x x x x x X x x x x x x x x x X x x N B C V L B V L U I H A O I A O M T A R N N R N E R C G A B G R H V R I V I A A Y N A C R R A R C R B H Y I A N R A R Y x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x X x x x x x X x x x x x X X x x x x X X x x x x x x x x x x x x X x x x x x X x x x Alcione de P. Oliveira, Vinícius V. Maciel - UFV D A T E T I M E T I M E S T A M P x x x x X x x X X Java na Prática – Volume II getBinaryStrea m getObject 83 x x X x x x x x x x x x x x x x x x x x x x “x” indica que o método pode ser usado para recuperar o valor no tipo SQL especificado. “X” indica que o método é recomendado para ser usado na recuperação do valor no tipo SQL especificado Tabela XIII.2 – Tabelas com os métodos indicados para recuperação de valores. É possível recuperar o valor da coluna passando como parâmetro o número da coluna no lugar de seu nome. Neste caso a recuperação no nome do aluno no exemplo XIII.1 ficaria na seguinte forma: rs.getString(1); Transações e Nível de Isolamento Transação Uma Transação é um conjunto de operações realizadas sobre um banco de dados tratadas atomicamente, em outras palavras, ou todas operações são realizadas e o seu resultado registrado permanentemente na base de dados ou nenhuma operação é realizada. Por default, o banco de dados trata cada operação como uma transação, realizando implicitamente uma operação de commit ao fim de cada uma delas. A operação de commit registra permanentemente o resultado da transação na tabela. No entanto, existem situações onde é necessário tratar como uma transação um conjunto de operações, e não apenas uma transação. Por exemplo, suponha que em um Banco de Dados de uma agência bancária exista uma tabela com informações sobre a conta de corrente e outra com informações sobre contas de poupança. Suponha também que um cliente deseje transferir o dinheiro da conta corrente para uma conta de poupança. Essa transação é constituída pelas seguintes operações: 1. Caso exista saldo suficiente, subtração do montante da transferência do saldo da conta corrente. 2. Adição do montante da transferência ao saldo da conta de poupança. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 84 As operações acima precisam ocorrer totalmente ou o efeito de nenhuma delas deve ser registrado na base de dados. Caso contrário podemos ter uma situação onde o dinheiro sai da conta corrente mas não entra na conta da poupança. Este estado, onde as informações do banco de dados não reflete a realidade, é chamado de estado inconsistente. De modo a obter esse controle sobre as transações é necessário desabilitar o modo de auto-commit. Isto é feito por meio método setAutoCommit() do objeto Connection. con.setAutoCommit(false); A partir do momento em que é executado o comando acima, o programador é responsável pela indicação do final da transação, por meio da execução do método commit() do objeto Connection. con.commit(); Se alguma exceção for levantada durante a execução de qualquer operação da transação, o programador pode usar o método rollback() para desfazer as operações já realizadas após o último commit(). con.setAutoCommit(false); try { Statement stmt = con.createStatement(); stmt.executeUpdate(“UPDATE ...” ); stmt.executeUpdate(“UPDATE ...” ); con.commit(); stmt.close(); } catch(Exception e){con.rollback();} finally { try{ con.setAutoCommit(true);} catch(SQLException sqle) {System.out.prinln(sql.getMessage());} } Exemplo XX.X – Uso dos métodos commit() e rollback(). Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 85 Níveis de isolamento Além da atomicidade outra propriedade desejável em uma transação é o isolamento. A propriedade de isolamento implica que uma transação não é afetada pelas operações realizadas por outras transações que estão sendo realizadas concorrentemente. O isolamento completo entre transações prejudica muito a execução concorrente de transações e pode ser desnecessário em determinados tipos de aplicações. Por isso os SGBDs permitem que o programador defina o nível de isolamento entre as transações. De acordo com o relaxamento do isolamento certos problemas devido a interferência entre as transações podem ocorrer e o programador deve estar ciente disso. O número de níveis de isolamento, sua nomenclatura e características depende do SGBD utilizado. Descreveremos os níveis de isolamento definidos no pacote java.sql. Para exemplificar os problemas que podem ocorrer devido a interferência entre transações utilizaremos um banco de dados exemplo com a seguinte tabela: NumCC 10189-9 20645-7 • Saldo 200,00 300,00 Read uncommitted - É o nível menos restritivo. Pode ocorrer leituras de registros não committed (Dirty reads). Usados em onde não existe concorrência ou não existem alterações em registros ou quando essas alterações não são relevantes. Exemplo de problema: Uma transação deve transferir R$50,00 da conta 10189-9 para a conta 20645-7 e uma segunda transação deve somar R$70,00 à conta 10189-9. A figura abaixo mostra o estado inicial e o estado final desejado da tabela: Estado antes das transações NumCC 10189-9 20645-7 Saldo 200,00 300,00 Estado desejado após as transações NumCC Saldo 10189-9 220,00 20645-7 350,00 Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 86 Cada transação é divida em operações de leitura e escrita. Suponha que o intercalamento das operações seja feito como mostrado abaixo: Transação 1 Transação 2 leitura do saldo 10189 Escrita do Saldo-50,00 leitura do saldo 10189 leitura do saldo 20645 (falha na transação, realizado rollback) Escrita do Saldo+70,00 Como a transação 1 falhou o valor lido pela transação 2 é um valor que não foi tornado permanente na tabela. Isto faz com que a transação 2 opere sobre um resultado desfeito. A tabela resultante, mostrada abaixo estará em um estado inconsistente. NumCC 10189-9 20645-7 Saldo 220,00 300,00 • Read committed - Somente registros committed podem ser lidos. Evita o problema de Dirty reads, no entanto duas leituras de um mesmo item em uma mesma transação podem possuir valores diferentes, uma vez que o valor pode ser mudado por outra transação entre duas leituras. • Repeatable Read - Somente registros committed podem ser lidos, além disso impede a alteração de um item lido pela transação. Evita o problema de Dirty reads e o problema do non-repeatable Read . • Serializable - É o nível mais restritivo. Impede Dirty reads e non-repeatable reads. Além disso impede o problema de phantom reads onde um conjunto de registros satisfazendo a condição WHERE é lido enquanto outra transação insere novos registros que satisfazem a condição. Para se definir o nível de isolamento na linguagem Java usa-se um objeto DatabaseMetaData que é obtido por meio do objeto getMetaData() do Connection. Primeiro é preciso saber se o SGBD suporta o nível de Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 87 isolamento desejado para depois definir o nível. O exemplo XX.X mostra uma sequência típica comandos. DatabaseMetaData meta=con.getMetaData(); if(meta.supportsTransactionIsolationLevel( con.TRANSACTION_READ_COMMITTED)) { con.setTransactionIsolation( con.TRANSACTION_READ_COMMITTED); } else return; Exemplo XX.X – Exemplo do estabelecimento do nível de isolamento. A tabela abaixo mostra as constantes relacionadas com os níveis de isolamento da linguagem Java: Constante TRANSACTION_NONE TRANSACTION_READ_UNCOMMITTED TRANSACTION_READ_COMMITTED TRANSACTION_REPEATABLE_READ TRANSACTION_SERIALIZABLE Tabela XX.X – Tabela com as constantes dos níveis de isolamento. Prepared Statements Cada vez que se executa um comando SQL passado por meio de uma String. Este String deve ser analisado pelo processador de SQL do SGBD que irá, no caso da String estar sintaticamente correta, gerar um código binário que será executado para atender à solicitação. Todo esse processo é caro e sua execução repetidas vezes terá um impacto significativo sobre o desempenho da aplicação e do SGBD como um todo. Existem duas abordagens para tentar solucionar esse problema: Comandos preparados (prepared statements) e procedimentos armazenados (stored procedures). Discutiremos primeiramente os prepared statements. Prepared Statement é indicado nos casos onde um comando será executado várias vezes em uma aplicação. Neste caso é melhor compilar o comando uma única vez e toda vez que for necessário executá-lo basta enviar o Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 88 comando compilado. Além disso, o comando pré-compilado pode ser parametrizado, tornando-o mais genérico e, portanto, apto a expressar um maior número de consultas. Para criar um Prepared Statement é necessário obter um objeto PreparedStatement por meio do método prepareStatement() do objeto Connection, passando como argumento um comando SQL. PreparedStatement pstmt = con.prepareStatement( “INSERT INTO alunos(matricula,nome) VALUES(?, ? )”); O comando anterior insere uma nova linha na tabela alunos com os valores das colunas matricula e nome passados por parâmetro. O caractere ‘?’ representa o parâmetro. Este tipo de comando só possui valor tendo parâmetros, caso contrário teria pouca chance de ser reutilizado. Para executar o comando devemos especificar o valor dos parâmetros e executar o comando, como mostrado no exemplo abaixo: pstmt.clearParameters(); pstmt.setInt(1,8); pstmt.setString(2,”Clara Maria”); pstmt.executeUpdate(); Antes de especificar os parâmetros é necessário limpar qualquer outro parâmetro previamente especificado. Para especificar os parâmetros são utilizados um conjunto de métodos com o nome no formato setXXX(), onde XXX é o tipo sendo passado. O primeiro parâmetro do método setXXX() é o índice da ocorrência do caractere ‘?’ que será substituído pelo valor. O segundo parâmetro é o valor que será transmitido. Procedimentos Armazenados (Stored Procedures) A maioria dos SGBDs possuem algum tipo de linguagem de programação interna, como por exemplo a PL/SQL do Oracle ou mesmo Java e C/C++. Estas linguagens permitem que os desenvolvedores insiram parte do código da aplicação diretamente no banco de dados e invoquem este código a partir da aplicação. Esta abordagem possui as seguintes vantagens: Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 89 •Reuso de código – o código precisa ser escrito apenas uma vez e usado em várias aplicações, comunicando com várias linguagens. •Independencia entre a aplicação e o esquema do BD – se o esquema mudar, provavelmente apenas os procedimentos armazenados. •Desempenho – os procedimentos são previamente compilados, eliminando esta etapa. •Segurança – as aplicações possuem privilégio apenas para execução de procedimentos armazenados, evitando assim acessos não autorizados. A sintaxe dos procedimentos armazenados depende do SGBD em questão. Utilizaremos um exemplo em PL/SQL. No exemplo abaixo o procedimento retorna o nome do aluno a partir de sua matricula. CREATE OR REPLACE PROCEDURE sp_obtem_nome (id IN INTEGER, Nome_aluno out VARCHAR2)IS BEGIN SELECT nome INTO Nome_aluno FROM alunos WHERE matricula = id; END; / Para invocar o procedimento anterior de dentro de uma aplicação Java é necessário obter um objeto CallableStatement por meio do método prepareCall() do objeto Connection, passando como argumento um comando SQL. CallableStatement cstmt = con.prepareCall("{ CALL sp_obtem_nome(?,?)}"); cstmt.registerOutParameter(2, Types.VARCHAR); cstmt.setInt(1, 3); cstmt.execute(); System.out.prinln(“O nome do aluno numero 3 :” +cstmt.getString(2); Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II Agenda Eletrônica versão JDBC Pessoa import java.io.*; public class pessoa implements Serializable { String Nome; String Tel; // Construtor public pessoa(String n, String t) {Nome = n; Tel = t;} public String getNome(){return Nome;} public String getTel(){return Tel;} } agenda import import import import java.util.*; java.io.*; java.sql.*; java.net.URL; public class agenda { Connection con=null; // Construtor public agenda()throws Exception { Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); con=DriverManager.getConnection("jdbc:odbc:agenda"); } /** CloseAgenda */ public void CloseAgenda() { if (con != null) try{con.close();}catch(Exception e){}; } /** inserir Alcione de P. Oliveira, Vinícius V. Maciel - UFV 90 Java na Prática – Volume II 91 */ public void inserir(pessoa p) { if (con == null) return; try { Statement stmt = con.createStatement(); stmt.executeUpdate("INSERT INTO pessoas(nome,telefone) "+ "values('"+p.getNome()+"','"+p.getTel()+"')"); stmt.close(); }catch(Exception e) {System.err.println(e);} } /** Consultar */ /** listar */ public Enumeration getLista() { if (con == null) return null; Vector pessoas = new Vector(); try { Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery ("SELECT Nome, Telefone FROM pessoas"); while(rs.next()) pessoas.addElement(new pessoa(rs.getString("Nome"), rs.getString("Telefone"))); stmt.close(); } catch(Exception e) {System.out.println(e.getMessage()); e.printStackTrace();} return pessoas.elements(); } } Servidor import java.util.*; import java.net.*; import java.io.*; Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II /** CLASS server */ class server { public static void main(String args[]) { String oHost="localhost"; ServerSocket ssocket=null; serversec oServersec; int pt = 4444; agenda ag; try {ag = new agenda();} catch(Exception e){System.err.println(e); return;}; if (args.length > 0) pt = Integer.parseInt(args[0]); try { ssocket = new ServerSocket(pt); pt = ssocket.getLocalPort(); oHost = ssocket.getInetAddress().getHostName().trim(); } catch(Exception e) {System.err.println(e); System.exit(1);} System.out.println("Porta:"+pt+" Host: "+oHost); while(true) { try { Socket clisocket = ssocket.accept(); oServersec = new serversec(ag,clisocket); oServersec.start(); } catch(Exception e) {System.err.println(e);} } } } /** CLASS serversec */ class serversec extends Thread { Socket oSocket; BufferedWriter soutput; Alcione de P. Oliveira, Vinícius V. Maciel - UFV 92 Java na Prática – Volume II BufferedReader agenda cinput; ag; public serversec(agenda ag, Socket aoSocket) { this.ag = ag; oSocket = aoSocket; } public void run() { try { soutput = new BufferedWriter(new OutputStreamWriter(oSocket.getOutputStream())); cinput = new BufferedReader(new InputStreamReader(oSocket.getInputStream())); String losLinha = cinput.readLine(); switch(losLinha.charAt(0)) { case 'i': String Nome = cinput.readLine(); String Tel = cinput.readLine(); ag.inserir(new pessoa(Nome,Tel)); soutput.write("OK\n#\n"); break; case 'l': pessoa p; for (Enumeration e = ag.getLista(); e.hasMoreElements() ;) { p = (pessoa) e.nextElement(); soutput.write(p.getNome()+"\n"+p.getTel()+"\n"); } soutput.write("#\n"); break; } soutput.flush(); Thread.yield(); soutput.close(); oSocket.close(); } catch(Exception e) {System.err.println(e);} } } Alcione de P. Oliveira, Vinícius V. Maciel - UFV 93 Java na Prática – Volume II Applet import import import import import java.awt.*; java.applet.*; java.util.*; java.net.*; java.io.*; public class agendaapplet extends Applet { port = 4444; int TextField txtNome = new TextField(); TextField txtTel = new TextField(); Label label1 = new Label(); Label label2 = new Label(); Button btIns = new Button(); Button btList = new Button(); Button btCons = new Button(); Button btSair = new Button(); TextArea Saida = new TextArea(); Button btLimpar = new Button(); public void init() { setLayout(null); setSize(376,224); add(txtNome); txtNome.setBounds(108,48,232,24); add(txtTel); txtTel.setBounds(108,84,232,24); label1.setText("Nome"); add(label1); label1.setBounds(24,48,60,26); label2.setText("Telefone"); add(label2); label2.setBounds(24,84,60,26); btIns.setActionCommand("button"); btIns.setLabel("Inserir"); add(btIns); btIns.setBackground(java.awt.Color.lightGray); btIns.setBounds(12,12,49,23); btList.setActionCommand("button"); btList.setLabel("Listar"); add(btList); btList.setBackground(java.awt.Color.lightGray); btList.setBounds(149,12,60,23); btCons.setActionCommand("button"); btCons.setLabel("Consultar"); add(btCons); Alcione de P. Oliveira, Vinícius V. Maciel - UFV 94 Java na Prática – Volume II btCons.setBackground(java.awt.Color.lightGray); btCons.setBounds(75,12,60,23); btSair.setActionCommand("button"); btSair.setLabel("Sair"); add(btSair); btSair.setBackground(java.awt.Color.lightGray); btSair.setBounds(297,12,60,23); add(Saida); Saida.setBounds(24,120,338,90); btLimpar.setActionCommand("button"); btLimpar.setLabel("Limpar"); add(btLimpar); btLimpar.setBackground(java.awt.Color.lightGray); btLimpar.setBounds(223,12,60,23); SymMouse aSymMouse = new SymMouse(); btSair.addMouseListener(aSymMouse); btIns.addMouseListener(aSymMouse); btList.addMouseListener(aSymMouse); btLimpar.addMouseListener(aSymMouse); } public void transmit(int port,String mensagem) { BufferedWriter soutput; BufferedReader cinput; Socket clisoc=null; try { if (clisoc != null) clisoc.close(); clisoc = new Socket(InetAddress.getByName(getCodeBase().getHost()),port); soutput = new BufferedWriter (new OutputStreamWriter(clisoc.getOutputStream())); cinput = new BufferedReader(new InputStreamReader(clisoc.getInputStream())); soutput.write(mensagem+"\n"); soutput.flush(); String losRet = cinput.readLine(); while (losRet.charAt(0)!='#') { Saida.setText(Saida.getText()+losRet+"\n"); losRet = cinput.readLine(); } Thread.sleep(500); Alcione de P. Oliveira, Vinícius V. Maciel - UFV 95 Java na Prática – Volume II soutput.close(); clisoc.close(); } catch(Exception e) {System.err.println(e);} } class SymMouse extends java.awt.event.MouseAdapter { public void mouseClicked(java.awt.event.MouseEvent event) { Object object = event.getSource(); btSair_MouseClick(event); if (object == btSair) btIns_MouseClick(event); else if (object == btIns) btList_MouseClick(event); else if (object == btList) else if (object == btLimpar) btLimpar_MouseClick(event); } } void btSair_MouseClick(java.awt.event.MouseEvent event) { System.exit(0); } void btIns_MouseClick(java.awt.event.MouseEvent event) { String nome = txtNome.getText(); String tel = txtTel.getText(); if (nome.length()>0 && tel.length()>0) transmit(port,"i\n"+nome+"\n"+tel+"\n"); } void btList_MouseClick(java.awt.event.MouseEvent event) { transmit(port,"l\n"); } void btLimpar_MouseClick(java.awt.event.MouseEvent event) { Saida.setText(""); } } Alcione de P. Oliveira, Vinícius V. Maciel - UFV 96 Java na Prática – Volume II 97 Capítulo VI Servlets e JSP Servlets e JSP são duas tecnologias desenvolvidas pela Sun para desenvolvimento de aplicações na Web a partir de componentes Java que executem no lado servidor. Essas duas tecnologias fazem parte da plataforma J2EE (Java 2 Platform Enterprise Edition) que fornece um conjunto de tecnologias para o desenvolvimento de soluções escaláveis e robustas para a Web. Neste livro abordaremos apenas as tecnologias Servlets e JSP, sendo o suficiente para o desenvolvimento de sites dinâmicos de razoável complexidade. Se a aplicação exigir uma grande robustez e escalabilidade o leitor deve considerar o uso em conjunto de outras tecnologias da plataforma J2EE. Servlets Servlets são classes Java que são instanciadas e executadas em associação com servidores Web, atendendo requisições realizadas por meio do protocolo HTTP. Ao serem acionados, os objetos Servlets podem enviar a resposta na forma de uma página HTML ou qualquer outro conteúdo MIME. Na verdade os Servlets podem trabalhar com vários tipos de servidores e não só servidores Web, uma vez que a API dos Servlets não assume nada a respeito do ambiente do servidor, sendo independentes de protocolos e plataformas. Em outras palavras Servlets é uma API para construção de componentes do lado servidor com o objetivo de fornecer um padrão para comunicação entre clientes e servidores. Os Servlets são tipicamente usados no desenvolvimento de sites dinâmicos. Sites dinâmicos são sites onde algumas de suas páginas são construídas no momento do atendimento de uma requisição HTTP. Assim é possível criar páginas com conteúdo variável, de acordo com o usuário, tempo, ou informações armazenadas em um banco de dados. Servlets não possuem interface gráfica e suas instâncias são executadas dentro de um ambiente Java denominado de Container. O container gerencia as instâncias dos Servlets e provê os serviços de rede necessários para as requisições e respostas. O container atua em associação com servidores Web recebendo as requisições reencaminhada por eles. Tipicamente existe apenas uma instância de cada Servlet, no entanto, o container pode criar vários threads de modo a permitir que uma única instância Servlet atenda mais de uma Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 98 requisição simultaneamente A figura XX fornece uma visão do relacionamento destes componentes. Máquina Virtual Java Requisições Servidor Web Respostas Container Instâncias de Servlets Figura VI-1. Relacionamento entre Servlets, container e servidor Web Servlets provêem uma solução interessante para o relacionamento cliente/servidor na Internet, tornando-se uma alternativa para a implantação de sistemas para a Web. Antes de entrarmos em detalhes na construção de Servlets, compararemos esta solução com outras duas soluções possíveis para implantação de aplicações na Internet. Applets X Servlets Apesar de ser uma solução robusta existem problemas no uso de Applets para validação de dados e envio para o servidor. O programador precisa contar com o fato do usuário possuir um navegador com suporte a Java e na versão apropriada. Você não pode contar com isso na Internet, principalmente se você deseja estender a um grande número de usuário o acesso às suas páginas. Em se tratando de Servlets, no lado do cliente pode existir apenas páginas HTML, evitando restrições de acesso às páginas. Em resumo, o uso de Applets não é recomendado para ambientes com múltiplos navegadores ou quando a semântica da aplicação possa ser expressa por componentes HTML. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 99 CGI X Servlets Como visto no Erro! A origem da referência não foi encontrada., scripts CGI (Common Gateway Interface), acionam programas no servidor. O uso de CGI sobrecarrega o servidor uma vez cada requisição de serviço acarreta a execução de um programa executável (que pode ser escrito em com qualquer linguagem que suporte o padrão CGI) no servidor, além disso, todo o processamento é realizado pelo CGI no servidor. Se houver algum erro na entrada de dados o CGI tem que produzir uma página HTML explicando o problema. Já os Servlets são carregados apenas uma vez e como são executados de forma multi-thread podem atender mais de uma mesma solicitação por simultaneamente. Versões posteriores de CGI contornam este tipo de problema, mas permanecem outros como a falta de portabilidade e a insegurança na execução de código escrito em uma linguagem como C/C++. A API Servlet A API Servlet é composta por um conjunto de interfaces e Classes. O componente mais básico da API é interface Servlet. Ela define o comportamento básico de um Servlet. A figura XX.XX mostra a interface Servlet. public interface Servlet { public void init(ServletConfig config) throws ServletException; public ServletConfig getServletConfig(); public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException; public String getServletInfo(); public void destroy(); } Figura XV.XX. Interface Servlet. O método service() é responsável pelo tratamento de todas das requisições dos clientes. Já os métodos init() e destroy() são chamados quando o Servlet é carregado e descarregado do container, respectivamente. O método getServletConfig() retorna um objeto ServletConfig que Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 100 contém os parâmetros de inicialização do Servlet. O método getServletInfo() retorna um String contendo informações sobre o Servlet, como versão e autor. Tendo como base a interface Servlet o restante da API Servlet se organiza hierarquicamente como mostra a figura XV.XX. Servlet GenericServlet HttpServlet Figura XV.XX. Hierarquia de classes da API Servlet. A classe GenericServlet implementa um servidor genérico e geralmente não é usada. A classe HttpServlet é a mais utilizada e foi especialmente projetada para lidar com o protocolo HTTP. A figura XX.XX mostra a definição da classe interface HttpServlet. HttpServlet public abstract class HttpServlet extends GenericServlet implements java.io.Serializable Figura XV.XX. Definição da classe HttpServlet. Note que a classe HttpServlet é uma classe abstrata. Para criar um Servlet que atenda requisições HTTP o programador deve criar uma classe derivada da HttpServlet e sobrescrever pelo menos um dos métodos abaixo: Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II doGet doPost doPut doDelete 101 Trata as requisições HTTP GET. Trata as requisições HTTP POST. Trata as requisições HTTP PUT. Trata as requisições HTTP DELETE. Tabela XV.XX. Métodos da classe HttpServlet que devem ser sobrescritos para tratar requisições HTTP. Todos esses métodos são invocados pelo servidor por meio do método service(). O método doGet() trata as requisições GET. Este tipo de requisição pode ser enviada várias vezes, permitindo que seja colocada em um bookmark. O método doPost() trata as requisições POST que permitem que o cliente envie dados de tamanho ilimitado para o servidor Web uma única vez, sendo útil para enviar informações tais como o número do cartão de crédito. O método doPut() trata as requisições PUT. Este tipo de requisição permite que o cliente envie um arquivo para o servidor à semelhança de como é feito via FTP. O método doPut() trata as requisições DELETE, permitindo que o cliente remova um documento ou uma página do servidor. O método service(), que recebe todas as requisições, em geral não é sobrescrito, sendo sua tarefa direcionar a requisição para o método adequado. Exemplo de Servlet Para entendermos o que é um Servlet nada melhor que um exemplo simples. O exemplo XV.XX gera uma página HTML em resposta a uma requisição GET. A página HTML gerada contém simplesmente a frase Ola mundo!!!. Este é um Servlet bem simples que ilustra as funcionalidades básicas da classe. import javax.servlet.*; import javax.servlet.http.*; public class Ola extends HttpServlet { public String getServletInfo() { return "Ola versão 0.1";} public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 102 { res.setContentType("text/html"); java.io.PrintWriter out = res.getWriter(); out.println("<html>"); out.println("<head>"); out.println("<title>Servlet</title>"); out.println("</head>"); out.println("<body>Ola mundo!!!"); out.println("</body>"); out.println("</html>"); out.close(); } } Exemplo XV.XX. Servlet Ola. O método doGet() recebe dois objetos: um da classe HttpServletRequest e outro da classe HttpServletResponse. O HttpServletRequest é responsável pela comunicação do cliente para o servidor e o HttpServletResponse é responsável pela comunicação do servidor para o cliente. Sendo o exemplo XV.XX apenas um exemplo simples ele ignora o que foi enviado pelo cliente, tratando apenas de enviar uma página HTML como resposta. Para isso é utilizado o objeto da classe HttpServletResponse. Primeiramente é usado o método setContentType() para definir o tipo do conteúdo a ser enviado ao cliente. Esse método deve ser usado apenas uma vez e antes de se obter um objeto do tipo PrintWriter ou ServletOutputStream para a resposta. Após isso é usado o método getWriter() para se obter um objeto do tipo PrintWriter que é usado para escrever a resposta. Neste caso os dados da resposta são baseados em caracteres. Se o programador desejar enviar a resposta em bytes deve usar o método getOutputStream() para obter um objeto OutputStream. A partir de então o programa passa usar o objeto PrintWriter para enviar a página HTML. Compilando o Servlet A API Servlet ainda não foi incorporado ao SDK, portanto, para compilar um Servlet é preciso adicionar a API Servlet ao pacote SDK. Existem várias formas de se fazer isso. A Sun fornece a especificação da API e diversos Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 103 produtores de software executam a implementação. Atualmente, a especificação da API Servlet está na versão 2.XX. Uma das implementações da API que pode ser baixada gratuitamente pela Internet é a fornecida pelo projeto Jakarta (http://jakarta.apache.org) denominada de Tomcat. A implementação da API Servlet feita pelo projeto Jakarta é a implementação de referência indicada pela Sun. Ou seja, é a implementação que os outros fabricantes devem seguir para garantir a conformidade com a especificação da API. No entanto, uma vez que o Tomcat é a implementação mais atualizada da API, é também a menos testada e, por consequência, pode não ser a mais estável e com melhor desempenho. Instalando o Tomcat Assim como para se executar um Applet era preciso de um navegador Web com Java habilitado no caso de Servlets é preciso de servidor Web que execute Java ou que passe as requisições feitas a Servlets para programas que executem os Servlets. O Tomcat é tanto a implementação da API Servlet como a implementação de um container, que pode trabalhar em associação com um servidor Web como o Apache ou o IIS, ou pode também trabalhar isoladamente, desempenhando também o papel de um servidor Web. Nos exemplos aqui mostrados usaremos o Tomcat isoladamente. Em um ambiente de produção esta configuração não é a mais adequada, uma vez que os servidores Web possuem um melhor desempenho no despacho de páginas estáticas. As instruções para configurar o Tomcat para trabalhar em conjunto com um servidor Web podem ser encontradas junto às instruções gerais do programa. As figuras XV.XX ilustram essas duas situações. Servidor Web Internet Servlet habilitado Figura XV.XX. Servidor Web habilitado para Servlet. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II Internet 104 Servidor Web Servlet Container Figura XV.XX. Servidor Web reencaminhando as requisições para o Servlet container. A versão estável do Tomcat é a 3.2.3.XX é após baixá-la do site do projeto Jakarta o usuário deve descomprimir o arquivo. Por exemplo, no ambiente Windows o usuário pode descomprimir o arquivo na raiz do disco C:, o que gerará a seguinte árvore de diretórios: C:\ jakarta-tomcat-3.2.3 | |______bin |______conf |______doc |______lib |______logs |______src |______webapps No diretório bin encontram-se os programas execução e interrupção do container Tomcat. No diretório conf encontram-se os arquivos de configuração. No diretório doc encontram-se os arquivos de documentação. No diretório lib encontram-se os bytecodes do container e da implementação da API. No diretório logs são registradas as mensagens da geradas durante a execução do sistema. No diretório src encontram-se os arquivos fontes do container e da implementação da API de configuração. Finalmente, No diretório webapps encontram-se as páginas e códigos das aplicações dos usuários. No ambiente MS-Windows aconselhamos usar um nome dentro do formato 8.3 (oito caracteres para o nome e três para o tipo). Assim o diretório jakarta-tomcat-3.2.3 poderia ser mudado para simplesmente tomcat. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 105 Antes de executar o Tomcat é necessário definir duas variáveis de ambiente. Por exemplo, supondo que no MS-Windows o Tomcat foi instalado no diretório c:\tomcat e que o SDK está instalado no diretório c:\jdk1.3 então as seguintes variáveis de ambiente devem ser definidas: set JAVA_HOME=C:\jdk1.3 set TOMCAT_HOME=C:\tomcat Agora é possível executar o Tomcat por meio do seguinte comando: C:\tomcat\bin\startup.bat Para interromper a execução servidor basta executar o arquivo c:\tomcat\bin\shutdown.bat Falta de espaço para variáveis de ambiente Caso ao iniciar o servidor apareça a mensagem “sem espaço de ambiente” clique com o botão direito do mouse no arquivo .bat e edite as propriedades definindo o ambiente inicial com 4096. Feche o arquivo é execute novamente. Ao entrar em execução o servidor lê as configurações constantes no arquivo server.xml e, por default, se anexa à porta 8080. Para verificar se o programa está funcionando corretamente execute um navegador como o Netscape ou o Internet Explorer e digite a seguinte URL: http://127.0.0.1:8080/index.html A figura XX.XX mostra a tela principal do Tomcat. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 106 Figura XV.XX. Tela inicial do Tomcat. A número porta default para recebimento das requisições HTTP pode ser alterada por meio da edição do arquivo server.xml do diretório conf como mostrado abaixo: <Connector className="org.apache.tomcat.service.PoolTcpConnector"> <Parameter name="handler" value="org.apache.tomcat.service.http.HttpConnectionHandler"/> <Parameter name="port" value="Número da porta"/> </Connector> No entanto, caso o Tomcat esteja operando em conjunto com um servidor, o ideal é que o Tomcat não responda requisições diretamente. Preparando para executar o Servlet Compilando o Servlet Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 107 Antes de executar o Servlet e preciso compilá-lo. Para compilá-lo é preciso que as classes que implementam a API Servlet estejam no classpath. Para isso é preciso definir a variável de ambiente. No ambiente MS-Windows seria set CLASSPATH=%CLASSPATH%;%TOMCAT_HOME%\lib\servlet.jar e no ambiente Unix seria CLASSPATH=${CLASSPATH}:${TOMCAT_HOME}/lib/servlet.jar Alternativamente, é possível indicar o classpath na própria linha de execução do compilador Java. Por exemplo, No ambiente MS-Windows ficaria na seguinte forma: javac -classpath "%CLASSPATH%;c:\tomcat\lib\servlet.jar Ola.java Criando uma aplicação no Tomcat Agora é preciso definir onde deve ser colocado o arquivo compilado. Para isso é preciso criar uma aplicação no Tomcat ou usar uma das aplicações já existentes. Vamos aprender como criar uma aplicação no Tomcat. Para isso é preciso cria a seguinte estrutura de diretórios abaixo do diretório webapps do Tomcat: webapps |_____ Nome aplicação |_____ Web-inf |_____classes Diretório de Aplicações Na verdade é possível definir outro diretório para colocar as aplicações do Tomcat. Para indicar outro diretório é preciso editar o arquivo server.xml e indicar o diretório por meio da diretiva home do tag ContextManager. O diretório de uma aplicação é denominado de contexto da aplicação. É preciso também editar o arquivo server.xml do diretório conf, incluindo as linhas: Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 108 <Context path="/nome aplicação" docBase="webapps/ nome aplicação" debug="0” reloadable="true" > </Context> Finalmente, é preciso criar (ou copiar de outra aplicação) um arquivo web.xml no diretório Web-inf com o seguinte conteúdo: <?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN" "http://java.sun.com/j2ee/dtds/web-app_2.2.dtd"> <web-app> </web-app> Copie então o arquivo compilado Ola.class para o subdiretório /webapps/nome aplicação/Web-inf/classes do Tomcat. Executando o Servlet Invocando diretamente pelo Navegador Podemos executar um Servlet diretamente digitando a URL do Servlet no navegador. A URL em geral possui o seguinte formato: http://máquina:porta/nome aplicação/servlet/nome servlet A palavra servlet que aparece na URL não indica um subdiretório no servidor. Ela indica que esta é uma requisição para um Servlet. Por exemplo, suponha que o nome da aplicação criada no Tomcat seja teste. Então a URL para a invocação do Servlet do exemplo XX.XX teria a seguinte forma: http://localhost:8080/teste/servlet/Ola A URL para a chamada do Servlet pode ser alterada de modo a ocultar qualquer referência à diretórios ou a tecnologias de implementação. No caso do Tomcat essa configuração é no arquivo web.xml do diretório Web-inf da aplicação. Por exemplo, para eliminar a palavra servlet da URL poderíamos Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 109 inserir as seguintes linhas no arquivo web.xml entre os tags <web-app> e </web-app>: <servlet> <servlet-name> Ola </servlet-name> <servlet-class> Ola </servlet-class> </servlet> <servlet-mapping> <servlet-name> Ola </servlet-name> <url-pattern> /Ola </url-pattern> </servlet-mapping> Invocando em uma página HTML No caso de uma página HTML basta colocar a URL na forma de link. Por exemplo, <a href="http://localhost:8080/teste/servlet/Ola>Servlet Ola</a> Neste caso o Servlet Ola será solicitado quando o link associado ao texto “Servlet Ola” for acionado. Diferenças entre as requisições GET e POST Os dois métodos mais comuns, definidos pelo protocolo HTTP, de se enviar uma requisições a um servidor Web são os métodos POST e GET. Apesar de aparentemente cumprirem a mesma função, existem diferenças importantes entre estes dois métodos. O método GET tem por objetivo enviar uma requisição por um recurso. As informações necessárias para a obtenção do recurso (como informações digitadas em formulários HTML) são adicionadas à URL e, por consequência, não são permitidos caracteres inválidos na formação de URLs, Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 110 como por espaços em branco e caracteres especiais. Já na requisição POST os dados são enviados no corpo da mensagem. O método GET possui a vantagem de ser idempotente, ou seja, os servidores Web podem assumir que a requisição pode ser repetida, sendo possível adicionar à URL ao bookmark. Isto é muito útil quando o usuário deseja manter a URL resultante de uma pesquisa. Como desvantagem as informações passadas via GET não podem ser muito longas, um vez o número de caracteres permitidos é por volta de 2K. Já as requisições POST a princípio podem ter tamanho ilimitado. No entanto, elas não são idempotente, o que as tornam ideais para formulários onde os usuários precisam digitar informações confidenciais, como número de cartão de crédito. Desta forma o usuário é obrigado a digitar a informação toda vez que for enviar a requisição, não sendo possível registrar a requisição em um bookmark. Concorrência Uma vez carregado o Servlet não é mais descarregado, a não ser que o servidor Web tenha sua execução interrompida. De modo geral, cada requisição que deve ser direcionada a determinada instância de Servlet é tratada por um thread sobre a instância de Servlet. Isto significa que se existirem duas requisições simultâneas que devem ser direcionadas para um mesmo objeto o container criará dois threads sobre o mesmo objeto Servlet para tratar as requisições. A figura XX.XX ilustra esta situação. usuário 1 usuário 2 usuário 3 thread1 Servlet1 thread2 thread1 Servlet2 Figura XV.XX. Relacionamento entre as instâncias dos Servlets e os threads. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 111 Em conseqüência disto temos o benefícios de uma sobrecarga para servidor, uma vez que a criação de threads é menos onerosa do que a criação de processos, e uma aparente melhora no tempo de resposta. Por outro lado, o fato dos Servlets operarem em modo multi-thread aumenta a complexidade das aplicações e cuidados especiais, como visto no capítulo sobre concorrência, devem tomados para evitar comportamentos erráticos. Por exemplo, suponha um Servlet que receba um conjunto de números inteiros e retorne uma página contendo a soma dos números. A exemplo XX.XX mostra o código do Servlet. O leitor pode imaginar um código muito mais eficiente para computar a soma de números, mas o objetivo do código do exemplo é ilustrar o problema da concorrência em Servlets. O exemplo contém também um trecho de código para recebimento de valores de formulários, o que será discutido mais adiante. import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public class Soma extends HttpServlet { Vector v = new Vector(5); protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, java.io.IOException { v.clear(); Enumeration e = req.getParameterNames(); while (e.hasMoreElements()) { String name = (String)e.nextElement(); String value = req.getParameter(name); if (value != null) v.add(value); } res.setContentType("text/html"); java.io.PrintWriter out = res.getWriter(); out.println("<html>"); out.println("<head><title>Servlet</title></head>"); out.println("<body>"); out.println("<h1> A soma e'"); int soma =0; for(int i =0; i< v.size() ; i++) { soma += Integer.parseInt((String)v.get(i)); } out.println(soma); out.println("<h1>"); out.println("</body>"); Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 112 out.println("</html>"); out.close(); } } Exemplo XX.XX- Servlet com problemas de concorrência. Note que o Servlet utiliza uma variável de instância para referenciar o Vector que armazena os valores. Se não forem usadas primitivas de sincronização (como no código do exemplo) e duas requisições simultâneas chegarem ao Servlet o resultado pode ser inconsistente, uma vez que o Vector poderá conter parte dos valores de uma requisição e parte dos valores de outra requisição. Neste caso, para corrigir esse problema basta declarar a variável como local ao método doPost() ou usar primitivas de sincronização. Obtendo Informações sobre a Requisição O objeto HttpServletRequest passado para o Servlet contém várias informações importantes relacionadas com a requisição, como por exemplo o método empregado (POST ou GET), o protocolo utilizado, o endereço remoto, informações contidas no cabeçalho e muitas outras. O Servlet do exemplo XX.XX retorna uma página contendo informações sobre a requisição e sobre o cabeçalho da requisição. import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public class RequestInfo extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { res.setContentType("text/html"); PrintWriter out = res.getWriter(); out.println("<html><head>"); out.println("<title>Exemplo sobre Requisicao de Info </title>"); out.println("</head><body>"); out.println("<h3> Exemplo sobre Requisicao de Info </h3>"); out.println("Metodo: " + req.getMethod()+”<br>”); out.println("Request URI: " + req.getRequestURI()+”<br>”); out.println("Protocolo: " + req.getProtocol()+”<br>”); Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 113 out.println("PathInfo: " + req.getPathInfo()+”<br>”); out.println("Endereco remoto: " + req.getRemoteAddr()+”<br><br>”); Enumeration e = req.getHeaderNames(); while (e.hasMoreElements()) { String name = (String)e.nextElement(); String value = req.getHeader(name); out.println(name + " = " + value+"<br>"); } out.println("</body></html>"); } public void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { doGet(req, res); } } Exemplo XX.XX- Servlet que retorna as informações sobre a requisição. Note que o método doPost() chama o método doGet(), de modo que o Servlet pode receber os dois tipos de requisição. A figura XX.XX mostra o resultado de uma execução do Servlet do exemplo XX.XX. Exemplo sobre Requisicao de Info Metodo: GET Request URI: /servlet/RequestInfo Protocolo: HTTP/1.0 PathInfo: null Endereco remoto: 127.0.0.1 Connection = Keep-Alive User-Agent = Mozilla/4.7 [en] (Win95; I) Pragma = no-cache Host = localhost:8080 Accept = image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */* Accept-Encoding = gzip Accept-Language = en Accept-Charset = iso-8859-1,*,utf-8 Figura XX.XX- Saída da execução do Servlet que exibe as informações sobre a requisição. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 114 Lidando com Formulários Ser capaz de lidar com as informações contidas em formulários HTML é fundamental para qualquer tecnologia de desenvolvimento de aplicações para Web. É por meio de formulários que os usuários fornecem dados, preenchem pedidos de compra e (ainda mais importante) digitam o número do cartão de crédito. As informações digitadas no formulário chegam até o Servlet por meio do objeto HttpServletRequest e são recuperadas por meio do método getParameter() deste objeto. Todo item de formulário HTML possui um nome e esse nome é passado como argumento para o método getParameter() que retorna na forma de String o valor do item de formulário. O Servlet do exemplo XX.XX exibe o valor de dois itens de formulários do tipo text. Um denominado nome e o outro denominado de sobrenome. Em seguida o Servlet cria um formulário contendo os mesmos itens de formulário. Note que um formulário é criado por meio do tag <form>. Como parâmetros opcionais deste tag temos método da requisição (method), é a URL para onde será submetida a requisição (action). No caso do exemplo, o método adotado é o POST e a requisição será submetida ao próprio Servlet Form. import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public class Form extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { res.setContentType("text/html"); PrintWriter out = res.getWriter(); out.println("<html>"); out.println("<head><title>Trata formulario</title></head>"); out.println("<body bgcolor=\"white\">"); out.println("<h3>Trata formulario</h3>"); String nome = req.getParameter("nome"); Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 115 String sobreNome = req.getParameter("sobrenome"); if (nome != null || sobreNome != null) { out.println("Nome = " + nome + "<br>"); out.println("Sobrenome = " + sobreNome); } out.println("<P>"); out.print("<form action=\"Form\" method=POST>"); out.println("Nome : <input type=text size=20 name=nome><br>"); out.println("Sobrenome: <input type=text size=20 name=sobrenome><br>"); out.println("<input type=submit>"); out.println("</form>"); out.println("</body></html>"); } public void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { doGet(req, res); } } Exemplo XX.XX- Servlet para lidar com um formulário simples. Lidando com Cookies Um cookie nada mais é que um bloco de informação que é enviado do servidor para o navegador no cabeçalho página. A partir de então, dependendo do tempo de validade do cookie, o navegador reenvia essa informação para o servidor a cada nova requisição. Dependo do caso o cookie é também armazenado no disco da máquina cliente e quando o site é novamente visitado o cookie enviado novamente para o servidor, fornecendo a informação desejada. Os cookies foram a solução adotada pelos desenvolvedores do Netscape para implementar a identificação de clientes sobre um protocolo HTTP que não é orientado à conexão. Esta solução, apesar das controvérsias sobre a possibilidade de quebra de privacidade, passou ser amplamente adotada e hoje os cookies são parte integrante do padrão Internet, normalizados pela norma RFC 2109. A necessidade da identificação do cliente de onde partiu a requisição e o monitoramento de sua interação com o site (denominada de sessão) é importante para o desenvolvimento de sistemas para a Web pelas seguintes razões: Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II • • • 116 É necessário associar os itens selecionados para compra com o usuário que deseja adquiri-los. Na maioria da vezes a seleção dos itens e compra é feita por meio da navegação de várias páginas do site e a todo instante é necessário distinguir os usuários que estão realizando as requisições. É necessário acompanhar as interação do usuário com o site para observar seu comportamento e, a partir dessas informações, realizar adaptações no site para atrair um maior número de usuários ou realizar campanhas de marketing. É necessário saber que usuário está acessando o site para, de acordo com o seu perfil, fornecer uma visualização e um conjunto de funcionalidades adequadas às suas preferências. Todas essas necessidades não podem ser atendidas com o uso básico do protocolo HTTP, uma vez que ele não é orientado à sessão ou conexão. Com os cookies é possível contornar essa deficiência, uma vez que as informações que são neles armazenadas podem ser usadas para identificar os clientes. Existem outras formas de contornar a deficiência do protocolo de HTTP, como a codificação de URL e o uso de campos escondidos nas páginas HTML, mas o uso de cookies é a técnica mais utiliza, por ser mais simples e padronizada. No entanto, o usuário pode impedir que o navegador aceite cookies, o que torna o ato de navegar pela Web muito desagradável. Neste caso, é necessário utilizar as outras técnicas para controle de sessão. A API Servlet permite a manipulação explicita de cookies. Para controle de sessão o programador pode manipular diretamente os cookies, ou usar uma abstração de nível mais alto, implementada por meio do objeto HttpSession. Se o cliente não permitir o uso de cookies a API Servlet fornece métodos para a codificação de URL. O exemplo XX.XX mostra o uso de cookies para armazenar as informações digitadas em um formulário. import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class CookieTeste extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II res.setContentType("text/html"); PrintWriter out = res.getWriter(); out.println("<html>"); out.println("<body bgcolor=\"white\">"); out.println("<head><title>Teste de Cookies</title></head>"); out.println("<body>"); out.println("<h3>Teste de Cookies</h3>"); Cookie[] cookies = req.getCookies(); if (cookies.length > 0) { for (int i = 0; i < cookies.length; i++) { Cookie cookie = cookies[i]; out.print("Cookie Nome: " + cookie.getName() + "<br>"); out.println(" Cookie Valor: " + cookie.getValue() +"<br><br>"); } } String cName = req.getParameter("cookienome"); String cValor = req.getParameter("cookievalor"); if (cName != null && cValor != null) { Cookie cookie = new Cookie(cName ,cValor); res.addCookie(cookie); out.println("<P>"); out.println("<br>"); out.print("Nome : "+cName +"<br>"); out.print("Valor : "+cValor); } out.println("<P>"); out.print("<form action=\"CookieTeste\" method=POST>"); out.println("Nome : <input type=text length=20 name=cookienome><br>"); out.println("Valor : <input type=text length=20 name=cookievalor><br>"); out.println("<input type=submit></form>"); out.println("</body>"); out.println("</html>"); } public void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { doGet(req, res); } } Alcione de P. Oliveira, Vinícius V. Maciel - UFV 117 Java na Prática – Volume II 118 Exemplo XX.XX- Servlet para lidar com Cookies. Para se criar um cookie é necessário criar um objeto Cookie, passando para o construtor um nome e um valor, sendo ambos instâncias de String. O cookie é enviado para o navegador por meio do método addCookie() do objeto HttpServletResponse. Um vez que os cookies são enviados no cabeçalho da página, o método addCookie() deve ser chamado antes do envio de qualquer conteúdo para o navegador. Para recuperar os cookies enviados pelo navegador usa-se o método getCookies() do objeto HttpServletRequest que retorna um array de Cookie. Os métodos getName() e getvalue() do objeto Cookie são utilizados para recuperar o nome o valor da informação associada ao cookie. Os objetos da classe Cookie possuem vários métodos para controle do uso de cookies. É possível definir tempo de vida máximo do cookie, os domínios que devem receber o cookie (por default o domínio que deve receber o cookie é o que o criou), o diretório da página que deve receber o cookie, se o cookie deve ser enviado somente sob um protocolo seguro e etc. Por exemplo, para definir a idade máxima de um cookie devemos utilizar o método setMaxAge(), passando um inteiro como parâmetro. Se o inteiro for positivo indicará em segundos o tempo máximo de vida do cookie. Um valor negativo indica que o cookie deve apagado quando o navegador terminar. O valor zero indica que o cookie deve ser apagado imediatamente. O trecho de código exemplo XX.XX mostra algumas alterações no comportamento default de um cookie. ... Cookie cookie = new Cookie(cName ,cValor); cookie.setDomain(“*.uvf.br”); // todos os domínios como dpi.ufv.br mas não *.dpi.ufv.br cookie.setMaxAge (3600); // uma hora de tempo de vida ... Exemplo XX.XX- Mudanças no comportamento default do cookie. Lidando com Sessões A manipulação direta de cookies para controle de sessão é um tanto baixo nível, uma vez que o usuário deve se preocupar com a identificação, tempo de vida e outros detalhes. Por isso a API Servlet fornece um objeto com controles Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 119 de nível mais alto para monitorar a sessão, o HttpSession. O objeto HttpSession monitora a sessão utilizando cookies de forma transparente. No entanto, se o cliente não aceitar o uso de cookies é possível utilizar como alternativa a codificação de URL para adicionar o identificador da sessão. Essa opção, apesar de ser mais genérica, não á primeira opção devido a possibilidade de criação de gargalos pela necessidade da análise prévia de todas requisições que chegam ao servidor. O exemplo XX.XX mostra o uso de um objeto HttpSession para armazenar as informações digitadas em um formulário. import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public class SessionTeste extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { resp.setContentType("text/html"); PrintWriter out = resp.getWriter(); out.println("<html><head>"); out.println("<title>Teste de Sessao</title>"); out.println("</head>"); out.println("<body>"); out.println("<h3>Teste de Sessao</h3>"); HttpSession session = req.getSession(true); out.println("Identificador: " + session.getId()); out.println("<br>"); out.println("Data: "); out.println(new Date(session.getCreationTime()) + "<br>"); out.println("Ultimo acesso: "); out.println(new Date(session.getLastAccessedTime())); String nomedado = req.getParameter("nomedado"); String valordado = req.getParameter("valordado"); if (nomedado != null && valordado != null) { session.setAttribute(nomedado, valordado); } out.println("<P>"); out.println("Dados da Sessao:" + "<br>"); Enumeration valueNames = session.getAttributeNames(); Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 120 while (valueNames.hasMoreElements()) { String name = (String)valueNames.nextElement(); String value = (String) session.getAttribute(name); out.println(name + " = " + value+"<br>"); } out.println("<P>"); out.print("<form action=\"SessionTeste\" method=POST>"); out.println("Nome: <input type=text size=20 name=nomedado><br>"); out.println("Valor: <input type=text size=20 name=valordado><br>"); out.println("<input type=submit>"); out.println("</form>"); out.println("</body></html>"); } public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { doGet(req, resp); } } Exemplo XX.XX- Servlet para lidar com Sessões. Para controlar a sessão é necessário obter um objeto HttpSession por meio do método getSession() do objeto HttpServletRequest. Opcionalmente, o método getSession() recebe como argumento um valor booleano que indica se é para criar o objeto HttpSession se ele não existir (argumento true) ou se é para retorna null caso ele não exista (argumento false). Para se associar um objeto ou informação à sessão usa-se o método setAttribute() do objeto HttpSession, passando para o método um String e um objeto que será identificado pelo String. Note que o método aceita qualquer objeto e, portanto, qualquer objeto pode ser associado à sessão. Os objetos associados a uma sessão são recuperados com o uso método getAttribute() do objeto HttpSession, que recebe como argumento o nome associado ao objeto. Para se obter uma enumeração do nomes associados à sessão usa-se o método getAttributeNames() do objeto HttpSession. A figura XX.XX mostra o resultado da execução do exemplo XX.XX. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 121 Teste de Sessao Identificador: session3 Data: Sun May 28 15:19:15 GMT-03:00 2000 Ultimo acesso: Sun May 28 15:19:43 GMT-03:00 2000 Dados da Sessao: Alcione = 4 Alexandra = 6 Nome Valor Enviar Consulta Figura VI-1 Saída resultante da execução do Servlet que lida com Sessões. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 122 JSP Servlets é uma boa idéia, mas você se imaginou montando uma página complexa usando println()? Muitas vezes o desenvolvimento de um site é uma tarefa complexa que envolve vários profissionais. A tarefa de projeto do layout da página fica a cargo do Web Designer, incluindo a diagramação dos textos e imagens, aplicação de cores, tratamento das imagens, definição da estrutura da informação apresentada no site e dos links para navegação pela mesma. Já o Desenvolvedor Web é responsável pela criação das aplicações que vão executar em um site. O trabalho destes dois profissionais é somado na criação de um único produto, mas durante o desenvolvimento a interferência mutua deve ser a mínima possível. Ou seja, um profissional não deve precisar alterar o que é foi feito pelo outro profissional para cumprir sua tarefa. A tecnologia Servlet não nos permite atingir esse ideal. Por exemplo, suponha que um Web Designer terminou o desenvolvimento de uma página e a entregou para o Desenvolvedor Web codificar em um Servlet. Se após a codificação o Web Designer desejar realizar uma alteração na página será necessário que ele altere o código do Servlet (do qual ele nada entende) ou entregar uma nova página para o Desenvolvedor Web para que ele a codifique totalmente mais uma vez. Qualquer uma dessas alternativas são indesejáveis e foi devido a esse problema a Sun desenvolveu uma tecnologia baseada em Servlets chamada de JSP. Java Server Pages (JSP) são páginas HTML que incluem código Java e outros tags especiais. Desta forma as partes estáticas da página não precisam ser geradas por println(). Elas são fixadas na própria página. A parte dinâmica é gerada pelo código JSP. Assim a parte estática da página pode ser projetada por um Web Designer que nada sabe de Java. A primeira vez que uma página JSP é carregada pelo container JSP o código Java é compilado gerando um Servlet que é executado, gerando uma página HTML que é enviada para o navegador. As chamadas subsequentes são enviadas diretamente ao Servlet gerado na primeira requisição, não ocorrendo mais as etapas de geração e compilação do Servlet. A figura XX.XX mostra um esquema das etapas de execução de uma página JSP na primeira vez que é requisitada. Na etapa (1) a requisição é enviada para um servidor Web que reencaminha a requisição (etapa 2) para o container Servlet/JSP. Na etapa (3) o container verifica que não existe nenhuma instância de Servlet correspondente à página JSP. Neste caso, a página JSP é traduzida para código fonte de uma classe Servlet que será usada na resposta à requisição. Na etapa (4) o código fonte do Servlet é compilado, e na etapa (5) é criada uma Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 123 instância da classe. Finalmente, na etapa (6) é invocado o método service() da instância Servlet para gerar a resposta à requisição. Navegador (1) Requisição de página JSP Servidor Http (2) Encaminha a requisição (6) Resposta à requisição Container Servlet/JSP (5) Instancia e executa Bytecode Servlet (3) Traduz Página jsp (4) Compila Fonte Servlet Figura VI-1 Etapas da primeira execução de uma página JSP. A idéia de se usar scripts de linguagens de programação em páginas HTML que são processados no lado servidor para gerar conteúdo dinâmico não é restrita à linguagem Java. Existem várias soluções desse tipo fornecida por outros fabricantes. Abaixo segue uma comparação de duas das tecnologias mais populares com JSP. PHP X JSP PHP (Personal Home Pages) é uma linguagem script para ser executada no lado servidor criada em 1994 como um projeto pessoal de Rasmus Lerdorf. Atualmente encontra-se na versão 4. A sintaxe é fortemente baseada em C mas possui elementos de C++, Java e Perl. Possui suporte à programação OO por meio de classes e objetos. Possui também suporte extensivo à Banco de dados ODBC, MySql, Sybase, Oracle e outros. PHP é uma linguagem mais fácil no desenvolvimento de pequenas aplicações para Web em relação à JSP, uma vez Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 124 que é uma linguagem mais simples e menos rígida do que JSP. No entanto, a medida que passamos para aplicações de maior porte, o uso de PHP não é indicado, uma vez que necessário o uso de linguagens com checagem mais rígidas e com maior suporte à escalabilidade, como é o caso de Java. ASP X JSP ASP (Active Server Pages) é a solução desenvolvida pela Microsoft® para atender as requisições feitas à servidores Web. Incorporada inicialmente apenas ao Internet Information Server (IIS), no entanto, atualmente já é suportada por outros servidores populares, como o Apache. O desenvolvimento de páginas que usam ASP envolve a produção de um script contendo HTML misturado com blocos de código de controle ASP. Este código de controle pode conter scripts em JavaScript ou VBScript. A primeira vantagem de JSP sobre ASP é que a parte dinâmica é escrita em Java e não Visual Basic ou outra linguagem proprietária da Microsoft, portanto JSP é mais poderoso e fácil de usar. Em segundo lugar JSP é mais portável para outros sistemas operacionais e servidores WEB que não sejam Microsoft. Primeiro exemplo em JSP Para que o leitor possa ter uma idéia geral da tecnologia JSP apresentaremos agora a versão JSP do Olá mundo. O exemplo XX.XX mostra o código da página. <html> <head> <title>Exemplo JSP</title> </head> <body> <% String x = "Ol&aacute; Mundo!"; %> <%=x%> </body> </html> Exemplo XX.XX- Versão JSP do Olá mundo. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 125 Quem está habituado aos tags HTML notará que se trata basicamente de uma página HTML contendo código Java delimitado pelos símbolos “<%” e “%>”. Para facilitar a visualização destacamos os scripts Java com negrito. No primeiro trecho de script é declarada uma variável x com o valor “Olá mundo” (a seqüência &acute; é denota ‘á’ em HTML). No segundo trecho de script o conteúdo da variável x é extraído e colocado na página resultante da execução do Servlet correspondente. Em seguida mostraremos como executar o exemplo XX.XX. Executando o arquivo JSP Para executar o exemplo XX.XX salve-o com a extensão .jsp. Por exemplo ola.jsp. Se você estiver usando o servidor Tomcat, coloque-o arquivo no subdiretório /webapps/examples/jsp do Tomcat. Por exemplo examples/jsp/teste. Para invocar o arquivo JSP basta embutir a URL em uma página ou digitar diretamente a seguinte URL no navegador. http://localhost:8080/examples/jsp/ola.jsp Usamos o diretório /webapps/examples/jsp para testar rapidamente o exemplo. Para desenvolver uma aplicação é aconselhável criar um diretório apropriado como mostrado na seção que tratou de Servlets. O Servlet criado a partir da página JSP é colocado em um diretório de trabalho. No caso do Tomcat o Servlet é colocado em subdiretório associado à aplicação subordinado ao diretório /work do Tomcat. O exemplo XX.XX mostra os principais trechos do Servlet criado a partir da tradução do arquivo ola.jsp pelo tradutor do Tomcat. Note que o Servlet é subclasse de uma classe HttpJspBase e não da HttpServlet. Além disso, o método que executado em resposta à requisição é o método _jspService() e não o método service(). Note também que todas as partes estáticas da página JSP são colocadas como argumentos do método write() do objeto referenciado out. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 126 public class _0002fjsp_0002fola_00032_0002ejspola_jsp_0 extends HttpJspBase { .... public void _jspService(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { .... PageContext pageContext = null; HttpSession session = null; ServletContext application = null; ServletConfig config = null; JspWriter out = null; ... try { ... out.write("<html>\r\n <head>\r\n <title>Exemplo JSP</title>\r\n </head>\r\n <body>\r\n"); String x = "Ol&aacute; Mundo!"; out.write("\r\n"); out.print(x); out.write("\r\n </body>\r\n</html>\r\n"); ... } catch (Exception ex) { ... } } } Exemplo XX.XX- Servlet correspondente à página JSP do Olá mundo. Objetos implícitos No exemplo XX.XX pode-se ver a declaração de variáveis que referenciam a alguns objetos importantes. Estas variáveis estão disponíveis para o projetista da página JSP. As variáveis mais importantes são: Classe Variável HttpServletRequest HttpServletResponse PageContext ServletContext HttpSession JspWriter request response pageContext application session out Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 127 Os objetos referenciados pelas variáveis request e response já tiveram seu uso esclarecido na seção sobre Servlets. O objeto do tipo JspWriter tem a mesma função do PrinterWriter do Servlet. Os outros objetos terão sua função esclarecida mais adiante. Tags JSP Os tags JSP possuem a seguinte forma geral: <% Código JSP %> O primeiro caractere % pode ser seguido de outros caracteres que determinam o significado preciso do código dentro do tag. Os tags JSP possuem correspondência com os tags XML. Existem cinco categorias de tags JSP: Expressões Scriptlets Declarações Diretivas Comentários Em seguida comentaremos cada uma dessas categorias. Expressões <%= expressões %> Expressões são avaliadas, convertidas para String e colocadas na página enviada. A avaliação é realizada em tempo de execução, quando a página é requisitada. Exemplos: <%= new java.util.Date() %> <%= request.getMethod() %> No primeiro exemplo será colocado na página a data corrente em milésimo de segundos e no segundo será colocado o método usado na Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 128 requisição. Note que cada expressão contém apenas um comando Java. Note também que o comando Java não é terminado pelo caractere ‘;’. Scriptlets <% código Java %> Quando é necessário mais de um comando Java ou o resultado da computação não é para ser colocado na página de resposta é preciso usar outra categoria de tags JSP: os Scriptlets . Os Scriptlets permitem inserir trechos de código em Java na página JSP. O exemplo XX.XX mostra uma página JSP contendo um Scriptlet que transforma a temperatura digitada em celcius para o equivalente em Fahrenheit. <html> <head><title>Conversao Celcius Fahrenheit </title></head> <body> <% String valor = request.getParameter("celcius"); if (valor != null ) { double f = Double.parseDouble(valor)*9/5 +32; out.println("<P>"); out.println("<h2>Valor em Fahrenheit:" +f +"<h2><br>"); } %> <form action=conversao.jsp method=POST> Celcius: <input type=text size=20 name=celcius><br> <input type=submit> </form> </body> </html> Exemplo XX.XX- Página JSP que converte graus Celcius para Fahrenheit. Note o uso das variáveis request e out sem a necessidade de declaração. Todo o código digitado é inserido no método _jspService(). A figura XX.XX mostra o resultado da requisição após a digitação do valor 30 na caixa de texto do formulário. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 129 Valor em Fahrenheit:86.0 Celcius: Enviar Consulta Figura VI-XX Resultado da conversão de 30 graus celcius. O código dentro do scriptlet é inserido da mesma forma que é escrito e todo o texto HTML estático antes e após ou um scriptlet é convertido para comandos print(). Desta forma o scriptlets não precisa conter comandos para código estático e blocos de controle abertos afetam o código HTML envolvidos por scriptlets. O exemplo XX.XX mostra dois formas de se produzir o mesmo efeito. No código da esquerda os Scriplets se intercalam com código HTML. O código HTML, quando da tradução da página JSP para Servlet é inserido como argumentos de métodos println() gerando o código da direita. Ambas as formas podem ser usadas em páginas JSP e produzem o mesmo efeito. Previs&atilde;o do Tempo <% if (Math.random() < 0.5) { %> Hoje vai <B>fazer sol</B>! <% } else { %> Hoje vai <B>chover</B>! <% } %> out.println("Previs&atilde;o do Tempo"); if (Math.random() < 0.5) { out.println(" Hoje vai <B>fazer sol</B>!"); } else { out.println(" Hoje vai <B>chover</B>!"); } Exemplo XX.XX- Dois códigos equivalentes. Declarações <%! Código Java %> Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 130 Uma declaração JSP permite definir variáveis ou métodos que são inseridos no corpo do Servlet. Como as declarações não geram saída, elas são normalmente usadas em combinação com expressões e scriptlets. O Exemplo XX.XX mostra a declaração de uma variável que é usada para contar o número de vezes que a página corrente foi requisitada desde que foi carregada. <%! Private int numAcesso = 0; %> Acessos desde carregada: <%= ++ numAcesso %> Exemplo XX.XX- Declaração de uma variável usando o tag de declaração. As variáveis declaradas desta forma serão variáveis de instância. Já as variáveis declaradas em Scriptlets são variáveis locais ao método _jspService(). Por isso é possível contar o número de requisições com o exemplo XX.XX. Se variável fosse declarada em um Scriptlet a variável seria local ao método _jspService() e, portanto, teria seu valor reinicializado a cada chamada. Como já foi dito, os tags de declarações permitem a declaração de métodos. O Exemplo XX.XX mostra a declaração de um método que converte celcius para Fahrenheit. <%! private double converte(double c) { return c*9/5 +32; } %> Exemplo XX.XX- Declaração de um método para a conversão de celcius para Fahrenheit. Comentários Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 131 Existem dois tipos de comentários utilizados em páginas JSP. O primeiro exclui todo o bloco comentado da saída gerada pelo processamento da página. A forma geral deste tipo de comentário é a seguinte: <%--comentário --%> O segundo tipo de comentário é o utilizado em páginas HTML. Neste caso o comentário é enviado dentro da página de resposta. A forma geral deste tipo de comentário é a seguinte: <!—comentário --> Diretivas Diretivas são mensagens para JSP container. Elas não enviam nada para a página mas são importantes para definir atributos JSP e dependências com o JSP container. A forma geral da diretivas é a seguinte: <%@ Diretiva atributo="valor" %> ou <%@ Diretiva atributo1 ="valor1" atributo2 ="valor2" ... atributoN =" valorN " %> Em seguida comentaremos as principais diretivas. Diretiva page <%@ page atributo1 ="valor1" ... atributoN =" valorN " %> A diretiva page permite a definição dos seguintes atributos: import contentType isThreadSafe Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 132 session buffer autoflush info errorPage isErrorPage language Segue a descrição de cada um desses atributos. Atributo e Forma Geral Descrição import="package.class" Permite especificar os pacotes que devem ser importados para serem usados na página JSP. ou Exemplo: import="package.class1,.. .,package.classN" <%@ page import="java.util.*" %> contentType="MIME-Type" Especifica o tipo MIME da saída. O default é text/html. Exemplo: <%@ page contentType="text/plain" %> possui o mesmo efeito do scriptlet <% response.setContentType("text/plain") ; %> isThreadSafe="true|false" Um valor true (default) indica um processamento normal do Servlet, onde múltiplas requisições são processadas simultaneamente. Um valor false indica que o processamento deve ser feito por instancias separadas do Servlet ou serialmente. session="true|false” Um valor true (default) indica que a variável predefinida session (HttpSession) deve ser associada à sessão, se existir, caso contrário uma nova sessão deve ser criada e associada a ela. Um valor false indica que nenhuma sessão será usada. buffer="sizekb|none" Especifica o tamanho do buffer para escrita usado pelo objeto JspWriter. O tamanho default não é menor que 8k.. autoflush="true|false” Um valor true (default) indica que o buffer deve ser Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II info="mensagem" errorPage="url” isErrorPage="true|false” Language="java” 133 esvaziado quando estiver cheio. Define uma cadeia de caracteres que pode ser recuperada via getServletInfo(). Especifica a página JSP que deve ser processada em caso de exceções não capturadas. Indica se a página corrente pode atuar como página de erro para outra página JSP. O default é false. Possibilita definir a linguagem que está sendo usada. No momento a única possibilidade é Java. Tabela VI.XX –Atributos da diretiva page. Diretiva include <%@ include file="relative url" %> Permite incluir arquivos no momento em que a página JSP é traduzida em um Servlet. Exemplo: <%@ include file="/meuarq.html" %> Extraindo Valores de Formulários Uma página JSP, da mesma forma que um Servlet, pode usar o objeto referenciado pela variável request para obter os valores dos parâmetros de um formulário. O exemplo XX.XX usado para converter graus Celcius em Fahrenheit fez uso deste recurso. O exemplo XX.XX mostra outra página JSP com formulário. Note que o scriptlet é usado para obter o nome e os valores de todos os parâmetros contidos no formulário. Como o método getParameterNames() retorna uma referência a um objeto Enumeration é preciso importar o pacote java.util, por meio da diretiva page. <%@ page import="java.util.*" %> <html><body> <H1>Formulário</H1> <% Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 134 Enumeration campos = request.getParameterNames(); While(campos.hasMoreElements()) { String campo = (String)campos.nextElement(); String valor = request.getParameter(campo); %> <li><%= campo %> = <%= valor %></li> <% } %> <form method="POST" action="form.jsp"> Nome: <input type="text" size="20" name="nome" ><br> Telefone: <input type="text" size="20" name="telefone"><br> <INPUT TYPE=submit name=submit value="envie"> </form> </body></html> Exemplo VI.XX – Página JSP com formulário. A figura XX.XX mostra o resultado da requisição após a digitação dos valores Alcione e 333-3333 nas caixas de texto do formulário. Formulário • • • telefone = 333-3333 nome = Alcione submit = envie Nome: Telefone: envie Figura VI.XX- Saída do exemplo XX.XX. Criando e Modificando Cookies Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 135 Da mesma for que em Servlets os cookies em JSP são tratados por meio da classe Cookie. Para recuperar os cookies enviados pelo navegador usa-se o método getCookies() do objeto HttpServletRequest que retorna um arranjo de Cookie. Os métodos getName() e getvalue() do objeto Cookie são utilizados para recuperar o nome o valor da informação associada ao cookie. O cookie é enviado para o navegador por meio do método addCookie() do objeto HttpServletResponse. O exemplo XX.XX mostra uma página JSP que exibe todos os cookies recebidos em uma requisição e adiciona mais um na resposta. <html><body> <H1>Session id: <%= session.getId() %></H1> <% Cookie[] cookies = request.getCookies(); For(int i = 0; i < cookies.length; i++) { %> Cookie name: <%= cookies[i].getName() %> <br> Value: <%= cookies[i].getValue() %><br> antiga idade máxima em segundos: <%= cookies[i].getMaxAge() %><br> <% cookies[i].setMaxAge(5); %> nova idade máxima em segundos: <%= cookies[i].getMaxAge() %><br> <% } %> <%! Int count = 0; int dcount = 0; %> <% response.addCookie(new Cookie( ”Cookie " + count++, ”Valor " + dcount++)); %> </body></html> Exemplo VI.XX – Página JSP que exibe os cookies recebidos. A figura XX.XX mostra o resultado após três acessos seguidos à página JSP. Note que existe um cookie a mais com o nome JSESSIONID e valor igual à sessão. Este cookie é o usado pelo container para controlar a sessão. Session id: 9ppfv0lsl1 Cookie name: Cookie 0 value: Valor 0 antiga idade máxima em segundos: -1 nova idade máxima em segundos: 5 Cookie name: Cookie 1 Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 136 value: Valor 1 antiga idade máxima em segundos: -1 nova idade máxima em segundos: 5 Cookie name: JSESSIONID value: 9ppfv0lsl1 antiga idade máxima em segundos: -1 nova idade máxima em segundos: 5 Figura VI.XX- Saída do exemplo XX.XX após três acessos. Lidando com sessões O atributos de uma sessão são mantidos em um objeto HttpSession referenciado pela variável session. Pode-se armazenar valores em uma sessão por meio do método setAttribute() e recuperá-los por meio do método getAttribute(). O tempo de duração default de uma sessão inativa (sem o recebimento de requisições do usuário) é 30 minutos mas esse valor pode ser alterado por meio do método setMaxInactiveInterval(). O exemplo XX.XX mostra duas páginas JSP. A primeira apresenta um formulário onde podem ser digitados dois valores recebe dois valores de digitados em um formulário e define o intervalo máximo de inatividade de uma sessão em 10 segundos. A segunda página recebe a submissão do formulário, insere os valores na sessão e apresenta os valores relacionados com a sessão assim como a identificação da sessão. <%@ page import="java.util.*" %> <html><body> <H1>Formulário</H1> <H1>Id da sess&atilde;o: <%= session.getId() %></H1> <H3><li>Essa sess&atilde;o foi criada em <%= session.getCreationTime() %></li></H3> <H3><li>Antigo intervalo de inatividade = <%= session.getMaxInactiveInterval() %></li> <% session.setMaxInactiveInterval(10); %> <li>Novo intervalo de inatividade= <%= session.getMaxInactiveInterval() %></li> </H3> Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 137 <% Enumeration atribs = session.getAttributeNames(); while(atribs.hasMoreElements()) { String atrib = (String)atribs.nextElement(); String valor = (String)session.getAttribute(atrib); %> <li><%= atrib %> = <%= valor %></li> <% } %> <form method="POST" action="sessao2.jsp"> Nome: <input type="text" size="20" name="nome" ><br> Telefone: <input type="text" size="20" name="telefone" > <br> <INPUT TYPE=submit name=submit value="envie"> </form> </body></html> <html><body> <H1>Id da sess&atilde;o: <%= session.getId() %></H1> <% String nome = request.getParameter("nome"); String telefone = request.getParameter("telefone"); if (nome !=null && nome.length()>0) session.setAttribute("nome",nome); if (telefone !=null &&telefone.length()>0) session.setAttribute("telefone",telefone); %> <FORM TYPE=POST ACTION=sessao1.jsp> <INPUT TYPE=submit name=submit Value="Retorna"> </FORM> </body></html> Exemplo VI.XX – Exemplo do uso de sessão. O exemplo XX.XX mostra que a sessão é mantida mesmo quando o usuário muda de página. As figura XX.XX e XX.XX mostram o resultado da requisição após a digitação dos valores Alcione e 333-3333 nas caixas de texto do formulário, à submissão para página sessao2.jsp e o retorno à página sessao1.jsp. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II Formulário Id da sessão: soo8utc4m1 Essa sessão foi criada em 1002202317590 Antigo intervalo de inatividade = 1800 Novo intervalo de inatividade= 10 telefone = 333-3333 nome = Alcione Nome: Telefone: envie Figura VI.XX- Tela da página sessao1.jsp. Id da sessão: soo8utc4m1 Retorna Figura VI.XX- Tela da página sessao2.jsp. O Uso de JavaBeans Alcione de P. Oliveira, Vinícius V. Maciel - UFV 138 Java na Prática – Volume II 139 A medida que o código Java dentro do HTML torna-se cada vez mais complexo o desenvolvedor pode-se perguntar: Java em HTML não é o problema invertido do HTML em Servlet? O resultado não será tão complexo quanto produzir uma página usando println()? Em outras palavras, estou novamente misturando conteúdo com forma? Para solucionar esse problema a especificação de JSP permite o uso de JavaBeans para manipular a parte dinâmica em Java. JavaBeans já foram descritos detalhadamente em um capítulo anterior, mas podemos encarar um JavaBean como sendo apenas uma classe Java que obedece a uma certa padronização de nomeação de métodos, formando o que é denominado de propriedade. As propriedades de um bean são acessadas por meio de métodos que obedecem a convenção getXxxx e setXxxx. , onde Xxxx é o nome da propriedade. Por exemplo, getItem() é o método usado para retornar o valor da propriedade item. A sintaxe para o uso de um bean em uma página JSP é: <jsp:useBean id="nome" class="package.class" /> Onde nome é o identificador da variável que conterá uma referência para uma instância do JavaBean. Você também pode modificar o atributo scope para estabelecer o escopo do bean além da página corrente. <jsp:useBean id="nome" scope="session" class="package.class" /> Para modificar as propriedades de um JavaBean você pode usar o jsp:setProperty ou chamar um método explicitamente em um scriptlet. Para recuperar o valor de uma propriedade de um JavaBean você pode usar o jsp:getProperty ou chamar um método explicitamente em um scriptlet. Quando é dito que um bean tem uma propriedade prop do tipo T significa que o bean deve prover um método getProp() e um método do tipo setProp(T). O exemplo XX.XX mostra uma página JSP e um JavaBean. A página instancia o JavaBean, altera a propriedade mensagem e recupera o valor da propriedade, colocando-o na página. Página bean.jsp <HTML> <HEAD> <TITLE>Uso de beans</TITLE> </HEAD> <BODY> <CENTER> Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 140 <TABLE BORDER=5> <TR><TH CLASS="TITLE"> Uso de JavaBeans </TABLE> </CENTER> <P> <jsp:useBean id="teste" class=”curso.BeanSimples" /> <jsp:setProperty name="teste" property="mensagem" value=”Ola mundo!" /> <H1> Mensagem: <I> <jsp:getProperty name="teste" property="mensagem" /> </I></H1> </BODY> </HTML> Arquivo Curso/BeanSimples.java package curso; public class BeanSimples { private String men = "Nenhuma mensagem"; public String getMensagem() { return(men); } public void setMensagem(String men) { this.men = men; } } Exemplo VI.XX – Exemplo do uso de JavaBean. A figura XX.XX mostra o resultado da requisição dirigida à página bean.jsp. Uso de JavaBeans Mensagem: Ola mundo! Figura VI.XX- Resultado da requisição à página bean.jsp. Se no tag setProperty usarmos o valor “*” para o atributo property então todos os valores de elementos de formulários que possuírem nomes iguais à propriedades serão transferidos para as respectivas propriedades Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 141 no momento do processamento da requisição. Por exemplo, seja uma página jsp contendo um formulário com uma caixa de texto com nome mensagem, como mostrado no exemplo XX.XX. Note que, neste caso, a propriedade mensagem do JavaBean tem seu valor atualizado para o valor digitado na caixa de texto, sem a necessidade de uma chamada explícita no tag setProperty. Os valores são automaticamente convertidos para o tipo correto no bean. <HTML> <HEAD><TITLE>Uso de beans</TITLE> </HEAD> <BODY> <CENTER> <TABLE BORDER=5> <TR><TH CLASS="TITLE"> Uso de JavaBeans </TABLE> </CENTER> <P> <jsp:useBean id="teste" class="curso.BeanSimples" /> <jsp:setProperty name="teste" property="*" /> <H1> Mensagem: <I> <jsp:getProperty name="teste" property="mensagem" /> </I></H1> <form method="POST" action="bean2.jsp"> Texto: <input type="text" size="20" name="mensagem" ><br> <INPUT TYPE=submit name=submit value="envie"> </form> </BODY> </HTML> Exemplo VI.XX – Exemplo de atualização automática da propriedade. A figura XX.XX mostra o resultado da requisição dirigida à página bean2.jsp após a digitação do texto Olá! Uso de JavaBeans Mensagem: Ola! Texto: envie Figura VI.XX- Resultado da requisição à página bean2.jsp. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 142 Escopo Existem quatro valores possíveis para o escopo de um objeto: page, request, session e application. O default é page. A tabela XX.XX descreve cada tipo de escopo. Escopo page request session application Descrição Objetos declarados com nesse escopo são válidos até a resposta ser enviada ou a requisição ser encaminhada para outro programa no mesmo ambiente, ou seja, só podem ser referenciados nas páginas onde forem declarados. Objetos declarados com escopo page são referenciados pelo objeto pagecontext. Objetos declarados com nesse escopo são válidos durante a requisição e são acessíveis mesmo quando a requisição é encaminhada para outro programa no mesmo ambiente. Objetos declarados com escopo request são referenciados pelo objeto request. Objetos declarados com nesse escopo são válidos durante a sessão desde que a página seja definida para funcionar em uma sessão. Objetos declarados com escopo session são referenciados pelo objeto session. Objetos declarados com nesse escopo são acessíveis por páginas no mesmo servidor de aplicação. Objetos application declarados com escopo são referenciados pelo objeto application. Tabela VI.XX –Escopo dos objetos nas páginas JSP. Implementação de um Carrinho de compras O exemplo abaixo ilustra o uso de JSP para implementar um carrinho de compras virtual. O carrinho de compras virtual simula um carrinho de compras de supermercado, onde o cliente vai colocando os produtos selecionados para compra até se dirigir para o caixa para fazer o pagamento. No carrinho de compras virtual os itens selecionados pelo usuário são armazenados em uma estrutura de dados até que o usuário efetue o pagamento. Esse tipo de exemplo Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 143 exige que a página JSP funcione com o escopo session para manter o carrinho de compras durante a sessão. O exemplo XX.XX mostra um exemplo simples de implementação de carrinho de compras. O exemplo é composto por dois arquivos: um para a página JSP e um para o JavaBean que armazena os itens selecionados. Página compras.jsp <html> <jsp:useBean id="carrinho" scope="session" class="compra.Carrinho" /> <jsp:setProperty name="carrinho" property="*" /> <body bgcolor="#FFFFFF"> <% carrinho.processRequest(request); String[] items = carrinho.getItems(); if (items.length>0) { %> <font size=+2 color="#3333FF">Voc&ecirc; comprou os seguintes itens:</font> <ol> <% for (int i=0; i<items.length; i++) { out.println("<li>"+items[i]); } } %> </ol> <hr> <form type=POST action= compras.jsp> <br><font color="#3333FF" size=+2>Entre um item para adicionar ou remover: </font><br> <select NAME="item"> <option>Televis&atilde;o <option>R&aacute;dio <option>Computador <option>V&iacute;deo Cassete </select> <p><input TYPE=submit name="submit" value="adicione"> <input TYPE=submit name="submit" value="remova"></form> </body> </html> JavaBean compra/Carrinho.java package compra; Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 144 import javax.servlet.http.*; import java.util.Vector; import java.util.Enumeration; public class Carrinho { Vector v = new Vector(); String submit = null; String item = null; private void addItem(String name) {v.addElement(name); } private void removeItem(String name) {v.removeElement(name); } public void setItem(String name) {item = name; } public void setSubmit(String s) { submit = s; } public String[] getItems() { String[] s = new String[v.size()]; v.copyInto(s); return s; } private void reset() { submit = null; item = null; } public void processRequest(HttpServletRequest request) { if (submit == null) return; if (submit.equals("adicione")) addItem(item); else if (submit.equals("remova")) removeItem(item); reset(); } } Exemplo VI.XX – Implementação de um carrinho de compras Virtual. O exemplo XX.XX implementa apenas o carrinho de compras, deixando de fora o pagamento dos itens, uma vez que esta etapa depende de cada sistema. Geralmente o que é feito é direcionar o usuário para outra página onde ele digitará o número do cartão de crédito que será transmitido por meio de uma conexão segura para o servidor. Existem outras formas de pagamento, como Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 145 boleto bancário e dinheiro virtual. O próprio carrinho de compras geralmente é mais complexo, uma vez que os para compra devem ser obtidos dinamicamente de um banco de dados. A figura XX.XX mostra a tela resultante de algumas interações com o carrinho de compras. Figura VI.XX- Carrinho de compras virtual. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 146 Reencaminhando ou Redirecionando requisições Existem algumas situações onde pode ser desejável transferir uma requisição para outra URL. Isto é feito com frequência em sistemas que combinam o uso de Servlets juntamente com JSP. No entanto, a transferência pode ser para qualquer recurso. Assim, podemos transferir uma requisição de um Servlet para uma página JSP, HTML ou um Servlet. Da mesma forma uma página JSP pode transferir uma requisição para uma página JSP, HTML ou um Servlet. Existem dois tipos de transferência de requisição: o redirecionamento e o reencaminhamento. O redirecionamento é obtido usando o método sendRedirect() de uma instância HttpServletResponse, passando como argumento a URL de destino. O exemplo XX.XX mostra o código de um Servlet redirecionando para uma página HTML. import javax.servlet.*; import javax.servlet.http.*; import java.io.*; public class Redireciona extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.sendRedirect("/test/index.html"); } } Exemplo VI.XX – Redirecionamento de requisição. Note pelo exemplo que é preciso passar o contexto do recurso (/teste). No caso de redirecionamento o a requisição corrente é perdida e uma nova requisição é feita para a URL de destino. Por isso não se deve associar nenhum objeto à requisição, uma vez que o objeto HttpServletRequest corrente será perdido. O que ocorre na prática é que o servidor envia uma mensagem HTTP 302 de volta para o cliente informando que o recurso foi transferido para outra URL e o cliente envia uma nova requisição para a URL informada. Já no caso de reencaminhamento a requisição é encaminhada diretamente para a nova URL mantendo todos os objetos associados e evitando uma nova ida Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 147 ao cliente. Portanto, o uso de reencaminhamento é mais eficiente do que o uso de redirecionamento. O reencaminhamento é obtido usando o método forward() de uma instância RequestDispatcher, passando como argumento os objetos HttpServletRequest e HttpServletResponse para a URL de destino. Uma instância RequestDispatcher é obtida por meio do método getRequestDispatcher()de uma instância ServletContext , que é obtido, por sua vez, por meio do método getServletContext() do Servlet. O exemplo XX.XX mostra o código de um Servlet reencaminhando a requisição para uma página JSP. import javax.servlet.*; import javax.servlet.http.*; public class Reencaminha extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) { try { getServletContext().getRequestDispatcher("/index.html"). forward(request,response); }catch (Exception e) { System.out.println("Servlet falhou: "); e.printStackTrace(); } } } Exemplo VI.XX – Reencaminhamento de requisição. Note que não é necessário passar o contexto na URL, como é feito no redirecionamento, uma vez que a requisição é encaminhada no contexto corrente. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 148 Uma Arquitetura para comércio eletrônico O projeto de uma solução para comércio eletrônico é uma tarefa complexa e deve atender diversos requisitos. Nesta seção mostraremos uma modelo de arquitetura básico para comércio eletrônico que pode ser adaptado para soluções mais específicas. Este modelo implementa o padrão de projeto MVC, procurando, desta forma, isolar esses aspectos de um sistema de computação. Tipos de aplicações na WEB Podemos enquadra as aplicações na Web em um dos seguintes tipos: • Business-to-consumer (B2C) – entre empresa e consumidor. Exemplo: uma pessoa compra um livro na Internet. • Business-to-business (B2B) – Troca de informações e serviços entre empresas. Exemplo: o sistema de estoque de uma empresa de automóveis detecta que um item de estoque precisa ser resposta e faz o pedido diretamente ao sistema de produção do fornecedor de autopeças. Neste tipo de aplicação a linguagem XML possui um papel muito importante, uma vez que existe a necessidade de uma padronização dos tags para comunicação de conteúdo. • User-to-data – acesso à bases de informação. Exemplo: uma usuário consulta uma base de informação. • User-to-user – chat, e troca de informações entre usuários (Morpheus). O exemplo que mostraremos é tipicamente um caso de User-to-data, (agenda eletrônica na Web) mas possui a mesma estrutura de um B2C. Arquitetura MVC para a Web A figura XX.XX contém um diagrama de blocos que mostra a participação de Servlets, JSP e JavaBeans na arquitetura proposta. A idéia é isolar cada aspecto do modelo MVC com a tecnologia mais adequada. A página Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 149 JSP é ótima para fazer o papel da visão, uma vez que possui facilidades para a inserção de componentes visuais e para a apresentação de informação. No entanto, é um pouco estranho usar uma página JSP para receber e tratar uma requisição. Esta tarefa, que se enquadra no aspecto de controle do modelo MVC é mais adequada a um Servlet, uma vez que neste momento componentes de apresentação são indesejáveis. Finalmente, é desejável que a modelagem do negócio fique isolada dos aspectos de interação. A proposta é que a modelagem do negócio fique contida em classes de JavaBeans. Em aplicações mais sofisticadas a modelagem do negócio deve ser implementada por classes de Enterprise JavaBeans (EJB), no entanto esta forma de implementação foge ao escopos deste livro. Cada componente participa da seguinte forma: • Servlets – Atuam como controladores, recebendo as requisições dos usuários. Após a realização das análises necessária sobre a requisição, instancia o JavaBean e o armazena no escopo adequado (ou não caso o bean já tenha sido criado no escopo) e encaminha a requisição para a página JSP. • JavaBeans – Atuam como o modelo da solução, independente da requisição e da forma de apresentação. Comunicam-se com a camada intermediária que encapsula a lógica do problema. • JSP – Atuam na camada de apresentação utilizando os JavaBeans para obtenção dos dados a serem exibidos, isolando-se assim de como os dados são obtidos. O objetivo é minimizar a quantidade de código colocado na página. • Camada Intermediária (Middleware) – Incorporam a lógica de acesso aos dados. Permitem isolar os outros módulos de problemas como estratégias de acesso aos dados e desempenho. O uso de EJB (Enterprise JavaBeans) é recomendado para a implementação do Middleware, uma vez que os EJBs possuem capacidades para gerência de transações e persistência. Isto implica na adoção de um servidor de aplicação habilitado para EJB. A figura XX.XX mostra a interação entre os componentes. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 150 Servidor de Aplicação 1 Navegador Web Requisição Cria uma instância Servlet (controlador) 2 3 JavaBean (modelo) 4 5 Resposta JSP (Apresentação) MiddleWare JDBC SGBD Figura XV.XX. Arquitetura de uma aplicação para Comércio Eletrônico. Essa arquitetura possui as seguintes vantagens: 1. Facilidade de manutenção: a distribuição lógica das funções entre os módulos do sistema isola o impacto das modificações. 2. Escalabilidade: Modificações necessária para acompanhar o aumento da demanda de serviços (database pooling, clustering, etc) ficam concentradas na camada intermediária. A figura abaixo mostra a arquitetura física de uma aplicação de comércio eletrônico. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 151 Figura XV.XX. Arquitetura física de uma aplicação para Comércio Eletrônico. Demilitarized Zone (DMZ) é onde os servidores HTTP são instalados. A DMZ é protegida da rede púbica por um firewall, também chamado de firewall de protocolo. O firewall de protocolo deve ser configurado para permitir tráfego apenas através da porta 80. Um segundo firewall, também chamado de firewall de domínio separa a DMZ da rede interna. O firewall de domínio deve ser configurado para permitir comunicação apenas por meio das portas do servidor de aplicação Agenda Web: Um Exemplo de uma aplicação Web usando a arquitetura MVC O exemplo a seguir mostra o desenvolvimento da agenda eletrônica para o funcionamento na Web. A arquitetura adotada é uma implementação do modelo MVC. Apenas, para simplificar a solução, a camada intermediária foi simplificada e é implementada por um JavaBean que tem a função de gerenciar a conexão com o banco de dados. O banco de dados será composto por duas tabelas, uma para armazenar os usuários autorizados a usar a tabela e outra para armazenar os itens da agenda. A figura XX.XX mostra o esquema conceitual do banco de dados e a figura XX.XX mostra o comando para a criação das tabelas. Note que existe um relacionamento entre a tabela USUARIO e a tabela PESSOA, mostrando que os dados pessoais sobre o usuário ficam armazenados na agenda. USUARIO PESSOA Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 1:1 152 1:1 Figura XV.XX. Esquema conceitual do banco de dados para a agenda. As tabelas do BD devem ser criadas de acordo com o seguinte script: CREATE TABLE PESSOA (ID INT PRIMARY KEY, NOME VARCHAR(50) NOT NULL, TELEFONE VARCHAR(50), ENDERECO VARCHAR(80), EMAIL VARCHAR(50), HP VARCHAR(50), CELULAR VARCHAR(20), DESCRICAO VARCHAR(80)); CREATE TABLE USUARIO (ID INT PRIMARY KEY, LOGIN VARCHAR(20) NOT NULL, SENHA VARCHAR(20) NOT NULL, CONSTRAINT FK_USU FOREIGN KEY (ID) REFERENCES PESSOA(ID)); Figura XV.XX. Script para criação das tabelas. Para se usar a agenda é necessário que exista pelo menos um usuário cadastrado. Como no exemplo não vamos apresentar uma tela para cadastro de usuários será preciso cadastrá-los por meio comandos SQL. Os comandos da figura XX.XX mostram como cadastrar um usuário. INSERT INTO PESSOA(ID,NOME,TELEFONE,ENDERECO,EMAIL) VALUES(0,'Alcione de Paiva Oliveira','3899-1769', 'PH Rolfs','[email protected]'); INSERT INTO USUARIO(ID,LOGIN,SENHA) VALUES(0,'Alcione','senha'); Figura XV.XX. Script para cadastra um usuário. O sistema e-agenda é composta pelos seguintes arquivos: Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 153 Arquivo Descrição Página inicial do site, contendo o formulário para a entrada do login e senha para entrar no restante do site. principal.jsp Página JSP contendo o formulário para entrada de dados para inserção, remoção ou consulta de itens da agenda. LoginBean.java JavaBean responsável por verificar se o usuário está autorizado a acessar a agenda. AgendaServlet.java Servlet responsável pelo tratamento de requisições sobre alguma função da agenda (consulta, inserção e remoção) AcaoBean.java JavaBean responsável pela execução da ação solicitada pelo usuário. ConnectionBean.java JavaBean responsável pelo acesso ao DB e controle das conexões. agenda.html Tabela XV.XX. Arquivos do sistema e-agenda. O diagrama de colaboração abaixo mostra as interação entre os componentes do sistema. agenda.html 1 AgendaServlet 4 2 5 principal.jsp 8 LoginBean AcaoBean 6 3 ConnectionBean 7 1 e 4 – Requisições 2 e 6 – instanciações 4 – reencaminhamento de requisições 3,5,7 e 8 – Chamadas de métodos Figura XV.XX. Interação entre os componentes do sistema. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 154 Descreveremos agora cada componente da aplicação. O exemplo XX.XX mostra código HTML da página agenda.html. Esta é a página inicial da aplicação. Ela contém o formulário para a entrada do login e senha para entrar no restante do site. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 20 31 32 33 34 35 36 37 38 39 <HTML> <HEAD> <TITLE>Agenda</TITLE> </HEAD> <BODY BGCOLOR="#FFFFFF"> <P align="center"><IMG src="tit.gif" width="350" height="100" border="0"></P> <BR> <CENTER> <FORM method="POST" name="TesteSub" onsubmit="return TestaVal()" action="/agenda/agenda"><BR> Login:<INPUT size="20" type="text" name="login"><BR><BR> Senha:<INPUT size="20" type="password" name="senha"><BR><BR><BR> <INPUT type="submit" name="envia" value="Enviar"> <INPUT size="3" type="Hidden" name="corrente" value="0"><BR> </FORM> </CENTER> <SCRIPT language="JavaScript"> <!-function TestaVal() { if (document.TesteSub.login.value == "") { alert ("Campo Login nao Preenchido...Form nao Submetido") return false } else if (document.TesteSub.senha.value == "") { alert ("Campo Senha nao Preenchido...Form nao Submetido") return false } else { return true } } //--></SCRIPT> </BODY></HTML> Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 155 Exemplo VI.XX – agenda.html. O formulário está definido nas linha 11 a 17. Na linha 12 o parâmetro action indica a URL que dever receber a requisição. A URL é virtual e sua associação com o Servlet AgendaServlet será definida no arquivo web.xml. Na linha 16 é definido um campo oculto (Hidden) como o nome de corrente e valor 0. Ele será usado pelo AgendaServlet reconhecer a página de onde saiu a requisição. As linha 19 a 31 definem uma função em JavaScript que será usada para verificar se o usuário digitou o nome e a senha antes de enviar a requisição ao usuário. O uso de JavaScript no lado cliente para criticar a entrada do usuário é muito comum pois diminui a sobrecarga do servidor. O exemplo XX.XX mostra código da página principal.jsp. Esta página contém o formulário para entrada de dados para inserção, remoção ou consulta de itens da agenda. Na linha 4 a diretiva page define que o servidor deve acompanhar a sessão do usuário e importa o pacote agenda. Na linha 7 um objeto da classe agenda.LoginBean é recuperado da sessão por meio do método getAttribute(). Para recuperar o objeto é preciso passar para o método o nome que está associado ao objeto na sessão. De forma semelhante, na linha 8 um objeto da classe agenda.AcaoBean é recuperado da requisição por meio do método getAttribute(). Este objeto é recuperado da requisição porque cada requisição possui uma ação diferente associada. Na linha 9 é verificado se objeto agenda.LoginBean foi recuperado e se o retorno do método getStatus() é true. Se o objeto agenda.LoginBean não foi recuperado significa que existe uma tentativa de acesso direto à página principal.jsp sem passar primeiro pela página agenda.html ou que a sessão se esgotou. Se o método getStatus() retornar false significa que o usuário não está autorizado a acessar essa página. Nestes casos é processado o código associado ao comando else da linha 51 que apaga a sessão por meio do método invalidate() do objeto HttpSession (linha 53) e mostra a mensagem “Usuário não autorizado” (linha 55). Caso o objeto indique que o usuário está autorizado os comandos internos ao if são executados. Na linha 11 é mostrada uma mensagem com o nome do usuário obtido por meio do método getNome() do objeto agenda.LoginBean . Na linha 13 é mostrado o resultado da ação anterior por meio do método toString() do objeto agenda.AcaoBean. A ação pode ter sido de consulta, inserção de um novo item na agenda e remoção de um item na agenda. No primeiro caso é mostrado uma lista dos itens que satisfizeram a consulta. No segundo e terceiro casos é exibida uma mensagem indicado se a operação foi bem sucedida. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 <HTML><HEAD> <TITLE>Tela da Agenda </TITLE> </HEAD><BODY bgcolor="#FFFFFF"> <%@ page session="true" import="agenda.*" %> <% agenda.LoginBean lb = (agenda.LoginBean) session.getAttribute("loginbean"); agenda.AcaoBean ab = (agenda.AcaoBean) request.getAttribute("acaobean"); if (lb != null && lb.getStatus()) { %> <H2>Sess&atilde;o do <%= lb.getNome() %></H2> <% if (ab!=null) out.println(ab.toString()); %> <P><BR></P> <FORM method="POST" name="formprin" onsubmit="return TestaVal()" action="/agenda/agenda"> Nome: <INPUT size="50" type="text" name="nome"><BR> Telefone: <INPUT size="20" type="text" name="telefone"><BR> Endere&ccedil;o: <INPUT size="50" type="text" name="endereco"><BR> Email: <INPUT size="50" type="text" name="email"><BR><BR> P&aacute;gina: <INPUT size="50" type="text" name="pagina"><BR> Celular: <INPUT size="20" type="text" name="celular"><BR> Descri&ccedil;&atilde;o: <INPUT size="20" type="text" name="descricao"> <BR><CENTER> <INPUT type="submit" name="acao" value="Consulta"> <INPUT type="submit" name="acao" value="Insere"> <INPUT type="submit" name="acao" value="Apaga"></CENTER> <INPUT size="3" type="Hidden" name="corrente" value="1"> </FORM> <SCRIPT language="JavaScript"><!-function TestaVal() { if (document.formprin.nome.value == "" && document.formprin.descricao.value== "") { alert ("Campo Nome ou Descricao devem ser Preenchidos!") return false } else { return true } } //--></SCRIPT> <% } else { session.invalidate(); Alcione de P. Oliveira, Vinícius V. Maciel - UFV 156 Java na Prática – Volume II 54 55 56 57 58 59 157 %> <H1>Usu&aacute;rio n&atilde;o autorizado</H1> <% } %> </BODY></HTML> Exemplo VI.XX – principal.jsp. As linhas 17 a 31 definem o código do formulário de entrada. Nas linhas 17 e 18 são definidos os atributos do formulário. O atributo method indica a requisição será enviada por meio do método POST. O atributo name define o nome do formulário como sendo formprin. O atributo onsubmit define que a função javaSript TestaVal() deve ser executada quando o formulário for submetido. Finalmente, o atributo action define a URL para onde a requisição deve ser enviada. Neste caso a URL é agenda/agenda que está mapeada para o Servlet AgendaServlet. O mapeamento é feito no arquivo web.xml do diretório web-inf do contexto agenda, como mostrado na figura XX.XX. As linhas 19 a 25 definem os campos de texto para entrada dos valores. As linhas 27 a 29 definem os botões de submit. Todos possuem o mesmo nome, de forma que o Servlet precisa apenas examinar o valor do parâmetro acao para determinar qual ação foi solicitada Na linha 30 é definido um campo oculto (Hidden) como o nome de corrente e valor 0. Ele será usado pelo AgendaServlet reconhecer a página de onde saiu a requisição. As linha 33 a 47 definem uma função em JavaScript que será usada para verificar se o usuário entrou com valores nos campos de texto nome ou decricao. No mínimo um desses campos deve ser preenchido para que uma consulta possa ser realizada. O exemplo XX.XX mostra código do JavaBean usado para intermediar a conexão com o banco de dados. O JavaBean ConnectionBean tem a responsabilidade de abrir uma conexão com o banco de dados, retornar uma referência desta conexão quando solicitado e registrar se a conexão esta livre ou ocupada. Neste exemplo o se encarrega apenas de obter a conexão e fechá-la, no entanto, em aplicação com maior número de acessos ao banco de dados pode ser necessário um maior controle sobre o uso das conexões, mantendo-as em uma estrutura de dados denominada de pool de conexões. Na linha 12 podemos observar que o construtor da classe foi declarado com o modificador de acesso private. Isto significa que não é possível invocar o construtor por meio de um objeto de outra classe. Isto é feito para que se possa ter um controle sobre a criação de instâncias da classe. No nosso caso permitiremos apenas que uma instância da classe seja criada, de modo que todas as referências apontem para Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 158 esse objeto. Esta técnica de programação, onde se permite uma única instância de uma classe é denominada de padrão de projeto Singleton. O objetivo de utilizarmos este padrão é porque desejamos que apenas um objeto controle a conexão com o banco de dados. Ma se o construtor não pode ser chamado internamente como uma instância da classe é criada e sua referência é passada para outros objetos? Esta é a tarefa do método estático getInstance() (linhas 14 a 19). Este método verifica se já existe a instância e retorna a referência. Caso a instância não exista ela é criada antes de se retornar a referência. O método init() (linhas 21 a 27) é chamado pelo construtor para estabelecer a conexão com o SGBD. Ele carrega o driver JDBC do tipo 4 para HSQLDB e obtém uma conexão com o SGBD. O método devolveConnection() (linhas 29 a 34) é chamado quando se deseja devolver a conexão. Finalmente, o método getConnection() (linhas 36 a 46) é chamado quando se deseja obter a conexão. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package agenda; import java.sql.*; import java.lang.*; import java.util.*; public class ConnectionBean { private Connection con=null; private static int clients=0; static private ConnectionBean instance=null; private ConnectionBean() { init(); } static synchronized public ConnectionBean getInstance() { if (instance == null) { instance = new ConnectionBean(); } return instance; } private void init() { try { Class.forName("org.hsqldb.jdbcDriver"); con= DriverManager.getConnection("jdbc:hsqldb:hsql://localhost","sa",""); } catch(Exception e){System.out.println(e.getMessage());}; } public synchronized void devolveConnection(Connection con) { Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 159 if (this.con==con) { clients--; notify(); } } public synchronized Connection getConnection() { if(clients>0) { try { wait(5000); } catch (InterruptedException e) {}; if(clients>0) return null; } clients ++; return con; } } Exemplo VI.XX – ConnectionBean.java. O exemplo XX.XX mostra código do JavaBean usado para verificar se o usuário está autorizado a usar a agenda. O JavaBean LoginBean recebe o nome e a senha do usuário, obtém a conexão com o SGBD e verifica se o usuário está autorizado, registrando o resultado da consulta na variável status (linha 10). Tudo isso é feito no construtor da classe (linhas 12 a 35). Note que na construção do comando SQL (linhas 17 a 20) é inserido uma junção entre as tabelas PESSOA e USUARIO de modo a ser possível recuperar os dados relacionados armazenados em ambas as tabelas. Os métodos getLogin(), getNome() e getStatus() (linhas 36 a 38) são responsáveis pelo retorno do login, nome e status da consulta respectivamente. 1 2 3 4 5 6 7 8 9 10 11 package agenda; import java.sql.*; import java.lang.*; import java.util.*; public class LoginBean { protected String nome = null; protected String login= null; protected boolean status= false; Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 160 public LoginBean(String login, String senha) { this.login = login; Connection con=null; Statement stmt =null; String consulta = "SELECT NOME FROM PESSOA, USUARIO "+ "WHERE USUARIO.ID = PESSOA.ID AND "+ "USUARIO.SENHA ='"+senha+"' AND "+ "USUARIO.LOGIN ='"+login+"'"; try { con=ConnectionBean.getInstance().getConnection(); stmt = con.createStatement(); ResultSet rs =stmt.executeQuery(consulta); if(rs.next()) { status = true; nome = rs.getString("NOME"); } } catch(Exception e){System.out.println(e.getMessage());} finally { ConnectionBean.getInstance().devolveConnection(con); try{stmt.close();}catch(Exception ee){}; } } public String getLogin(){return login;} public String getNome(){return nome;} public boolean getStatus(){return status;} } Exemplo VI.XX – LoginBean.java. O exemplo XX.XX mostra código do Servlet que implementa a camada de controle do modelo MVC. O Servlet AgendaServlet recebe as requisições e, de acordo com os parâmetros, instância os JavaBeans apropriados e reencaminha as requisições para as páginas corretas. Tanto o método doGet() (linhas 9 a 12) quanto o método doPost()(linhas 13 a 17) invocam o método performTask()(linhas 19 a 61) que realiza o tratamento da requisição. Na linhas 24 a 26 do método performTask() é obtido o valor do parâmetro corrente que determina a página que originou a requisição. Se o valor for nulo é assumido o valor default zero. Na linha 30 é executado um comando switch sobre esse valor, de modo a desviar para bloco de comandos adequado. O bloco que vai da linha 32 até a linha 43 trata a requisição originada em uma página com a identificação 0 (página agenda.html). Nas linhas 32 e 33 são recuperados o valor de login e senha digitados pelo usuário. Se algum Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 161 desses valores for nulo então a requisição deve ser reencaminhada para a página de login (agenda.html) novamente (linha 35). Caso contrário é instanciado um objeto LoginBean, inserido na sessão corrente e definida a página principal.jsp como a página para o reencaminhamento da requisição (linhas 38 a 41). Já o bloco que vai da linha 44 até a linha 54 trata a requisição originada em uma página com a identificação 1 (página principal.jsp). Na linha 44 é recuperado o objeto HttpSession corrente. O argumento false é utilizado para impedir a criação de um novo objeto HttpSession caso não exista um corrente. Se o valor do objeto for null, então a requisição deve ser reencaminhada para a página de login (linha 47). Caso contrário é instanciado um objeto AcaoBean, inserido na requisição corrente e definida a página principal.jsp como a página para o reencaminhamento da requisição (linhas 50 a 52). Na linha 56 a requisição é reencaminhada para a página definida (página agenda.html ou principal.jsp). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package agenda; import javax.servlet.*; import javax.servlet.http.*; import agenda.*; public class AgendaServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) { performTask(request,response); } public void doPost(HttpServletRequest request, HttpServletResponse response) { performTask(request,response); } public void performTask(HttpServletRequest request, HttpServletResponse response) { String url; HttpSession sessao; String corrente = request.getParameter("corrente"); int icorr=0; if (corrente != null) icorr = Integer.parseInt(corrente); try Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 162 { switch(icorr) { case 0: String login = request.getParameter("login"); String senha = request.getParameter("senha"); if (login == null||senha == null) url= "/agenda.html"; else { sessao = request.getSession(true); sessao.setAttribute("loginbean", new agenda.LoginBean(login,senha)); url= "/principal.jsp"; }; break; case 1: sessao = request.getSession(false); if (sessao == null) url= "/agenda.html"; else { request.setAttribute("acaobean", new agenda.AcaoBean(request)); url= "/principal.jsp"; }; break; } getServletContext().getRequestDispatcher(url).forward(request,response); }catch (Exception e) { System.out.println("AgendaServlet falhou: "); e.printStackTrace(); } } } Exemplo VI.XX – AgendaServlet.java. O exemplo XX.XX mostra código do JavaBean usado para realizar a manutenção da agenda. O JavaBean AcaoBean é responsável pela consulta, remoção e inserção de novos itens na agenda. Um objeto StringBuffer referenciado pela variável retorno é utilizado pelo JavaBean para montar o resultado da execução. O construtor (linhas 16 a 27) verifica o tipo de requisição e invoca o método apropriado. O método consulta() (linhas 29 a 77) é responsável pela realização de consultas. As consultas podem ser realizadas sobre o campo nome ou Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 163 descrição e os casamentos podem ser parciais, uma vez que é usado o operador LIKE. A consulta SQL é montada nas linhas 40 a 47. Na linha 50 é obtida uma conexão com SGBD por meio do objeto ConnectionBean. Na linha 57 o comando SQL é executado e as linhas 59 a 72 montam o resultado da consulta. O método insere() (linhas 79 a 148) é responsável por inserir um item na agenda. Na linha 95 é obtida uma conexão com SGBD por meio do objeto ConnectionBean. Para inserir um novo item é preciso obter o número do último identificador usado, incrementar o identificador e inserir na base o item com o identificador incrementado. Esta operação requer que não seja acrescentado nenhum identificador entre a operação de leitura do último identificador e a inserção de um novo item. Ou seja, é necessário que essas operações sejam tratadas como uma única transação e o isolamento entre as transações sejam do nível Repeatable Read. A definição do inicio da transação é feita no comando da linha 102. A mudança do nível de isolamento é feita pelos comandos codificados nas linha 103 a 109. Na linha 112 é invocado o método obtemUltimo() (linhas 150 a 171) para obter o último identificador utilizado. As linhas 114 a 128 montam o comando SQL para a execução. O comando SQL é executado na linha 131. O fim da transação é definido pelo comando da linha 132. Ao fim da transação, de forma a não prejudicar a concorrência, o nível de isolamento deve retornar para um valor mais baixo. Isto é feito pelos comandos das linhas 133 a 137. O método apaga() (linhas 173 a 201) é responsável por remover um item da agenda. As linhas 175 a 180 contém o código para verificar se o usuário digitou o nome associado ao item que deve ser removido. A linha 181 montam o comando SQL para a execução. Na linha 184 é obtida uma conexão com SGBD por meio do objeto ConnectionBean. O comando SQL é executado na linha 191. 1 2 3 4 5 6 7 8 9 10 11 12 13 package agenda; import java.lang.*; import java.util.*; import java.sql.*; public class AcaoBean { private Connection con=null; private StringBuffer retorno = null; private Statement stmt=null; private String [] legenda= {"C&oacute;digo","Nome","Telefone", "Endere&ccedil;o", "email","hp", Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 164 "celular","Descri&ccedil;&atilde;o"}; public AcaoBean(javax.servlet.http.HttpServletRequest request) { String acao = request.getParameter("acao"); if (acao.equals("Consulta")) { String nome = request.getParameter("nome"); String descri = request.getParameter("descricao"); consulta(nome,descri); } else if (acao.equals("Insere")) insere(request); else if (acao.equals("Apaga")) apaga(request); } private void consulta(String nome,String descri) { String consulta = null; if ((nome == null||nome.length()<1) && (descri == null|| descri.length()<1)) { retorno = new StringBuffer("Digite o nome ou descricao!"); return; } if (descri == null|| descri.length()<1) consulta = "SELECT * FROM PESSOA WHERE NOME LIKE '%"+ nome+"%'"+" ORDER BY NOME"; else if (nome == null|| nome.length()<1) consulta = "SELECT * FROM PESSOA WHERE DESCRICAO LIKE '%"+ descri+"%'"+" ORDER BY NOME"; else consulta="SELECT * FROM PESSOA WHERE DESCRICAO LIKE '%"+ descri+"%' AND NOME LIKE '%"+nome+"%' ORDER BY NOME"; try { con=ConnectionBean.getInstance().getConnection(); if (con == null) { retorno = new StringBuffer("Servidor ocupado. Tente mais tarde.!"); return; } stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(consulta); retorno = new StringBuffer(); retorno.append("<br><h3>Resultado</h3><br>"); while(rs.next()) { Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 165 63 64 retorno.append("ID:").append(rs.getString("id")); retorno.append("<br>Nome:").append(rs.getString("Nome")); 65 retorno.append("<br>Telefone:").append(rs.getString("Telefone")); 66 retorno.append("<br>Endereco:").append(rs.getString("Endereco")); 67 retorno.append("<br>email:").append(rs.getString("email")); 68 retorno.append("<br>hp:").append(rs.getString("hp")); 69 retorno.append("<br>celular:").append(rs.getString("celular")); 70 retorno.append("<br>descricao:").append(rs.getString("descricao")); 71 retorno.append("<br><br>"); 72 } 73 } catch(Exception e){System.out.println(e.getMessage());} 74 finally {ConnectionBean.getInstance().devolveConnection(con); 75 try{stmt.close();}catch(Exception ee){}; 76 } 77 } 78 79 private void insere(javax.servlet.http.HttpServletRequest request) 80 { 81 String[] par = {"telefone","endereco","email","hp","celular","descricao"}; 82 83 StringBuffer comando = new StringBuffer("INSERT INTO PESSOA("); 84 StringBuffer values = new StringBuffer(" VALUES("); 85 86 String aux = request.getParameter("nome"); 87 if (aux == null || aux.length()<1) 88 { 89 retorno = new StringBuffer("<br><h3>Digite o nome!</h3><br>"); 90 return; 91 } 92 93 try 94 { 95 con=ConnectionBean.getInstance().getConnection(); 96 if (con == null) 97 { 98 retorno = new StringBuffer("Servidor ocupado. Tente mais tarde!"); 99 return; 100 } 101 102 con.setAutoCommit(false); 103 DatabaseMetaData meta=con.getMetaData(); 104 105 if(meta.supportsTransactionIsolationLevel( 106 con.TRANSACTION_REPEATABLE_READ)) { 107 con.setTransactionIsolation( 108 con.TRANSACTION_REPEATABLE_READ); 109 } 110 111 Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 166 int ultimo = obtemUltimo(con); if (ultimo==-1) return; ultimo++; comando.append("id,nome"); values.append(ultimo+",'").append(aux).append("'"); for(int i=0;i<par.length;i++) { aux = request.getParameter(par[i]); if (aux != null && aux.length()>0) { comando.append(",").append(par[i]); values.append(",'").append(aux).append("'"); } } comando.append(")"); values.append(")"); aux = comando.toString()+values.toString(); stmt = con.createStatement(); stmt.executeUpdate(aux); con.setAutoCommit(true); if(meta.supportsTransactionIsolationLevel( con.TRANSACTION_READ_COMMITTED)) { con.setTransactionIsolation( con.TRANSACTION_READ_COMMITTED); } retorno = new StringBuffer("<br><h3>Inserido!</h3><br>"); return; } catch(Exception e) {retorno = new StringBuffer("<br><h3>Erro:"+e.getMessage()+"!</h3><br>"); } finally { ConnectionBean.getInstance().devolveConnection(con); try{stmt.close();}catch(Exception ee){}; } } private int obtemUltimo(Connection con) { String consulta = "SELECT MAX(ID) AS maior FROM PESSOA"; try { if (con == null) { retorno = new StringBuffer("Servidor ocupado. Tente mais tarde.!"); return -1; } Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 167 stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(consulta); if(rs.next()) return Integer.parseInt(rs.getString("maior")); else return 0; } catch(Exception e) { retorno = new StringBuffer("<br><h3>Erro:"+e.getMessage()+"!</h3><br>"); return -1; } finally {try{stmt.close();}catch(Exception ee){};} 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 } private void apaga(javax.servlet.http.HttpServletRequest request) { String aux = request.getParameter("nome"); if (aux == null || aux.length()<1) { retorno = new StringBuffer("<br><h3>Digite o nome!</h3><br>"); return; } String consulta = "DELETE FROM PESSOA WHERE NOME ='"+aux+"'"; try { con=ConnectionBean.getInstance().getConnection(); if (con == null) { retorno = new StringBuffer("Servidor ocupado. Tente mais tarde.!"); return; } stmt = con.createStatement(); stmt.executeUpdate(consulta); retorno = new StringBuffer("<br><h3>Removido!</h3><br>"); return; } catch(Exception e){ retorno = new StringBuffer("<br><h3>Erro:"+e.getMessage()+"!</h3><br>"); } finally { ConnectionBean.getInstance().devolveConnection(con); try{stmt.close();}catch(Exception ee){};} } public String[] getLeg(){return legenda;} public String toString(){return retorno.toString();} } Exemplo VI.XX – AcaoBean.java. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 168 Instalação do SGBD O HSQLDB (www.hsqldb.org) é um SGBD de código aberto desenvolvido em Java. Ele obedece os padrões SQL e JDBC. Possui as seguintes características: • • • • • • Tamanho pequeno (≅ 100KB). Funciona como servidor, standalone e in-memory. Suporta transação. Integridade referencial. Procedimentos Armazenados em Java. Direitos de acessos Para instalá-lo em no MS-Windows execute as seguintes etapas: 1) Descompacte o arquivo hsqldb_v.1.61.zip em um diretório qualquer. Por exemplo : c:\sgbd 2) Coloque o seguinte comando em seu autoexec.bat SET CLASSPATH=%CLASSPATH%;c:\sgbd\hsqldb_v.1.61\lib\hsqldb.jar Execução em modo servidor c:\sgbd\hsqldb_v.1.61\demo\runServer.bat Execução do gerenciador gráfico c:\sgbd\hsqldb_v.1.61\demo\runManager.bat Instalação da Aplicação Para instalar crie a seguinte estrutura de diretório abaixo do diretório webapps do Tomcat: páginas HTML e JSP Alcione de P. Oliveira, Vinícius V. Maciel - UFV arquivo web.xml Servlets e JavaBeans Java na Prática – Volume II 169 webapps |_____ agenda |_____ Web-inf |_____classes |_______agenda Figura XV.XX. Estrutura de diretórios para a aplicação agenda. O arquivo web.xml deve ser alterado para conter mapeamento entre a URL agenda e o Servlet AgendaServlet. ... <web-app> <servlet> <servlet-name> agenda </servlet-name> <servlet-class> agenda.AgendaServlet </servlet-class> </servlet> <servlet-mapping> <servlet-name> agenda </servlet-name> <url-pattern> /agenda </url-pattern> </servlet-mapping> ... </web-app> Figura XV.XX. Arquivo web.xml para a agenda. Considerações sobre a solução Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 170 A aplicação acima implementa uma agenda que pode ser acessada por meio da Internet, no entanto, devido à falta de espaço e à necessidade de destacarmos os pontos principais, alguns detalhes foram deixados de lado, como por exemplo uma melhor interface com o usuário. Abaixo seguem alguns comentários sobre algumas particularidades da aplicação: 1. O JavaBean da classe LoginBean é armazenado na sessão para permitir a verificação se o acesso ao site é autorizado. Isto impede que os usuários tentem acessar diretamente a página principal.jsp da agenda. Caso tentem fazer isso, a sessão não conterá um objeto LoginBean associado e, portanto, o acesso será recusado. 2. O JavaBean da classe AcaoBean é armazenado no objeto request uma vez que sua informações são alteradas a cada requisição. Uma forma mais eficiente seria manter o objeto AcaoBean na sessão e cada novo requisição invocar um método do AcaoBean para gerar os resultados. No entanto, o objetivo da nossa implementação não é fazer a aplicação mais eficiente possível, e sim mostrar para o leitor uma aplicação com variadas técnicas. 3. Apesar de termos adotado o padrão MVC de desenvolvimento a aplicação não exibe uma separação total da camada de apresentação (Visão) em relação à camada do modelo. Parte do código HTML da visão é inserido pelo AcaoBean no momento da construção da String contendo o resultado da ação. Isto foi feito para minimizar a quantidade de código Java na página JSP. Pode-se argumentar que neste caso a promessa da separação entre as camadas não é cumprida totalmente. Uma solução para o problema seria gerar o conteúdo em XML e utilizar um analisador de XML para gerar a página de apresentação. No entanto, o uso da tecnologia XML foge ao escopo deste livro. 4. A solução apresenta código redundante para criticar as entradas do usuário. Existe código JavaScript nas páginas, e código Java no Servlet e JavaBeans. O uso de código JavaScript nas páginas para críticas de entrada é indispensável para aliviarmos a carga sobre o servidor. Já o código para crítica no servidor não causa impacto perceptível e útil para evitar tentativas de violação. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 171 Capítulo VII Perguntas Frequentes Como executar um programa a partir de uma aplicação em Java? Resposta: isso pode ser feito com o método de instância exec da classe Runtime. Como criar um TextField que não exiba o que está sendo digitado para se usado como campo de entrada de senhas? Resposta: use o método setEchoChar() do TextField para definir qual caractere que deve ser ecoado. Como aumentar a área de ambiente do DOS para caber o CLASSPATH? Resposta: coloque a seguinte linha no autoexec.bat set shell=c:\command.com /e:4096 Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 172 Bibliografia Eckel B. Thinking in Java. 2nd Ed. New Jersey : Prentice Hall, 2000. Gosling J., Joy W., Steele G. The Java Language Specification. Massachusetts : Addison-Wesley, 1996. Oaks S. Java Security. California : O’Reilly & Associates, Inc, 1998. Oaks S., Wong H. Java Threads. 2ª Ed. California : O’Reilly & Associates, Inc, 1999. Watt D. A. Programming Language Concepts and Paradigms. Great Britain : Prentice Hall, 1990. Ethan H., Lycklama E. How do you Plug Java Memory Leaks? Dr. Dobb´s Journal, San Francisco, CA, No. 309, February 2000. Wahli U. e outros. Servlet and JSP Programming with IBM WebSphere Studio and VisualAge for Java, IBM RedBooks, California, May 2000. Sadtler C. e outros. Patterns for e-business: User-to-Business Patterns for Topology 1 and 2 using WebSphere Advanced Edition, IBM RedBooks, California, April 2000. Bagwel D. e outros. An Approach to Designing e-business Solutions, IBM RedBooks, California, December 1998. Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II 173 Links Revistas http://www.javaworld.com/ Revista online sobre Java. Livros http://www.eckelobjects.com/ Página do autor do livro Thinking in Java, atualmente em segunda edição. O livro pode ser baixado gratuitamente no site. http://www.redbooks.ibm.com/booklist.html Livros da IBM Servidores http://jakarta.apache.org Página do projeto Jakarta que desenvolveu o Tomcat. http://www.metronet.com/~wjm/tomcat Lista Tomcat http://www.jboss.org Servidor de aplicação gratuito habilitado para EJB Dicas Java e recursos http://java.sun.com/ Página da Sun com informações, tutoriais e produtos Java. http://gamelan.earthweb.com/ Página da com informações, Applets, Lista de discussão, tutoriais. http://www.inquiry.com/techtips/java_pro Ask the Java Pro http://www.jguru.com/ jGuru.com(Home): Your view of the Java universe http://www.soujava.org.br Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II Bem Vindo ao SouJava! Servlets e JSP http://www.servlet.com/srvdev.jhtml Servlet Inc : Developers Forum http://www.servlets.com Servlets.com http://www.jspin.com/home Jspin.com - The JSP Resource Index http://www.burridge.net/jsp/jspinfo.html Web Development with JSP: JSP, Java Servlet, and Java Bean Information http://www.apl.jhu.edu/~hall/java/ServletTutorial A Tutorial on Java Servlets and Java Server Pages (JSP) Alcione de P. Oliveira, Vinícius V. Maciel - UFV 174 Java na Prática – Volume II 175 Índice MVC.............................................. 148, 151 A ASP ........................................................124 N Níveis de isolamento ............................... 85 Nível de Isolamento................................. 83 C Calendar ................................................1 CGI ..........................................................99 contexto da aplicação .............................107 Cookies ..................................................115 CORBA....................................................69 D Demilitarized Zone ................................151 DMZ...............Consulte Demilitarized Zone P PHP........................................................ 123 pool de conexões.................................... 157 Prepared Statements ................................ 87 Procedimentos Armazenados................... 88 R RMI ......................................................... 69 E S EJB.........................................................149 Enterprise JavaBeans ............. Consulte EJB J Servlets .................................................... 97 Singleton................................................ 158 sites dinâmicos......................................... 97 Stored Procedures.................................... 88 JSP ...................................................97, 122 T M Transação................................................. 83 Mudança de Contexto..............................5 Alcione de P. Oliveira, Vinícius V. Maciel - UFV Java na Prática – Volume II Alcione de P. Oliveira, Vinícius V. Maciel - UFV 1