CodeGym/Blog Java/Random-PL/Co to jest AOP? Zasady programowania aspektowego
John Squirrels
Poziom 41
San Francisco

Co to jest AOP? Zasady programowania aspektowego

Opublikowano w grupie Random-PL
Cześć, chłopaki i dziewczęta! Bez zrozumienia podstawowych pojęć dość trudno jest zagłębić się w ramy i podejścia do budowania funkcjonalności. Dlatego dzisiaj porozmawiamy o jednej z takich koncepcji — AOP, czyli programowaniu zorientowanym aspektowo . Co to jest AOP?  Zasady programowania aspektowego - 1Ten temat nie jest łatwy i rzadko jest używany bezpośrednio, ale wiele frameworków i technologii używa go pod maską. I oczywiście czasami podczas wywiadów możesz zostać poproszony o ogólne opisanie, co to za bestia i gdzie można ją zastosować. Przyjrzyjmy się więc podstawowym pojęciom i kilku prostym przykładom AOP w Javie . Teraz AOP oznacza programowanie zorientowane aspektowo, który jest paradygmatem mającym na celu zwiększenie modułowości różnych części aplikacji poprzez oddzielenie zagadnień przekrojowych. Aby to osiągnąć, do istniejącego kodu dodaje się dodatkowe zachowanie bez wprowadzania zmian w oryginalnym kodzie. Innymi słowy, możemy myśleć o tym jako o zawieszeniu dodatkowej funkcjonalności na metodach i klasach bez zmiany zmodyfikowanego kodu. Dlaczego jest to konieczne? Prędzej czy później dochodzimy do wniosku, że typowe podejście obiektowe nie zawsze może skutecznie rozwiązać pewne problemy. A kiedy ten moment nadejdzie, z pomocą przychodzi AOP i daje nam dodatkowe narzędzia do budowania aplikacji. A dodatkowe narzędzia to większa elastyczność w tworzeniu oprogramowania, co oznacza więcej możliwości rozwiązania konkretnego problemu.

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:
  1. Kod do logowania jest łatwy do dodania i usunięcia: wystarczy dodać lub usunąć kilka konfiguracji jakiegoś aspektu.

  2. 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.

  3. 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.

  4. Aspekty to oddzielny kod, który można ulepszać i używać wielokrotnie.
Co to jest AOP?  Zasady programowania aspektowego - 2AOP jest również używany do obsługi wyjątków, buforowania i wyodrębniania niektórych funkcji, aby umożliwić ich ponowne użycie.

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 :
  1. 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() .

  2. 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() .

  3. 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) .

  4. 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) .

  5. 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() .

Join Point — punkt w uruchomionym programie (tj. wywołanie metody, utworzenie obiektu, dostęp do zmiennej), w którym należy zastosować radę. Innymi słowy, jest to rodzaj wyrażenia regularnego służącego do znajdowania miejsc do wstrzyknięcia kodu (miejsc, w których należy zastosować porady). Pointcut — zbiór punktów łączenia . Punkt cięcia określa, czy dana rada ma zastosowanie do danego punktu połączenia. Aspekt — moduł lub klasa, która implementuje funkcjonalność przekrojową. Aspect zmienia zachowanie pozostałego kodu, stosując porady w punktach łączenia zdefiniowanych przez jakiś punkt przecięcia . Innymi słowy, jest to połączenie porad i punktów łączenia. Wstęp— zmiana struktury klasy i/lub zmiana hierarchii dziedziczenia w celu dodania funkcjonalności aspektu do obcego kodu. Cel — obiekt, do którego zostanie zastosowana porada. Tkanie — proces łączenia aspektów z innymi obiektami w celu stworzenia zalecanych obiektów zastępczych. Można to zrobić w czasie kompilacji, w czasie ładowania lub w czasie wykonywania. Istnieją trzy rodzaje tkania:
  • 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”.

AspectJ — Specyficzna implementacja paradygmatu AOP , która implementuje zdolność wykonywania zadań przekrojowych. Dokumentację można znaleźć tutaj .

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:
Tanner Wiktor Sasza
Teraz nadszedł czas, aby skorzystać z mocy AOP. Teraz musimy utworzyć plik aspektu . Są dwojakiego rodzaju: pierwszy ma rozszerzenie pliku .aj . Druga to zwykła klasa, która używa adnotacji do implementacji możliwości AOP . Najpierw spójrzmy na plik z rozszerzeniem .aj :
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:
Cześć, Tanner Cześć, Victor Cześć, Sasha
Widzimy, że każde wywołanie metody printName zostało zmodyfikowane dzięki aspektowi. Przyjrzyjmy się teraz, jak ten aspekt wyglądałby jako klasa Java z adnotacjami:
@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() .
Uruchomienie main z tym aspektem nie zmienia wyjścia konsoli:
Cześć, Tanner Cześć, Victor Cześć, Sasha

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:
Otwarcie transakcji... Wykonywanie operacji dla Klienta Tannera Zamykanie transakcji...
Ale jeśli rzucimy i wyjątek w naszej metodzie (aby zasymulować nieudaną operację):
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:
Otwieranie transakcji... Wykonywanie pewnych operacji dla Klienta Tannera Operacja nie powiodła się. Wycofuję transakcję...
Skończyło się więc na czymś w rodzaju możliwości obsługi błędów.

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:
  1. Gdy metoda zwraca wartość — if (returningValue! = Null) {
  2. Gdy nie ma wartości zwracanej — else {
@AfterThrowing(value = "methodExecuting()", throwing = "exception") to rada, która zostanie wywołana w przypadku błędu, czyli gdy metoda zgłosi wyjątek. I odpowiednio, uruchamiając main , otrzymamy rodzaj logowania opartego na konsoli:
Pomyślne wykonanie: method — setValue, class — Main Pomyślne wykonanie: method — getValue, class — Main, return value — <jakaś wartość> Zgłoszony wyjątek: method — checkValue, class — Główny wyjątek — java.lang.Exception Zgłoszony wyjątek: method — main, class — Main, wyjątek — java.lang.Exception
A ponieważ nie obsłużyliśmy wyjątków, nadal otrzymamy ślad stosu: Możesz Co to jest AOP?  Zasady programowania aspektowego - 3przeczytać o wyjątkach i obsłudze wyjątków w tych artykułach: Wyjątki w Javie i Wyjątki: przechwytywanie i obsługa . To wszystko dla mnie dzisiaj. Dzisiaj zapoznaliśmy się z AOP i mogliście się przekonać, że ta bestia nie jest taka straszna, jak niektórzy ją przedstawiają. Do widzenia wszystkim!
Komentarze
  • Popularne
  • Najnowsze
  • Najstarsze
Musisz się zalogować, aby dodać komentarz
Ta strona nie ma jeszcze żadnych komentarzy