Аспектно-ориентированное программирование, 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 {
    
        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
    Поделиться публикацией

    Комментарии 20

      +1

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

        0
        В чем зло то?
          0
          Так всё те же проблемы, которые были сразу высказаны: unknown side effects (или, говоря по-русски, хрен поймешь, сколько кода и в какой последовательности у тебя выполнится через все аспекты) и fragile pointcut (или, говоря по-русски, изменения в базовом коде изменят порядок и объем связанных с этим pointcut). Проще говоря, вот эти вот
          @Pointcut(«execution(* com.example.demoAspects.MyService.check())»)

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

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

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

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

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

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

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

                  0
                  Порекомендуйте как тестировать такой код, чтобы быть спокойным, что аспект пророс куда нужно и не пророс куда не нужно?
                  Если правка в бизнес коде нечаянно ломает аспект, как это обнаружить и чья ответственность чинить поломанное?
                  0
                  @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 более наглядный. А кода примерно столько же, что аннтоация, что строчка вызова.
                    0
                    Я попытался использовать AOP по Вашему примеру, но что-то не заходит в сам метод Before. Я описал проблему на stackoverflow, посмотрите, пожалуйста, и дайте совет, что не так.
                      0
                      Проверьте pom, на предмет spring-boot-starter-aop, spring-boot-starter-test
                      В тесте должен
                      @RunWith(SpringRunner.class)
                      @SpringBootTest
                      Как будто АОП не стартует
                        0
                        всё оказалось проще — не хватало @EnableAspectJAutoProxy
                          0
                          Чувствовал, что аоп не стартует
                      0
                      Тогда ещё один вопрос. Вот у меня два метода, на которые я хочу повесить аспект.

                      @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) — почему-то нет. В чём проблема, как думаете?
                        0
                        Ошибку нашёл, просьба не беспокоиться :)
                        0
                        А есть ли возможность сетить значение из @Aspect-метода обратно в обрабатываемый метод?
                        Например, у меня есть контроллер:

                            @ApiLogRequest(httpMethod = HttpMethod.POST, path = "/planet")
                            @PostMapping
                            public ResponseEntity<PlanetDto> save(@RequestBody PlanetDto dto) {
                                Long requestId;
                                return ResponseEntity.ok(service.save(dto));
                            }

                        и метод, обрабатывающий запрос:

                            @AfterReturning(value = "@annotation(after)", returning = "responseEntity")
                            public void after(ResponseEntity responseEntity, ApiLogResponse after) throws JsonProcessingException {
                                service.save(ApiLog.of(
                                        TransferType.RESPONSE.name(),
                                        after.httpMethod().name(),
                                        after.path(),
                                        new ObjectMapper().writeValueAsString(responseEntity)
                                ));
                            }

                        Можно ли засетить из метода after значение в Long requestId метода save?

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое