O que são Servlets em Java

O que são Servlets em Java

Nesta aula teremos uma grande mudança no tipo de conteúdo aplicado. Até o presente momento estávamos trabalhando com tecnologias que são denominadas de “front-end”. Estas tecnologias são destinadas ao cliente (navegador) e estão intimamente relacionadas a experiência do usuário. A partir deste momento estaremos trabalhando com tecnologias voltadas para o “back-end”, ou seja, tecnologias voltadas para implementação do lado do servidor.  

Como usar o HTTP em Java

Como já foi explicado na Aula 01 – Introdução a programação web o protocolo HTTP é a base para a programação web. Ao buscar desenvolver aplicações web em Java não é necessário implementar classes que vão dar suporte ao HTTP. O próprio java já traz em suas bibliotecas uma tecnologia com o nome de servlet (mini-servidor).  

Uma primeira ideia da servlet seria que cada uma delas é responsável por uma página, sendo que ela lê dados da requisição do cliente e responde com outros dados (uma página HTML, uma imagem GIF etc). Como no Java tentamos sempre que possível trabalhar orientado a objetos, nada mais natural que uma servlet seja representada como um objeto a partir de uma classe Java.  

Cada servlet é, portanto, um objeto Java que recebe tais requisições (request) e produz algo (response), como uma página HTML dinamicamente gerada.   O diagrama abaixo mostra três clientes acessando o mesmo servidor através do protocolo HTTP:  

O comportamento das servlets que vamos ver neste capítulo foi definido na classe HttpServlet do pacote javax.servlet. A interface Servlet é a que define exatamente como uma servlet funciona, mas não é o que vamos utilizar, uma vez que ela possibilita o uso de qualquer protocolo baseado em requisições e respostas, e não especificamente o HTTP.   Para escrevermos uma servlet, criamos uma classe Java que estenda HttpServlet e sobrescreva um método chamado service. Esse método será o responsável por atender requisições e gerar as respostas adequadas. Sua assinatura:  

    protected void service (HttpServletRequest request,
            HttpServletResponse response)
            throws ServletException, IOException {
        ...
    }

Repare que o método recebe dois objetos que representam, respectivamente, a requisição feita pelo usuário e a resposta que será exibida no final. Veremos que podemos usar esses objetos para obter informações sobre a requisição e para construir a resposta final para o usuário.   Nosso primeiro exemplo de implementação do método service não executa nada de lógica e apenas mostra uma mensagem estática de bem vindo para o usuário. Para isso, precisamos construir a resposta que a servlet enviará para o cliente.   É possível obter um objeto que represente a saída a ser enviada ao usuário através do método getWriter da variável response. E, a partir disso, utilizar um PrintWriter para imprimir algo na resposta do cliente:  

public class OiMundo extends HttpServlet {
    protected void service (HttpServletRequest request,
            HttpServletResponse response)
            throws ServletException, IOException {
        PrintWriter out = response.getWriter();
        // escreve o texto
        out.println("<html>");
        out.println("<body>");
        out.println("Primeira servlet");
        out.println("</body>");
        out.println("</html>");
    }
}

  O único objetivo da servlet acima é exibir uma mensagem HTML simples para os usuários que a requisitarem. Mas note como seria muito fácil escrever outros códigos Java mais poderosos para gerar as Strings do HTML baseadas em informações dinâmicas vindas, por exemplo, de um banco de dados.    

Mapeando uma servlet no Web.xml

  Acabamos de definir uma Servlet, mas como vamos acessá-la pelo navegador? Qual o endereço podemos acessar para fazermos com que ela execute? O container não tem como saber essas informações, a não ser que digamos isso para ele. Para isso, vamos fazer um mapeamento de uma URL específica para uma servlet através do arquivo web.xml, que fica dentro do WEB-INF.   Uma vez que chamar a servlet pelo pacote e nome da classe acabaria criando URLs estranhas e complexas, é comum mapear, por exemplo, uma servlet como no exemplo, chamada OiMundo para o nome primeiraServlet.   Começamos com a definição da servlet em si, dentro da tag <servlet>:  

<servlet>
      <servlet-name>primeiraServlet</servlet-name>
      <servlet-class>br.com.projeto.servlet01.OiMundo</servlet-class>
</servlet>

  Em seguida, mapeie nossa servlet para a URL /oi. Perceba que isso acontece dentro da tag <servlet-mapping> (mapeamento de servlets) e que você tem que indicar que está falando daquela servlet que definimos logo acima: passamos o mesmo servlet-name para o mapeamento.  

<servlet-mapping>
     <servlet-name>primeiraServlet</servlet-name>
     <url-pattern>/oi</url-pattern>
</servlet-mapping>

  Portanto, são necessários dois passos para mapear uma servlet para uma URL:   Definir o nome e classe da servlet; Usando o nome da servlet, definir a URL. A servlet pode ser acessada através da seguinte URL:  

http://localhost:8080/fj21-agenda/oi

  Assim que o arquivo web.xml e a classe da servlet de exemplo forem colocados nos diretórios corretos, basta configurar o Tomcat para utilizar o diretório de base como padrão para uma aplicação Web.   Mais sobre o url-pattern A tag <url-pattern> também te dá a flexibilidade de disponibilizar uma servlet através de várias URLs de um caminho, por exemplo o código abaixo fará com que qualquer endereço acessado dentro de /oi seja interpretado pela sua servlet:  

<servlet-mapping>
      <servlet-name>primeiraServlet</servlet-name>
      <url-pattern>/oi/*</url-pattern>
</servlet-mapping>

  Você ainda pode configurar “extensões” para as suas servlets, por exemplo, o mapeamento abaixo fará com que sua servlet seja chamada por qualquer requisição que termine com .php:  

<servlet-mapping>
      <servlet-name>primeiraServlet</servlet-name>
      <url-pattern>*.php</url-pattern>
</servlet-mapping>

Facilidades das Servlets 3.0

  Como foi visto no exercício anterior, criar Servlets usando o Java EE 5 é um processo muito trabalhoso. Um dos grandes problemas é que temos que configurar cada um de nossas Servlets no web.xml e se quisermos acessar essa servlet de maneiras diferentes, temos que criar vários mapeamentos para a mesma servlet, o que pode com o tempo tornar-se um problema devido a difícil manutenção.   Na nova especificação Servlets 3.0, que faz parte do Java EE 6, podemos configurar a maneira como vamos acessar a nossa Servlet de maneira programática, utilizando anotações simples.   De modo geral, não é mais preciso configurar as nossas Servlets no web.xml, sendo suficiente usar a anotação @WebServlet apenas:  

@WebServlet("/oi")
public class oiServlet extends HttpServlet {
            ...
}

  Isso é equivalente a configurar a Servlet acima com a url-pattern configurada como /oi.   Na anotação @WebServlet, podemos colocar ainda um parâmetro opcional chamado name que define um nome para a Servlet (equivalente ao servlet-name). Se não definirmos esse atributo, por padrão, o nome da Servlet é o nome completo da classe da sua Servlet, também conhecido como Fully Qualified Name.   Se quisermos que nossa Servlet seja acessado através de apenas uma URL, recomenda-se definir a URL diretamente no atributo value como no exemplo acima. Mas se precisarmos definir mais de uma URL para acessar a Servlet, podemos utilizar o atributo urlPatterns e passar um vetor de URLs:  

@WebServlet(name = "MinhaServlet3", urlPatterns = {"/oi", "/ola"})
public class OiServlet3 extends HttpServlet{
       ...
}

  É bom reforçar que, mesmo a Servlet estando anotado com @WebServlet(), ele deve obrigatoriamente realizar um extends a classe javax.servlet.http.HttpServlet.  

Enviando parâmetros na requisição

  Ao desenvolver uma aplicação Web, sempre precisamos realizar operações no lado do servidor, com dados informados pelo usuário, seja através de formulários ou seja através da URL.   Por exemplo, para gravarmos um contato no banco de dados, precisamos do nome, e-mail, endereço e a data de nascimento dele. Temos uma página com um formulário que o usuário possa preencher e ao clicar em um botão esses dados devem, de alguma forma, ser passados para um Servlet. Já sabemos que a Servlet responde por uma determinada URL (através do url-pattern/v2.5 ou do urlPatterns/v3.0), portanto, só precisamos indicar que ao clicar no botão devemos enviar uma requisição para essa Servlet.   Para isso, vamos criar uma página HTML, chamada adiciona-contato.html, contendo um formulário para preenchermos os dados dos contatos:  

        <html>
            <body>
                <form action="adicionaContato">
                    Nome: <input type="text" name="nome" /><br />
                    E-mail: <input type="text" name="email" /><br />
                    Endereço: <input type="text" name="endereco" /><br />
                    Data Nascimento: <input type="text" name="dataNascimento" /><br />
                    <input type="submit" value="Gravar" />
                </form>
            </body>
        </html>

Esse código possui um formulário, determinado pela tag <form>. O atributo action indica qual endereço deve ser chamado ao submeter o formulário, ao clicar no botão Gravar. Nesse caso, estamos apontando o action para um endereço que será uma Servlet que já vamos criar.     Ao acessar a página adiciona-contato.html, o resultado deverá ser similar à figura abaixo:  

Pegando parâmetros da requisição

  Para recebermos os valores que foram preenchidos na tela e submetidos, criaremos uma Servlet, cuja função será receber de alguma maneira esses dados e convertê-los, se necessário.   Dentro do método service da nossa Servlet para adição de contatos, vamos buscar os dados que foram enviados na requisição. Para buscarmos esses dados, precisamos utilizar o parâmetro request do método service chamando o método getParameter(“nomeDoParametro”), onde o nome do parâmetro é o mesmo nome do input que você quer buscar o valor. Isso vai retornar uma String com o valor do parâmetro. Caso não exista o parâmetro, será retornado null:           String valorDoParametro = request.getParameter(“nomeDoParametro”); O fato de ser devolvida uma String nos traz um problema, pois, a data de nascimento do contato está criada como um objeto do tipo Calendar. Então, o que precisamos fazer é converter essa String em um objeto Calendar. Mas a API do Java para trabalhar com datas não nos permite fazer isso diretamente. Teremos que converter antes a String em um objeto do tipo java.util.Date com auxílio da classe SimpleDateFormat, utilizando o método parse, da seguinte forma:  

        String dataEmTexto = request.getParameter("dataNascimento");
        Date date = new SimpleDateFormat("dd/MM/yyyy").parse(dataEmTexto);

Repare que indicamos também o pattern (formato) com que essa data deveria chegar para nós, através do parâmetro passado no construtor de SimpleDateFormat com o valor dd/MM/yyyy. Temos que tomar cuidado pois o método parse lança uma exceção do tipo ParseException. Essa exceção indica que o que foi passado na data não pôde ser convertido ao pattern especificado. Com o objeto do tipo java.util.Date que foi devolvido, queremos criar um Calendar. Para isso vamos usar o método setTime da classe Calendar, que recebe um Date.  

Get, Post e Métodos HTTP

  Repare que no exercício anterior, ao clicarmos no botão salvar, todos os dados que digitamos no formulário aparecem na URL da página de sucesso. Isso acontece porque não definimos no nosso formulário a forma com que os dados são enviados para o servidor, através do atributo method para o <form> da seguinte forma:  

    <form action="adicionaContato" method="POST">

Como não tínhamos definido, por padrão então é usado o método GET, que indica que os valores dos parâmetros são passados através da URL junto dos nomes dos mesmos, separados por &, como em: nome=Adriano&[email protected]   Podemos também definir o método para POST e, dessa forma, os dados são passados dentro do corpo do protocolo HTTP, sem aparecer na URL que é mostrada no navegador.   Podemos, além de definir no formulário como os dados serão passados, também definir quais métodos HTTP nossa servlet aceitará.   O método service aceita todos os métodos HTTP, portanto, tanto o método GET quanto o POST. Para especificarmos como trataremos cada método, temos que escrever os métodos doGet e/ou doPost na nossa servlet:  

    void doGet(HttpServletRequest req, HttpServletResponse res);
    void doPost(HttpServletRequest req, HttpServletResponse res);

Init e Destroy

Toda Servlet deve possuir um construtor sem argumentos para que o container possa criá-lo. Após a criação, o servlet container inicializa a Servlet com o método init(ServletConfig config) e o usa durante todo o seu período ativo, até que vai desativá-lo através do método destroy(), para então liberar o objeto.   É importante perceber que a sua Servlet será instanciado uma única vez pelo container e esse único objeto será usado para atender a todas as requisições de todos os clientes em threads separadas. Aliás, é justo isso que traz uma melhoria em relação aos CGI comuns que disparavam diversos processos.   Na inicialização de uma Servlet, quando parâmetros podem ser lidos e variáveis comuns a todas as requisições devem ser inicializadas, é um bom momento, por exemplo, para carregar arquivos diversos de configurações da aplicação:       void init (ServletConfig config); Na finalização, devemos liberar possíveis recursos que estejamos segurando:       void destroy(); Os métodos init e destroy, quando reescritos, são obrigados a chamar o super.init() e super.destroy() respectivamente. Isso acontece pois um método é diferente de um construtor. Quando estendemos uma classe e criamos o nosso próprio construtor da classe filha, ela chama o construtor da classe pai sem argumentos, preservando a garantia da chamada de um construtor. O mesmo não acontece com os métodos.   Supondo que o método init (ou destroy) executa alguma tarefa fundamental em sua classe pai, se você esquecer de chamar o super, terá problemas.   O exemplo a seguir mostra uma Servlet implementando os métodos de inicialização e finalização. Os métodos init e destroy podem ser bem simples (lembre-se que são opcionais):  

    @WebServlet("/minhaServlet")
    public class MinhaServlet extends HttpServlet {
        public void init(ServletConfig config) throws ServletException {
            super.init(config);
            log("Iniciando a servlet");
        }
        public void destroy() {
            super.destroy();
            log("Destruindo a servlet");
        }
        protected void service(HttpServletRequest request,
                            HttpServletResponse response)
                            throws IOException, ServletException {
            //código do seu método service
        }
    }

Uma única instância de cada servlet

  De acordo com a especificação de Servlets, por padrão, existe uma única instância de cada Servlet declarada. Ao chegar uma requisição para a Servlet, uma nova Thread é aberta sobre aquela instância que já existe.   Isso significa que, se colocássemos em nossa Servlet uma variável de instância, ela seria compartilhada entre todas as threads que acessam essa Servlet! Em outras palavras, seria compartilhado entre todas as requisições e todos os clientes enxergariam o mesmo valor. Provavelmente não é o que queremos fazer.   Um exemplo simples para nos auxiliar enxergar isso é uma Servlet com uma variável para contar a quantidade de requisições:  

@WebServlet("/contador")
public class Contador extends HttpServlet {
    private int contador = 0; //variavel de instância
    protected void service(HttpServletRequest request,                    HttpServletResponse response)throws ServletException, IOException {
        contador++; // a cada requisição a mesma variável é incrementada
        // recebe o writer
        PrintWriter out = response.getWriter();
        // escreve o texto
        out.println("<html>");
        out.println("<body>");
        out.println("Contador agora é: " + contador);
        out.println("</body>");
        out.println("</html>");
    }
}

  Quando a Servlet for inicializada, o valor do contador é definido para 0 (zero). Após isso, a cada requisição que é feita para essa Servlet, devido ao fato da instância ser sempre a mesma, a variável utilizada para incrementar será sempre a mesma, e por consequência imprimirá o número atual para o contador.   Sabemos que compartilhar variáveis entre múltiplas Threads pode nos trazer problemas graves de concorrência. Se duas threads (no caso, duas requisições) modificarem a mesma variável ao “mesmo tempo”, podemos ter perda de informações mesmo em casos simples como o do contador acima.   Há duas soluções para esse problema. A primeira seria impedir que duas threads acessem ao mesmo tempo o mesmo objeto crítico; para isso, podemos sincronizar o método service. Mas isso traria muitos problemas de escalabilidade (apenas uma pessoa por vez poderia requisitar minha página). A outra solução, mais simples, é apenas não compartilhar objetos entre threads.     Quando se fala de Servlets, a boa prática diz para evitar usar atributos compartilhados.    

Exemplos de códigos

Para ajudar você nessa jornada, eu disponibilizei alguns exemplos bem simples e fáceis de compreender.

Vinicius dos Santos

Apenas um apaixonado por Ciência da Computação e forma com que ela pode transformar vidas!

Deixe uma resposta