CodeGym /בלוג Java /Random-HE /מה זה AOP? עקרונות של תכנות מונחה היבטים
John Squirrels
רָמָה
San Francisco

מה זה AOP? עקרונות של תכנות מונחה היבטים

פורסם בקבוצה
היי, חבר'ה ובנות! בלי להבין את המושגים הבסיסיים, די קשה להתעמק במסגרות ובגישות לבניית פונקציונליות. אז היום נדבר על מושג אחד כזה - AOP, הלא הוא תכנות מונחה היבטים . מה זה AOP?  עקרונות של תכנות מונחה היבטים - 1נושא זה אינו קל ולעתים רחוקות נעשה בו שימוש ישיר, אך מסגרות וטכנולוגיות רבות משתמשות בו מתחת למכסה המנוע. וכמובן, לפעמים במהלך ראיונות, ייתכן שתתבקשו לתאר באופן כללי איזו מין חיה זו והיכן ניתן ליישם אותה. אז בואו נסתכל על המושגים הבסיסיים וכמה דוגמאות פשוטות של AOP ב-Java . כעת, AOP מייצג תכנות מונחה היבט , שהיא פרדיגמה שנועדה להגביר את המודולריות של החלקים השונים של יישום על ידי הפרדת חששות רוחביים. כדי להשיג זאת, התנהגות נוספת מתווספת לקוד הקיים מבלי לבצע שינויים בקוד המקורי. במילים אחרות, אנו יכולים לחשוב על זה כתליית פונקציונליות נוספת על גבי שיטות ומחלקות מבלי לשנות את הקוד שהשתנה. למה זה נחוץ? במוקדם או במאוחר, אנו מסיקים שגישה טיפוסית מונחה עצמים לא תמיד יכולה לפתור ביעילות בעיות מסוימות. וכשהרגע הזה מגיע, AOP נחלצת לעזרה ונותנת לנו כלים נוספים לבניית אפליקציות. וכלים נוספים פירושם גמישות מוגברת בפיתוח תוכנה, כלומר אפשרויות רבות יותר לפתרון בעיה מסוימת.

החלת AOP

תכנות מונחה היבטים נועד לבצע משימות צולבות, שיכולות להיות כל קוד שעלול לחזור על עצמו פעמים רבות בשיטות שונות, שלא ניתן לבנות לחלוטין במודול נפרד. בהתאם לכך, AOP מאפשר לנו לשמור את זה מחוץ לקוד הראשי ולהכריז על כך בצורה אנכית. דוגמה לכך היא שימוש במדיניות אבטחה באפליקציה. בדרך כלל, אבטחה עוברת דרך אלמנטים רבים של יישום. יתרה מכך, מדיניות האבטחה של האפליקציה צריכה להיות מיושמת באופן שווה על כל החלקים הקיימים והחדשים של האפליקציה. במקביל, מדיניות אבטחה בשימוש יכולה להתפתח בעצמה. זה המקום המושלם להשתמש ב-AOP . כמו כן, דוגמה נוספת היא רישום . ישנם מספר יתרונות לשימוש בגישת AOP לרישום ולא בהוספת פונקציונלי רישום ידנית:
  1. קל להוסיף ולהסיר את הקוד לרישום: כל מה שאתה צריך לעשות הוא להוסיף או להסיר כמה תצורות של היבט כלשהו.

  2. כל קוד המקור לרישום נשמר במקום אחד, כך שלא תצטרך לחפש ידנית את כל המקומות שבהם נעשה בו שימוש.

  3. ניתן להוסיף קוד רישום בכל מקום, בין אם בשיטות ומחלקות שכבר נכתבו ובין אם בפונקציונליות חדשה. זה מקטין את מספר שגיאות הקידוד.

    כמו כן, בעת הסרת היבט מתצורת עיצוב, אתה יכול להיות בטוח שכל קוד המעקב נעלם וששום דבר לא הוחמצ.

  4. היבטים הם קוד נפרד שניתן לשפר ולהשתמש בו שוב ושוב.
מה זה AOP?  עקרונות של תכנות מונחה היבטים - 2AOP משמש גם לטיפול בחריגים, שמירה במטמון וחילוץ פונקציונליות מסוימת כדי להפוך אותו לשימוש חוזר.

עקרונות בסיסיים של AOP

כדי להתקדם בנושא זה, בואו נכיר תחילה את המושגים העיקריים של AOP. עצה - הגיון נוסף או קוד שנקרא מנקודת הצטרפות. ניתן לבצע ייעוץ לפני, אחרי או במקום נקודת הצטרפות (עוד עליהם בהמשך). סוגי עצות אפשריות :
  1. לפני - ייעוץ מסוג זה מופעל לפני ביצוע שיטות יעד, כלומר נקודות צירוף. בעת שימוש בהיבטים כשיעורים, אנו משתמשים בהערת @Before כדי לסמן את העצה כמגיעה לפני. בעת שימוש באספקטים כקבצי .aj , זו תהיה שיטת before() .

  2. לאחר - עצה שמתבצעת לאחר ביצוע שיטות (נקודות צירוף) הושלמה, הן בביצוע רגיל והן בעת ​​זריקת חריגה.

    כאשר משתמשים בהיבטים כשיעורים, אנו יכולים להשתמש בביאור @After כדי לציין שזו עצה שמגיעה לאחר מכן.

    כאשר משתמשים באספקטים כקבצי .aj , זוהי שיטת after() .

  3. לאחר החזרה - עצה זו מבוצעת רק כאשר שיטת היעד מסתיימת כרגיל, ללא שגיאות.

    כאשר היבטים מיוצגים כשיעורים, אנו יכולים להשתמש בביאור @AfterReturning כדי לסמן את העצה כמבוצעת לאחר סיום מוצלח.

    בעת שימוש באספקטים כקבצי .aj , זו תהיה שיטת after() המחזירה (Object obj) .

  4. לאחר השלכה - עצה זו מיועדת למקרים שבהם שיטה, כלומר נקודת הצטרפות, זורקת חריגה. אנו יכולים להשתמש בעצה זו כדי לטפל בסוגים מסוימים של ביצוע כושל (לדוגמה, לבטל עסקה שלמה או יומן רישום עם רמת המעקב הנדרשת).

    עבור היבטי הכיתה, ההערה @AfterThrowing משמשת כדי לציין שהעצה הזו משמשת לאחר זריקת חריגה.

    בעת שימוש באספקטים כקבצי ‎.aj , זו תהיה שיטת after() throwing (Exception e) .

  5. מסביב - אולי אחד מסוגי העצות החשובים ביותר. הוא מקיף שיטה, כלומר נקודת צירוף שבה נוכל להשתמש כדי, למשל, לבחור אם לבצע שיטת נקודת צירוף נתונה או לא.

    אתה יכול לכתוב קוד עצה שרץ לפני ואחרי ביצוע שיטת ה-join point.

    העצה סביב אחראית לקריאה לשיטת ה-join point ולערכי ההחזרה אם השיטה מחזירה משהו. במילים אחרות, בעצה זו תוכלו פשוט לדמות פעולת שיטת מטרה מבלי לקרוא לה, ולהחזיר מה שתרצו כתוצאה מהחזרה.

    בהתחשב בהיבטים כשיעורים, אנו משתמשים בביאור @Around כדי ליצור עצות שעוטפות נקודת הצטרפות. בעת שימוש בהיבטים בצורה של קבצי .aj , שיטה זו תהיה שיטת around() .

Join Point - הנקודה בתוכנית פועלת (כלומר קריאת שיטה, יצירת אובייקט, גישה משתנה) שבה יש ליישם את העצה. במילים אחרות, זהו סוג של ביטוי רגולרי המשמש למציאת מקומות להזרקת קוד (מקומות שבהם יש ליישם עצות). Pointcut - קבוצה של נקודות צירוף . חיתוך נקודתי קובע אם העצה שניתנה חלה על נקודת הצטרפות נתונה. Aspect - מודול או מחלקה המיישמים פונקציונליות חוצתית. Aspect משנה את התנהגות הקוד הנותר על ידי יישום עצות בנקודות הצטרפות המוגדרות על ידי נקודת חיתוך כלשהי . במילים אחרות, מדובר בשילוב של עצות ונקודות הצטרפות. מבוא - שינוי המבנה של מחלקה ו/או שינוי היררכיית הירושה כדי להוסיף את הפונקציונליות של ההיבט לקוד זר. יעד - האובייקט שעליו יוחל העצה. אריגה - תהליך של קישור היבטים לאובייקטים אחרים כדי ליצור אובייקטי פרוקסי מומלצים. זה יכול להיעשות בזמן קומפילציה, זמן טעינה או זמן ריצה. ישנם שלושה סוגי אריגה:
  • אריגה בזמן קומפילציה — אם יש לך את קוד המקור של ההיבט ואת הקוד שבו אתה משתמש בהיבט, אז אתה יכול לקמפל את קוד המקור ואת ההיבט ישירות באמצעות מהדר AspectJ;

  • אריגה לאחר הידור (אריגה בינארית) - אם אינך יכול או רוצה להשתמש בטרנספורמציות של קוד מקור כדי לארוג היבטים לתוך הקוד, אתה יכול לקחת מחלקות או קבצי jar שהידור קודם לכן ולהחדיר לתוכם היבטים;

  • אריגה בזמן טעינה - זוהי רק אריגה בינארית שמתעכבת עד שה-classloader יטען את קובץ המחלקה ויגדיר את המחלקה עבור ה-JVM.

    נדרש אחד או יותר מעמיסים מסוג אריגה כדי לתמוך בכך. הם מסופקים במפורש על ידי זמן הריצה או מופעלים על ידי "סוכן אריגה".

AspectJ - יישום ספציפי של פרדיגמת AOP המיישמת את היכולת לבצע משימות צולבות. את התיעוד ניתן למצוא כאן .

דוגמאות בג'אווה

לאחר מכן, להבנה טובה יותר של AOP , נסתכל על דוגמאות קטנות בסגנון "Hello World". מימין למחבט, אציין שהדוגמאות שלנו ישתמשו באריגה בזמן הידור . ראשית, עלינו להוסיף את התלות הבאה בקובץ pom.xml שלנו :

<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjrt</artifactId>
  <version>1.9.5</version>
</dependency>
ככלל, מהדר ה-ajc המיוחד הוא האופן שבו אנו משתמשים בהיבטים. IntelliJ IDEA אינו כולל אותו כברירת מחדל, כך שכאשר אתה בוחר בו כקומפיילר היישומים, עליך לציין את הנתיב להפצה 5168 75 AspectJ . זו הייתה הדרך הראשונה. השני, שהוא זה שהשתמשתי בו, הוא לרשום את התוסף הבא בקובץ 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>
לאחר מכן, מומלץ לייבא מחדש מ- Maven ולהפעיל את mvn clean compile . כעת נמשיך ישירות לדוגמאות.

דוגמה מס' 1

בואו ניצור מחלקה ראשית . בו תהיה לנו נקודת כניסה ושיטה שמדפיסה שם עבר בקונסולה:

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);
  }
}
אין כאן שום דבר מסובך. העברנו שם והצגנו אותו בקונסולה. אם נריץ את התוכנית כעת, נראה את הדברים הבאים בקונסולה:
הבורסקאי ויקטור סשה
עכשיו, הגיע הזמן לנצל את הכוח של AOP. כעת עלינו ליצור קובץ היבט . הם משני סוגים: לראשון יש את סיומת הקובץ .aj . השני הוא מחלקה רגילה שמשתמשת בהערות כדי ליישם יכולות AOP . בואו נסתכל תחילה על הקובץ עם סיומת .aj :

public aspect GreetingAspect {
 
  pointcut greeting() : execution(* Main.printName(..));
 
  before() : greeting() {
     System.out.print("Hi, ");
  }
}
הקובץ הזה הוא די כמו מחלקה. בואו נראה מה קורה כאן: pointcut היא קבוצה של נקודות צירוף; greeting() הוא השם של נקודת חיתוך זו; : ביצוע מציין להחיל אותו במהלך ביצוע כל ( * ) הקריאות של השיטה Main.printName(...) . לאחר מכן מגיעה עצה ספציפית - before() - שמתבצעת לפני שנקראת שיטת המטרה. : greeting() היא נקודת החיתוך שהעצה הזו מגיבה אליה. ובכן, ולמטה אנו רואים את גוף השיטה עצמה, שנכתב בשפת ג'אווה, אותה אנו מבינים. כשנריץ את ה-main עם ההיבט הזה נוכח, נקבל את הפלט הזה של המסוף:
היי, טאנר היי, ויקטור היי, סשה
אנו יכולים לראות שכל קריאה לשיטת printName שונתה הודות להיבט. עכשיו בואו נסתכל איך ההיבט ייראה כמחלקת Java עם הערות:

@Aspect
public class GreetingAspect{
 
  @Pointcut("execution(* Main.printName(String))")
  public void greeting() {
  }
 
  @Before("greeting()")
  public void beforeAdvice() {
     System.out.print("Hi, ");
  }
}
אחרי קובץ ה- .aj aspect, הכל הופך ברור יותר כאן:
  • @Aspect מציין שמחלקה זו היא היבט;
  • @Pointcut("execution(* Main.printName(String))") היא נקודת החיתוך המופעלת עבור כל הקריאות ל- Main.printName עם ארגומנט קלט שהסוג שלו הוא String ;
  • @Before("greeting()") היא עצה המיושמת לפני קריאה לקוד שצוין בנקודת החיתוך של greeting() .
הפעלת ראשי עם היבט זה לא משנה את פלט המסוף:
היי, טאנר היי, ויקטור היי, סשה

דוגמה מס' 2

נניח שיש לנו שיטה כלשהי שמבצעת פעולות מסוימות עבור לקוחות, ואנחנו קוראים לשיטה הזו מהראשי :

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);
  }
}
בואו נשתמש בביאור @Around כדי ליצור "פסאודו-עסקה":

@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...");
     }
  }
  }
עם שיטת ההמשך של האובייקט ProceedingJoinPoint , אנו קוראים לשיטת הגלישה כדי לקבוע את מיקומה בעצה. לכן, הקוד בשיטה שלמעלה joinPoint.proceed(); הוא לפני , בעוד שהקוד שמתחתיו הוא אחרי . אם נריץ את main , נקבל את זה בקונסולה:
פתיחת עסקה... מבצעת כמה פעולות עבור Client Tanner סוגרת עסקה...
אבל אם נזרוק וחריגה בשיטה שלנו (כדי לדמות פעולה שנכשלה):

public static void performSomeOperation(String clientName) throws Exception {
  System.out.println("Performing some operations for Client " + clientName);
  throw new Exception();
}
ואז נקבל את הפלט הזה של המסוף:
פותח עסקה... מבצע כמה פעולות עבור Client Tanner הפעולה נכשלה. מחזיר את העסקה לאחור...
אז מה שסיימנו כאן הוא סוג של יכולת טיפול בשגיאות.

דוגמה מס' 3

בדוגמה הבאה שלנו, בואו נעשה משהו כמו כניסה לקונסולה. ראשית, תסתכל על Main , שם הוספנו קצת היגיון עסקי פסאודו:

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();
     }
  }
}
ב- main , אנו משתמשים ב-setValue כדי להקצות ערך למשתנה מופע הערך . לאחר מכן אנו משתמשים ב-getValue כדי לקבל את הערך, ולאחר מכן אנו קוראים checkValue כדי לראות אם הוא ארוך מ-10 תווים. אם כן, אזי יוטל חריגה. עכשיו בואו נסתכל על ההיבט שבו נשתמש כדי לרשום את עבודת השיטות:

@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);
  }
}
מה קורה פה? @Pointcut("execution(* *(..))") יצטרף לכל הקריאות של כל השיטות. @AfterReturning(value = "methodExecuting()", returning = "returningValue") היא עצה שתבוצע לאחר ביצוע מוצלח של שיטת המטרה. יש לנו כאן שני מקרים:
  1. כאשר למתודה יש ​​ערך החזרה - if (returningValue! = Null) {
  2. כשאין ערך החזרה - else {
@AfterThrowing(value = "methodExecuting()", throwing = "exception") היא עצה שתופעל במקרה של שגיאה, כלומר כאשר השיטה זורקת חריג. ובהתאם לכך, על ידי הפעלת main , נקבל מעין רישום מבוסס קונסולה:
ביצוע מוצלח: שיטה — setValue, class — ראשי ביצוע מוצלח: שיטה — getValue, class — ראשי, ערך החזרה — <כמה ערך> נזרק חריג: שיטה — checkValue, class — חריג ראשי — java.lang.Exception נזרק חריג: שיטה — main, class - Main, exception - java.lang.Exception
ומכיוון שלא טיפלנו בחריגים, עדיין נקבל מעקב מחסנית: מה זה AOP?  עקרונות של תכנות מונחה היבטים - 3ניתן לקרוא על חריגים וטיפול בחריגים במאמרים הבאים: חריגים ב-Java ו- Exceptions: catching and treatment . זה הכל בשבילי היום. היום התוודענו ל- AOP , והצלחתם לראות שהחיה הזו לא מפחידה כמו שחלק מהאנשים עושים אותה. שלום לכולם!
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION