Java Servlets: Suporte Assíncrono e E/S Sem Bloqueio
Veja neste artigo uma introdução ao suporte assíncrono fornecido para Servlets e Filter, E/S Sem bloqueio que complementa o suporte assíncrono e o Java Web Fragments.
Nas últimas versões os Servlets têm adquirido novidades indispensáveis aos desenvolvedores. O suporte assíncrono e o novo recurso de E/S sem bloqueio permitem que possamos utilizar melhores os recursos do servidor, cada vez mais indispensáveis quanto mais aumentamos o tamanho das aplicações e a base de usuários que cresce a cada dia. No restante do artigo veremos esses dois assuntos e também o Web Fragments que vem sendo cada vez mais utilizado nas aplicações corporativas.
Suporte Assíncrono
Os recursos do servidor são importantes e devem ser utilizados de forma conservadora. Consideremos um Servlet que precisa esperar uma conexão JDBC estar disponível a partir de um pool de conexões, ou um Servlet que espera receber uma mensagem JMS ou ainda um Servlet aguardando a leitura de um recurso do sistema de arquivos. Aguardar pela execução de processos de longa execução bloqueando a thread não é considerado uma boa prática de uso dos recursos do servidor.
Este é um caso onde o servidor pode ser processado de forma assíncrona. O processamento assíncrono permite que o controle (ou a thread) seja retornado para o container para que ele possa assim executar outras tarefas enquanto aguardamos que um processo de longa execução seja completado. O processamento de solicitações continua na mesma thread após a resposta do processo de longa execução ser retornado, ou então pode ser encaminhado para um novo recurso a partir do processo de longa execução.
O comportamento assíncrono precisa ser explicitamente habilitado em um Servlet. Podemos fazer isso apenas adicionando o atributo “asyncSupported” em @WebServlet. Segue na Listagem 1 um exemplo.
Listagem 1. Habilitando o processamento assíncrono no Servlet.
@WebServlet(urlPatterns="/async", asyncSupported=true)
public class MyAsyncServlet extends HttpServlet {
//. . .
}
Também podemos habilitar o comportamento assíncrono configurando o elemento no arquivo web.xml para “true” ou chamando “ServletRegistration.setAsyncSupported(true)” programaticamente.
Podemos então iniciar o processamento assíncrono em uma thread separada usando o método “startAsync” na requisição. Este método retorna “AsyncContext” que representa o contexto de execução da requisição assíncrona. Dessa forma, podemos completar a requisição assíncrona chamando explicitamente “AsyncContext.complete” ou de forma implícita enviando para outro recurso. Caso quisermos completar a requisição de forma implícita o container completará a invocação da requisição assíncrona.
Podemos implementar um processamento assíncrono conforme o código da Listagem 2.
Listagem 2. Exemplo implementação de um processo assíncrono.
class MeuServicoAssincrono implements Runnable {
AsyncContext ac;
public MyAsyncService(AsyncContext ac) {
this.ac = ac;
}
@Override
public void run() {
//. . .
ac.complete();
}
}
Este serviço pode ser invocado através do método doGet, conformeexemplificado na Listagem 3.
Listagem 3. Exemplo invocando o serviço implementado anteriormente.
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
AsyncContext ac = request.startAsync();
ac.addListener(new AsyncListener() {
public void onComplete(AsyncEvent event) throws IOException {
//. . .
}
public void onTimeout(AsyncEvent event) throws IOException {
//. . .
}
//. . .
});
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(10);
executor.execute(new MeuServicoAssincrono (ac));
}
Neste código o “request” é colocado em um modo assíncrono. AsyncListener é registrado para escutar eventos quando o processamento de uma requisição esta completo, ou tem o tempo excedido, ou ainda se resultou em algum erro.
O serviço de longa execução é invocado em uma thread separada e chama AsyncContext.complete para sinalizar a conclusão do processamento da solicitação.
Uma requisição pode ser enviada de um Servlet assíncrono para um síncrono, mas ao contrário é ilegal.
O comportamento assíncrono está disponível no Servlet Filter também.
E/S Sem bloqueio
O Servlet 3.0 permitiu que utilizássemos o processamento de requisições assíncronas, mas só é permitido o tradicional I/O, que dessa forma restringiu a escalabilidade dos aplicativos.
Em uma aplicação típica, o ServletInputStream é lido em um loop while, conforme exemplificado na Listagem 4.
Listagem 4. Exemplo de como é lido um ServletInputStream.
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
ServletInputStream input = request.getInputStream();
byte[] b = new byte[1024];
int tam = -1;
while ((tam = input.read(b)) != -1) {
//. . .
}
}
Se os dados de entrada estão bloqueando ou transmitindo mais lentamente do que o servidor pode ler, então a thread do servidor fica à espera desses dados. O mesmo pode acontecer se os dados são gravados em ServletOutputStream. Este tipo de comportamento acaba restringindo a escalabilidade do Container Web.
O Nonblocking I/O (Entrada/Saída Sem Bloqueio) permite que os desenvolvedores leiam dados quando eles estiverem disponíveis ou então gravar os dados quando for possível. Isto não aumenta somente a escalabilidade do Container Web, mas também o número de conexões que podem ser gerenciadas simultaneamente.
A E/S Sem Bloqueio apenas funciona com o processamento de requisições assíncronas em Servlets, Filter e Upgrade Processing.
O Servlet 3.1 permite a funcionalidade E/S Sem Bloqueio através da introdução de duas novas interfaces: ReadListener e WriteListener.
Esses listeners possuem métodos de callback que são invocados quando o conteúdo está disponível para ser lido ou quando ele pode ser escrito sem bloqueio.
Para isso precisamos reescrever o método doGet anterior, conforme exemplificado no código da Listagem 5.
Listagem 5. Exemplo do método doGet reescrito.
AsyncContext context = request.startAsync();
ServletInputStream input = request.getInputStream();
input.setReadListener(new MeuReadListener(input, context));
Invocando métodos setXXXListener indica que a E/S Sem Bloqueio é usada ao invés da E/S tradicional.
O ReadListener tem três métodos de callback, são eles:
- O método onDataAvailable. Este método é chamado sempre que um dado pode ser lido sem bloqueio.
- O método onAllDataRead. Este método é invocado sempre que os dados para a requisição atual são completamente lidos.
- O método onError. Este método é invocado se houver um erro ao processar o pedido.
Listagem 6. Exemplo utilizando os métodos do ReadListener.
@Override
public void onDataAvailable() {
try {
StringBuilder sb = new StringBuilder();
int tam = -1;
byte b[] = new byte[1024];
while (input.isReady() && (tam = input.read(b)) != -1) {
String data = new String(b, 0, tam);
}
} catch (IOException ex) {
//. . .
}
}
@Override
public void onAllDataRead() {
context.complete();
}
@Override
public void onError(Throwable t) {
t.printStackTrace();
context.complete();
}
Neste código o método onDataAvailable é invocado sempre que a informação pode ser lida sem bloqueio. O método ServletInputStream.isReady é usado para checar se o dado pode ser lido sem bloqueio e então o dado é lido.
O context.complete é chamado no método onAllDataRead e onError para sinalizar a conclusão da leitura de dados.
ServletInputStream.isFinished pode ser usado para checar o status de uma leitura sem bloqueio de I/O.
No máximo, um ReadListener pode ser registrado em ServletIntputStream.
O WriteListener por sua vez tem dois métodos de callback:
- O método onWritePossible. Este método é chamado sempre que o dado pode ser escrito sem bloqueio.
- O método onError. Este método é invocado se existe um erro ao processar a resposta.
No máximo, um WriteListener pode ser registrado em um ServletOutputStream.
Por fim, ServletOutputStream.canWrite é um novo método para checar se o dado pode ser escrito sem bloqueio.
Web Fragments
Um Web Fragment é parte ou todo o arquivo web.xml incluído em uma biblioteca ou no diretório “META-INF” do JAR. Se o arquivo estiver no diretório “WEB-INF/lib”, o container faz a configuração automática sem exigir que o desenvolvedor o faça explicitamente.
Este arquivo pode incluir praticamente todos os elementos que podem ser especificadas em web.xml. No entanto, o elemento de nível superior deve ser chamado “webfragment” e o arquivo correspondente deve ser chamado webfragment.xml. Isso permite o particionamento lógico da aplicação web:
Segue na Listagem 7 um exemplo de um arquivo “webfragment.xml”.
Listagem 7. Exemplo de um arquivo webfragment.xml.
<web-fragment>
<filter>
<filter-name>MeuFilterExemplo</filter-name>
<filter-class>app.exemplo.MeuFilterExemplo</filter-class>
<init-param>
<param-name>meuInitParam</param-name>
<param-value>...</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>MeuFilterExemplo</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-fragment>
O desenvolvedor pode especificar a ordem que os recursos especificados no “web.xml” e “web-fragment.xml” devem ser carregados. O elemento no web.xml é usado para especificar a ordem exata ou absoluta que os recursos deveriam ser carregados, e o elemento dentro de web-fragment.xml é usado para especificar a ordem relativa.
As duas ordens são mutuamente exclusivas, e a ordem absoluta sobrescreve a ordem relativa. A ordem absoluta contém um ou mais elementos “” que especifica o nome dos recursos e a ordem na qual eles necessitam ser carregados.
Especificando o elemento “” permitimos que outros recursos não mencionados na ordem sejam carregados. Segue na Listagem 8 um exemplo de como podemos especificar uma ordem absoluta:
Listagem 8. Exemplo de como especificamos a ordem absoluta no arquivo web.xml.
<web-app>
<name>MinhaAplicacao</name>
<absolute-ordering>
<name>MeuServletExemplo</name>
<name>MeuFilterExemplo</name>
</absolute-ordering>
</web-app>
Neste código, os recursos especificados em web.xml são carregados primeiro e seguido por MeuServletExemplo e MeuFilterExemplo.
Também podemos ter zero ou um elemento e em para especificarmos os recursos que precisam ser carregados antes e depois do recurso chamado no “webfragment” ser carregado:
<web-fragment>
<name>MeuFilterExemplo</name>
<ordering>
<after>MeuServletExemplo</after>
</ordering>
</web-fragment>
Este código vai exigir que o Container carregue o recurso MeuFilterExemplo depois que o recurso MeuServletExemplo (definido em outro lugar) for carregado.
Se web.xml tem um metadata-complete setado para true, então o arquivo web-fragment.xml não é processado.
O arquivo web.xml sempre possui uma prioridade mais alta ao resolver os conflitos entre “web.xml” e “web-fragment.xml”.
Por fim, vale ressaltar que se um arquivo “web-fragment.xml” não tem um elemento “” e o web.xml não tem um elemento “”, os recursos são considerados como não possuírem qualquer dependência de ordem.
Bibliografia
[1]Josh Juneau. Java EE 7 Recipes: A Problem-Solution Approach. Apress, 2013.
[2]Josh Juneau. Introducing Java EE 7: A Look at What's New. Apress, 2013.
[3]Arun Gupta. Java EE 7 Essentials. O'Reilly, 2013.
Artigos relacionados
-
Artigo
-
Artigo
-
Artigo
-
Artigo
-
Vídeo