Аспектно-ориентированное программирование, 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 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 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 list) { }


Advice для него

 @Before("callAtMyServiceMethod1(list)")
    public void beforeCallAtMethod1(List 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 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 файл


        4.0.0

        com.example
        demoAspects
        0.0.1-SNAPSHOT
        jar

        demoAspects
        Demo project for Spring Boot Aspects

        
                org.springframework.boot
                spring-boot-starter-parent
                2.0.6.RELEASE
                 
        

        
                UTF-8
                UTF-8
                1.8
        

        
                
                        org.springframework.boot
                        spring-boot-starter-aop
                

                
                        org.springframework.boot
                        spring-boot-starter-test
                        test
                
        

        
                
                        
                                org.springframework.boot
                                spring-boot-maven-plugin
                        
                
        






Материалы
Aspect Oriented Programming with Spring

© Habrahabr.ru