Аспектно-ориентированное программирование (АОП) — это парадигма программирования являющейся дальнейшим развитием процедурного и объектно-ориентированного программирования (ООП). Идея АОП заключается в выделении так называемой сквозной функциональности. И так все по порядку, здесь я покажу как это сделать в 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 {

    public void method1(List<String> list) {
        list.add("method1");
        System.out.println("MyService method1 list.size=" + list.size());
    }

    @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 с любым типом возврата * и количеством аргументов (..)

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

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

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

Комментарии (19)


  1. vba
    05.11.2018 12:19

    На дворе 2018 вроде, а значит AOP все еще зло.


  1. arylkov Автор
    05.11.2018 14:34

    В чем зло то?


    1. JustDont
      05.11.2018 14:51

      Так всё те же проблемы, которые были сразу высказаны: unknown side effects (или, говоря по-русски, хрен поймешь, сколько кода и в какой последовательности у тебя выполнится через все аспекты) и fragile pointcut (или, говоря по-русски, изменения в базовом коде изменят порядок и объем связанных с этим pointcut). Проще говоря, вот эти вот

      @Pointcut(«execution(* com.example.demoAspects.MyService.check())»)

      легко ломаются, и попробуйте разберитесь, что конкретно у вас сломается в сложном проекте с хотя бы сотнями pointcut (молчу уж про тысячи).


      1. arylkov Автор
        05.11.2018 15:07

        Что, ломаются, так это еще пока от не совершенства языка и реализации framework, а сама парадигма дает развитие, может через некоторое время все это и др., будет стандартом.


        1. JustDont
          05.11.2018 15:52

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

          ЗЫ: И не думаю, что в будущем небезопасное программирование когда-нибудь выиграет общий тренд у безопасного, причина-то банальна: хороших программистов, могущих сознательно не стрелять себе в ноги — значительно меньше, чем программистов, иногда стреляющих, если есть такая возможность.


      1. sshikov
        05.11.2018 22:00
        -1

        Дело в том, что AOP — это всего-лишь инструмент. Бездумное применение инструмента — вот это и есть зло.

        Вы попробуйте в чужой код (скажем, вендорский, без исходников) внедрить логирование, чтобы отловить ошибки, если там его нет. Или мониторинг со сбором метрик добавить.

        Я пробовал. И по большому счету, ничего лучше чем AOP (только не в таком «штатном» виде, как тут описано, а скорее как вот тут).

        Проблемы? Сломается? Но иногда в ситуациях вроде чужого кода это чуть ли не единственно возможный способ внести изменения в чужой код. Точнее, не единственный, потому что есть похожие инструменты типа byteman, но это по большому счету тот же AOP, только в профиль, с другим синтаксисом описания аспектов и их внедрения.


  1. arylkov Автор
    05.11.2018 16:05

    Имел в виду, что язык не позволит делать трудно находимые ошибки, сейчас reflection, и аннотации, чуть ли не единственный способ расширить сам язык, отсюда и вероятность выстрелить себе в ногу ). Но желание сообщества иметь более совершенные способы разработки опережают предложение разработчика языка. Тут нужен пока разумеый компромисс


  1. xdenser
    05.11.2018 18:52

    Могу подтвердить злобность AspectJ. Поначалу было вроде красиво и круто.
    Был load time weaving. Потом после перехода на Java 8 полезли странные баги, которые трудно воспроизвести. Ну и время старта приложения не прилично росло — ему же надо весь код перелопатить. Перешли на обработку во время компиляции — тепепь хоть есть уверенность, что сервер не упадет от этих ошибок. Но все равно билды падают иногда в местах, где все синтактически правильно. Баг репорты заполнены, но поскольку оно падает может 1 раз из ста, никто их не пофиксил. И врядли пофиксит. Ну о том что оно скачет неизвестно куда в дебагере и в стектрейсы странные видят люди, которые не разбираются в том, что аспект делает, я уже молчу.


    1. arylkov Автор
      05.11.2018 20:38

      Не берусь судить, как используется АОП, но соглашусь, что сделать сложным к пониманию, отладки, проект с ним можно. Но это вопрос к архитектуре, но так как на проектах унас большая текучка, то и допускаю что много перегибов. Меня больше интересовал академический аспект АОП и его частная реализация, ну а пока мы заложники прогресса. Спасибо.


  1. Throwable
    05.11.2018 22:34

    Декларативное описание, да еще не type-safe, да еще по имени класса/метода с сигнатурой и с wildcards… Прям как новая серия игр "Что? Где? Когда?". Тут простой рефакторинг все сразу поломает.
    И главное, для чего? Есть же Proxy.newProxyInstance(), ByteBuddy и BeanPostProcessor для детерминированного инжекта.


  1. funca
    06.11.2018 00:02

    Порекомендуйте как тестировать такой код, чтобы быть спокойным, что аспект пророс куда нужно и не пророс куда не нужно?
    Если правка в бизнес коде нечаянно ломает аспект, как это обнаружить и чья ответственность чинить поломанное?


    1. arylkov Автор
      06.11.2018 19:38

      spring testing, AopTestUtils, тут можно посмотреть


  1. vyatsek
    06.11.2018 14:40

    @Around("callAtMyServiceSecurityAnnotation(user)")
    АОП сковзная функциональность, которую вполне можно вызывать через статический вызов класса, внутри которого инстанс соаздается через ServiceLocator.
    public static Security{
    private static final Lazy<ISecurityService> lazySecurityService = new Lazy(Container::Resolve<ISecurityService>)

    public void logEvent(anyparams){
    lazy.value.logEvent(anyparams);
    }
    }

    В общем случае это тоже не идеальное решение, непонятно в какое время будет проинстанциирован ISecurityService и когда уничтожен, но такой подход избавляет от ненужных параметров в конструкторе и в случае падения stacktrace более наглядный. А кода примерно столько же, что аннтоация, что строчка вызова.


  1. xpendence
    06.11.2018 16:37

    Я попытался использовать AOP по Вашему примеру, но что-то не заходит в сам метод Before. Я описал проблему на stackoverflow, посмотрите, пожалуйста, и дайте совет, что не так.


    1. arylkov Автор
      06.11.2018 17:31

      Проверьте pom, на предмет spring-boot-starter-aop, spring-boot-starter-test
      В тесте должен
      @RunWith(SpringRunner.class)
      @SpringBootTest
      Как будто АОП не стартует


      1. xpendence
        06.11.2018 17:45

        всё оказалось проще — не хватало @EnableAspectJAutoProxy


        1. arylkov Автор
          06.11.2018 17:47

          Чувствовал, что аоп не стартует


  1. xpendence
    07.11.2018 12:07

    Тогда ещё один вопрос. Вот у меня два метода, на которые я хочу повесить аспект.

    @ApiLogBefore(transferType = TransferType.REQUEST, httpMethod = HttpMethod.GET, path = "", param = "transactionId")
    public ResponseEntity save(@RequestParam("transactionId") String transactionId) {

    и

    @ApiLogBefore(transferType = TransferType.REQUEST, httpMethod = HttpMethod.GET, path = "/id", param = "id")
    public ResponseEntity get(@RequestParam("id") Long id, HttpServletRequest request) {

    Метод, который должен перехватывать обе аннотации, выглядит так:

        @Before(value = "@annotation(before) && args(param,..)")
        public void before(ApiLogBefore before, String param) {


    Но почему-то перехватывается только первый, а второй (где вторым аргументом HttpServletRequest) — почему-то нет. В чём проблема, как думаете?


    1. xpendence
      07.11.2018 12:54

      Ошибку нашёл, просьба не беспокоиться :)