Back-End

31 out, 2013

Auditoria a Spring MVC Webapp com AspectJ – Parte 02

Publicidade

Agora, este é o artigo que você quer ler, se estiver interessado em criar um Webapp MVC Spring que utiliza Programação Orientada a Aspectos (AOP) na forma das anotações @Aspect e @Before do AspectJ para auditar uma visita de um usuário a uma tela.

Como eu disse em meu último artigo, auditar blog visitas de um usuário a uma tela é uma das poucas preocupações transversais que a Programação Orientada a Aspectos (AOP) resolve muito bem. A ideia, no caso do meu código de demonstração, é que você adicione uma anotação para os controladores apropriados e, cada vez que um usuário visita uma página, a visita é gravada. Usando essa técnica, você pode construir uma imagem das telas mais populares e, portanto, os pedaços mais populares de funcionalidade em seu aplicativo. Saber esses detalhes faz com que seja mais fácil decidir para onde apontar o seu esforço de desenvolvimento, uma vez que não vale a pena desenvolver os pedaços de seu aplicativo que quase ninguém usa.

Para o código de demonstração, eu criei um simples aplicativo Spring MVC que tem duas telas: uma home page e uma página de ajuda. Além de tudo isso, eu criei uma simples anotação: @Audit, que é usada para marcar um controller como aquele que precisa de auditoria (não todos eles, especialmente se você optar por auditar os pontos de função em vez de telas individuais) e para dizer ao conselho de objeto a id da tela. Isso eu já demonstrei no trecho de código abaixo:

  @Audit("Home")
  @RequestMapping(value = "/", method = RequestMethod.GET)
  public String home(Locale locale, Model model) {

Antes de ficar preso no lado do AspectJ das coisas, a primeira coisa a fazer é criar um aplicativo web Spring MVC padrão usando o template Spring projetado para o trabalho:

template-spring

Em seguida, é necessário fazer um monte de alterações no arquivo POM como descrito no meu artigo anterior. Elas são necessárias para que tudo funcione, embora não sejam todas essenciais; no entanto, tenha certeza de que você adicionou a dependência aspectJwearver e removeu a definição plugin AspectJ.

O aplicativo tem dois controllers e dois JSPs simples. O primeiro controller é o HomeController retirado do  aplicativo Spring MVC, enquanto que o segundo é um HelpController projetado para exibir ajuda em qualquer página da aplicação. Eu incluí abaixo o método showHelp(…) do HelpController, mas isso é só para ser completo. Realmente não importa, neste caso, o que os controllers fazem, desde que haja um par de auditoria.

@Controller()
public class HelpController {

  @Audit("Help")  // User has visited the help page
  @RequestMapping(value = "/help", method = RequestMethod.GET)
  public String showHelp(@RequestParam int pageId, Model model) {

    String help = getHelpPage(pageId);

    model.addAttribute("helpText", help);
    return "help";
  }

A partir do código acima, você pode ver que ambos os meus métodos RequestMapping são anotados com uma anotação @Audit, então o próximo passo é a sua definição:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Audit {

  String value();
}

As coisas mais importantes sobre esse código são a política de retenção e o alvo. A política de retenção deve ser definida como RetentionPolicy.RUNTIME, o que significa que o compilador não joga a anotação fora e garante que ela está lá, carregada na JVM, em tempo de execução. @Target define onde você pode aplicar a anotação. Nesse caso, eu o quero apenas aplicados a métodos, então o alvo é ElementType.METHOD. A anotação deve conter um valor, que nesse caso é usado para manter o nome da tela que o usuário está visitando.

A próxima abstração chave é a classe AuditAdvice, como mostrado abaixo:

@Aspect
public class AuditAdvice {

  @Autowired
  private AuditService auditService;

  /**
   * Advice for auditing a user's visit to a page. The rule is that the Before annotation
   * applies to any method in any class in the com.captaindebug.audit.controller package
   * where the class name ends in 'Controller' and the method is annotated by @Audit.
   *
   * @param auditAnnotation
   *            Audit annotation holds the name of the screen we're auditing.
   */
  @Before("execution(public String com.captaindebug.audit.controller.*Controller.*(..)) && @annotation(auditAnnotation) ")
  public void myBeforeLogger(Audit auditAnnotation) {

    auditService.audit(auditAnnotation.value());
  }

}

Isso é anotado com duas anotações AspectJ: @Aspect e @Before. A anotação @Aspect marca a classe AuditAdvice como um aspect, enquanto a anotação @Before significa que o auditScreen(…) é chamado antes de qualquer método cuja definição corresponde à expressão que é o argumento da anotação @Before.

Essa expressão é uma ideia bem legal. Eu já falei sobre a construção da expressão execution neste artigo, mas, para resumir isso, vou aplicar o método anotado @Before a qualquer método que tenha visibilidade pública, retornar uma String, está no pacote com.captaindebug.audit.controller e tem a palavra controller como parte do nome da classe. Em outras palavras, eu estou dificultando a aplicação da expressão execution a qualquer coisa, mas os controllers da minha aplicação e esses controllers devem ser anotados por uma anotação @Audit, conforme descrito pela expressão @annotation(auditAnnotation) e o argumento do método Audit auditAnnotation auditScreen(…). Isso significa que eu não posso aplicar, inadvertidamente, a anotação @Audit para qualquer coisa, além de a um controller.

A classe AuditAdvice delega a responsabilidade pela auditoria real para um AuditService. Esse é um serviço fictício, então em vez de fazer algo útil, como armazenar o evento de auditoria em um banco de dados, ele simplesmente o adiciona a um arquivo de log.

@Service
public class AuditService {

  private static Logger logger = LoggerFactory.getLogger(AuditService.class);

  /**
   * Audit this screen against the current user name
   *
   * It's more useful to put this info into a database so that that you can count visits to
   * pages and figure out how often they're used. That way, you can focus your design on the
   * popular parts of your application. The logger is just for demo purposes.
   */
  public void audit(String screenName) {

    String userName = getCurrentUser();

    logger.info("Audit: {} - {}", userName, screenName);

  }

  /**
   * Get the current logged on user name by whatever mechanism available
   */
  private String getCurrentUser() {
    return "Fred";
  }

}

Então, esse é o código coberto, tudo o que resta a fazer agora é resolver o arquivo de configuração Spring, e não há muito a fazer aqui. Em primeiro lugar, como acontece com qualquer aplicação AOP, você precisa adicionar a seguinte linha de habilitação AOP:

<aop:aspectj-autoproxy/>

…junto com seus detalhes do esquema:

xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"

E, em segundo lugar, é preciso dizer o contexto Spring sobre o seu conselho de classe (s), atualizando o elemento context:components-scan:

<context:component-scan base-package="com.captaindebug.audit">
  <context:include-filter type="aspectj"
   expression="com.captaindebug.audit.aspectj.AuditAdvice" />
 </context:component-scan>

Você também pode remover opcionalmente os números de versão do final das URIs. Por exemplo:

http://www.springframework.org/schema/context/spring-context.3.0.xsd

torna-se:

http://www.springframework.org/schema/context/spring-context.xsd

A razão para isso é que ele simplifica a atualização de versões Spring, em algum momento no futuro, já que os URIs sem quaisquer números de versão parecem apontar para a versão mais recente desse esquema.

Para completar, meu arquivo de configuração parece com isto:

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xmlns:beans="http://www.springframework.org/schema/beans"
 xmlns:context="http://www.springframework.org/schema/context"
 xmlns:aop="http://www.springframework.org/schema/aop"
 xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
  http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
  http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

 <!-- DispatcherServlet Context: defines this servlet's request-processing 
  infrastructure -->

 <!-- Enables the Spring MVC @Controller programming model -->
 <annotation-driven />

 <!-- Handles HTTP GET requests for /resources/** by efficiently serving 
  up static resources in the ${webappRoot}/resources directory -->
 <resources mapping="/resources/**" location="/resources/" />

 <!-- Resolves views selected for rendering by @Controllers to .jsp resources 
  in the /WEB-INF/views directory -->
 <beans:bean
  class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <beans:property name="prefix" value="/WEB-INF/views/" />
  <beans:property name="suffix" value=".jsp" />
 </beans:bean>

 <aop:aspectj-autoproxy/>

 <context:component-scan base-package="com.captaindebug.audit">
  <context:include-filter type="aspectj"
   expression="com.captaindebug.audit.aspectj.AuditAdvice" />
 </context:component-scan>

</beans:beans>

Finalmente, quando você executar o aplicativo, a visita do usuário à página inicial é gravada. Quando o usuário clica no link ajuda, a visita à página de ajuda também é gravada. A saída no arquivo de log é algo como isto:

INFO : com.captaindebug.audit.service.AuditService - Audit: Fred - Home
INFO : com.captaindebug.audit.controller.HomeController - Welcome home! the client locale is en_US
INFO : com.captaindebug.audit.service.AuditService - Audit: Fred - Help

***

Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em http://www.captaindebug.com/2013/07/auditing-spring-mvc-webapp-with-aspectj.html#.Uj9qpxaxOON