CodeGym /Blog Java /Aleatoriu /Ce este AOP? Principiile programării orientate pe aspecte...
John Squirrels
Nivel
San Francisco

Ce este AOP? Principiile programării orientate pe aspecte

Publicat în grup
Bună, băieți și fete! Fără înțelegerea conceptelor de bază, este destul de dificil să se aprofundeze cadrele și abordările pentru construirea funcționalității. Așa că astăzi vom vorbi despre un astfel de concept - AOP, alias programarea orientată pe aspecte . Ce este AOP?  Principiile programării orientate pe aspecte - 1Acest subiect nu este ușor și este rar folosit direct, dar multe cadre și tehnologii îl folosesc sub capotă. Și, desigur, uneori, în timpul interviurilor, vi se poate cere să descrieți în termeni generali ce fel de fiară este aceasta și unde poate fi aplicată. Deci, să aruncăm o privire la conceptele de bază și la câteva exemple simple de AOP în Java . Acum, AOP înseamnă programare orientată pe aspecte, care este o paradigmă menită să crească modularitatea diferitelor părți ale unei aplicații prin separarea preocupărilor transversale. Pentru a realiza acest lucru, se adaugă un comportament suplimentar codului existent fără a face modificări codului original. Cu alte cuvinte, ne putem gândi la asta ca agățarea de funcționalități suplimentare peste metode și clase, fără a modifica codul modificat. De ce este necesar acest lucru? Mai devreme sau mai târziu, ajungem la concluzia că abordarea tipică orientată pe obiecte nu poate rezolva întotdeauna în mod eficient anumite probleme. Și când sosește acel moment, AOP vine în ajutor și ne oferă instrumente suplimentare pentru construirea de aplicații. Iar instrumentele suplimentare înseamnă o flexibilitate sporită în dezvoltarea de software, ceea ce înseamnă mai multe opțiuni pentru rezolvarea unei anumite probleme.

Aplicarea AOP

Programarea orientată pe aspecte este concepută pentru a îndeplini sarcini transversale, care poate fi orice cod care poate fi repetat de mai multe ori prin metode diferite, care nu poate fi structurat complet într-un modul separat. În consecință, AOP ne permite să păstrăm acest lucru în afara codului principal și să îl declarăm vertical. Un exemplu este utilizarea unei politici de securitate într-o aplicație. De obicei, securitatea rulează prin multe elemente ale unei aplicații. În plus, politica de securitate a aplicației ar trebui să fie aplicată în mod egal tuturor părților existente și noi ale aplicației. În același timp, o politică de securitate în uz poate evolua ea însăși. Acesta este locul perfect pentru a utiliza AOP . De asemenea, un alt exemplu este înregistrarea în jurnal. Există mai multe avantaje în utilizarea abordării AOP pentru înregistrare, mai degrabă decât adăugarea manuală a funcțiilor de înregistrare:
  1. Codul pentru înregistrare este ușor de adăugat și de eliminat: tot ce trebuie să faceți este să adăugați sau să eliminați câteva configurații cu un anumit aspect.

  2. Tot codul sursă pentru înregistrare este păstrat într-un singur loc, astfel încât nu este nevoie să vânați manual toate locurile în care este utilizat.

  3. Codul de înregistrare poate fi adăugat oriunde, fie în metode și clase care au fost deja scrise, fie în funcționalități noi. Acest lucru reduce numărul de erori de codare.

    De asemenea, atunci când eliminați un aspect dintr-o configurație de design, puteți fi sigur că tot codul de urmărire a dispărut și că nimic nu a fost ratat.

  4. Aspectele sunt coduri separate care pot fi îmbunătățite și utilizate din nou și din nou.
Ce este AOP?  Principiile programării orientate pe aspecte - 2AOP este, de asemenea, utilizat pentru gestionarea excepțiilor, stocarea în cache și extragerea anumitor funcționalități pentru a-l face reutilizabil.

Principiile de bază ale AOP

Pentru a merge mai departe în acest subiect, să cunoaștem mai întâi conceptele principale ale AOP. Sfat — Logică suplimentară sau cod apelat dintr-un punct de conectare. Sfatul poate fi efectuat înainte, după sau în loc de un punct de unire (mai multe despre ele mai jos). Tipuri posibile de sfaturi :
  1. Înainte — acest tip de consiliere este lansat înainte ca metodele țintă, adică punctele de unire, să fie executate. Când folosim aspecte ca clase, folosim adnotarea @Before pentru a marca sfatul ca fiind înainte. Când utilizați aspecte ca fișiere .aj , aceasta va fi metoda before() .

  2. După — sfatul care este executat după executarea metodelor (punctele de unire) este complet, atât în ​​execuția normală, cât și atunci când se aruncă o excepție.

    Când folosim aspecte ca clase, putem folosi adnotarea @After pentru a indica că acesta este un sfat care vine după.

    Când utilizați aspecte ca fișiere .aj , aceasta este metoda after() .

  3. După revenire — acest sfat este efectuat numai când metoda țintă se termină normal, fără erori.

    Când aspectele sunt reprezentate ca clase, putem folosi adnotarea @AfterReturning pentru a marca sfatul ca fiind executat după finalizarea cu succes.

    Când utilizați aspecte ca fișiere .aj , aceasta va fi metoda de returnare after() (Object obj) .

  4. După aruncare — acest sfat este destinat cazurilor în care o metodă, adică un punct de unire, aruncă o excepție. Putem folosi acest sfat pentru a gestiona anumite tipuri de execuție eșuată (de exemplu, pentru a derula înapoi o întreagă tranzacție sau pentru a înregistra cu nivelul de urmărire necesar).

    Pentru aspectele de clasă, adnotarea @AfterThrowing este folosită pentru a indica faptul că acest sfat este folosit după aruncarea unei excepții.

    Când utilizați aspecte ca fișiere .aj , aceasta va fi metoda de aruncare after() (Excepția e) .

  5. În jur - poate unul dintre cele mai importante tipuri de sfaturi. Acesta înconjoară o metodă, adică un punct de îmbinare pe care îl putem folosi, de exemplu, pentru a alege dacă să executăm sau nu o anumită metodă a punctului de unire.

    Puteți scrie un cod de sfat care rulează înainte și după executarea metodei punctului de unire.

    Sfatul around este responsabil pentru apelarea metodei punctului de unire și a valorilor returnate dacă metoda returnează ceva. Cu alte cuvinte, în acest sfat, puteți să simulați pur și simplu funcționarea unei metode țintă fără a o apela și să returnați ceea ce doriți ca rezultat returnat.

    Având în vedere aspectele ca clase, folosim adnotarea @Around pentru a crea sfaturi care înglobează un punct de îmbinare. Când utilizați aspecte sub formă de fișiere .aj , această metodă va fi metoda around() .

Punct de alăturare — punctul dintr-un program care rulează (adică apelul unei metode, crearea obiectelor, accesul la variabile) în care ar trebui aplicat sfatul. Cu alte cuvinte, acesta este un fel de expresie regulată folosită pentru a găsi locuri pentru injectarea codului (locuri în care ar trebui să se aplice sfatul). Pointcut — un set de puncte de unire . O tăietură de punct determină dacă sfatul dat este aplicabil unui punct de unire dat. Aspect — un modul sau o clasă care implementează funcționalitatea transversală. Aspect modifică comportamentul codului rămas prin aplicarea de sfaturi la punctele de îmbinare definite de un pointcut . Cu alte cuvinte, este o combinație de sfaturi și puncte de unire. Introducere— modificarea structurii unei clase și/sau modificarea ierarhiei de moștenire pentru a adăuga funcționalitatea aspectului la codul străin. Țintă — obiectul căruia i se va aplica sfatul. Weaving — procesul de conectare a aspectelor cu alte obiecte pentru a crea obiecte proxy recomandate. Acest lucru se poate face în timpul compilării, în timpul încărcării sau în timpul executării. Există trei tipuri de țesut:
  • Tesare în timp de compilare — dacă aveți codul sursă al aspectului și codul în care utilizați aspectul, atunci puteți compila codul sursă și aspectul direct folosind compilatorul AspectJ;

  • Țesătură post-compilare (împletire binară) — dacă nu puteți sau nu doriți să utilizați transformări de cod sursă pentru a țese aspecte în cod, puteți lua clase sau fișiere jar compilate anterior și puteți injecta aspecte în ele;

  • Încărcare în timp de țesere — aceasta este doar țesere binară care este întârziată până când încărcătorul de clasă încarcă fișierul de clasă și definește clasa pentru JVM.

    Unul sau mai multe încărcătoare din clasa de țesut sunt necesare pentru a sprijini acest lucru. Acestea sunt fie furnizate în mod explicit de runtime, fie activate de un „agent de țesut”.

AspectJ — O implementare specifică a paradigmei AOP care implementează capacitatea de a efectua sarcini transversale. Documentația poate fi găsită aici .

Exemple în Java

În continuare, pentru o mai bună înțelegere a AOP , ne vom uita la mici exemple în stil „Hello World”. În dreptul liliacului, voi reține că exemplele noastre vor folosi țeserea în timp de compilare . Mai întâi, trebuie să adăugăm următoarea dependență în fișierul nostru pom.xml :

<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjrt</artifactId>
  <version>1.9.5</version>
</dependency>
De regulă, compilatorul special ajc este modul în care folosim aspectele. IntelliJ IDEA nu îl include în mod implicit, așa că atunci când îl alegeți ca compilator al aplicației, trebuie să specificați calea către distribuția 5168 75 AspectJ . Aceasta a fost prima cale. Al doilea, care este cel pe care l-am folosit, este să înregistrez următorul plugin în fișierul 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>
După aceasta, este o idee bună să reimportați din Maven și să rulați mvn clean compile . Acum să trecem direct la exemple.

Exemplul nr. 1

Să creăm o clasă principală . În el, vom avea un punct de intrare și o metodă care imprimă un nume transmis pe consolă:

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);
  }
}
Nu e nimic complicat aici. Am dat un nume și l-am afișat pe consolă. Dacă rulăm programul acum, vom vedea următoarele pe consolă:
Tanner Victor Sasha
Acum, este timpul să profitați de puterea AOP. Acum trebuie să creăm un fișier de aspect . Sunt de două feluri: primul are extensia de fișier .aj . A doua este o clasă obișnuită care utilizează adnotări pentru a implementa capabilitățile AOP . Să ne uităm mai întâi la fișierul cu extensia .aj :

public aspect GreetingAspect {
 
  pointcut greeting() : execution(* Main.printName(..));
 
  before() : greeting() {
     System.out.print("Hi, ");
  }
}
Acest fișier este oarecum ca o clasă. Să vedem ce se întâmplă aici: pointcut este un set de puncte de unire; greeting() este numele acestui pointcut; : execuția indică aplicarea acestuia în timpul execuției tuturor apelurilor ( * ) ale metodei Main.printName(...) . Urmează un sfat specific — before() — care este executat înainte ca metoda țintă să fie apelată. : greeting() este punctul de tăiere la care răspunde acest sfat. Ei bine, și mai jos vedem corpul metodei în sine, care este scris în limbajul Java, pe care îl înțelegem. Când rulăm main cu acest aspect prezent, vom obține această ieșire din consolă:
Salut, Tanner Salut, Victor Salut, Sasha
Putem vedea că fiecare apel la metoda printName a fost modificat datorită unui aspect. Acum să aruncăm o privire la cum ar arăta aspectul ca o clasă Java cu adnotări:

@Aspect
public class GreetingAspect{
 
  @Pointcut("execution(* Main.printName(String))")
  public void greeting() {
  }
 
  @Before("greeting()")
  public void beforeAdvice() {
     System.out.print("Hi, ");
  }
}
După fișierul aspect .aj , totul devine mai evident aici:
  • @Aspect indică faptul că această clasă este un aspect;
  • @Pointcut("execution(* Main.printName(String))") este punctul de tăiere care este declanșat pentru toate apelurile către Main.printName cu un argument de intrare al cărui tip este String ;
  • @Before(„greeting()”) este un sfat care se aplică înainte de a apela codul specificat în punctul de tăiere greeting() .
Rularea principală cu acest aspect nu modifică ieșirea consolei:
Salut, Tanner Salut, Victor Salut, Sasha

Exemplul nr. 2

Să presupunem că avem o metodă care efectuează unele operații pentru clienți și numim această metodă din 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);
  }
}
Să folosim adnotarea @Around pentru a crea o „pseudo-tranzacție”:

@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...");
     }
  }
  }
Cu metoda de procedare a obiectului ProceedingJoinPoint , apelăm metoda de împachetare pentru a determina locația acesteia în sfat. Prin urmare, codul din metoda de mai sus joinPoint.proceed(); este Înainte , în timp ce codul de mai jos este După . Dacă rulăm main , obținem asta în consolă:
Deschiderea unei tranzacții... Efectuarea unor operațiuni pentru Client Tanner Închiderea unei tranzacții...
Dar dacă aruncăm o excepție în metoda noastră (pentru a simula o operație eșuată):

public static void performSomeOperation(String clientName) throws Exception {
  System.out.println("Performing some operations for Client " + clientName);
  throw new Exception();
}
Apoi obținem această ieșire de consolă:
Deschiderea unei tranzacții... Efectuarea unor operațiuni pentru Client Tanner Operația a eșuat. Se anulează tranzacția...
Deci, ceea ce am ajuns aici este un fel de capacitate de gestionare a erorilor.

Exemplul nr. 3

În exemplul următor, să facem ceva de genul logării la consolă. Mai întâi, aruncați o privire la Main , unde am adăugat o pseudo-logică de afaceri:

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();
     }
  }
}
În main , folosim setValue pentru a atribui o valoare variabilei de instanță valoare . Apoi folosim getValue pentru a obține valoarea și apoi apelăm la checkValue pentru a vedea dacă are mai mult de 10 caractere. Dacă da, atunci va fi aruncată o excepție. Acum să ne uităm la aspectul pe care îl vom folosi pentru a înregistra activitatea metodelor:

@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);
  }
}
Ce se petrece aici? @Pointcut ("execuție(* *(..))") va uni toate apelurile tuturor metodelor. @AfterReturning(value = "methodExecuting()", returning = "returningValue") este un sfat care va fi executat după executarea cu succes a metodei țintă. Avem două cazuri aici:
  1. Când metoda are o valoare returnată — dacă (returningValue! = Null) {
  2. Când nu există o valoare returnată — else {
@AfterThrowing(value = "methodExecuting()", throwing = "exception") este un sfat care va fi declanșat în cazul unei erori, adică atunci când metoda aruncă o excepție. Și, în consecință, rulând main , vom obține un fel de înregistrare bazată pe consolă:
Execuție cu succes: method — setValue, class — Principal Execuție cu succes: method — getValue, class — Main, return value — <o anumită valoare> Excepție aruncată: method — checkValue, class — Excepție principală — java.lang.Exception Excepție aruncată: method — main, class — Principal, excepție — java.lang.Exception
Și din moment ce nu ne-am ocupat de excepții, vom primi în continuare o urmărire a stivei: Ce este AOP?  Principiile programării orientate pe aspecte - 3Puteți citi despre excepții și gestionarea excepțiilor în aceste articole: Excepții în Java și Excepții: captura și manipulare . Asta e tot pentru mine azi. Astăzi ne-am familiarizat cu AOP și ați putut vedea că această fiară nu este atât de înfricoșătoare pe cât cred unii oameni. La revedere, tuturor!
Comentarii
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION