
Stosowanie AOP
Programowanie zorientowane aspektowo jest przeznaczone do wykonywania zadań przekrojowych, którymi może być dowolny kod, który można wielokrotnie powtarzać różnymi metodami, których nie można całkowicie ustrukturyzować w oddzielny moduł. W związku z tym AOP pozwala nam trzymać to poza głównym kodem i deklarować je pionowo. Przykładem jest użycie polityki bezpieczeństwa w aplikacji. Zazwyczaj zabezpieczenia obejmują wiele elementów aplikacji. Co więcej, polityka bezpieczeństwa aplikacji powinna być stosowana jednakowo do wszystkich istniejących i nowych części aplikacji. Jednocześnie sama stosowana polityka bezpieczeństwa może ewoluować. To idealne miejsce do korzystania z AOP . Innym przykładem jest logowanie. Korzystanie z podejścia AOP do rejestrowania ma kilka zalet zamiast ręcznego dodawania funkcji rejestrowania:-
Kod do logowania jest łatwy do dodania i usunięcia: wystarczy dodać lub usunąć kilka konfiguracji jakiegoś aspektu.
-
Cały kod źródłowy do logowania jest przechowywany w jednym miejscu, więc nie musisz ręcznie wyszukiwać wszystkich miejsc, w których jest używany.
-
Kod logowania można dodać w dowolnym miejscu, czy to w metodach i klasach, które zostały już napisane, czy też w nowej funkcjonalności. Zmniejsza to liczbę błędów kodowania.
Ponadto, usuwając aspekt z konfiguracji projektu, możesz mieć pewność, że cały kod śledzenia zniknął i nic nie zostało pominięte.
- Aspekty to oddzielny kod, który można ulepszać i używać wielokrotnie.

Podstawowe zasady AOP
Aby przejść dalej w tym temacie, najpierw poznajmy główne koncepcje AOP. Porada — Dodatkowa logika lub kod wywoływany z punktu łączenia. Porady można wykonać przed, po lub zamiast punktu połączenia (więcej o nich poniżej). Możliwe rodzaje porad :-
Przed — ten typ porady jest uruchamiany przed wykonaniem metod docelowych, czyli punktów łączenia. Używając aspektów jako klas, używamy adnotacji @Before , aby oznaczyć poradę jako poprzedzającą. W przypadku używania aspektów jako plików .aj będzie to metoda before() .
- After — rada, która jest wykonywana po zakończeniu wykonywania metod (punktów łączenia), zarówno podczas normalnego wykonywania, jak i podczas zgłaszania wyjątku.
Używając aspektów jako klas, możemy użyć adnotacji @After , aby wskazać, że jest to rada, która pojawia się później.
W przypadku używania aspektów jako plików .aj jest to metoda after() .
-
After Returning — ta rada jest wykonywana tylko wtedy, gdy metoda docelowa zakończy się normalnie, bez błędów.
Kiedy aspekty są reprezentowane jako klasy, możemy użyć adnotacji @AfterReturning , aby oznaczyć poradę jako wykonywaną po pomyślnym zakończeniu.
W przypadku używania aspektów jako plików .aj będzie to metoda zwracająca after() (Object obj) .
-
After Throwing — ta rada jest przeznaczona dla przypadków, gdy metoda, czyli punkt łączenia, zgłasza wyjątek. Możemy skorzystać z tej porady, aby poradzić sobie z niektórymi rodzajami nieudanych wykonań (na przykład wycofać całą transakcję lub dziennik z wymaganym poziomem śledzenia).
W przypadku aspektów klasowych adnotacja @AfterThrowing służy do wskazania, że ta rada jest używana po zgłoszeniu wyjątku.
Używając aspektów jako plików .aj , będzie to metoda rzucania after() (Exception e) .
-
Wokół — być może jeden z najważniejszych rodzajów porad. Otacza metodę, czyli punkt łączenia, za pomocą którego możemy na przykład wybrać, czy wykonać daną metodę punktu łączenia.
Możesz napisać kod porady, który jest uruchamiany przed i po wykonaniu metody punktu łączenia.
Porada Around jest odpowiedzialna za wywołanie metody punktu łączenia i zwracanie wartości, jeśli metoda coś zwraca. Innymi słowy, w tej radzie możesz po prostu zasymulować działanie metody docelowej bez jej wywoływania i zwrócić dowolny wynik jako wynik.
Biorąc pod uwagę aspekty jako klasy, używamy adnotacji @Around do tworzenia porad, które zawijają punkt połączenia. W przypadku korzystania z aspektów w postaci plików .aj tą metodą będzie metoda Around() .
-
Tkanie w czasie kompilacji — jeśli masz kod źródłowy aspektu i kod, w którym używasz aspektu, możesz skompilować kod źródłowy i aspekt bezpośrednio za pomocą kompilatora AspectJ;
-
Tkanie po kompilacji (tkanie binarne) — jeśli nie możesz lub nie chcesz używać transformacji kodu źródłowego do wplatania aspektów w kod, możesz wziąć wcześniej skompilowane klasy lub pliki jar i wstrzyknąć do nich aspekty;
-
Tkanie w czasie ładowania — jest to po prostu tkanie binarne, które jest opóźnione, dopóki moduł ładujący klasy nie załaduje pliku klasy i nie zdefiniuje klasy dla maszyny JVM.
Do obsługi tego wymagane jest co najmniej jedno ładowanie klas tkackich. Są albo jawnie dostarczane przez środowisko wykonawcze, albo aktywowane przez „agenta tkania”.
Przykłady w Javie
Następnie, aby lepiej zrozumieć AOP , przyjrzymy się małym przykładom w stylu „Hello World”. Na prawo od nietoperza zwrócę uwagę, że w naszych przykładach użyjemy tkania w czasie kompilacji . Najpierw musimy dodać następującą zależność w naszym pliku pom.xml :
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.5</version>
</dependency>
Z reguły specjalnym kompilatorem ajc jest sposób, w jaki używamy aspektów. IntelliJ IDEA nie zawiera go domyślnie, dlatego wybierając go jako kompilator aplikacji, należy podać ścieżkę do dystrybucji 5168 75 AspectJ . To był pierwszy sposób. Drugim, którego użyłem, jest zarejestrowanie następującej wtyczki w pliku pom.xml :
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.7</version>
<configuration>
<complianceLevel>1.8</complianceLevel>
<source>1.8</source>
<target>1.8</target>
<showWeaveInfo>true</showWeaveInfo>
<<verbose>true<verbose>
<Xlint>ignore</Xlint>
<encoding>UTF-8</encoding>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Po tym dobrze jest ponownie zaimportować z Maven i uruchomić mvn clean compile . Przejdźmy teraz bezpośrednio do przykładów.
Przykład nr 1
Stwórzmy klasę główną . Będziemy w nim mieć punkt wejścia i metodę, która drukuje przekazaną nazwę na konsoli:
public class Main {
public static void main(String[] args) {
printName("Tanner");
printName("Victor");
printName("Sasha");
}
public static void printName(String name) {
System.out.println(name);
}
}
Nie ma tu nic skomplikowanego. Przekazaliśmy nazwę i wyświetlamy ją na konsoli. Jeśli teraz uruchomimy program, na konsoli zobaczymy:
public aspect GreetingAspect {
pointcut greeting() : execution(* Main.printName(..));
before() : greeting() {
System.out.print("Hi, ");
}
}
Ten plik jest trochę jak klasa. Zobaczmy, co się tutaj dzieje: punkt przecięcia to zbiór punktów złączenia; pozdrowienia() to nazwa tego punktu; : wykonanie oznacza zastosowanie go podczas wykonywania wszystkich ( * ) wywołań metody Main.printName(...) . Następnie pojawia się konkretna rada — before() — która jest wykonywana przed wywołaniem metody docelowej. : pozdrowienia() to punkt odcięcia, na który odpowiada ta rada. Cóż, poniżej widzimy treść samej metody, która jest napisana w zrozumiałym dla nas języku Java. Kiedy uruchomimy main z obecnym aspektem, otrzymamy następujące wyjście konsoli:
@Aspect
public class GreetingAspect{
@Pointcut("execution(* Main.printName(String))")
public void greeting() {
}
@Before("greeting()")
public void beforeAdvice() {
System.out.print("Hi, ");
}
}
Po pliku aspektu .aj tutaj wszystko staje się bardziej oczywiste:
- @Aspect wskazuje, że ta klasa jest aspektem;
- @Pointcut("execution(* Main.printName(String))") jest punktem odcięcia wyzwalanym dla wszystkich wywołań Main.printName z argumentem wejściowym, którego typem jest String ;
- @Before("greeting()") to rada, która jest stosowana przed wywołaniem kodu określonego w punkcie odcięcia pozdrowienia() .
Przykład nr 2
Załóżmy, że mamy jakąś metodę, która wykonuje pewne operacje dla klientów i wywołujemy tę metodę z main :
public class Main {
public static void main(String[] args) {
performSomeOperation("Tanner");
}
public static void performSomeOperation(String clientName) {
System.out.println("Performing some operations for Client " + clientName);
}
}
Użyjmy adnotacji @Around do stworzenia „pseudo-transakcji”:
@Aspect
public class TransactionAspect{
@Pointcut("execution(* Main.performSomeOperation(String))")
public void executeOperation() {
}
@Around(value = "executeOperation()")
public void beforeAdvice(ProceedingJoinPoint joinPoint) {
System.out.println("Opening a transaction...");
try {
joinPoint.proceed();
System.out.println("Closing a transaction...");
}
catch (Throwable throwable) {
System.out.println("The operation failed. Rolling back the transaction...");
}
}
}
Za pomocą metody ProceedingJoinPoint obiektu ProceedingJoinPoint wywołujemy metodę zawijania, aby określić jej położenie w audycji. Dlatego kod w powyższej metodzie joinPoint.proceed(); to Before , podczas gdy poniższy kod to After . Jeśli uruchomimy main , otrzymamy to w konsoli:
public static void performSomeOperation(String clientName) throws Exception {
System.out.println("Performing some operations for Client " + clientName);
throw new Exception();
}
Następnie otrzymujemy to wyjście konsoli:
Przykład nr 3
W naszym następnym przykładzie zróbmy coś w rodzaju logowania do konsoli. Najpierw spójrz na Main , gdzie dodaliśmy trochę pseudologiki biznesowej:
public class Main {
private String value;
public static void main(String[] args) throws Exception {
Main main = new Main();
main.setValue("<some value>");
String valueForCheck = main.getValue();
main.checkValue(valueForCheck);
}
public void setValue(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
public void checkValue(String value) throws Exception {
if (value.length() > 10) {
throw new Exception();
}
}
}
W main używamy setValue do przypisania wartości zmiennej instancji value . Następnie używamy getValue, aby uzyskać wartość, a następnie wywołujemy checkValue, aby sprawdzić, czy jest dłuższa niż 10 znaków. Jeśli tak, zostanie zgłoszony wyjątek. Przyjrzyjmy się teraz aspektowi, którego użyjemy do zarejestrowania działania metod:
@Aspect
public class LogAspect {
@Pointcut("execution(* *(..))")
public void methodExecuting() {
}
@AfterReturning(value = "methodExecuting()", returning = "returningValue")
public void recordSuccessfulExecution(JoinPoint joinPoint, Object returningValue) {
if (returningValue != null) {
System.out.printf("Successful execution: method — %s method, class — %s class, return value — %s\n",
joinPoint.getSignature().getName(),
joinPoint.getSourceLocation().getWithinType().getName(),
returningValue);
}
else {
System.out.printf("Successful execution: method — %s, class — %s\n",
joinPoint.getSignature().getName(),
joinPoint.getSourceLocation().getWithinType().getName());
}
}
@AfterThrowing(value = "methodExecuting()", throwing = "exception")
public void recordFailedExecution(JoinPoint joinPoint, Exception exception) {
System.out.printf("Exception thrown: method — %s, class — %s, exception — %s\n",
joinPoint.getSignature().getName(),
joinPoint.getSourceLocation().getWithinType().getName(),
exception);
}
}
Co tu się dzieje? @Pointcut("execution(* *(..))") dołączy do wszystkich wywołań wszystkich metod. @AfterReturning(value = "methodExecuting()", return = "returningValue") to rada, która zostanie wykonana po pomyślnym wykonaniu metody docelowej. Mamy tutaj dwa przypadki:
- Gdy metoda zwraca wartość — if (returningValue! = Null) {
- Gdy nie ma wartości zwracanej — else {

GO TO FULL VERSION