CodeGym /Java-Blog /Random-DE /Was ist AOP? Prinzipien der aspektorientierten Programmie...
John Squirrels
Level 41
San Francisco

Was ist AOP? Prinzipien der aspektorientierten Programmierung

Veröffentlicht in der Gruppe Random-DE
Hallo Jungs und Mädels! Ohne Verständnis der Grundkonzepte ist es ziemlich schwierig, sich mit Frameworks und Ansätzen zum Aufbau von Funktionalität zu befassen. Deshalb werden wir heute über ein solches Konzept sprechen – AOP, auch bekannt als aspektorientierte Programmierung . Was ist AOP?  Prinzipien der aspektorientierten Programmierung - 1Dieses Thema ist nicht einfach und wird selten direkt verwendet, aber viele Frameworks und Technologien nutzen es unter der Haube. Und natürlich werden Sie in Vorstellungsgesprächen manchmal gebeten, allgemein zu beschreiben, um was für ein Biest es sich handelt und wo es angewendet werden kann. Werfen wir also einen Blick auf die Grundkonzepte und einige einfache Beispiele von AOP in Java . Nun denn, AOP steht für aspektorientierte ProgrammierungHierbei handelt es sich um ein Paradigma, das darauf abzielt, die Modularität der verschiedenen Teile einer Anwendung durch die Trennung übergreifender Belange zu erhöhen. Um dies zu erreichen, wird dem vorhandenen Code zusätzliches Verhalten hinzugefügt, ohne dass Änderungen am Originalcode vorgenommen werden. Mit anderen Worten: Wir können uns das so vorstellen, dass zusätzliche Funktionalität über Methoden und Klassen gehängt wird, ohne dass Änderungen am geänderten Code vorgenommen werden. Warum ist das notwendig? Früher oder später kommen wir zu dem Schluss, dass der typische objektorientierte Ansatz bestimmte Probleme nicht immer effektiv lösen kann. Und wenn dieser Moment kommt, kommt AOP zur Rettung und stellt uns zusätzliche Tools zum Erstellen von Anwendungen zur Verfügung. Und zusätzliche Tools bedeuten eine größere Flexibilität in der Softwareentwicklung und damit mehr Möglichkeiten zur Lösung eines bestimmten Problems.

Anwenden von AOP

Aspektorientierte Programmierung dient der Ausführung übergreifender Aufgaben, bei denen es sich um beliebigen Code handeln kann, der mit verschiedenen Methoden viele Male wiederholt werden kann und nicht vollständig in ein separates Modul strukturiert werden kann. Dementsprechend ermöglicht uns AOP , dies außerhalb des Hauptcodes zu halten und vertikal zu deklarieren. Ein Beispiel ist die Verwendung einer Sicherheitsrichtlinie in einer Anwendung. Typischerweise erstreckt sich die Sicherheit über viele Elemente einer Anwendung. Darüber hinaus sollte die Sicherheitsrichtlinie der Anwendung gleichermaßen auf alle bestehenden und neuen Teile der Anwendung angewendet werden. Gleichzeitig kann sich eine verwendete Sicherheitsrichtlinie weiterentwickeln. Dies ist der perfekte Ort, um AOP zu verwenden . Ein weiteres Beispiel ist die Protokollierung. Die Verwendung des AOP-Ansatzes für die Protokollierung bietet mehrere Vorteile gegenüber dem manuellen Hinzufügen von Protokollierungsfunktionen:
  1. Der Code für die Protokollierung lässt sich einfach hinzufügen und entfernen: Sie müssen lediglich einige Konfigurationen eines Aspekts hinzufügen oder entfernen.

  2. Der gesamte Quellcode für die Protokollierung wird an einem Ort gespeichert, sodass Sie nicht alle Orte, an denen er verwendet wird, manuell aufspüren müssen.

  3. Protokollierungscode kann überall hinzugefügt werden, sei es in bereits geschriebenen Methoden und Klassen oder in neuen Funktionen. Dies reduziert die Anzahl der Codierungsfehler.

    Wenn Sie einen Aspekt aus einer Designkonfiguration entfernen, können Sie außerdem sicher sein, dass der gesamte Tracing-Code verschwunden ist und nichts übersehen wurde.

  4. Aspekte sind separater Code, der verbessert und immer wieder verwendet werden kann.
Was ist AOP?  Prinzipien der aspektorientierten Programmierung - 2AOP wird auch zur Ausnahmebehandlung, zum Caching und zum Extrahieren bestimmter Funktionen verwendet, um sie wiederverwendbar zu machen.

Grundprinzipien von AOP

Um in diesem Thema weiter voranzukommen, lernen wir zunächst die Hauptkonzepte von AOP kennen. Hinweis – Zusätzliche Logik oder zusätzlicher Code, der von einem Verbindungspunkt aufgerufen wird. Ratschläge können vor, nach oder anstelle eines Verbindungspunkts durchgeführt werden (mehr dazu weiter unten). Mögliche Beratungsarten :
  1. Vorher – diese Art von Hinweis wird gestartet, bevor Zielmethoden, also Verbindungspunkte, ausgeführt werden. Wenn wir Aspekte als Klassen verwenden, verwenden wir die Annotation @Before , um den Rat als vorher kommend zu markieren. Wenn Aspekte als .aj- Dateien verwendet werden, ist dies die Methode before() .

  2. After – Ratschläge, die ausgeführt werden, nachdem die Ausführung von Methoden (Join-Points) abgeschlossen ist, sowohl bei der normalen Ausführung als auch beim Auslösen einer Ausnahme.

    Wenn wir Aspekte als Klassen verwenden, können wir die Annotation @After verwenden , um anzugeben, dass es sich um einen nachfolgenden Ratschlag handelt.

    Bei der Verwendung von Aspekten als .aj- Dateien ist dies die Methode after() .

  3. Nach der Rückkehr – dieser Hinweis wird nur ausgeführt, wenn die Zielmethode normal und ohne Fehler beendet wird.

    Wenn Aspekte als Klassen dargestellt werden, können wir die Annotation @AfterReturning verwenden , um den Hinweis nach erfolgreichem Abschluss als ausgeführt zu markieren.

    Wenn Aspekte als .aj- Dateien verwendet werden, ist dies die Methode after(), die (Object obj) zurückgibt .

  4. Nach dem Auslösen – dieser Rat ist für Fälle gedacht, in denen eine Methode, also ein Verbindungspunkt, eine Ausnahme auslöst. Wir können diesen Hinweis verwenden, um bestimmte Arten fehlgeschlagener Ausführung zu behandeln (z. B. um eine gesamte Transaktion zurückzusetzen oder mit der erforderlichen Trace-Ebene zu protokollieren).

    Für Klassenaspekte wird die Annotation @AfterThrowing verwendet, um anzugeben, dass dieser Hinweis nach dem Auslösen einer Ausnahme verwendet wird.

    Wenn Aspekte als .aj- Dateien verwendet werden, ist dies die Methode after() throwing (Exception e) .

  5. Rund – vielleicht eine der wichtigsten Arten von Ratschlägen. Es umgibt eine Methode, also einen Verbindungspunkt, mit dem wir beispielsweise auswählen können, ob eine bestimmte Verbindungspunktmethode ausgeführt werden soll oder nicht.

    Sie können Empfehlungscode schreiben, der vor und nach der Ausführung der Join-Point-Methode ausgeführt wird.

    Der Around-Hinweis ist für den Aufruf der Join-Point-Methode und die Rückgabewerte verantwortlich, wenn die Methode etwas zurückgibt. Mit anderen Worten: In diesem Ratschlag können Sie einfach die Operation einer Zielmethode simulieren, ohne sie aufzurufen, und als Rückgabeergebnis alles zurückgeben, was Sie möchten.

    Wenn Aspekte als Klassen vorliegen, verwenden wir die Annotation @Around , um Anweisungen zu erstellen, die einen Verbindungspunkt umschließen. Bei der Verwendung von Aspekten in Form von .aj- Dateien ist diese Methode die Methode around() .

Verbindungspunkt – der Punkt in einem laufenden Programm (z. B. Methodenaufruf, Objekterstellung, Variablenzugriff), an dem der Hinweis angewendet werden soll. Mit anderen Worten handelt es sich um eine Art regulären Ausdruck, der verwendet wird, um Orte für die Code-Injektion zu finden (Orte, an denen Ratschläge angewendet werden sollten). Pointcut – eine Reihe von Verbindungspunkten . Ein Pointcut bestimmt, ob gegebene Ratschläge auf einen bestimmten Verbindungspunkt anwendbar sind. Aspekt – ein Modul oder eine Klasse, die übergreifende Funktionen implementiert. Aspect ändert das Verhalten des verbleibenden Codes, indem es Ratschläge an Verbindungspunkten anwendet , die durch einen Pointcut definiert werden . Mit anderen Worten, es handelt sich um eine Kombination aus Ratschlägen und Verbindungspunkten. Einführung– Ändern der Struktur einer Klasse und/oder Ändern der Vererbungshierarchie, um die Funktionalität des Aspekts zu fremdem Code hinzuzufügen. Ziel – das Objekt, auf das die Empfehlung angewendet wird. Weben – der Prozess der Verknüpfung von Aspekten mit anderen Objekten, um empfohlene Proxy-Objekte zu erstellen. Dies kann zur Kompilierungszeit, Ladezeit oder Laufzeit erfolgen. Es gibt drei Arten des Webens:
  • Weben zur Kompilierungszeit – Wenn Sie über den Quellcode des Aspekts und den Code verfügen, in dem Sie den Aspekt verwenden, können Sie den Quellcode und den Aspekt direkt mit dem AspectJ-Compiler kompilieren.

  • Weben nach dem Kompilieren (binäres Weben) – Wenn Sie keine Quellcodetransformationen verwenden können oder wollen, um Aspekte in den Code einzubinden, können Sie zuvor kompilierte Klassen oder JAR-Dateien nehmen und Aspekte in sie einfügen;

  • Ladezeit-Weben – hierbei handelt es sich lediglich um binäres Weben, das verzögert wird, bis der Klassenlader die Klassendatei lädt und die Klasse für die JVM definiert.

    Zur Unterstützung sind ein oder mehrere Webklassenlader erforderlich. Sie werden entweder explizit von der Laufzeit bereitgestellt oder von einem „Weaving Agent“ aktiviert.

AspectJ – Eine spezifische Implementierung des AOP- Paradigmas, die die Fähigkeit implementiert, übergreifende Aufgaben auszuführen. Die Dokumentation finden Sie hier .

Beispiele in Java

Als nächstes werden wir uns zum besseren Verständnis von AOP kleine Beispiele im „Hello World“-Stil ansehen. Gleich zu Beginn möchte ich anmerken, dass unsere Beispiele Weaving zur Kompilierungszeit verwenden werden . Zuerst müssen wir die folgende Abhängigkeit in unserer pom.xml- Datei hinzufügen:

<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjrt</artifactId>
  <version>1.9.5</version>
</dependency>
Der besondere AJC- Compiler ist in der Regel die Art und Weise, wie wir Aspekte verwenden. IntelliJ IDEA enthält es nicht standardmäßig. Wenn Sie es als Anwendungscompiler auswählen, müssen Sie daher den Pfad zur 5168 75 AspectJ- Distribution angeben. Dies war der erste Weg. Das zweite, das ich verwendet habe, besteht darin, das folgende Plugin in der Datei pom.xml zu registrieren :

<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>
Danach ist es eine gute Idee, erneut aus Maven zu importieren und mvn clean compile auszuführen . Kommen wir nun direkt zu den Beispielen.

Beispiel Nr. 1

Lassen Sie uns eine Hauptklasse erstellen . Darin haben wir einen Einstiegspunkt und eine Methode, die einen übergebenen Namen auf der Konsole ausgibt:

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);
  }
}
Hier gibt es nichts Kompliziertes. Wir haben einen Namen übergeben und ihn auf der Konsole angezeigt. Wenn wir das Programm jetzt ausführen, sehen wir Folgendes auf der Konsole:
Gerber Victor Sasha
Dann ist es an der Zeit, die Leistungsfähigkeit von AOP zu nutzen. Jetzt müssen wir eine Aspect- Datei erstellen. Es gibt zwei Arten: Die erste hat die Dateierweiterung .aj . Die zweite ist eine gewöhnliche Klasse, die Annotationen verwendet, um AOP- Funktionen zu implementieren. Schauen wir uns zunächst die Datei mit der Erweiterung .aj an :

public aspect GreetingAspect {
 
  pointcut greeting() : execution(* Main.printName(..));
 
  before() : greeting() {
     System.out.print("Hi, ");
  }
}
Diese Datei ist so etwas wie eine Klasse. Schauen wir uns an, was hier passiert: Pointcut ist eine Reihe von Verbindungspunkten; Greeting() ist der Name dieses Pointcuts; : Ausführung gibt an, es während der Ausführung aller ( * ) Aufrufe der Main.printName(...)- Methode anzuwenden. Als nächstes kommt ein spezifischer Hinweis – before() – der ausgeführt wird, bevor die Zielmethode aufgerufen wird. : Greeting() ist der Cutpoint, auf den dieser Rat reagiert. Nun, und unten sehen wir den Hauptteil der Methode selbst, der in der Java-Sprache geschrieben ist, die wir verstehen. Wenn wir main mit diesem Aspekt ausführen, erhalten wir diese Konsolenausgabe:
Hallo, Tanner. Hallo, Victor. Hallo, Sasha
Wir können sehen, dass jeder Aufruf der printName- Methode dank eines Aspekts geändert wurde. Schauen wir uns nun an, wie der Aspekt als Java-Klasse mit Annotationen aussehen würde:

@Aspect
public class GreetingAspect{
 
  @Pointcut("execution(* Main.printName(String))")
  public void greeting() {
  }
 
  @Before("greeting()")
  public void beforeAdvice() {
     System.out.print("Hi, ");
  }
}
Nach der .aj- Aspektdatei wird hier alles klarer:
  • @Aspect gibt an, dass diese Klasse ein Aspekt ist;
  • @Pointcut("execution(* Main.printName(String))") ist der Cutpoint, der für alle Aufrufe von Main.printName mit einem Eingabeargument vom Typ String ausgelöst wird ;
  • @Before("greeting()") ist ein Hinweis, der vor dem Aufruf des im Greeting()- Schnittpunkt angegebenen Codes angewendet wird.
Das Ausführen von main mit diesem Aspekt ändert die Konsolenausgabe nicht:
Hallo, Tanner. Hallo, Victor. Hallo, Sasha

Beispiel Nr. 2

Angenommen, wir haben eine Methode, die einige Operationen für Clients ausführt, und wir rufen diese Methode von main aus auf :

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);
  }
}
Lassen Sie uns die @Around- Annotation verwenden, um eine „Pseudotransaktion“ zu erstellen:

@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...");
     }
  }
  }
Mit der continue- Methode des ProceedingJoinPoint- Objekts rufen wir die Wrapping-Methode auf, um deren Position im Hinweis zu bestimmen. Daher ist der Code in der obigen Methode joinPoint.proceed(); ist Before , während der Code darunter After ist . Wenn wir main ausführen , erhalten wir Folgendes in der Konsole:
Öffnen einer Transaktion... Durchführen einiger Vorgänge für den Kunden Tanner. Schließen einer Transaktion...
Aber wenn wir in unserer Methode eine Ausnahme auslösen (um eine fehlgeschlagene Operation zu simulieren):

public static void performSomeOperation(String clientName) throws Exception {
  System.out.println("Performing some operations for Client " + clientName);
  throw new Exception();
}
Dann erhalten wir diese Konsolenausgabe:
Eine Transaktion wird geöffnet... Es werden einige Vorgänge für Client Tanner ausgeführt. Der Vorgang ist fehlgeschlagen. Die Transaktion wird rückgängig gemacht...
Am Ende haben wir also eine Art Fehlerbehandlungsfunktion erhalten.

Beispiel Nr. 3

In unserem nächsten Beispiel führen wir beispielsweise eine Anmeldung an der Konsole durch. Schauen Sie sich zunächst Main an , wo wir einige Pseudo-Geschäftslogiken hinzugefügt haben:

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();
     }
  }
}
In main verwenden wir setValue , um der Instanzvariablen value einen Wert zuzuweisen. Dann verwenden wir getValue, um den Wert abzurufen, und rufen dann checkValue auf, um zu prüfen, ob er länger als 10 Zeichen ist. Wenn ja, wird eine Ausnahme ausgelöst. Schauen wir uns nun den Aspekt an, den wir verwenden werden, um die Arbeit der Methoden zu protokollieren:

@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);
  }
}
Was ist denn hier los? @Pointcut("execution(* *(..))") verbindet alle Aufrufe aller Methoden. @AfterReturning(value = "methodExecuting()", return = "returningValue") ist ein Hinweis, der nach erfolgreicher Ausführung der Zielmethode ausgeführt wird. Wir haben hier zwei Fälle:
  1. Wenn die Methode einen Rückgabewert hat – if (returningValue! = Null) {
  2. Wenn kein Rückgabewert vorhanden ist – sonst {
@AfterThrowing(value = "methodExecuting()", throwing = "Exception") ist ein Hinweis, der im Fehlerfall ausgelöst wird, d. h. wenn die Methode eine Ausnahme auslöst. Und dementsprechend erhalten wir durch die Ausführung von main eine Art konsolenbasierte Protokollierung:
Erfolgreiche Ausführung: Methode – setValue, Klasse – Main Erfolgreiche Ausführung: Methode – getValue, Klasse – Main, Rückgabewert – <einiger Wert> Ausgelöste Ausnahme: Methode – checkValue, Klasse – Hauptausnahme – java.lang.Exception Ausgelöste Ausnahme: Methode – main, Klasse – Main, Ausnahme – java.lang.Exception
Und da wir die Ausnahmen nicht behandelt haben, erhalten wir trotzdem einen Stack-Trace: Was ist AOP?  Prinzipien der aspektorientierten Programmierung - 3Über Ausnahmen und Ausnahmebehandlung können Sie in diesen Artikeln lesen: Ausnahmen in Java und Ausnahmen: Abfangen und Behandeln . Das ist alles für mich heute. Heute haben wir AOP kennengelernt und Sie konnten sehen, dass dieses Biest nicht so gruselig ist, wie manche Leute es darstellen. Auf Wiedersehen, alle zusammen!
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION