CodeGym /Java blogg /Slumpmässig /Vad är AOP? Principer för aspektorienterad programmering
John Squirrels
Nivå
San Francisco

Vad är AOP? Principer för aspektorienterad programmering

Publicerad i gruppen
Hej killar och tjejer! Utan att förstå de grundläggande begreppen är det ganska svårt att fördjupa sig i ramverk och tillvägagångssätt för att bygga funktionalitet. Så idag kommer vi att prata om ett sådant koncept — AOP, aka aspektorienterad programmering . Vad är AOP?  Principer för aspektorienterad programmering - 1Det här ämnet är inte lätt och används sällan direkt, men många ramverk och tekniker använder det under huven. Och naturligtvis, ibland under intervjuer, kan du bli ombedd att beskriva i allmänna termer vilken sorts best detta är och var det kan tillämpas. Så låt oss ta en titt på de grundläggande begreppen och några enkla exempel på AOP i Java . Nu står AOP för aspektorienterad programmering, vilket är ett paradigm som är avsett att öka modulariteten hos de olika delarna av en applikation genom att separera tvärgående problem. För att åstadkomma detta läggs ytterligare beteende till i den befintliga koden utan att göra ändringar i den ursprungliga koden. Med andra ord kan vi se det som att hänga ytterligare funktionalitet ovanpå metoder och klasser utan att ändra den modifierade koden. Varför är detta nödvändigt? Förr eller senare drar vi slutsatsen att det typiska objektorienterade tillvägagångssättet inte alltid effektivt kan lösa vissa problem. Och när det ögonblicket kommer kommer AOP till undsättning och ger oss ytterligare verktyg för att bygga applikationer. Och ytterligare verktyg innebär ökad flexibilitet i mjukvaruutveckling, vilket innebär fler alternativ för att lösa ett visst problem.

Tillämpar AOP

Aspektorienterad programmering är utformad för att utföra tvärgående uppgifter, vilket kan vara vilken kod som helst som kan upprepas många gånger med olika metoder, som inte helt kan struktureras i en separat modul. Följaktligen låter AOP oss hålla detta utanför huvudkoden och deklarera det vertikalt. Ett exempel är att använda en säkerhetspolicy i en applikation. Säkerhet går vanligtvis igenom många delar av en applikation. Dessutom bör applikationens säkerhetspolicy tillämpas lika på alla befintliga och nya delar av applikationen. Samtidigt kan en säkerhetspolicy som används själv utvecklas. Detta är det perfekta stället att använda AOP . Ett annat exempel är också loggning. Det finns flera fördelar med att använda AOP-metoden för loggning istället för att manuellt lägga till loggningsfunktioner:
  1. Koden för loggning är lätt att lägga till och ta bort: allt du behöver göra är att lägga till eller ta bort ett par konfigurationer av någon aspekt.

  2. All källkod för loggning förvaras på ett ställe, så du behöver inte manuellt jaga alla platser där den används.

  3. Loggningskod kan läggas till var som helst, antingen i metoder och klasser som redan har skrivits eller i ny funktionalitet. Detta minskar antalet kodningsfel.

    Dessutom, när du tar bort en aspekt från en designkonfiguration kan du vara säker på att all spårningskod är borta och att ingenting har missats.

  4. Aspekter är separat kod som kan förbättras och användas om och om igen.
Vad är AOP?  Principer för aspektorienterad programmering - 2AOP används också för undantagshantering, cachning och extrahering av viss funktionalitet för att göra den återanvändbar.

Grundläggande principer för AOP

För att komma vidare i det här ämnet, låt oss först lära känna huvudkoncepten för AOP. Råd — Ytterligare logik eller kod anropad från en kopplingspunkt. Råd kan ges före, efter eller istället för en kopplingspunkt (mer om dem nedan). Möjliga typer av råd :
  1. Före — den här typen av råd lanseras innan målmetoder, dvs sammanfogningspunkter, exekveras. När vi använder aspekter som klasser använder vi @Before- kommentaren för att markera råden som kommer tidigare. När du använder aspekter som .aj- filer kommer detta att vara before()- metoden.

  2. Efter — råd som exekveras efter exekvering av metoder (join points) är klar, både i normal exekvering såväl som när ett undantag görs.

    När vi använder aspekter som klasser kan vi använda @After -kommentaren för att indikera att detta är råd som kommer efter.

    När du använder aspekter som .aj- filer är det här after()- metoden.

  3. Efter återkomst — detta råd utförs endast när målmetoden slutar normalt, utan fel.

    När aspekter representeras som klasser kan vi använda @AfterReturning -kommentaren för att markera rådet som exekverande efter framgångsrikt slutförande.

    När du använder aspekter som .aj- filer kommer detta att vara metoden after() som returnerar (Object obj) .

  4. After Throwing — det här rådet är avsett för tillfällen när en metod, det vill säga join point, ger ett undantag. Vi kan använda detta råd för att hantera vissa typer av misslyckad exekvering (till exempel för att återställa en hel transaktion eller logg med den erforderliga spårningsnivån).

    För klassaspekter används @AfterThrowing -kommentaren för att indikera att detta råd används efter att ett undantag har kastats.

    När du använder aspekter som .aj- filer kommer detta att vara after() throwing- metoden (Undantag e) .

  5. Runt — kanske en av de viktigaste typerna av råd. Den omger en metod, det vill säga en kopplingspunkt som vi kan använda för att till exempel välja om vi ska utföra en given kopplingspunktsmetod eller inte.

    Du kan skriva rådgivningskod som körs före och efter att join point-metoden exekveras.

    Rådgivningen runt är ansvarig för att anropa join point-metoden och returvärdena om metoden returnerar något. Med andra ord, i detta råd kan du helt enkelt simulera driften av en målmetod utan att anropa den, och returnera vad du vill som ett returresultat.

    Med tanke på aspekter som klasser använder vi @Around -kommentaren för att skapa råd som omsluter en kopplingspunkt. När du använder aspekter i form av .aj- filer kommer denna metod att vara around()- metoden.

Join Point — punkten i ett pågående program (dvs. metodanrop, objektskapande, variabel åtkomst) där råden ska tillämpas. Detta är med andra ord ett slags reguljärt uttryck som används för att hitta platser för kodinjektion (ställen där råd bör tillämpas). Pointcut — en uppsättning kopplingspunkter . En pointcut avgör om ett givet råd är tillämpligt på en given kopplingspunkt. Aspekt — en modul eller klass som implementerar tvärgående funktionalitet. Aspect ändrar beteendet för den återstående koden genom att tillämpa råd vid kopplingspunkter som definieras av någon punktklipp . Det är med andra ord en kombination av råd och sammanfogningspunkter. Introduktion— ändra strukturen för en klass och/eller ändra arvshierarkin för att lägga till aspektens funktionalitet till främmande kod. Mål — det objekt som rådet kommer att tillämpas på. Vävning — processen att länka aspekter till andra objekt för att skapa rekommenderade proxyobjekt. Detta kan göras vid kompileringstid, laddningstid eller körtid. Det finns tre typer av vävning:
  • Kompileringstidsvävning — om du har aspektens källkod och koden där du använder aspekten, kan du kompilera källkoden och aspekten direkt med hjälp av AspectJ-kompilatorn;

  • Post-compile weaving (binär vävning) — om du inte kan eller vill använda källkodstransformationer för att väva in aspekter i koden, kan du ta tidigare kompilerade klasser eller jar-filer och injicera aspekter i dem;

  • Last-time weaving — detta är bara binär vävning som fördröjs tills klassladdaren laddar klassfilen och definierar klassen för JVM.

    En eller flera lastare av vävklass krävs för att stödja detta. De tillhandahålls antingen uttryckligen av körtiden eller aktiveras av ett "vävmedel".

AspectJ — En specifik implementering av AOP- paradigmet som implementerar förmågan att utföra tvärgående uppgifter. Dokumentationen finns här .

Exempel i Java

Därefter, för en bättre förståelse av AOP , kommer vi att titta på små "Hello World"-liknande exempel. Till höger om fladdermusen ska jag notera att våra exempel kommer att använda kompileringstidsvävning . Först måste vi lägga till följande beroende i vår pom.xml- fil:

<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjrt</artifactId>
  <version>1.9.5</version>
</dependency>
Som regel är den speciella ajc- kompilatorn hur vi använder aspekter. IntelliJ IDEA inkluderar det inte som standard, så när du väljer det som programkompilator måste du ange sökvägen till 5168 75 AspectJ -distributionen. Detta var det första sättet. Den andra, som är den jag använde, är att registrera följande plugin i filen 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>
Efter detta är det en bra idé att återimportera från Maven och köra mvn clean compile . Låt oss nu gå direkt till exemplen.

Exempel nr 1

Låt oss skapa en huvudklass . I den kommer vi att ha en ingångspunkt och en metod som skriver ut ett passerat namn på konsolen:

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);
  }
}
Det är inget komplicerat här. Vi skickade ett namn och visar det på konsolen. Om vi ​​kör programmet nu ser vi följande på konsolen:
Tanner Victor Sasha
Nu är det dags att dra nytta av kraften i AOP. Nu måste vi skapa en aspektfil . De är av två slag: den första har filtillägget .aj . Den andra är en vanlig klass som använder anteckningar för att implementera AOP- funktioner. Låt oss först titta på filen med filtillägget .aj :

public aspect GreetingAspect {
 
  pointcut greeting() : execution(* Main.printName(..));
 
  before() : greeting() {
     System.out.print("Hi, ");
  }
}
Den här filen är lite som en klass. Låt oss se vad som händer här: pointcut är en uppsättning sammanfogningspunkter; greeting() är namnet på denna pointcut; : exekvering indikerar att det ska tillämpas under körningen av alla ( * ) anrop av metoden Main.printName(...) . Därefter kommer ett specifikt råd — before() — som exekveras innan målmetoden anropas. : greeting() är brytpunkten som detta råd svarar på. Tja, och nedan ser vi själva metoden, som är skriven på Java-språket, som vi förstår. När vi kör main med denna aspekt närvarande kommer vi att få denna konsolutgång:
Hej, Tanner Hej, Victor Hej, Sasha
Vi kan se att varje anrop till metoden printName har modifierats tack vare en aspekt. Låt oss nu ta en titt på hur aspekten skulle se ut som en Java-klass med kommentarer:

@Aspect
public class GreetingAspect{
 
  @Pointcut("execution(* Main.printName(String))")
  public void greeting() {
  }
 
  @Before("greeting()")
  public void beforeAdvice() {
     System.out.print("Hi, ");
  }
}
Efter .aj -aspektfilen blir allt mer uppenbart här:
  • @Aspect indikerar att denna klass är en aspekt;
  • @Pointcut("execution(* Main.printName(String))") är cutpointen som utlöses för alla anrop till Main.printName med ett inmatningsargument vars typ är String ;
  • @Before("greeting()") är råd som tillämpas innan man anropar koden som anges i greeting() cutpoint.
Att köra main med denna aspekt ändrar inte konsolutgången:
Hej, Tanner Hej, Victor Hej, Sasha

Exempel nr 2

Anta att vi har någon metod som utför vissa operationer för klienter, och vi kallar den här metoden från 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);
  }
}
Låt oss använda @Around- kommentaren för att skapa en "pseudotransaktion":

@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...");
     }
  }
  }
Med fortsätt -metoden för ProceedingJoinPoint -objektet anropar vi inpackningsmetoden för att bestämma dess plats i aviseringen. Därför blir koden i metoden ovan joinPoint.proceed(); är före , medan koden nedan är efter . Om vi ​​kör main får vi detta i konsolen:
Öppnar en transaktion... Utför några åtgärder för Client Tanner Avsluter en transaktion...
Men om vi kastar och undantag i vår metod (för att simulera en misslyckad operation):

public static void performSomeOperation(String clientName) throws Exception {
  System.out.println("Performing some operations for Client " + clientName);
  throw new Exception();
}
Då får vi denna konsolutgång:
Öppnar en transaktion... Utför några operationer för Client Tanner Åtgärden misslyckades. Återställer transaktionen...
Så det vi slutade med här är en slags felhanteringsförmåga.

Exempel nr 3

I vårt nästa exempel, låt oss göra något som att logga till konsolen. Ta först en titt på Main , där vi har lagt till lite pseudo affärslogik:

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();
     }
  }
}
I main använder vi setValue för att tilldela värdeförekomstvariabeln ett värde . Sedan använder vi getValue för att få värdet, och sedan anropar vi checkValue för att se om det är längre än 10 tecken. Om så är fallet kommer ett undantag att kastas. Låt oss nu titta på aspekten vi kommer att använda för att logga arbetet med metoderna:

@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);
  }
}
Vad händer här? @Pointcut("execution(* *(..))") kommer att sammanfoga alla anrop av alla metoder. @AfterReturning(value = "methodExecuting()", returning = "returningValue") är råd som kommer att exekveras efter framgångsrik exekvering av målmetoden. Vi har två fall här:
  1. När metoden har ett returvärde — if (returningValue! = Null) {
  2. När det inte finns något returvärde — else {
@AfterThrowing(value = "methodExecuting()", throwing = "exception") är råd som kommer att triggas vid ett fel, det vill säga när metoden ger ett undantag. Och följaktligen, genom att köra main , kommer vi att få en slags konsolbaserad loggning:
Lyckad exekvering: metod — setValue, class — Main Lyckad exekvering: metod — getValue, class — Main, returvärde — <något värde> Exception thrown: method — checkValue, class — Main exception — java.lang.Exception Exception thrown: method — main, class — Main, undantag — java.lang.Exception
Och eftersom vi inte hanterade undantagen får vi ändå ett stackspår: Vad är AOP?  Principer för aspektorienterad programmering - 3Du kan läsa om undantag och undantagshantering i dessa artiklar: Undantag i Java och Undantag: fånga och hantera . Det var allt för mig idag. Idag har vi bekantat oss med AOP , och du kunde se att det här odjuret inte är så skrämmande som vissa människor gör det till. Adjö, alla!
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION