J2EE και Aspect Oriented Programming

ΕΙΣΑΓΩΓΗ

Όπως οι περισσότεροι προγραμματιστές J2EE, έχω γαλουχηθει, στο μεγαλύτερο μέρος της καριέρας μου, και για πολλά χρόνια, στην ανάπτυξη συστημάτων λογισμικού με τη χρήση τεχνικών αντικειμενοστραφούς προγραμματισμού (OOP). Η μέθοδος αυτή είναι η πλέον διαδεδομένη με κύριο χαρακτηριστικό τον κατακερματισμό ενός προβλήματος σε αντικείμενα που χαρακτηρίζονται από ιδιότητες (μέθοδους) και δεδομένα (μεταβλητές).

Παρά το γεγονός ότι ο OOP έχει μεγάλη επιτυχία στη διαμόρφωση και υλοποίηση πολύπλοκων συστημάτων λογισμικού, έχει και τα προβλήματά του. Η πρακτική εμπειρία με μεγάλα έργα έχει δείξει ότι οι προγραμματιστές ενδέχεται να αντιμετωπίσουν  προβλήματα με τη διατήρηση του κώδικα τους, δεδομένου ότι όσο μεγαλύτερο το λογισμικό που υλοποιείται τόσο και πιο δύσκολος γίνεται ο ξεκάθαρος διαχωρισμός του έργου σε ενότητες (αντικείμενα), πράγμα και το οποίο  αποτελεί τη βάση του OOP. Για παράδειγμα, μια μικρή αλλαγή σε μία επαναχρησιμοποιούμενη ενότητα μπορεί τελικά να προκαλέσει πολλές αλλαγές σε άλλες, ανεξάρτητες, ενότητες του κώδικα.

Τέτοιου είδους προβλήματα και πολλές άλλες ανησυχίες έρχεται να επιλύσει μία διαφορετική τεχνική, ο πτυχοστρεφής  ή κατά άλλους θεματοστρεφής προγραμματισμός (AOP). Πάμε να δούμε  λοιπόν κάποια προκατάρκτικά στοιχέια για αυτόν.

Σημείωση: Προτείνω γενικά τον όρο πτυχοστρεφή γιατί εκφράζει καλύτερα την έννοια aspect στα ελληνικά ακόμη και σε λειτουργικό επίπεδο.

ΤΙ ΕΙΝΑΙ O AOP

Ο AOP είναι μια νέα σχετικά μεθοδολογία για το διαχωρισμό ενός προβλήματος σε μεμονωμένες μονάδες που ονομάζονται πτυχές (aspects). Μια πτυχή είναι μια μονάδα που “κόβει” εγκάρσια τη λογική ροή μίας εφαρμογής (εξού και η καλύτερη απόδοση στα ελληνικά αντί της λέξης “θέμα”). Έτσι συμπυκνώνει συμπεριφορές (μέθοδους) που επηρεάζουν πολλαπλές κλάσεις και ενότητες του κωδικά μας και είναι επαναχρησιμοποιήσιμες αλλά η αλλαγή τους δεν έχει απολύτως καμία επίδραση στον υπόλοιπο κώδικα.

Mε τον AOP, ξεκινάμε την υλοποίηση του έργου μας, χρησιμοποιώντας την αντικειμενοστρεφή γλώσσα μας (για παράδειγμα, Java), και στη συνέχεια απασχολούμαστε ξεχωριστά με τα θέματα που σχετίζονται στη δημιουργία επαναχρησιμοποιούμενου κώδικα που τέμνει εγκάρσια τη ροή του προγράμματος, με τη δημιουργία πτυχών. Τέλος, τόσο τα αντικείμενα όσο και οι πτυχές  συνδυάζονται σε μια τελική εκτελέσιμη μορφή μέσω ενός “υφαντή πτυχών” (Αspect Weaver). Ως εκ τούτου, μια ενιαία πτυχή μπορεί να συμβάλει αποφασιστικά στην υλοποίηση μιας σειράς μεθόδων, ενοτήτων, ή αντικειμένων του λογισμικού, αυξάνοντας τόσο την δυνατότητα επαναχρησιμοποίησης όσο και της συντήρησης του κώδικα. Θα πρέπει να σημειωθεί ότι ο αρχικός κώδικας δεν χρειάζεται να έχει επίγνωση για κάθε λειτουργία που έχει προσθέσει μία πτυχή.

Το παρακάτω σχήμα εξηγεί τη διαδικασία της ύφανσης.

ΒΑΣΙΚΕΣ ΕΝΝΟΙΕΣ

Για να κατανοήσουμε καλύτερα τον πτυχο-στρεφή προγραμματισμό ας δούμε αναλυτιοκότερα κάποιες από τις βασικές έννοιες πα΄νω στις οποίες στηρίζεται.

  • Aspect (πτυχή): Όπως είπαμε και πριν, μια πτυχή είναι μια μονάδα που μπάινει εγκάρσια στη λογική ροή μίας εφαρμογής διακόπτωντάς την για να εκτελέσει μία λειτουργία.
  • Join point: ένα σημείο κατά τη διάρκεια εκτέλεσης του προγράμματος όπως πχ μία μέθοδος ή το handing κάποιου exception.
  • Advice: Η ενέργεια που κάνει μία πτυχή για ένα συγκεκιμένο join point. Υπάρχουν διάφοροι τύποι advice και περιλαμβάνουν τα “around”, “before” και “after”. (Θα τα δούμε παρακάτω). Πολλά frameworks όπως το Spring, μοντελοποιούν το advice σαν interceptor.
  • Pointcut: Εκφράζει το σημείο που γίνεται η τομή στη ροή των join points δηλαδή στη ροή του προγράμματος. Ένα Advice συσχετίζεται με μία pointcut expression και καλείται σε οποιοδήποτε join point ταιριάζει σε αυτή την expression (π.χ. η εκτέλεση μίας μεθόδου η ονομασία της οποίας ταιριάζει στην expression του pointcut). Η έννοια του join points που συχετίζεται με pointcut expressions είναι κεντρική στον AOP.
  • Weaving: Το τελικό κομμάτι όπου γίνεται η σύνδεση πτυχών (aspects) με τους άλλους τύπους εφαρμογών ή αντικειμένων για τη δημιουργία του advice αντικειμένου που θα εκτελείται στις τομές. Ο συνυφασμός αυτός μπορεί να συμβεί κατά το compile (χρησιμοποιώντας τον AspectJ compiler), κατά τη φόρτωση ή και κατά την εκτέλεση.

Ο ΠΤΥΧΟΣΤΡΕΦΗΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΣ ΣΤΗΝ ΠΡΑΞΗ

Ένα κλασσικό παράδειγμα όπου κώδικας τέμνει εγκάρσια τη ροή του προγράμματος είναι το logging. Σε αυτή τη περίπτωση θέλουμε για συγκεκριμένες πράξεις που λαμβάνουν χώρα, να σταματάει η ροή για να αφήσει το σύστημα ένα αποτύπωμα με κάποιες συγκεκριμένες πληροφορίες.

Για το παράδειγμά μας θα χρησιμοποιήσουμε τις βιβλιοθήκες του AspectJ που είναι ισως το ποιο επιτυχημένο implementation του πτυχο-στροφή προγραμματισμού.

Παρακάτω είναι το maven dependency της βιβλιοθήκης.

[xml]
<dependency>
<groupid>org.aspectj</groupid>
<artifactid>aspectjrt</artifactid>
<version>1.6.8</version>
</dependency>
[/xml]

Στο παράδειγμά μας θέλουμε να γράφουμε μία εγγραφή στη βάση δεδομένων κάθε φορά που γίνεται καλείται ένα αντικείμενο του κώδικά μας.

Ας υποθέσουμε λοιπόν ότι η κλάση που μας ενδιαφέρει είναι η ακόλουθη:

[java]
package gr.zenika.app.aopexample
public class MyClass
public MyClass (){

// some logic

}
public void doSomething(){

// some more logic

}
}
[/java]

Θέλουμε λοιπόν να κάνουμε log κάθε φορά που καλείται η μέθοδος doSomething. ο κλασσικός τρόπος θα ήταν να έχουμε μία Utility class η οποία θα έχει κάποια στατική μέθοδο, MyLogger.doLog(var1, var2…), που να περνάμε κάποιο μήνυμα και ίσως δεδομένα της doSomething.Όπως είναι κατανοητό αν αλλάξει η παραμετροποίηση της στατικής μεθόδου θα πρέπει να αλλάξει το σύνολο του κώδικα όπου καλείται με ότι συνέπειες μπορεί να έχει αυτό σε χρόνο και ρίσκο.

Στον πτυχο-στρεφή προγραμματισμό όμως ο κυρίως κώδικας δεν επηρεάζεται από τέτοιες αλλαγές. Ας δούμε γιατι:

Ξεκινάμε λοιπόν να φιάξουμε την πτυχή (aspect) που θα κάνει το audit trail. Αν και θα χρησιμοποιήσουμε Spring, προφανώς το AOP μπορεί να χρησιμοποιηθεί σε οποιοδήποτε framework.

1) Δήλωση της πτυχής

Για να δηλώσουμε μία πτυχή μέσα στο Spring απλώς βάζουμε το annotation της AspectJ @Aspect.

[java]
package gr.zenika.common.lib.aop

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

// Annotation that declares a class as an aspect
@Aspect
public class AuditLogger {

}

[/java]

2) Δήλωση του Pointcut

Εδώ δηλώνουμε σε ποιες περιπτώσεις πρέπει να διακόπτεται η ροή του προγράμματος γκαι να ενεργοποιείται η πτυχή. Μόνο αν ικανοποιείται η συνθήκη αυτή διακοπτεται η ροή. Όπως είπαμε θέλουμε να γίνεται audit trail όταν καλείται η μέθοδος doSomething. Άρα στην πτυχή μας βάζουμε μία τέτοια συνθήκη με Annotation.

[java]
@Pointcut(execution("* doSomething(..))")
private void stopForAuditTrail() {}
[/java]

Το παραπάνω διακόπτει τη ροή του προγράμματος για όλες τις περιπτώσεις που καλέιται μία μέθοδος (δηλαδή join point) που ονομάζεται doSomething ανεξαρτήτως τύπου (public, private κτλ.) και με οποιονδήποτε αριθμό παραμέτρων έχει. Με άλλα λόγια αν στον κώδικά μας έχουμε μία μέθοδο που ονομάζεται doSomething η ροή θα περάσει οπωσδήποτε μέσα από την πτυχή.

Σημείωση: η stopForAuditTrail δεν χρειάζεται body. Απλά είναι ένα σημείο όπου γίνεται η τομή.

3) Δήλωση του Advice

Σταματήσαμε λοιπόν την ροή του προγράμματος, τώρα ήρθε η ώρα να γίνει κάποιο action. Πάμε λοιπόν να δηλώσουμε ένα advice. Όπως είπαμε και νωρίτερα υπάρχουν διάφορα είδη advice ανάλογα με το timing κυρίως που θέλουμε να εκτελεστούνε. Έτσι:

  • αν θέλουμε να γίνει εκτέλεση πριν τη μέθοδο doSomething στο κυρίως πρόγραμμα (το join point) το δηλώνουμε με το annotation @Before
  • αν θέλουμε μετά το δηλώνουμε με το @After
  • αν θέλουμε πρίν και μετά με το @Around. Η συγκεκριμένη έχει πολλές δυνατότητες αλλά ταυτόχρονα και ριψοκίνδυνη αν δεν ξέρετε να τη χρησιμοποιήσεται.
  • για τα Exception handling υπάρχει η @AfterThrowing

Για το παράδειγμά μας θέλουμε να καλείται αφού έχει ολοκληρωθεί η doSomething

[java]
@After("gr.zenika.common.lib.aop.stopForAuditTrail()")
public void doAuditLog(JoinPoint point){

// e.g. write to database

}
[/java]

Όπως παρατηρούμε συνδέουμε το Pointcut (stopForAuditTrail) με το Advice (doAuditLog). Υπάρχει η δυνατότητα να συνδυάσουμε και τα δύο με ένα annotation:

[java]
@After("execution("* doSomething(..))")
[/java]

Έγκειται στη κάθε περίσταση αν θα το έχουμε ξεχωριστά ή όχι. Προτείνω όμως ξεχωριστά ώστε να υπάρχει η δυνατότητα διαφορετικά Advice με διαφορετικές λειτουργίες να πιάνονται σε ένα pointcut οποτε σε οποιαδήποτε αλλαγή να αλλάζει ένα σημείο.

4) Στοιχεία του Join Point

Η παράμετρος τύπου JoinPoint μας είναι η γέφυρα με τα στοιχεία από την μέθοδο του κευρίως προγράμματος (στην προκειμένη περίπτωση η doSomething) από όπου μπορούμε να πάρουμε τις πληροφορίες που επεξεργάστηκε η με΄θοδος αυτή και τις οποίες θέλουμε να περάσουμε στο Audit Trail

Για παράδειγμα, αν ξέρουμε ότι τα στοιχεία είναι σε πεδίο τύπου Map θα κάναμε τα εξής:

[java]
for (int i = 0 ; i &lt; point.getArgs().length ; i++){
Object o = point.getArgs()[i];
if (o.getClass().getCanonicalName().contains("Map")){
this.processDataMap((Map)o);
}

[/java]

Ο τελικός κώδικας λοιπόν έχει ώς εξής:

[java]
package gr.zenika.common.lib.aop

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

import java.util.Map

// Annotation that declares a class as an aspect
@Aspect
public class AuditLogger {

// The pointcut
@Pointcut(execution("* doSomething(..))")
private void stopForAuditTrail() {}
}

// The advice that uses data from the join point (i.e the method doSomething)
@After("gr.zenika.common.lib.aop.stopForAuditTrail()")
public void doAuditLog(JoinPoint point){
for (int i = 0 ; i &lt; point.getArgs().length ; i++){
Object o = point.getArgs()[i];
if (o.getClass().getCanonicalName().contains("Map")){
this.processDataMap((Map)o);
}
}
}

private void processDataMap((Map)o)
{
// write to db the data

}
[/java]

Να προσθέσω ότι στην περίπτωση του Spring 3.0 πρέπει να προστεθεί η ακόλουθη δήλωση για να γίνεται αυτόματη η αναζήτηση και το instantiation σε classes που έχουν δηλωθεί ως πτυχές:

[xml]
<aop:aspectj-autoproxy />
[/xml]

ΣΥΜΠΕΡΑΣΜΑ

Είναι εμφανές από τα παραπάνω ότι το AOP και το OOP αποτελούν 2 συμπληρωματικά μοντέλα προγραμματισμού. Ουσιαστικά το AOP έρχεται να καλύψει κενά του OOP τα οποία γίνονται εμφανή σε μεγάλα έργα. Ακόμη όμως και σε μικρού μεγέθους έργα το AOP δίνει μία μεγάλη δυναμική ιδιαίτερα στον τομέα διατηρισιμότητας του κώδικα και ευελιξίας σε επεκτάσεις.

Είναι λοιπόν άποψη μου ότι στα σύγχρονα μοντέλα προγραμματισμού το AOP δεν θα πρέπει να λείπει. Στο κειμενο αυτό καλύψαμε ένα πολύ μικρό κομμάτι από τις δυνατότητές του. Παροτρύνω λοιπόν τον αναγνώστη να διαβάζει τα παρακάτω reference που παραθέτω και τα οποία περιέχουν πλουσία στοιχεία για αυτό το μοντέλο προγραμματισμού.

ΒΙΒΛΙΟΓΡΑΦΙΑ

Passionate Archer, Runner, Linux lover and JAVA Geek! That's about everything! Alexius Dionysius Diakogiannis is a Senior Java Solutions Architect and Squad Lead at the European Investment Bank. He has over 20 years of experience in Java/JEE development, with a strong focus on enterprise architecture, security and performance optimization. He is proficient in a wide range of technologies, including Spring, Hibernate and JakartaEE. Alexius is a certified Scrum Master and is passionate about agile development. He is also an experienced trainer and speaker, and has given presentations at a number of conferences and meetups. In his current role, Alexius is responsible for leading a team of developers in the development of mission-critical applications. He is also responsible for designing and implementing the architecture for these applications, focusing on performance optimization and security.