Статьи

Аудит Spring MVC Webapp с AspectJ. Часть 2

Теперь это блог, который вы хотите прочитать, если вы заинтересованы в создании Spring MVC Webapp, который использует Aspect-Oriented Programming (AOP) в форме аннотаций Aspectj’s @Aspect и @Before для аудита посещения пользователем экрана. 

Как я уже говорил в  моем последнем блоге Аудит посещений пользователем экрана — одна из немногих сквозных задач, которые очень хорошо решает Аспектно-ориентированное программирование (AOP). Идея моего демо-кода заключается в том, что вы добавляете аннотацию к соответствующим контроллерам, и каждый раз, когда пользователь посещает страницу, этот визит записывается. Используя эту технику, вы можете создать изображение самых популярных экранов и, следовательно, самых популярных функциональных блоков в вашем приложении. Знание этих подробностей облегчает решение о том, куда направить ваши усилия по разработке, поскольку разработка тех частей вашего приложения, которые вряд ли кто-либо когда-либо использует, не окупается.

Для демонстрационного кода я создал простое приложение Spring MVC, которое имеет два экрана: домашняя страница и страница справки. Вдобавок к этому я создал простую аннотацию: @Audit, которая используется для обозначения контроллера как требующего аудита (не все из них будут, особенно если вы решите проверять функциональные точки, а не отдельные экраны) и сообщать объект совета идентификатор экрана. Это я продемонстрировал в фрагменте кода ниже:

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

Прежде чем приступить к рассмотрению AspectJ, сначала нужно создать стандартное веб-приложение Spring MVC с использованием Spring Template, предназначенного для этой работы:

Следующее, что нужно сделать, это внести целый ряд изменений в файл POM, как описано в  моем предыдущем блоге . Они необходимы для того, чтобы все работало, хотя не все они необходимы; однако убедитесь, что вы добавили зависимость aspectJwearver и удалили определение плагина AspectJ.

Приложение имеет два контроллера и два простых JSP. Первый контроллер — это HomeController, взятый из приложения Spring MVC, а второй — это HelpController, предназначенный для отображения справки на любой странице приложения. Я включил метод HelpController’sshowHelp (…) ниже, но это только для полноты. В данном случае не имеет значения, что контроллеры делают, если есть пара для аудита.

@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";}

Из приведенного выше кода вы можете видеть, что оба моих метода RequestMapping снабжены аннотацией @Audit, поэтому следующим шагом является определение:

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface Audit {
String value();}

Ключевыми моментами в этом коде являются политика хранения и цель. Политика хранения должна быть установлена ​​на RetentionPolicy.RUNTIME, что означает, что компилятор не выбрасывает аннотацию и гарантирует, что он там, загруженный в JVM, во время выполнения. @Target определяет, где вы можете применить аннотацию. В этом случае я хочу, чтобы он применялся только к методам, поэтому целью является ElementType.METHOD. Аннотация ДОЛЖНА содержать значение, которое в этом случае используется для хранения имени экрана, который пользователь посещает в данный момент.

Следующая ключевая абстракция — это класс AuditAdvice, как показано ниже:

@Aspectpublic class AuditAdvice {
@Autowiredprivate 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());}

}

Это аннотируется двумя аннотациями AspectJ: @Aspect и @Before. Аннотация @Aspect помечает класс AuditAdvice как аспект , в то время как аннотация @Before означает, что метод audScreen (…) вызывается  перед  любым методом, определение которого соответствует выражению, являющемуся аргументом аннотации @Before. 

Это выражение идея довольно крутая. Я уже рассмотрел конструкцию выражения выполнения в своем блоге об  использовании @AfterThrowing AdviceJ в вашем приложении Spring.; однако, чтобы подвести итог, я собираюсь применить аннотированный метод @Before к любому методу, который имеет публичную видимость, возвращает строку, находится в пакете com.captaindebug.audit.controller и имеет слово Controlleras, являющееся частью класса имя. Другими словами, я затрудняю применение этого выражения выполнения ко всему, кроме контроллеров моего приложения, и эти контроллеры ДОЛЖНЫ быть аннотированы аннотацией @Audit, как описано выражением @annotation (auditAnnotation) и аудитом метода AuditScreen (…) Аргумент аудита аннотации. Это означает, что я не могу случайно применить аннотацию @ Audit ни к чему, кроме контроллера

Класс AuditAdvice делегирует ответственность за фактический аудит AuditService. Это фиктивная служба, поэтому вместо того, чтобы делать что-то полезное, например, сохранять событие аудита в базе данных, она просто добавляет его в файл журнала. 

@Servicepublic 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";}

}

Итак, этот код рассмотрен, все, что осталось сделать, — это разобраться в конфигурационном файле Spring, и здесь не так много дел. Во-первых, как и в любом приложении AOP, вам необходимо добавить следующую строку AOP:

<aop:aspectj-autoproxy/>

… вместе с деталями схемы:

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"

И, во-вторых, вам нужно сообщить контексту Spring о ваших классах рекомендаций, обновив элемент 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>

Вы также можете при желании удалить номера версий из конца URI расположения схемы. Например:

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

будет выглядеть так:

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

Причиной этого является то, что это упрощает обновление версий Spring в какой-то момент в будущем, так как URI схемы  без  номеров версий, похоже, указывают на последнюю версию этой схемы.

Для полноты, мой конфигурационный файл выглядит так:

<?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>

Наконец, при запуске приложения посещение пользователя на домашней странице записывается. Когда пользователь нажимает на   ссылку справки, посещение страницы справки также записывается. Вывод в файле журнала выглядит примерно так:

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

Код для этого и следующего блога доступен на github:  https://github.com/roghughe/captaindebug/tree/master/audit-aspectj