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

в 4:43, , рубрики: java, аспектно-ориентированное, модули

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

Автор: arylkov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js