Аспектно-ориентированное программирование, Spring AOP
Аспектно-ориентированное программирование (АОП) — это парадигма программирования являющейся дальнейшим развитием процедурного и объектно-ориентированного программирования (ООП). Идея АОП заключается в выделении так называемой сквозной функциональности. И так все по порядку, здесь я покажу как это сделать в Java — Spring @AspectJ annotation стиле (есть еще schema-based xml стиль, функциональность аналогичная).
Выделении сквозной функциональности
До
и после
Т.е. есть функциональность которая затрагивает несколько модулей, но она не имеет прямого отношения к бизнес коду, и ее хорошо бы вынести в отдельное место, это и показано на рисунке выше.
Join point
Join point — следующее понятие АОП, это точки наблюдения, присоединения к коду, где планируется введение функциональности.
Pointcut
Pointcut — это срез, запрос точек присоединения, — это может быть одна и более точек. Правила запросов точек очень разнообразные, на рисунке выше, запрос по аннотации на методе и конкретный метод. Правила можно объединять по &&, ||,!
Advice
Advice — набор инструкций выполняемых на точках среза (Pointcut). Инструкции можно выполнять по событию разных типов:
- Before — перед вызовом метода
- After — после вызова метода
- After returning — после возврата значения из функции
- After throwing — в случае exception
- After finally — в случае выполнения блока finally
- Around — можно сделать пред., пост., обработку перед вызовом метода, а также вообще обойти вызов метода.
на один Pointcut можно «повесить» несколько Advice разного типа.
Aspect
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 это не обязательный параметр который, предоставляет дополнительную информацию, но если он используется, то он должен быть первым.
Все разнесено в разные модули! Вызывающий код, целевой, логирование.
Результат в консоли
Правила 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;
}
и.т.д.
Структура проекта
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