Trabalhando com o Design Pattern Interceptor no Java EE
Veja neste artigo o que é o padrão de projeto Interceptor, como ele é implementado em código puro (POJO) e como podemos implementar este padrão na plataforma Java EE 7.
Um conceito muito importante ao se estudar os interceptores é a programação orientada a aspectos (Aspect‐oriented Programming ou AOP), que, apesar de não estar descrita no livro do GoF e não ser um padrão clássico da literatura, introduziu um novo conceito e um novo paradigma para a programação. A ideia por trás é basear a ordem de execução do código através de aspectos. Assim, cada aspecto intercepta a execução do programa e adiciona seu próprio comportamento antes de continuar com a chamada. Os aspectos basicamente adicionam lógica e comportamento para o código em tempo de execução. Contudo, essa técnica também traz uma ordem de execução do código bastante difícil de debugar.
Apesar da AOP possuir muitos seguidores, a plataforma Java EE tem uma implementação bastante limpa que pode ser muito útil para muitos desenvolvedores. No restante do artigo será visto o que é a programação orientada a aspectos e como os interceptores podem ser implementados utilizando a técnica de AOP.
Programação Orientada a Aspectos
A programação orientada a aspectos visa adicionar comportamento nos códigos existentes ou nas aplicações para resolver determinados problemas. A AOP tornou-se um paradigma popular na década passada e com isso surgiram diversos frameworks de terceiros a oferecendo. Entre esses frameworks destacam-se o AspectJ e o Spring. Tanto o AspectJ quanto o Spring são largamente aceitos e já são utilizados há um longo tempo nos projetos Java. O Java SE também possui uma abordagem similar, porém mais básica através da utilização de servlet filters, embora essa abordagem seja limitada para requisições na web. Utilizando esses filtros, qualquer requisição ou resposta pode ser interceptada, e assim sendo, qualquer comportamento pode ser adicionado. A plataforma Java EE 7 também adotou a AOP e introduziu o conceito de interceptores.
A AOP não é classificada como um padrão de projeto, mas é aceita como um paradigma de programação, por isso ela não é encontrada na bibliografia de referência, como o livro do GoF e tampouco em outros livros de padrões de projeto.
A literatura afirma que a AOP é basicamente a injeção de código durante tempo de compilação ou de execução para adicionar comportamento desejado ou funcionalidade.
Os Frameworks que executam a injeção em tempo de compilação alteram o arquivo “.class”. O problema disso é que esse arquivo fica incompatível com o código fonte devido esse código que foi injetado. Por outro lado, a injeção em tempo de execução não modifica o código fonte ou o arquivo ".class". Essa injeção é realizada através de chamadas de interceptação e execução do código desejado antes ou depois da ordem de execução original. Isso será exemplificado mais adiante quando for discutida a plataforma Java EE.
A AOP é muito útil para adicionar ações repetitivas, como logging ou segurança para um código, sendo assim uma ótima ferramenta para encapsular conceitos que não envolvem o negócio. A desvantagem da AOP é que a sua implementação causa uma descentralização e distribuição, tornando assim difícil testar e debugar o código.
Os aspectos podem ser ligados ou desligados, o que é interessante dependendo do ambiente ou fase do projeto.
O restante do artigo mostra de forma prática o funcionamento do padrão Interceptor.
Implementando Interceptores em Código Puro (POJO)
O Java SE suporta todas as características da AOP apenas através de frameworks de terceiros utilizando AspectJ ou Spring. Eles também são uma alternativa à implementação do Java EE. Contudo, as aplicações Java web possuem vantagens ao utilizarem Servlets para interceptar requisições ou respostas. Para implementar um servlet filter basta criar uma nova classe e implementar a interface filter e após, é necessário implementar o método doFilter(), conforme mostrado no exemplo da Listagem 1.
package br.com.devmedia.interceptor.filter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class SegurancaFilter implements Filter {
@SuppressWarnings("unused")
private FilterConfig filterConfig = null;
//Implementação de doFilter()
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
Log.info(((HttpServletRequest) request).getRemoteAddr());
//executar alguma checagem de segurança aqui
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;
}
}
Além do código acima, o container web também precisa das configurações do arquivo web.xml mostrado no código da Listagem 2.
<?xml version= "1.0" encoding="UTF‐8" ?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
<filter>
<filter‐name>SegurancaFilter</filter‐name>
<filter‐class>br.com.devmedia.interceptor.filter.SegurancaFilter </filter‐class>
</filter>
<filter‐mapping>
<filter‐name>SegurancaFilter</filter‐name>
<url‐pattern>/*</url‐pattern>
</filter‐mapping>
</web-app>
Como pode ser visto, este arquivo XML faz o mapeamento das URLs para as classes específicas no Java.
Na plataforma Java EE 7 é ainda mais simples implementar os filtros, bastando utilizar anotações, conforme o exemplo da Listagem 3.
package br.com.devmedia.interceptor.filter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.Filter;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
@WebFilter(filterName = "SegurancaFilter", urlPatterns = {"/*"})
public class SegurancaFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
Log.info(((HttpServletRequest) request).getRemoteAddr());
//executar alguma checagem de segurança aqui
}
}
Apesar de serem simples de implementar, os filtros são bastante utilizados em aplicações empresariais. Porém, infelizmente, os filtros são limitados apenas para requisições da web. Para interceptar outros tipos de chamadas de métodos é preciso utilizar outra forma, como CDI ou EJBs como será discutido na próxima seção.
Implementando Interceptores na plataforma Java EE
Os interceptores foram introduzidos no Java EE a partir da versão 5, porém na plataforma Java EE 5 os interceptores estavam limitados ao Enterprise JavaBeans (EJB). Na plataforma Java EE 7 com a introdução do Context and Dependency Injection (CDI) os interceptores não precisam mais dos EJBs para serem utilizados.
De forma similar aos interceptores utilizados na plataforma Java SE, cada interceptor possui um bloco de código para ser adicionado. O destino a ser decorado é chamado de "Advice". Cada chamada a um "Advice" dentro do escopo do interceptor é interceptado. O local exato do aspecto a ser executado é chamado de "Pointcut".
Interceptadores básicos podem apenas trabalhar com EJBs. Um exemplo de uso de um interceptor seria em uma aplicação com centenas de EJBs e configurarmos a aplicação para logar todas as chamadas de EJB através de um deploy de um interceptor com o destino apontando para todos os EJBs.
A implementação de um interceptor em Java EE é direta. Primeiramente é preciso criar uma nova classe interceptor e anota-la com a anotação @Interceptor. Qualquer método anotado com @AroundInvoke é executado no pointcut (local exato do aspecto a ser executado). No entanto, existem algumas regras de sintaxe para a assinatura do método pointcut. A primeira regra diz que qualquer método pointcut deve retornar um objeto do tipo Object e possuir um parâmetro do tipo InvocationContext, e a segunda regra diz que o método pointcut deve lançar uma exceção.
Assim, pode ser utilizado o parâmetro InvocationContext para acessar informações sobre o contexto atual. Segue na Listagem 4 um exemplo de como essa funcionalidade pode ser realizada.
package br.com.devmedia.interceptor;
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
@Interceptor
public class SegurancaInterceptor {
@AroundInvoke
public Object verificaSeguranca(InvocationContext context) throws Exception {
Logger.getLogger("SecurityLog")
.info(context.getMethod().getName()+ "foi acessado!");
return context.proceed();
}
}
Para colocar a classe interceptor em ação é preciso anotar a classe destino Advice com a anotação @Interceptors. A anotação @Interceptors deve ser utilizada apenas em um EJB ou MDB (Message Driven Bean).
Segue na Listagem 5 um exemplo da implementação de um Advice.
package br.com.devmedia.interceptor;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.enterprise.event.Event;
import javax.inject.Inject;
import javax.interceptor.Interceptors;
@Interceptors(SegurancaInterceptor.class)
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class ExemploServicoNegocio {
public void iniciaServicoDeNegocio() {
System.out.println(“Exemplo de serviço de negócio interceptado.”);
}
public void iniciaOutroServicoDeNegocio() {
System.out.println(“Exemplo de outro serviço de negócio interceptado.”);
}
}
A anotação Interceptors é bastante flexível podendo ser utilizada em nível de classe e em nível de método. A anotação Interceptors também suporta múltiplas entradas de interceptores, que permite habilitar múltiplos interceptadores no Advice destino.
O exemplo acima usou interceptadores em nível de classe, o que significa que o interceptor SegurancaInterceptor interceptará qualquer uma das chamadas de serviço. Caso não seja necessário interceptar todas as chamadas de métodos na classe pode ser utilizado anotações em nível de método conforme exemplo da Listagem 6.
package br.com.devmedia.interceptor;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.enterprise.event.Event;
import javax.inject.Inject;
import javax.interceptor.Interceptors;
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class ExemploServicoNegocio {
@Interceptors(SegurancaInterceptor.class)
public void iniciaServicoDeNegocio() {
System.out.println(“Exemplo de serviço de negócio interceptado.”);
}
public void iniciaOutroServicoDeNegocio() {
System.out.println(“Exemplo de outro serviço de negócio interceptado.”);
}
}
No exemplo acima apenas chamadas para o método iniciaServicoDeNegocio() são interceptadas, diferente da listagem anterior onde todos os métodos da classe são interceptados. Cada método deve ser anotado separadamente.
Além das anotações vistas pode-se utilizar a interface InvocationContext para extrair informações de contexto ou interagir com o contexto Advice. Segue na Tabela 1 os métodos mais úteis utilizados na interface InvocationContext
Método | Descrição |
public Object getTarget(); | Retorna o Advice destino. |
public Method getMethod(); | Retorna o método executado pelo Advice. |
public Object[] getParameters(); | Acessa os parâmetros do método do Advice. |
public void setParameters(Object[]); | Configura os parâmetros do método do Advice. |
public java.util.Map getContextData(); | Acessa informações do context. |
public Object proceed() throws Exception; | Continua a execução. |
No exemplo da Listagem 7 o nome do método é acessado. Além disso, é verificado se o interceptor havia autorizado o acesso ao usuário anteriormente, caso não tenha sido o usuário é autorizado para acessar esse método.
package br.com.devmedia.interceptor;
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
@Interceptor
public class SegurancaInterceptor {
@AroundInvoke
public Object verificaSeguranca(InvocationContext context) throws Exception {
System.out.println(context.getMethod()
.getName()+ " foi acessado!");
String usuario = context.getContextData.get("usuario");
if (usuario == null) {
usuario = (String) context.getParameters()[0];
context.getContextData.put("usuario", usuario);
}
return context.proceed();
}
}
Outra forma de utilizar interceptores é utilizar CDI. Antes do surgimento do CDI, os interceptores eram aplicáveis apenas para os EJBs e MDBs. O CDI possui um enorme poder e tornou os interceptores possíveis funcionando em qualquer objeto.
A implementação dos interceptores utilizando CDI é bastante direta e flexível. Primeiramente é preciso especificar um "binding" (ou uma vinculação) que é uma anotação customizada anotada com @InterceptorBinding. Segue na Listagem 8 um exemplo.
@InterceptorBinding
@Target({TYPE, METHOD})
@Retention(RUNTIME)
public @interface Seguranca {}
A anotação @InterceptorBinding é utilizada para vincular interceptores com um código destino. Em seguida, pode-se implementar e anotar o interceptor com um binding customizado. Os interceptores CDI são implementados da mesma forma que os interceptores EJB, a única diferença significativa é a anotação conforme pode ser visto no exemplo abaixo:
package br.com.devmedia.interceptor;
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
@Seguranca
@Interceptor
public class SegurancaInterceptor {
@AroundInvoke
public Object verificaSeguranca(InvocationContext context) throws Exception {
System.out.println(context.getMethod()
.getName()+ " foi acessado!");
String usuario = context.getContextData.get("usuario");
if (usuario == null) {
usuario = (String)context.getParameters()[0];
context.getContextData.put("usuario", usuario);
}
return context.proceed();
}
@PostConstruct
public void ativa(){
System.out.println("Ativando…");
}
@PreDestroy
public void desativa(){
System.out.println("Desativando…");
}
}
Assim como os interceptores EJB, a anotação @Interceptor precisa ser usada para promover a classe para um interceptor. A anotação @Seguranca vincula o interceptor. Por fim, a anotação @AroundInvoke marca o método que será executado durante as chamadas interceptadas. O próximo passo é anotar o interceptor em um Advice, conforme o exemplo da Listagem 10.
package br.com.devmedia.interceptor;
import javax.interceptor.Interceptors;
@Seguranca
public class ExemploServicoNegocio {
public void iniciaServicoDeNegocio() {
System.out.println(“Exemplo de serviço de negócio interceptado.”);
}
public void iniciaOutroServicoDeNegocio() {
System.out.println(“Exemplo de outro serviço de negócio interceptado.”);
}
}
Interceptores CDI requerem um passo adicional que é declarar os interceptores em um arquivo beans.xml. Este passo é utilizado para determinar a ordem de execução dos interceptores. O container CDI não é iniciado se o arquivo beans.xml estiver faltando.
Segue na Listagem 11 um exemplo do arquivo beans.xml.
<beans xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=" http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
<interceptors>
<class>br.com.devmedia.interceptor.SegurancaInterceptor</class>
<class>br.com.devmedia.interceptor.OutroInterceptor</class>
</interceptors>
</beans>
A ordem de execução do interceptor depende da ordem de declaração no arquivo beans.xml, portanto apenas esse arquivo é responsável por ditar a ordem de execução dos interceptores.
Uma situação que pode ocorrer é uma mistura de interceptores CDI e EJB, nesse caso os interceptores EJB executam antes dos interceptores CDI.
Se for preciso fazer essa mistura de interceptores CDI com EJB deve-se saber que isso traz mais complexidade para a aplicação, principalmente na arquitetura do aplicativo, e com isso, desenvolvedores que não estão familiarizados com o código podem ter problemas no entendimento e na codificação da aplicação.
- Erich Gamma, Ricard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).
- Eric Freeman, Elisabeth Robson, Bert Bates, Kathy Sierra. Head First Design Patterns. O'Reilly Media, 2004.
- Murat Yener, Alex Theedom. Proffesional Java EE Design Patterns. Wrox, 2015.
- Documentação Oracle da interface InvocationContext
Artigos relacionados
-
Artigo
-
Artigo
-
Artigo
-
Artigo
-
Vídeo