Хабрахабр

Аспектно-ориентированное программирование, Spring AOP

Аспектно-ориентированное программирование (АОП) — это парадигма программирования являющейся дальнейшим развитием процедурного и объектно-ориентированного программирования (ООП). Идея АОП заключается в выделении так называемой сквозной функциональности. И так все по порядку, здесь я покажу как это сделать в Java — Spring @AspectJ annotation стиле (есть еще schema-based xml стиль, функциональность аналогичная).

Выделении сквозной функциональности

До

image

и после

image

есть функциональность которая затрагивает несколько модулей, но она не имеет прямого отношения к бизнес коду, и ее хорошо бы вынести в отдельное место, это и показано на рисунке выше. Т.е.

Join point

image

Join point — следующее понятие АОП, это точки наблюдения, присоединения к коду, где планируется введение функциональности.

Pointcut

image

Правила запросов точек очень разнообразные, на рисунке выше, запрос по аннотации на методе и конкретный метод. Pointcut — это срез, запрос точек присоединения, — это может быть одна и более точек. Правила можно объединять по &&, ||,!

Advice

image

Инструкции можно выполнять по событию разных типов: Advice — набор инструкций выполняемых на точках среза (Pointcut).

  • Before — перед вызовом метода
  • After — после вызова метода
  • After returning — после возврата значения из функции
  • After throwing — в случае exception
  • After finally — в случае выполнения блока finally
  • Around — можно сделать пред., пост., обработку перед вызовом метода, а также вообще обойти вызов метода.

на один Pointcut можно «повесить» несколько Advice разного типа.

Aspect

image

Aspect — модуль в котором собраны описания Pointcut и Advice.

Все знаем про логирование кода который пронизывает многие модули, не имея отношения к бизнес коду, но тем не менее без него нельзя. Сейчас приведу пример и окончательно все встанет (или почти все) на свои места. И так отделяю этот функционал от бизнес кода.

Пример — логирование кода

Целевой сервис

@Service
public class MyService @AspectAnnotation public void method2() { System.out.println("MyService method2"); } public boolean check() { System.out.println("MyService check"); return true; }
}

Аспект с описанием Pointcut и Advice.

@Aspect
@Component
public class MyAspect { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Pointcut("execution(public * com.example.demoAspects.MyService.*(..))") public void callAtMyServicePublic() { } @Before("callAtMyServicePublic()") public void beforeCallAtMethod1(JoinPoint jp) { String args = Arrays.stream(jp.getArgs()) .map(a -> a.toString()) .collect(Collectors.joining(",")); logger.info("before " + jp.toString() + ", args=[" + args + "]"); } @After("callAtMyServicePublic()") public void afterCallAt(JoinPoint jp) { logger.info("after " + jp.toString()); }
}

И вызывающий тестовый код

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoAspectsApplicationTests { @Autowired private MyService service; @Test public void testLoggable() { List<String> list = new ArrayList(); list.add("test"); service.method1(list); service.method2(); Assert.assertTrue(service.check()); } }

Пояснения. В целевом сервисе нет никакого упоминания про запись в лог, в вызывающем коде тем более, в все логирование сосредоточено в отдельном модуле
@Aspect
class MyAspect ...

В Pointcut

@Pointcut("execution(public * com.example.demoAspects.MyService.*(..))") public void callAtMyServicePublic() { }

я запросил все public методы MyService с любым типом возврата * и количеством аргументов (..)

JoinPoint это не обязательный параметр который, предоставляет дополнительную информацию, но если он используется, то он должен быть первым.
Все разнесено в разные модули! В Advice Before и After которые ссылаются на Pointcut (callAtMyServicePublic), я написал инструкции для записи в лог. Вызывающий код, целевой, логирование.

Результат в консоли

image

Правила Pointcut могут быть различные

Несколько примеров Pointcut и Advice:

Запрос по аннотации на методе.

@Pointcut("@annotation(AspectAnnotation)")
public void callAtMyServiceAnnotation() { }

Advice для него

@Before("callAtMyServiceAnnotation()") public void beforeCallAt() { }

Запрос на конкретный метод с указанием параметров целевого метода

@Pointcut("execution(* com.example.demoAspects.MyService.method1(..)) && args(list,..))")
public void callAtMyServiceMethod1(List<String> list) { }

Advice для него

@Before("callAtMyServiceMethod1(list)") public void beforeCallAtMethod1(List<String> list) { }

Pointcut для результата возврата

@Pointcut("execution(* com.example.demoAspects.MyService.check())") public void callAtMyServiceAfterReturning() { }

Advice для него

@AfterReturning(pointcut="callAtMyServiceAfterReturning()", returning="retVal") public void afterReturningCallAt(boolean retVal) { }

Пример проверки прав на Advice типа Around, через аннотацию

@Retention(RUNTIME) @Target(METHOD) public @interface SecurityAnnotation { } // @Aspect @Component public class MyAspect { @Pointcut("@annotation(SecurityAnnotation) && args(user,..)") public void callAtMyServiceSecurityAnnotation(User user) { } @Around("callAtMyServiceSecurityAnnotation(user)") public Object aroundCallAt(ProceedingJoinPoint pjp, User user) { Object retVal = null; if (securityService.checkRight(user)) { retVal = pjp.proceed(); } return retVal; }

Методы которые необходимо проверять перед вызовом, на право, можно аннотировать «SecurityAnnotation», далее в Aspect получим их срез, и все они будут перехвачены перед вызовом и сделана проверка прав.

Целевой код:

@Service
public class MyService { @SecurityAnnotation public Balance getAccountBalance(User user) { // ... } @SecurityAnnotation public List<Transaction> getAccountTransactions(User user, Date date) { // ... } }

Вызывающий код:

balance = myService.getAccountBalance(user);
if (balance == null) { accessDenied(user);
} else { displayBalance(balance);
}

Т.е. в вызывающем коде и целевом, проверка прав отсутствует, только непосредственно бизнес код.

Пример профилирование того же сервиса с использованием Advice типа Around

@Aspect
@Component
public class MyAspect { @Pointcut("execution(public * com.example.demoAspects.MyService.*(..))") public void callAtMyServicePublic() { } @Around("callAtMyServicePublic()") public Object aroundCallAt(ProceedingJoinPoint call) throws Throwable { StopWatch clock = new StopWatch(call.toString()); try { clock.start(call.toShortString()); return call.proceed(); } finally { clock.stop(); System.out.println(clock.prettyPrint()); } }
}

Если запустить вызывающий код с вызовами методов MyService, то получим время вызова каждого метода. Таким образом не меняя вызывающий код и целевой я добавил новые функциональности: логирование, профайлер и безопасность.

Пример использование в UI формах

есть код который по настройке скрывает/показывает поля на форме:

public class EditForm extends Form { @Override
public void init(Form form) { formHelper.updateVisibility(form, settingsService.isVisible(COMP_NAME)); formHelper.updateVisibility(form, settingsService.isVisible(COMP_LAST_NAME)); formHelper.updateVisibility(form, settingsService.isVisible(COMP_BIRTH_DATE)); // ...
}

так же можно updateVisibility убрать в Advice типа Around

@Aspect
public class MyAspect { @Pointcut("execution(* com.example.demoAspects.EditForm.init() && args(form,..))") public void callAtInit(Form form) { } // ... @Around("callAtInit(form)") public Object aroundCallAt(ProceedingJoinPoint pjp, Form form) { formHelper.updateVisibility(form, settingsService.isVisible(COMP_NAME)); formHelper.updateVisibility(form, settingsService.isVisible(COMP_LAST_NAME)); formHelper.updateVisibility(form, settingsService.isVisible(COMP_BIRTH_DATE)); Object retVal = pjp.proceed(); return retVal; }

и.т.д.

Структура проекта

image

pom файл

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>demoAspects</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>demoAspects</name> <description>Demo project for Spring Boot Aspects</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

Материалы

Aspect Oriented Programming with Spring

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть