Spring AOP İle Aspect-Oriented Programlamaya Giriş

Spring AOP yani Aspect-Oriented Programming, Spring ekosisteminin en güçlü araçlarından birisidir. Aspect ile uygulamanızı manipüle edebilir ve istediğiniz şekilde logic'i değiştirebilirsiniz.

Şimdiye kadar, Spring Context'ini detaylıca işledik ve kullandığımız tek Spring yeteneği IoC prensibi tarafından desteklenen DI (Dependency Injection) oldu. DI ile framework tanımladığınız nesneleri yönetir ve bu nesneleri ihtiyacınız olan yerde size kolayca ve efektif olarak sunar. "Spring Context Bean'lerine Erişim ve Injection - (Wiring Beans)" başlıklı yazıda gösterdiğimiz gibi, bir bean'in referansını istemek için, çoğu durumda, @Autowired anotasyonunu kullanırız. Spring Context'ten bu şekilde bir nesne talep ettiğinizde, Spring'in nesneyi istediğiniz yere "enjekte ettiğini (inject)" de artık biliyorsunuz. Bu yazıda, IoC ilkesi tarafından desteklenen başka bir güçlü tekniğin nasıl kullanılacağını öğreneceksiniz: Aspects.

Aspect, framework'ün metod çağrılarını yakalamasının ve bu metodların yürütülmesini değiştirebilmesinin bir yoludur. Seçtiğiniz belirli metod çağrısının yürütülmesini etkileyebilirsiniz. Bu teknik, yürütülmekte olan metoda ait logic'in bir kısmını ayırmanıza (extract) yardımcı olur. Belirli senaryolarda, kodun bir bölümünü ayırma, bu metodun anlaşılmasını kolaylaştırmaya yardımcı olur (Şekil 1). Geliştiricinin yalnızca metod logic'ini okurken tartışılan ilgili ayrıntılara odaklanmasını sağlar.

Bu bölümde, aspect'lerin nasıl uygulanacağı ve ne zaman kullanmanız gerektiğini tartışacağız. Aspect güçlü bir araçtır ve Peter Parker'ın amcasının dediği gibi, "Büyük güç büyük sorumluluk getirir!". Aspect'leri dikkatli bir şekilde kullanmazsanız, elde etmek istediğinizin tam tersi olan daha az bakımlı bir uygulamayla karşılaşabilirsiniz. Bu yaklaşıma aspect-poriented programlama (AOP) denir.

Şekil 1: AOP kullanılabilecek bir durum

Bazen kodun bazı bölümlerinin business logic'le aynı yerde olması doğru değildir, çünkü uygulamanın anlaşılmasını zorlaştırır. Bir çözüm olarak, kodun bir kısmını, aspect kullanarak business logic'ten ayrı bir kenara taşıyabiliriz. Bu sahnede, programcı Jane, business logic ve log ekleme komutlarını aynı yere yazıyor. Kont Drakula, logları bir aspect ile birlikte ayırarak ona aspect'lerin büyüsünü gösteriyor.

AOP öğrenmenin bir diğer önemli nedeni, Spring'in sunduğu birçok önemli yeteneğin uygulanmasında bunları kullanmasıdır. Framework'ün nasıl çalıştığını anlamak, daha sonra belirli bir sorunla karşılaştığınızda size saatlerce hata ayıklama tasarrufu sağlayabilir. Spring'in aspect kullandığı ve bizim ileride anlatacağımız özelliklerinden birisi "transactionality" dir. Transactionality, çoğu uygulamanın kalıcı verilerin tutarlılığını korumak için bugün kullandığı ana özelliklerden biridir. Aspect kullanan bir diğer önemli özellik, uygulamanızın verilerini korumasına ve verilerin istenmeyen kişiler tarafından görülmemesine veya değiştirilememesine yardımcı olan "security" yapılandırmalarıdır. Bu işlevleri kullanan uygulamalarda neler olduğunu doğru bir şekilde anlamak için önce aspect'leri öğrenmeniz gerekir.

İlk olarak Aspect'lere teorik bir girişle başlayacağız. Burada bazı aspect'lerin nasıl çalıştığını öğreneceksiniz. Bu temel bilgileri anladıktan sonra, bir aspect'in nasıl uygulanacağı hakkında bilgi edineceksiniz. Basit bir senaryoyla başlayacağız ve aspect'leri kullanmak için en pratik kodlar kullanacağımız bir örnek geliştireceğiz. Son olarak, aynı metodu yakalamak ve bu tür senaryolarla başa çıkmak için birden çok aspect tanımladığınızda neler olduğunu öğreneceksiniz.

1. Spring Aspect Nasıl Çalışır?

Bu bölümde, aspect'lerin nasıl çalıştığını ve aspect'leri kullanırken karşılaşacağınız temel terminolojiyi öğreneceksiniz. Aspect kullanmayı öğrenerek, uygulamanızı daha sürdürülebilir hale getirmek için yeni teknikler kullanabilirsiniz. Ayrıca, belirli Spring özelliklerinin uygulamalara nasıl eklenebileceğini de anlayacaksınız. Bunları önce tartışacağız ve sonraki bölümde bir uygulama örneğine geçeceğiz. Ancak kod yazmaya dalmadan önce neyi implemente ettiğimiz hakkında bir fikrinizin oluşmasına yardımcı olalım.

Bir aspect, seçtiğiniz belirli metodları çağırdığınızda framework'ün yürüttüğü bir logic parçasıdır. Bir aspect'i tasarlarken, aşağıdakileri tanımlarsınız:

  • Bir metodu çağırdığınızda Spring'in hangi kodları yürütmesini istiyorsunuz? Bunun adı aspect'tir.
  • Uygulama bu aspect logic'i ne zaman çalıştırmalı (örneğin metod çağrısından önce veya sonra). Buna advice adı verilmiştir.
  • Framework, yakalamak ve yürütmek için hangi metodlara ihtiyacı duyar? Buna pointcut adı verilmiştir.

Aspects terminolojosinde, bir aspect'in yürütülmesini tetikleyen event'i tanımlayan join point kavramını da bulacaksınız. Ancak Spring ile bu event her zaman bir metod çağrısıdır.

Dependency Injection durumunda olduğu gibi, aspect'leri kullanmak için ve aspect'leri uygulamak istediğiniz nesneleri yönetmek için framework'e ihtiyacınız vardır. Daha önce öğrendiğiniz yaklaşımları, framework'ün bunları denetlemesini ve tanımladığınız aspect'ler üzerlerinde uygulamasını sağlamak için Spring Context'e bean eklemek için kullanacaksınız. Bir aspect tarafından yakalanan metodu bildiren bean target (hedef) nesne olarak adlandırılır. Şekil 2 bu terimleri size özetleyecek.

Şekil 2: Bütün Aspect terminolojisinin özeti

Spring, birisi belirli bir metodu (pointcut) çağırdığında bazı logic'leri (aspect) yürütür. Logic'in pointcut'tan ne zaman (advice) (örneğin, önce) yürütülüceğini belirtmemiz gerekir. Spring'in metoda müdahale edebilmesi için, yakalanan metodu içeren nesnenin Spring Context'inde bir bean olması gerekir. Böylece, bean aspect'in hedef nesnesi (target) haline gelir.

Ama Spring her yöntemi nasıl yakalar ve aspect logic'i  uygular? Bu bölümde daha önce de anlatıldığı gibi, nesnenin Spring Context'inde bir bean olması gerekir. Böylece nesneyi bir aspect target'i haline getirdiğinizden, Spring, context'ten bu bean'i istediğinizde size doğrudan bir instance referansı vermez. Bunun yerine, Spring size gerçek metod yerine aspect logic'i çağıran bir nesne verir. Yani Spring'in size gerçek bean yerine bir proxy nesnesi verdiğini söylüyoruz. Artık, context'in getBean() metodunu doğrudan kullanırsanız veya DI (Şekil 3) kullanıyorsanız, bean'i context'ten her aldığınızda bean yerine proxy'yi alacaksınız. Bu yaklaşım weaving olarak adlandırılır.

Şekil 3: Aspect kullandığınızda gerçek instance yerine proxy instance elde edersiniz.

Weaving bir aspect'tir. Spring, size gerçek bean'e ait bir referans vermek yerine, bir proxy nesnesinin referansını verir. Böylece metod çağrılarını yakalayabilir ve aspect logic'i yönetebilir hale gelmiş olur.

Şekil 4'te, bir metodun aspect tarafından yakalandığındaki ve yakalanmadığındaki karşılaştırmasını görebilirsiniz. Aspected bir metodu çağırmanın, metodu Spring tarafından sağlanan proxy nesnesi aracılığıyla çağırdığınızı varsaydığını gözlemlersiniz. Proxy, aspect logici yürütür ve çağrıyı gerçek metoda devreder.

Şekil 4: Aspect kullanıldığında ve kullanılmadığında ki metod akışı

Bir metod aspected olmadığında, çağrı doğrudan bu metoda gider. Bir metod için bir aspect tanımladığımız zaman ise, çağrı proxy nesnesinden geçer. Proxy nesnesi, aspect ile tanımlanan logic'i uygular ve sonra çağrıyı gerçek metoda devreder.

Artık aspect'in büyük resmine ve Spring'in bunları nasıl yönettiğine dair fikir sahipbi olduğunuzdan, daha da ileri gidiyor ve Spring ile aspect'leri uygulamak için ihtiyacınız olan syntax'a geçiyoruz. Sıradaki bölümde, bir senaryoyu açıklıyorum ve sonra senaryonun gereksinimlerini aspect kullanarak uyguluyoruz.

2. Spring AOP İle Aspect'leri Implemente Etmek

Bu bölümde, gerçek dünyadaki örneklerde kullanılan en alakalı aspect syntax'larını öğreneceksiniz. Öncelikle bir senaryoyu değerlendireceğiz ve gereksinimlerini aspect'lerle implemente edeceğiz.

Service sınıflarında birden çok use case implemte eden bir uygulamanız olduğunu varsayalım. Bazı yeni düzenlemeler, uygulamanızın artık her use case çalıştırma zamanının başlangış ve bitiş süresini depolamanız gerektiriyor olsun. Ve sizde, bu use case'lerin başladığı ve bittiği zamanları loga kaydetmek için bir sorumluluk almaya karar verdiniz.

Bölüm 2.1'de, bu senaryoyu mümkün olan en basit şekilde çözmek için bir aspect kullanacağız. Bunu yaparak, bir aspect uygulamak için neye ihtiyacınız olduğunu öğreneceksiniz. Ayrıca, bir sonraki bölümde, aspect'leri kullanma konusunda aşamalı olarak daha fazla ayrıntı ekleyeceğim. Bölüm 2.2'de, bir aspect'in yakaladığı metodun parametrelerini veya metod tarafından döndürülen değeri nasıl kullandığını ve hatta değiştirdiğini göreceğiz. Bölüm 2.3'te, belirli bir amaç için ele geçirmek istediğiniz metodları işaretlemek için anotasyonların nasıl kullanılacağını öğreneceksiniz. Geliştiriciler genellikle bir aspect'in yakalaması gereken metodu işaretlemek için anotasyonlar kullanır. Spring'teki birçok özellikte, sonraki bölümlerde öğreneceğiniz gibi anotasyonlar kullanır. Bölüm 2.4, Srping aspect'lerle kullanabileceğiniz advice anotasyonları için size daha fazla alternatif sunacak.

2.1 Bir Aspect Basitçe Nasıl Implemente Edilir?

Bu bölümde, senaryomuzu çözmek için basit bir aspect'in nasıl implemente edileceğini tartışıyoruz. Bunun için yeni bir proje oluşturacağız ve uygulamamızı test etmek için kullanacağımız bir metod içeren service sınıfını tanımlayacağız. Bu tanımladığımız metodu ise aspectlerin doğru çalışıp çalışmadığını göstermekte kullanacağız.

Örneğimizde spring-context bağımlılığına ek olarak, spring-aspects bağımlılığına da ihtiyacımız var. Pom.xml dosyanızı güncelleştirdiğinizden ve bir sonraki kod snippet'inde sunulduğu gibi gerekli bağımlılıkları eklediğinizden emin olunuz:

<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-context</artifactId>
   <version>5.2.8.RELEASE</version>
</dependency>

<dependency>                               ❶
   <groupId>org.springframework</groupId>
   <artifactId>spring-aspects</artifactId>
   <version>5.2.8.RELEASE</version>
</dependency>

❶ Aspect'leri implemente etmek için bu bağımlılığa ihtiyacımız var.

Örneğimizi kısaltmak ve aspect'lerle ilgili syntax'a odaklanabilmeniz için, CommentService adlı yalnızca bir service nesnesini ve publishComment(Comment comment) adlı bir use case kullanım örneğini ele alacağız. CommentService sınıfında tanımlanan bu metod, Comment tipinde bir parametre alır. Comment bir model sınıfıdır ve bir sonraki kod snippet'inde görebilirsiniz:

public class Comment {
 
  private String text;
  private String author;
  
}

NOT: Model sınıfının uygulama tarafından işlenen verileri modelleyen bir sınıf olduğunu hatırlayın. Bizim örneğimizde, Comment sınıfı bir yorumu nitelikleriyle açıklar: metin ve yazar. Service sınıfı ise, bir uygulamanın use case'ini implemente eder.

Bir sonraki kod snippetinde, CommentService sınıfının tanımını bulabilirsiniz. CommentService sınıfın, Spring Context'inde bir bean yapmak için @Service stereotype anotasyonunu ekliyoruz. CommentService sınıfı, senaryomuzun use case'ini temsil eden publishComment(Comment comment) metodunu tanımlar.

Ayrıca bu örnekte, System.out'u kullanmak yerine konsola ileti yazmak için Logger türünde bir nesne kullandığımı gözlemleyebilirsiniz. Gerçek dünyadaki uygulamalarda, konsola mesaj yazmak için System.out'u kullanmazsınız. Genellikle log yeteneklerini özelleştirme ve log iletilerini standartlaştırmada size daha fazla esneklik sunan bir logging framework kullanırsınız. Logging framework için bazı iyi seçenekler şunlardır:

Logging framework'leri, Spring kullansa da kullanmasa da herhangi bir Java uygulamasıyla uyumludur. Spring'le ilgili olmadıkları için, onları örneklerimizde dikkatinizi dağıtmamak için kullanmadım. Ancak artık Spring'le yeterince yol aldık ve bu ek framework'leri de örneklerimizde kullanmaya başlayabiliriz.

@Service                                                   ❶
public class CommentService {
 
  private Logger logger =                                  ❷
    Logger.getLogger(CommentService.class.getName());
 
  public void publishComment(Comment comment) {            ❸
    logger.info("Publishing comment:" + comment.getText());
 }
}

❶ Bunu Spring Context'inde bir bean yapmak için stereotype anotasyonu olarak ekliyoruz.

❷ Birisi use case'i her çağırdığında bir ileti loglamak için logger nesnesi tanımlıyoruz.

❸ Bu metod, gösterimimiz için use case'i tanımlıyor.

Bu örnekte, projemize başka bağımlılıklar eklememek için JDK logging yeteneğini kullanıyorum. Bir logger nesnesi tanımlarken, parametre olarak bir ad vermeniz gerekir. Bu ad daha sonra loglarda görünür ve log iletisinin kaynağını gözlemlemenizi kolaylaştırır. Genellikle, örneğimizde yaptığım gibi sınıf adını kullanırız: CommentService.class.getName().

Ayrıca, Spring'e stereotype anotasyonları eklenmiş sınıfları nerede arayacağını söylemek için bir yapılandırma sınıfı eklememiz gerekir. Benim örneğimde, service sınıfını "services" adlı pakete ekledim ve bir sonraki kod snippet'inden göreceğiniz gibi, @ComponentScan anotasyonuyla belirtmem gereken şey şudur:

@Configuration
@ComponentScan(basePackages = "services")   ❶
public class ProjectConfig {
 
}

❶ Spring'e stereotype anotasyonlu sınıfları nerede arayacağını söylemek için @ComponentScan kullanıyoruz.

Service sınıfında publishComment() metodunu çağıran Main sınıfını yazalım ve aşağıdaki kodda gösterildiği gibi geçerli davranışı gözlemleyelim.

public class Main {
 
  public static void main(String[] args) {
    var c = new AnnotationConfigApplicationContext(ProjectConfig.class);
 
    var service = c.getBean(CommentService.class);    ❶
 
    Comment comment = new Comment();                  ❷
    comment.setText("Kerteriz Blog");
    comment.setAuthor("Ismet");
 
    service.publishComment(comment);                  ❸
  }
}

❶ CommentService bean'ini context'ten alır

❷ PublishComment() metoduna parametre olarak vermek için bir Comment instance'ı oluşturur

❸ publishComment() metodunu çağırır

Uygulamayı çalıştırırsanız, konsolda bir sonraki snippet'te gördüğünüze benzer bir çıktı görürsünüz:

Mar 24, 2022 12:39:53 PM services.CommentService publishComment
INFO: Publishing comment:Kerteriz Blog

PublishComment() metodu tarafından oluşturulan çıktıyı görüyorsunuz. Tartıştığımız örneği çözmeden önce uygulama böyle görünüyor. Unutmayın, service metodu çağrısından önce ve sonra konsolda iletileri yazdırmamız gerekiyor. Şimdi projeyi metod çağrısını kesen ve çağrıdan önce ve sonra bir çıktı ekleyen bir aspect sınıfıyla geliştirelim.

Bir aspect oluşturmak için şu adımları izlersiniz (Şekil 5):

  1. @EnableAspectJAutoProxy anotasyonunu yapılandırma sınıfına ekleyerek Spring uygulamanızdaki aspect mekanizmasını etkinleştirin.
  2. Yeni bir sınıf oluşturun ve @Aspect anotasyonunu ekleyin. @Bean veya stereotype anotasyonlarını kullanarak, Spring Context'e bu sınıf için bir bean ekleyin.
  3. Aspect logic'i implemente edecek ve advice anotasyonu kullanarak hangi metodların ne zaman ve hangi metodlarla kesileceğini Spring'e söyleyecek bir metod tanımlayın.
  4. Apect logic'i implemente edin.
Şekil 5: Aspect oluşturmak için gereken adımlar

Bir aspect'i implement etmek için dört kolay adımı izlersiniz. İlk olarak, uygulamanızdaki aspect özelliğini etkinleştirmeniz gerekir. Sonra bir aspect sınıfı oluşturursunuz, bir metod tanımlarsınız ve Spring'e ne zaman ve nelerin kesilmeleri için talimat verirsiniz. Son olarak, aspect logic'i uygularsınız.

ADIM 1: Aspect Mekanizmasını Uygulamanızda Aktif Etmek

İlk adım için Spring'e uygulamanızdaki aspect'leri kullanacağınızı söylemeniz gerekir. Spring tarafından sağlanan belirli bir mekanizmayı her kullandığınızda, yapılandırma sınıfınıza belirli bir anotasyonu ekleyerek açıkça etkinleştirmeniz gerekir. Çoğu durumda, bu anotasyonların adları "Enable" ile başlar. Yazılarımızda ilerledikçe farklı Spring yetenekleri sağlayan bu tür anotasyonlar öğreneceksiniz. Bu örnekte, aspect yeteneklerini etkinleştirmek için @EnableAspectJAutoProxy  anotasyonunu kullanmamız gerekir. Yapılandırma sınıfının aşağıdaki kodda sunulana benzemesi gerekir.

@Configuration
@ComponentScan(basePackages = "services")
@EnableAspectJAutoProxy                     ❶
public class ProjectConfig {
}

❶ Spring uygulamamızdaki aspect mekanizmasını etkinleştirir

ADIM 2: Aspect'i Tanımlayan ve Context'e Bean'ini Ekleyen Bir Sınıf Oluşturmak

Spring Context'te aspect'i tanımlayan yeni bir bean yaratmamız gerekiyor. Bu nesne, belirli metod çağrılarını yakalayacak ve bunları belirli bir mantıkla artıracak metodları tutar. Bir sonraki kodda, bu yeni sınıfın tanımını bulabilirsiniz.

@Aspect
public class LoggingAspect {
  public void log() {
    // To implement later
  }
}

daha önceki yazılarımızda öğrendiğiniz yaklaşımlardan herhangi birini, bu sınıfın bir instance'ını Spring Context'e eklemek için kullanabilirsiniz. @Bean anotasyonunu kullanmaya karar verirseniz, bir sonraki kod snippet'inde sunulduğu gibi yapılandırma sınıfını değiştirmeniz gerekir. Tabii ki, isterseniz stereotype anotasyonlarını da kullanabilirsiniz:

@Configuration
@ComponentScan(basePackages = "services")
@EnableAspectJAutoProxy
public class ProjectConfig {
 
  @Bean                            ❶
  public LoggingAspect aspect() {
    return new LoggingAspect();
  }
}

❶ Spring Context'e LoggingAspect sınıfının bir instance'ını ekler

Unutmayın, bu nesneyi Spring Context'inde bir beanyapmanız gerekir, çünkü Spring'in yönetmesi gereken herhangi bir nesneden haberdar olması gerekir.

Ayrıca, @Aspect anotasyonu bir stereotype anotasyon değildir. @Aspect kullanarak, Spring'e sınıfın bir aspect tanımını implement ettiğini söylersiniz, ancak Spring bu sınıf için bir bean oluşturmaz. Sınıfınız için bir bean oluşturmak ve Spring'in bu şekilde yönetmesine izin vermek için daha önce öğrendiğiniz syntax'lardan birini (@Bean veya streotype) açıkça kullanmanız gerekir. Sınıfa @Aspect anotasyonu eklemenin context'e bir bean eklemediğini unutmak yaygın bir hatadır.

ADIM 3: Advice Anotasyonu Kullanarak Spring'e Ne Zaman ve Hangi Metod Çağrılarını Yakalayacağını Söylemek

Artık aspect sınıfını tanımladığımıza göre, advice seçiyoruz ve metoda buna göre bir anotasyon ekliyoruz. Bir sonraki kodda, @Around anotasyonu ile metoda nasıl açıklama eklediğimizi göreceksiniz.

@Aspect
public class LoggingAspect {
 
  @Around("execution(* services.*.*(..))")            ❶
  public void log(ProceedingJoinPoint joinPoint) {   
    joinPoint.proceed();                              ❷
  }
}

❶ Yakalanan metodları tanımlar

❷ Yakalanan gerçek metodu temsil eder

@Around anotasyonunu kullanmak dışında, anotasyon değeri olarak olağandışı bir dize ifadesi yazdığımı ve aspect metoduna bir parametre eklediğimi görüyorsunuz. Peki bunlar da ne?

Tek tek ele alalım. @Around anotasyonuna parametre olarak kullanılan tuhaf ifade, Spring'e hangi metodları yakalayacağını söyler. Bu ifadeden korkmayın! Bu ifade dili AspectJ pointcut dili olarak adlandırılır ve kullanmak için ezberlemenize gerek yoktur. Pratikte karmaşık ifadeler kullanmazsınız. Böyle bir ifade yazmam gerektiğinde, her zaman belgelere (https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-ataspectj) başvururum.

Teorik olarak, yakalanacak belirli bir metod çağrısı kümesini tanımlamak için çok karmaşık AspectJ pointcut ifadeleri yazabilirsiniz. Bu dil gerçekten çok güçlüdür. Ancak bu bölümün ilerleyen bölümlerinde tartışacağımız gibi, karmaşık ifadeler yazmaktan kaçınmak her zaman daha iyidir. Çoğu durumda, daha basit alternatifler bulabilirsiniz.

Kullandığım ifadeye bakın (Şekil 6). Spring' in, metodun dönüş türüne, ait olduğu sınıfa, metodun adına veya metodun aldığı parametrelere bakılmaksızın, service paketinde bulunan bir sınıfta tanımlanan herhangi bir metodu yakaladığı anlamına geliyor.

Şekil 6: Aspect anotasyonuna verdiğimiz parametrenin detaylı açıklaması

Örnekte kullanılan AspectJ pointcut ifadesi, Spring'e, dönüş tiplerine, ait oldukları sınıfa, ada veya aldıkları parametrelere bakılmaksızın service paketindeki tüm metodların çağrılarını yakalamasını söyler.

İkinci bakışta, ifade o kadar karmaşık görünmüyor, değil mi? Bu AspectJ pointcut ifadelerinin yeni başlayanları korkutma eğiliminde olduğunu biliyorum, ancak güvenin bana, Spring uygulamalarında bu ifadeleri kullanmak için aspectj uzmanı olmak zorunda değilsiniz.

Şimdi metoda eklemiş olduğum ikinci öğeye bakalım: yakalanan metodu temsil eden ProceedingJoinPoint parametresi. Bu parametreyle yaptığınız en önemli şey, gerçek metodu ne zaman çalıştıracağınız gerektiğini söylemektir.

ADIM 4: Aspect Logic'i Implement Etmek

Bir sonraki kod snippetinde aspect logic'i görebilirsiniz. Artık aspect;

  1. Metodu yakalar
  2. Yakalanan metod çağrılmadan önce consola bir şeyler yazdırır
  3. Yakalanan metodu çağırır
  4. Yakalanan metod çağrıldıktan sonra consola bir şeyler yazdırır

Şekil 7, aspect davranışını görsel olarak sunar:

Şekil 7: Aspect davranışının sunumu

LoggingAspect, metod çağrısından önce ve sonra bir şey görüntüleyerek metod yürütmesini sarmalar. Bu şekilde, aspect'in basit bir uygulamasını gözlemleyebilirsiniz.

@Aspect
public class LoggingAspect {
 
  private Logger logger = Logger.getLogger(LoggingAspect.class.getName());
 
  @Around("execution(* services.*.*(..))")
  public void log(ProceedingJoinPoint joinPoint) throws Throwable {
    logger.info("Method will execute");                              ❶
    joinPoint.proceed();                                             ❷
    logger.info("Method executed");                                  ❸
  }
}

❶ Yakalanan metodun yürütülmesinden önce konsola bir ileti yazdırır

❷ Yakalanan metodu çağırır

❸ Yakalanan metodun yürütülmesinden sonra konsola bir ileti yazdırır

ProceedingJoinPoint parametresinin proceed() metodu, CommentService bean'inin ele geçirilen metodu publishComment() i çağırır. proceed() metodunu çağırmazsanız, bu aspect hiçbir zaman yakalanan metoda yürütmez (Şekil 8).

Şekil 8: Gerçek metodu çağırmak için proceed() metodunu çağırmalısınız

Aspect'in ProceedingJoinPoint parametresinin proceed() metodunu çağırmazsanız, bu aspect hiçbir zaman yakalanan metoda başvurmaz. Bu durumda, aspect yalnızca yakalanan metod yerine yürütülür. Metodu çağıran, gerçek metodun hiçbir zaman yürütülmediğini bilemez.

Gerçek metodun artık çağrılmadığı logic'i bile uygulayabilirsiniz. Örneğin, bazı yetkilendirme kurallarını uygulayan bir aspect, uygulamanın koruduğu bir metoda daha fazla başvurulup başvurulamayacağına karar verebilir. Yetkilendirme kuralları yerine getirilmezse, bu husus koruduğu yakaladığı metodu yürütmez (Şekil 9).

Şekil 9: Gerçek metodları yürütüp yürütmemek tamamen sizin elinizde!

Bir aspect, yakaladığı metodu yürütmemeye karar verebilir. Bu davranış, metodu çağırana bir zihin hilesi uygular gibi görünüyor. Metodu çağıran, gerçekte istediği logic'ten başka bir logic elde eder.

Ayrıca, proceed() metodunun bir Throwable throw ettiğini gözlemleyin. proceed() metodu, yakalanan metoddan gelen herhangi bir hatayı throw etmek için de tasarlanmıştır. Bu exception'ları handle etmek için try-catch-finally bloğu kullanabilirsiniz. İlerleyen başlıklarda detaylarını göreceğiz.

2.2 Yakalanan Metodların Parametrelerini ve Dönüş Değerlerini Değiştirmek

Size aspect'lerin gerçekten güçlü olduğunu söylemiştim. Yalnızca bir metodu yakalayıp yürütülmesini değiştirmezler, aynı zamanda metodun dönüş değerini ve parametrelerini de değiştirebilirler. Bu bölümde, bir aspect'in parametreleri nasıl etkilediğini ve yakalanan metodun döndürdüğü değeri nasıl etkileyebileceğini kanıtlamak için çalıştığımız örneği değiştireceğiz. Bunun nasıl yapılacağını bilmek, aspect kullanarak uygulayabileceklerimiz konusunda size daha fazla fırsat sunar.

Service metodunu çağırmak için kullanılan parametreleri ve metodun ne döndürdüğünü loga kaydetmek istediğinizi varsayalım. Metodun ne döndürdüğüne de atıfta bulunduğumuzdan, service metodunu değiştirdim ve bir sonraki kod snippet'inde sunulduğu gibi bir değer döndürmesini sağladım:

@Service
public class CommentService {
 
  private Logger logger = Logger.getLogger(CommentService.class.getName());
 
  public String publishComment(Comment comment) {
    logger.info("Publishing comment:" + comment.getText());
    return "SUCCESS";                                          ❶
  }
}

❶ Gösterimimiz için metod artık bir değer döndürüyor.

Aspect, yakalanan metodun adını ve yöntem parametrelerini kolayca bulabilir. Aspect metodunun ProceedingJoinPoint parametresinin yakalanan metodu temsil ettiğini unutmayın. Yakalanan metodla ilgili bilgileri (parametreler, metod adı, hedef nesne vb.) almak için bu parametreyi kullanabilirsiniz. Bir sonraki kod parçacığı, metodu engellemeden önce metod adını ve metodu çağırmak için kullanılan parametreleri nasıl alacağınızı gösterir:

String methodName = joinPoint.getSignature().getName();
Object [] arguments = joinPoint.getArgs();

Artık bu ayrıntıları loglamak için aspect'i de değiştirebiliriz. Bir sonraki kodda, aspect metodunda yapmanız gereken değişikliği bulabilirsiniz.

@Aspect
public class LoggingAspect {
 
  private Logger logger = Logger.getLogger(LoggingAspect.class.getName());
 
  @Around("execution(* services.*.*(..))")
  public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
    String methodName =                                                ❶
      joinPoint.getSignature().getName();                              ❶
    Object [] arguments = joinPoint.getArgs();                         ❶
 
    logger.info("Method " + methodName +                               ❷
        " with parameters " + Arrays.asList(arguments) +
        " will execute");
 
    Object returnedByMethod = joinPoint.proceed();                     ❸
 
    logger.info("Method executed and returned " + returnedByMethod);
 
    return returnedByMethod;                                           ❹
  }
}

❶ Yakalanan metodun adını ve parametrelerini alır

❷ Yakalanan metodun adını ve parametrelerini loglar

❸ Yakalanan metodu çağırır

❹ Yakalanan metodun döndürdüğü değeri döndürür

Şekil 10, akışın anlaşılmasını kolaylaştırır. Aspect'in çağrıyı nasıl yakaladığını ve parametrelere ve döndürülen değere nasıl erişebileceğini gözlemleyebilirsiniz.

Şekil 10: Aspect, çağıran tarafın haberi olmadan metodu yakalar ve parametreler iledönüş değerini değiştirebilir.

Aspect metod çağrısını yakalar, böylece çağrıldıktan sonra yakalanan metodun parametrelerine ve döndürülen değerine erişebilir. Main() metodu için, CommentService bean'inin publishComment() metodunu doğrudan çağırıyor gibi görünüyor. Çünkü, bir aspect'in çağrıyı yakaladığının farkında olmaz.

Aşağıdaki kodda gösterildiği gibi publishComment() tarafından döndürülen değeri yazdırmak için main() metodunu değiştirdim.

public class Main {
 
  private static Logger logger = Logger.getLogger(Main.class.getName());
 
  public static void main(String[] args) {
    var c = new AnnotationConfigApplicationContext(ProjectConfig.class);
 
    var service = c.getBean(CommentService.class);
 
    Comment comment = new Comment();
    comment.setText("Demo comment");
    comment.setAuthor("Natasha");
 
    String value = service.publishComment(comment);
 
    logger.info(value);     ❶
  }
}

❶ publishComment() metodu tarafından döndürülen değeri yazdırır

Uygulamayı çalıştırdığınızda konsolda, aspect tarafından loglanan çıktıları ve main() metoduyla loga kaydedilen dönüş değeri görürsünüz:

Mar 24, 2022 10:49:39 AM aspects.LoggingAspect log                         ❶
INFO: Method publishComment with parameters [Comment{text='Kerteriz Blog', ❶
➥ author='Ismet'}] will execute                                        ❶
Sep 28, 2020 10:49:39 AM services.CommentService publishComment
INFO: Publishing comment:Kerteriz Blog                                  ❷
Sep 28, 2020 10:49:39 AM aspects.LoggingAspect log
INFO: Method executed and returned SUCCESS                              ❸
Sep 28, 2020 10:49:39 AM main.Main main
INFO: SUCCESS  

❶ Aspect tarafından loglanan parametreler

❷ Yakalanan metod içinden yazdırılan loglar

❸ Aspect tarafından loglanan dönüş değeri

❹ Main metod tarafından loglanan dönüş değeri

Ancak aspect'ler daha da güçlüdür. Yakalanan metodun yürütülmesini şu şekilde de değiştirebilirler:

  • Metoda gönderilen parametrelerin değerini değiştirme
  • Metodu çağıran tarafından alınan dönüş değerini değiştirme
  • Metodu çağırana bir exception atma veya yakalanan metoddan atılan bir exception'u yakalama ve işleme

gibi işlevleri de aspect'ler aracılığıyla kolaylıkla sağlayabiliriz.

Yakalanan bir metodun çağrısını değiştirmede son derece esnek olabilirsiniz. Davranışını tamamen bile değiştirebilirsiniz (şekil 11). Ama dikkatli olun! Bir aspect ile logic'i değiştiriyorsanız, logic'i tamamen şeffaf bir parça yapmalısınız. Belli olmayan şeyleri saklamadığından emin olmanız gerekir. Logic'in bir bölümünü çıkartma fikri, kodun tekrarlanmasını önlemek ve alakasız olanları gizlemektir, böylece bir geliştirici business logic koduna kolayca odaklanabilir. Bir aspect yazmayı düşünürken, kendinizi geliştiricinin yerine koyun. Kodu anlaması gereken biri neler olduğunu kolayca anlamalıdır.

Şekil 11: Bir metodun paarametrelerini ve dönüş değerini aspect ile değiştirebilirsiniz.

Bir aspect, yakalanan metodu çağırmak için kullanılan parametreleri ve yakalanan metodun çağıran tarafa döndürdüğü dönüş değerini değiştirebilir. Bu yaklaşım güçlüdür ve yakalanan metodun esnek bir şekilde kontrolünü sağlar.

Aşağıdaki kodda yakalanan metod tarafından döndürülen dönüş değerinin ve parametrelerin aspect tarafından değiştirilerek çağrıyı nasıl etkilediğini gösteriyoruz. Herhangi bir parametre içermeyen proceed() metodunu çağırdığınızda aspect orijinal parametreleri yakalanan metoda gönderir. Ancak proceed() metodunu çağırırken bir parametre sağlamayı seçebilirsiniz. Bu parametre, aspect'in orijinal parametre değerleri yerine yakalanan metoda gönderdiği değiştirilmiş parametrelerin dizisidir. Aspect, yakalanan metoddan döndürülen değeri loglar, ancak çağırana farklı bir değer döndürür.

@Aspect
public class LoggingAspect {
 
  private Logger logger = 
  Logger.getLogger(LoggingAspect.class.getName());
 
  @Around("execution(* services.*.*(..))")
  public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
    String methodName = joinPoint.getSignature().getName();
    Object [] arguments = joinPoint.getArgs();
 
 
    logger.info("Method " + methodName +
        " with parameters " + Arrays.asList(arguments) +
        " will execute");
 
    Comment comment = new Comment();
    comment.setText("Some other text!");
    Object [] newArguments = {comment};
 
    Object returnedByMethod = joinPoint.proceed(newArguments);    ❶
 
    logger.info("Method executed and returned " + returnedByMethod);
 
    return "FAILED";                                              ❷
  }
} 

❶ Method parametresine değer olarak farklı bir Comment instance'ı gönderiyoruz.

❷ Yakalanan metoddan döndürülen değeri logluyoruz, ancak çağırana farklı bir değer döndürüyoruz.

Uygulamayı çalıştırdığınızda, bir sonraki snippet'tekine benzer bir çıktı görürsünüz. publishComment() metodu tarafından alınan parametrelerin değerleri, metodu çağırırken gönderilenlerden farklıdır. publishComment() metodu bir değer döndürür, ancak main() farklı bir değer alır:

Sep 29, 2020 10:43:51 AM aspects.LoggingAspect log
INFO: Method publishComment with parameters [Comment{text='Kerteriz Blog', 
➥ author='Ismet'}] will execute                                ❶
Sep 29, 2020 10:43:51 AM services.CommentService publishComment
INFO: Publishing comment:Some other text!                         ❷
Sep 29, 2020 10:43:51 AM aspects.LoggingAspect log
INFO: Method executed and returned SUCCESS                        ❸
Sep 29, 2020 10:43:51 AM main.Main main
INFO: FAILED  

❶ publishComment() metodu, “Kerteriz Blog" metnine sahip bir Comment parametresiyle çağrılır.

❷ publishComment() metodu, “Some other text!” metnine sahip başka bir Comment parametresi alıyor

❸ publishComment() metodu “SUCCESS" değerini döndürür.

❹ Main() öğesinin aldığı dönüş değeri ise “FAILED" dır.

NOT: Tekrar ettiğimi biliyorum ama bu nokta oldukça önemli. Aspect'leri kullanırken dikkatli olun! Aspect'leri yalnızca alakasız kod satırlarını gizlemek için kullanmalısınız. Aksi halde aspect'ler alakalı ve önemli kodları gizleyerekkodun bakımını çok zor hale getirecek kadar güçlüdür. Aspect'leri dikkatli kullanın!

Tamam ama yakalanan metodun parametrelerini veya dönüş değerini değiştiren bir aspect'imizin olmasını ister miydik? Evet. Bazen böyle bir yaklaşımın yararlı olduğu görülür. Tüm bu yaklaşımları açıkladım çünkü sonraki bölümlerde aspect'lere dayanan bazı Spring yeteneklerini kullanacağız. Örneğin, ileride göreceğimiz transactions konusu buna örnek olacak.

İlk önce aspect'lerin nasıl çalıştığını anlayarak Spring'i anlamada önemli bir avantaj elde edersiniz. Geliştiricilerin, kullandıkları işlevlerin arkasında ne olduğunu anlamadan bir framework kullanmaya başladıklarını sık sık görüyorum. Şaşırtıcı olmayan bir şekilde, çoğu durumda, bu geliştiriciler uygulamalarına hatalar veya güvenlik açıkları getirir veya bunları daha az performanslı ve sürdürülebilir hale getirirler. Benim tavsiyem, kullanmadan önce her zaman işlerin nasıl yürüdüğünü öğrenmektir.

2.3 Aspect ile Anotasyonlu Metodları Yakalamak

Bu bölümde, Spring uygulamalarında sıklıkla kullanılan, aspect'ler tarafından ele geçirilmesi gereken metodları annotated etme yaklaşımını tartışıyoruz. Anotasyonların kullanımı rahattır ve Java 5 ile ortaya çıktıklarından beri, belirli framework'leri kullanan uygulamaları yapılandırmada öncelikli yaklaşım haline gelmişlerdir. Bugün muhtemelen anotasyon kullanmayan bir Java framework'ü yoktur. Bu anotasyonları, karmaşık AspectJ pointcut ifadeleri yazmak yerine bir aspect'in yakalamasını istediğiniz metodları işaretlemek için de kullanabilirsiniz.

Bu bölümde şimdiye kadar tartıştıklarımıza benzer şekilde bu yaklaşımı öğrenmek için ayrı bir örnek oluşturacağız. CommentService sınıfına üç metod ekleyeceğiz: publishComment(), deleteComment() ve editComment(). Özel bir anotasyon tanımlamak ve yalnızca bu özel anotasyonu kullanarak işaretlediğimiz metodların yürütülmesini loglamak istiyoruz. Bu amaca ulaşmak için aşağıdakileri yapmanız gerekir:

  1. Özel bir anotasyon tanımlayın ve çalışma zamanında erişilebilir hale getirin. Bu anotasyonu @ToLog olarak adlandıracağız.
  2. Aspect metoduna, aspect'e özel anotasyonlu metodları yakalaması söylemek için farklı bir AspectJ pointcut ifadesi kullanacağız.

Şekil 12 bu adımları görsel olarak göstermektedir.

Bu arada aspect logic'ini değiştirmemize gerek yok. Bu örnekte, aspect önceki örneklerle aynı şeyi yapacak ve yakalanan metodun yürütülmesini loglayacak.

Şekil 12: Özel oluşturduğumuz anotasyon ile aspect'i kullanma adımları

Aspect'in yakalamasını istediğimiz metodlara anotasyon eklemek için özel bir anotasyon oluşturmamız gerekir. Ardından, oluşturduğumuz anotasyonu kullanan metodları yakalayabilmesi için farklı bir AspectJ pointcut ifadesi kullanırız.

Bir sonraki kod parçacığında, özel anotasyon tanımlanmasını bulabilirsiniz. @Retention(RetentionPolicy.RUNTIME) kritiktir. Varsayılan olarak, Java'da anotasyonlar çalışma zamanında ele geçirilemez. Retention ilkesini çalışma zamanı olarak ayarlayarak birinin anotasyonları yakalayabileceğini açıkça belirtmeniz gerekir. @Target anotasyonu, bu anotasyonu hangi dil öğeleri için kullanabileceğimizi belirtir. Varsayılan olarak, herhangi bir dil öğesine açıklama ekleyebilirsiniz, ancak açıklamayı yalnızca sizin için yaptığınız şeyle sınırlamak her zaman iyi bir fikirdir. Bu, bizim durumumuzda metodlar olacak:

@Retention(RetentionPolicy.RUNTIME)         ❶
@Target(ElementType.METHOD)                 ❷
public @interface ToLog {
}

❶ Runtime, anotasyonun çalışma zamanında yakalanabilmesini sağlar

❷ Bu anotasyonu yalnızca metodlarla kullanılmak üzere kısıtlar

Aşağıdaki kodda, bahsettiğimiz üç metodu tanımlayan CommentService sınıfının tanımını bulabilirsiniz. Yalnızca deleteComment() metoduna anotasyon, bu nedenle aspect'in yalnızca bunu yakalamasını bekliyoruz.

@Service
public class CommentService {
 
  private Logger logger = Logger.getLogger(CommentService.class.getName());
 
  public void publishComment(Comment comment) {
    logger.info("Publishing comment:" + comment.getText());
  }
 
  @ToLog                                         ❶
  public void deleteComment(Comment comment) {
    logger.info("Deleting comment:" + comment.getText());
  }
 
  public void editComment(Comment comment) {
    logger.info("Editing comment:" + comment.getText());
  }
}

❶ Aspect'in yakalamasını istediğimiz metodlar için özel anotasyonu kullanıyoruz

Özel notasyon ile anotated edilen metodların aspect ile yakalanması için @annotation(ToLog) AspectJ pointcut syntax'ını kullanırız. Bu syntax, @ToLog anotasyonu eklenmiş herhangi bir metodu ifade eder. Bir sonraki kodda yeni pointcut syntax'ını kullanan aspect sınıfını ve özel anotasyonumuzu kullanan metodu göreceksiniz. Oldukça basit, değil mi?

Şekil 13: Yeni pointcut syntax'ı ve özel anotasyonumuz

AspectJ pointcır expression kullanarak, aspect logic'i tanımladığımız özel anotasyonlu herhangi bir yöntemi yakalayabiliriz. Bu, belirli bir aspect logic'in implemente edildiği metodları işaretlemenin rahat bir yoludur.

@Aspect
public class LoggingAspect {
 
  private Logger logger = Logger.getLogger(LoggingAspect.class.getName());
 
  @Around("@annotation(ToLog)")                                        ❶
  public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
    // Omitted code
  }
}

❶ @ToLog anotasyonlu metodların aspect'ini sarmalama

Uygulamayı çalıştırdığınızda, yalnızca anotasyonlu metod olan deleteComment() yakalanır ve aspect bu metoduloglar. Konsolda, bir sonraki snippet'te sunulana benzer bir çıktı görmeniz gerekir:

Sep 29, 2020 2:22:42 PM services.CommentService publishComment
INFO: Publishing comment:Kerteriz Blog
Sep 29, 2020 2:22:42 PM aspects.LoggingAspect log                          ❶
INFO: Method deleteComment with parameters [Comment{text='Kerteriz Blog',   ❶
➥ author='Ismet'}] will execute                                         ❶
Sep 29, 2020 2:22:42 PM services.CommentService deleteComment              ❶
INFO: Deleting comment:Demo comment                                        ❶
Sep 29, 2020 2:22:42 PM aspects.LoggingAspect log                          ❶
INFO: Method executed and returned null                                    ❶
Sep 29, 2020 2:22:42 PM services.CommentService editComment
INFO: Editing comment:Kerteriz Blog

❶ Aspect, yalnızca özel @ToLog anotasyonu eklenmiş olan deleteComment() metodunu yakalar.

2.4 Kullanabileceğiniz Diğer Advice Anotasyonları

Bu bölümde, Spring aspect için alternatif advice anotasyonlarını göreceğiz. Şu ana kadar sadece @Around advice anotasyonunu kullandık. Bu gerçekten de Spring uygulamalarındaki advice anotasyonlarından en çok kullanılanıdır, çünkü yakalanan metoddan önce, sonra veya yakalanan metod yerine olmak üzere herhangi bir implement durumunu kapsayabilirsiniz. Logic'i istediğiniz şekilde değiştirebilirsiniz.

Ancak her zaman bu esnekliğe ihtiyacınız yoktur. İyi bir fikir olarak, implement etmeniz gerekenler için en basit yolu aramanız daha iyidir. Herhangi bir uygulama basitlik ile tanımlanmalıdır. Karmaşıklığı önleyerek, uygulamanın bakımını kolaylaştırırsınız. Basit senaryolar için Spring, @Around dan daha az güçlü dört alternatif advice anotasyon sunar. Uygulamaları basit tutmak için yetenekleri yeterli olduğunda bunları kullanmanız önerilir.

Spring, @Around dışında aşağıdaki advice anotasyonlarınıa sahiptir:

  • @Before — Yakalanan metodun yürütülmesinden önce aspect logic'i tanımlayan methodu çağırır.
  • @AfterReturning — Metod başarıyla döndükten sonra aspect logic'i tanımlayan metodu çağırır ve döndürülen değeri aspect metoduna parametre olarak sağlar. Yakalanan metodda exception oluşursa, aspect metodu çağrılmaz.
  • @AfterThrowing — Yakalanan metod bir exception oluşturursa ve exception instance'ını aspect metoduna parametre olarak sağlarsa, aspect logic'i tanımlayan metodu çağırır.
  • @After — Metodun başarıyla değer döndürüp döndürülmediği veya bir exception atıp atmadığı durumlarda yakalanan metod yürütülmesinden sonra aspect logic'i tanımlayan metodu çağırır.

Bu advice anotasyonlarını tıpkı @Around da olduğu gibi kullanabilirsiniz. Belirli metod yürütmelerine aspect logic'i sarmak için bu anotasyonlara bir AspectJ pointcut expression sağlarsınız. Aspect metodları ProceedingJoinPoint parametresini almaz ve yakalanan metoda ne zaman geçeceklerine karar veremezler. Fakat bu olay zaten anotasyonun amacına göre gerçekleşir (örneğin, @Before için, yakalanan metod çağrısı her zaman aspect logic'in yürütülmesinden sonra gerçekleşir).

Bir sonraki kod snippet'inde, kullanılan @AfterReturning anotasyonunu bulabilirsiniz. @Around'da kullandığımız gibi kullandığımızı gözlemleyin.

@Aspect
public class LoggingAspect {
 
  private Logger logger = Logger.getLogger(LoggingAspect.class.getName());
 
  @AfterReturning(value = "@annotation(ToLog)",                    ❶
                  returning = "returnedValue")                     ❷
  public void log(Object returnedValue) {                          ❸
    logger.info("Method executed and returned " + returnedValue);
  }
}

❶ AspectJ pointcut expression, bu aspect logic'in hangi metodlara uygulanacağını belirtir.

❷ İsteğe bağlı olarak, @AfterReturning kullandığınızda, yakalanan metod tarafından döndürülen değeri alabilirsiniz. Bu durumda, bu değerin sağlanacağı metodun parametresinin adına karşılık gelen bir değerle "returning" özniteliğini ekleriz.

❸ Parametre adı, anotasyonun "returning" özniteliğinin değeriyle aynı olmalıdır veya döndürülen değeri kullanmamız gerekmezse parametre hiç olmamalıdır.

3. Aspect Execution Zinciri

Şimdiye kadarki tüm örneklerimizde, bir aspectin bir metodu yakaladığında ne olacağını tartıştık. Gerçek dünyadaki uygulamalarda, bir metod genellikle birden fazla aspect'le yakalanır. Örneğin, yürütmeyi loglamak ve bazı güvenlik kısıtlamaları uygulamak istediğimiz bir metodumuz olsun. Genellikle bu tür sorumlulukları üstlenen aspect'lerimiz vardır, bu senaryoda aynı metodun yürütülmesine göre hareket eden iki aspect bulunur. İhtiyacımız kadar çok aspect'in olmasında tabiki yanlış bir şey yok, ancak bu olduğunda kendimize aşağıdaki soruları sormamız gerekiyor:

  • Spring bu aspect'leri hangi sırayla yürütür?
  • Execution emrinin bir önemi var mıdır?

Bu bölümde, bu iki soruyu yanıtlamak için bir örneği analiz edeceğiz.

Bir metod için bazı güvenlik kısıtlamaları uygulamamız ve yürütmelerini loga kaydetmemiz gerektiğini varsayalım. Bu sorumlulukları dikkate alan iki aspect var:

  • SecurityAspect — Güvenlik kısıtlamalarını uygular. Bu aspect metodu yakalar, çağrıyı validate eder ve bazı koşullarda çağrıyı yakalanan metoda iletmez (SecurityAspect'in nasıl çalıştığına ilişkin ayrıntılar mevcut tartışmamızla ilgili değildir; bazen bu aspect'in yakalanan metodu çağırmadığını unutmayın).
  • LoggingAspect — Yakalanan metod yürütmenin başlangıcını ve sonunu loga kaydeder.

Aynı metodu sarmalayan birden çok aspect olduğunda, birbiri ardına yürütülmeleri gerekir. Bir yol, önce SecurityAspect'in yürütülmesini sağlamak ve sonra akışı LoggingAspect'e yönlendirdikten sonra yakalanan metodu çağırmaktır. İkinci seçenek, LoggingAspect'in önce yürütülmesini sağlamak ve daha sonra akışı SecurityAspect'e yönlendirdikten sonra yakalanan metodu çağırmaktır. Bu şekilde, aspect'ler bir execution (yürütme) zinciri oluşturur.

Sıralı zincirlerdeki aspect'lerin farklı sırayla yürütülmesi farklı sonuçlara sahip olabileceğinden, aspect'lerin yürütülme sırası önemlidir. Örneğimizi ele alalım: SecurityAspect'in tüm durumlarda yürütmeyi devretmediğini biliyoruz, bu nedenle önce yürütmek için bu aspect'i seçersek, bazen LoggingAspect yürütülmez. LoggingAspect'in güvenlik kısıtlamaları nedeniyle başarısız olan yürütmeleri günlüğe kaydetmesini bekliyorsak, kullanmamız gereken yol bu değildir (Şekil 14).

Şekil 14: Farklı yürütme zincirileri, farklı sonuçlara yol açar

Yürütme zincirinin sırası önemlidir. Uygulamanızın gereksinimlerine bağlı olarak, aspect'lerin yürütülmesi için belirli bir sıra seçmeniz gerekir. Bu senaryoda, LoggingAspect, SecurityAspect'ten önce yürütülürse tüm metod yürütmelerini loga kaydedemez.

Tamam, aspect'lerin yürütme sırası bazen birbirlerine bağlıdır. Ama o zaman bu sırayı tanımlayabilir miyiz? Varsayılan olarak, Spring aynı yürütme zincirindeki iki aspect'in çağrılma sırasını garanti etmez. Yürütme sırası birbirlerine bağlı değilse, yalnızca aspect'leri tanımlamanız ve bunları hangi sırada yürütürseniz yürütün framework'ün kendisine bırakmanız önemli değildir. Eğer aspect'lerin yürütme sırasını tanımlamanız gerekiyorsa, @Order anotasyonu kullanabilirsiniz. Bu anotasyon, belirli bir aspect için yürütme zincirindeki sırayı temsil eden bir sıra (sayı) alır. Sayı ne kadar küçükse, bu aspect o kadar erken yürütülür. İki değer aynıysa, yürütme sırası yeniden tanımlanmaz. Bir örnekte @Order anotasyonunu deneyelim.

Şimdiki örneğimizde CommentService bean'inin publishComment() metodunu yakalayan iki aspect tanımlıyorum. Bir sonraki kodda LoggingAspect adlı aspect'i bulabilirsiniz. Başlangıçta aspect'lerimiz için herhangi bir sıra tanımlamıyoruz.

@Aspect
public class LoggingAspect {
 
  private Logger logger = 
    Logger.getLogger(LoggingAspect.class.getName());
 
  @Around(value = "@annotation(ToLog)")
  public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
    logger.info("Logging Aspect: Calling the intercepted method");
 
    Object returnedValue = joinPoint.proceed();     ❶
 
    logger.info("Logging Aspect: Method executed and returned " + 
                returnedValue);
 
    return returnedValue;
  }
}

❶ Burada proceed() metodu, yürütme zincirinde sıradaki durağı belirler. Bir sonraki durağı aspect veya yakalanan metod olarak belirleyebilirsiniz.

Örneğimiz için tanımladığımız ikinci aspect, aşağıdaki kodda gösterildiği gibi SecurityAspect olarak adlandırılır. Örneğimizi basit tutmak ve tartışmaya odaklanmanıza izin vermek için bu aspect özel bir şey yapmaz. LoggingAspect gibi, konsola bir mesaj yazdırır, böylece ne zaman yürütüldüğünü kolayca gözlemleriz.

@Aspect
public class SecurityAspect {
 
  private Logger logger =   
    Logger.getLogger(SecurityAspect.class.getName());
 
  @Around(value = "@annotation(ToLog)")
  public Object secure(ProceedingJoinPoint joinPoint) throws Throwable {
    logger.info("Security Aspect: Calling the intercepted method");
 
    Object returnedValue = joinPoint.proceed();           ❶
 
    logger.info("Security Aspect: Method executed and returned " + 
                 returnedValue);
 
    return returnedValue;
  }
}

❶ Burada proceed() metodu, yürütme zincirindeki sıradaki durağı belirler. Bir sonraki durağı aspect veya yakalanan metod olarak belirleyebilirsiniz.

CommentService sınıfı, önceki örneklerde tanımladığımız sınıfa benziyor. Ancak okumanızı daha rahat hale getirmek için aşağıdaki kodda da bulabilirsiniz.

@Service
public class CommentService {
 
  private Logger logger = 
    Logger.getLogger(CommentService.class.getName());
 
  @ToLog
  public String publishComment(Comment comment) {
    logger.info("Publishing comment:" + comment.getText());
    return "SUCCESS";
  }
 
}

Ayrıca, her iki aspect'in de Spring Context'inde bean olması gerektiğini unutmayın. Bu örnekte, bean'leri context'e eklemek için @Bean yaklaşımını kullanmayı seçtim. Yapılandırma sınıfım şu şekilde olacaktır.

@Configuration
@ComponentScan(basePackages = "services")
@EnableAspectJAutoProxy
public class ProjectConfig {
 
  @Bean                                   ❶
  public LoggingAspect loggingAspect() {
    return new LoggingAspect();
  }
 
  @Bean                                   ❶
  public SecurityAspect securityAspect() {
    return new SecurityAspect();
  }
}

❶ Spring Context'ine her iki aspect'inde de bean olarak eklenmesi gerekiyor.

main() metodu, CommentService bean'inin publishComment() metodunu çağırır. Benim durumumda, yürütmeden sonraki çıktı bir sonraki kod snippet'indekine benziyor:

Sep 29, 2020 6:04:22 PM aspects.LoggingAspect log                ❶
INFO: Logging Aspect: Calling the intercepted method             ❶
Sep 29, 2020 6:04:22 PM aspects.SecurityAspect secure            ❷
INFO: Security Aspect: Calling the intercepted method            ❷
Sep 29, 2020 6:04:22 PM services.CommentService publishComment   ❸
INFO: Publishing comment:Kerteriz Blog                           ❸
Sep 29, 2020 6:04:22 PM aspects.SecurityAspect secure            ❹
INFO: Security Aspect: Method executed and returned SUCCESS      ❹
Sep 29, 2020 6:04:22 PM aspects.LoggingAspect log                ❺
INFO: Logging Aspect: Method executed and returned SUCCESS       ❺

❶ LoggingAspect önce çağrılır ve yürütmeyi SecurityAspect'e devreder.

❷ SecurityAspect çağrılır ve yürütmeyi yakalanan metoda devreder.

❸ Yakalanan metod yürütülür.

❹ Yakalanan metod SecurityAspect'e geri döner.

❺ SecurityAspect, LoggingAspect'e geri döner.

Şekil 15, yürütme zincirini görselleştirmenize ve konsoldaki logları anlamanıza yardımcı olur.

Şekil 15: Örneğimizdeki yürütme zinciri

LoggingAspect metod çağrısını ilk yakalayan oldu. LoggingAspect, yürütme zincirinde sıradaki durak olarak SecurityAspect olarak belirler. Yakalanan metod ise, LoggingAspect'e dönüş yapan SecurityAspect'e döner.

LoggingAspect ve SecurityAspect'in yürütülme sırasını tersine çevirmek için @Order anotasyonunu kullanırız. Bir sonraki kod snippet'inde SecurityAspect için bir yürütme sırası belirtmek üzere @Order anotasyonunu nasıl kullandığımı gözlemleyebilirsiniz.

@Aspect
@Order(1)                        ❶
public class SecurityAspect {
  // Omitted code
}

❶ Aspect'e bir yürütme sırası verir

LoggingAspect için, bir sonraki snippet'te sunulduğu gibi, aspect'i daha yüksek bir sıra konumuna yerleştirmek için @Order kullanıyorum:

@Aspect
@Order(2)                     ❶
public class LoggingAspect {
  // Omitted code
}

❶ LoggingAspect'i yürütülecek ikinci aspect olarak yerleştirir

Uygulamayı yeniden çalıştırın ve aspect'lerin yürütüldüğü sıranın değiştiğini gözlemleyin. Loglar artık bir sonraki snippet'tekine benzemelidir:

Sep 29, 2020 6:38:20 PM aspects.SecurityAspect secure           ❶
INFO: Security Aspect: Calling the intercepted method           ❶
Sep 29, 2020 6:38:20 PM aspects.LoggingAspect log               ❷
INFO: Logging Aspect: Calling the intercepted method            ❷
Sep 29, 2020 6:38:20 PM services.CommentService publishComment  ❸
INFO: Publishing comment:Demo comment                           ❸
Sep 29, 2020 6:38:20 PM aspects.LoggingAspect log               ❹
INFO: Logging Aspect: Method executed and returned SUCCESS      ❹
Sep 29, 2020 6:38:20 PM aspects.SecurityAspect secure           ❺
INFO: Security Aspect: Method executed and returned SUCCESS     ❺

❶ SecurityAspect, metod çağrısını ilk olarak yakalar ve yürütme zincirinde sırayı LoggingAspect'e devreder.

❷ LoggingAspect yürütülür ve yakalanan metoda yürütmeyi devreder.

❸ Yakalanan metod yürütülür ve LoggingAspect'e geri döner.

❹ LoggingAspect yürütülür ve SecurityAspect'e geri döner.

❺ SecurityAspect, ilk çağrıyı yapan main() metoda döner.

Şekil 16, yürütme zincirini görselleştirmenize ve konsoldaki logları anlamanıza yardımcı olur.

Şekil 16: Order verdikten sonra yürütme zincirimizin durumu

4. ÖZET

  • Bir aspect, bir metod çağrısını kesen ve ele geçirilen metodu yürütmeden önce, sonra ve hatta yakalanan metod yerine yürütülebilen bir nesnedir. Bu, kodun bir kısmını Business logic'ten ayırmanıza yardımcı olur ve uygulamanızın bakımını kolaylaştırır.
  • Bir aspect'i kullanarak, yaklanan metod yürütülürken bu metoddan tamamen farklı yürütülen bir logic yazabilirsiniz. Bu şekilde, kodu okuyan biri yalnızca business logic kısmını görür.
  • Ancak, aspect'ler tehlikeli bir araç olabilir. Kodunuzu aspect'lere aşırı dayandırmak, uygulamanızı daha az sürdürülebilir hale getirecektir. Her yerde aspect'leri kullanmanıza gerek yok. Bunları kullanırken, uygulamanıza gerçekten yardımcı olduklarından emin olun.
  • Aspect'ler, transactions ve security gibi birçok temel Spring yeteneğini destekler.
  • Spring'te bir aspect'i tanımlamak için, @Aspect anotasyonunu ile aspect logic'i uygulayan sınıfa ekleriz. Ancak, Spring'in bu sınıfın bir instance'ını yönetmesi gerektiğini unutmayın, bu nedenle Spring context'inde kendi tipinde bir bean de eklemeniz gerekir.
  • Spring'e bir aspect'in hangi metodları yakalaması gerektiğini söylemek için AspectJ pointcut expression ifadelerini kullanırsınız. Bu ifadeleri advice anotasyonları için değer olarak yazarsınız. Spring size beş advice anotasyonu daha sunar: @Around, @Before, @After, @AfterThrowing ve @AfterReturning. Çoğu durumda, aynı zamanda en güçlü olan @Around kullanırız.
  • Birden çok aspect aynı metod çağrısını yakalayabilir. Bu durumda, @Order anotasyonu kullanılarak yürütülecek aspect'ler için bir sıra tanımlamanız önerilir.