Java - Aspect-Oriented Programming

Basitçe AOP birbiriyle kesişen problemleri ayırarak modülerlik sağlamayı amaçlayan bir programlama paradigmasıdır.

Java için bir AOP çözümü olarak da AspectJ framework’ünü kullanacağım. AspectJ ile Spring AOP arasında kalmıştım fakat karşılaşmalara baktığımda insanların deneyimleri ve AspectJ’nin yazım biçiminin hoşuma gitmesi, yaygın olarak kullanılması ve haliyle bol kaynağının olması AspectJ yönünde kesin karar kılmamda etkili oldu.

Bu arada benim AOP paradigmasına olan ihtiyacım SOLID prensiplerini uygulama çabamdan ortaya çıktı. Spesifik olarak bu prensiplerden ilk ikisi olan Single Responsibility ve Open/Closed prensiplerinden ortaya çıktı.

Single Responsibility ve Open/Closed Prensipleri

Solid prensiplerinden ikisi olan bu prensiplerin ilkinde amaç tek sorumluluktur. Bu prensibe göre iş gören her parçacık sadece kendi görevini üstlenmelidir.

Open/Closed prensibinde ise amaç yazdığımız kodların gelişime açık ve değişime kapalı olmasıdır.

AOP bu iki paradigmayı uygulamayı oldukça kolaylaştırıyor.

AspectJ

Problemi çözmeden önce AOP paradigması ve AspectJ framework’ü ile ilgili temel olarak bilmemiz gerekenleri öğrenelim.

AOP paradigması başta tanımladığımız amacını yerine getirmek için Aspect‘leri kullanır. Bir Aspect bizim kesişen problemlerimizi ayırıp, kesişme noktalarını belirlediğimiz ve bu kesişime bağlı olarak nasıl çalışması gerektiğini belirlediğimiz yapıdır.

Bir Aspect’in çalışma prensibini belirlerken aşağıdaki yapıları kullanırız.

   
PointCut Methodları kesişim noktası olarak belirlememizi sağlar.
Before Methodumuzun kesişim noktasından önce çalışmasını sağlar.
After Methodumuzun kesişim noktasından sonra çalışmasını sağlar.
AfterReturning Methodumuzun kesişim noktası değer döndükten sonra çalışmasını sağlar.
AfterThrowing Methodumuzun kesişim noktasında bir exception oluşmasından sonra çalışmasını sağlar.
JoinPoint Kesişim noktası olarak belirlediğimiz methodun parametrelerine ulaşmamızı sağlayan çok biçimli veri yapısı.

Örnek Kullanım

Bir Maven projesi oluşturalım ve pom.xml dosyasını aşağıdaki gibi ayarlayalım.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>aop</groupId>
    <artifactId>aop-example</artifactId>
    <version>1.0-SNAPSHOT</version>

    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>aspectj-maven-plugin</artifactId>
                <version>1.10</version>
                <configuration>
                    <complianceLevel>1.8</complianceLevel>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>test-compile</goal>
                        </goals>
                        <configuration>
                            <source>1.8</source>
                            <target>1.8</target>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.6.0</version>
                <configuration>
                    <mainClass>aop.App</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjrt -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.8.10</version>
        </dependency>

    </dependencies>
</project>

AOP, AspectJ başlığında anlattığımız terimlerin çalışma esnasındaki davranışlarını gözlemlemek için aşağıdaki AspectExample sınıfını oluşturalım.

package aop;

import org.aspectj.lang.annotation.*;

@Aspect
public class AspectExample {

    public static void main(String[] args) {
        test();
        test2();
    }

    public static boolean test() {
        System.out.println(1);
        return false;
    }

    @AfterReturning("pointCutOrnek()")
    public void testReturnSonrasi() {
        System.out.println(2);
    }

    @Pointcut("execution(* aop.AspectExample.test())")
    public void pointCutOrnek() {
    }

    public static void test2() {
        System.out.println(4);
        throw new NullPointerException();
    }

    @Before("execution(* aop.AspectExample.test2())")
    public void testMethodundanOnce() {
        System.out.println(3);
    }

    @AfterThrowing("execution(* aop.AspectExample.test2())")
    public void afterThrowTest()  {
        System.out.println(5);
    }

    @After("execution(* aop.AspectExample.test2())")
    public void testMethodundanSonra(){
        System.out.println(6);
    }
}

Konsol Çıktısı;

1
Exception in thread "main" java.lang.NullPointerException
2
3
	at aop.AspectExample.test2(AspectExample.java:29)
4
	at aop.AspectExample.main(AspectExample.java:10)
5
6

Görüldüğü gibi bir Aspect tanımladık ve yukarıda bahsettiğimiz yapıları kullanarak çalışma mantığını ortaya koymuş olduk. Bu örnekte kullandığımız Before, After, AfterThrowing ve bir de AfterReturning AspectJ literatüründe Advice olarak adlandırılmıştır. Pointcut ve Advice annotationlarımızın içerisinde tanımladıklarımız da PointCut Expression olarak adlandırılmıştır. AfterReturning örneğini incelediğimizde ona expression değeri olarak bir pointCutOrnek() methodunu verdik çünkü bu methodu @PointCut annotation’ı ile bir PointCut Expression olarak tanımladık.

Özet olarak @PointCut ile PointCut Expression tanımlayabiliyoruz. Advice annotationları ile de expression kullanarak istediğimiz methodu istediğimiz methoddan ya da methodlardan önce, sonra gibi çalışmasını sağlayabiliyoruz.

PointCut Expression Syntax

@Before("execution(ReturnTipi paketismi.sınıf.method(ParametreTipi))")

ReturnTipi yerine * koyarsak return değeri farketmeksizin belirlediğimiz method için çalışmasını sağlar.

ParametreTipi yerine .. yazarsak herhangi tipde ve sayıda parametreli ya da parametresiz methodlar için çalışmasını sağlar.

@Before("execution(String paket.Sinif.test(..))")
public void greet() {
    System.out.println("Greetings Traveler");
}

Yukarıdaki örnekte greet() methodu, String değeri dönen, paket package’ı altında, Sinif class’ında bulunan test isimli method’da ya da method’larda -overload edilmiş olabilir- parametre farketmeksizin test() methodundan önce çalışacaktır.

@Before("execution(* paket.*.test())")
public void greet() {
    System.out.println("Greetings Traveler");
}

Bu örnekte görüldüğü gibi * işaretini paket, sınıf ve method isimlerinin yazıldığı kısımlara da yazabiliyoruz. Örneğin burada sınıf yerine yazdım. Bu da diğer şartları sağladığı takdirde sınıf farketmeksizin greet() methodunun test() methodundan önce çalışmasını sağlıyor.

Compile Time Weaving

Bu noktada farketmişsinizdir ki AspectExample sınıfının bir örneğini oluşturmuyoruz. Bunun sebebi AspectJ framework’ünün Aspect olarak tanımlanan sınıfları araya bir compiler sokarak kendisinin oluşturmasıdır. pom.xml dosyasındaki aspectj-maven-plugin bu işe yarıyor. Buna da AspectJ literatüründe Compile Time Weaving deniyor.

Gerçek Bir Problem Üzerine

Yazdığım bir projede veriyi manipüle ettiğim yerlerde loglama yapmak istiyordum ancak önceki başlıklarda belirttiğim gibi SOLID prensiplerini uygulama konusunda sıkıntı çıkartıyordu. Sorunu ve çözümünü gözlemlemek için daha önce açtığımız projede aşağıdaki sınıfı oluştralım.

package aop.model;

public class Person {

    private Integer id;
    private String name;
    private String surname;
    
    public Person(Integer id, String name, String surname) {
        this.id = id;
        this.name = name;
        this.surname = surname;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    @Override
    public String toString() {
        return this.id.toString() + " " + this.name + " " + this.surname;
    }
}

Veriye erişim sınıfı için bir arayüz oluşturalım.

package aop.repository;

import java.util.List;

import aop.model.Person;

public interface PersonDao {

    Person getPersonById(Integer id);

    List<Person> getPersonList();

    void insert(Person person);

    void update(Person person);

    void remove(Integer id);
}

Şimdi bu arayüzü aşağıdaki şekilde implement edelim.

package aop.repository;

import java.util.List;

import aop.model.Person;

public class PersonDaoImpl implements PersonDao {

    public Person getPersonById(Integer id) {
        return null;
    }

    public List<Person> getPersonList() {
        return null;
    }

    public void insert(Person person) {
        System.out.println("Insert methodu çalıştı ve tamamlandı.");
    }

    public void update(Person person) {
        System.out.println("Update methodu çalıştı ve tamamlandı.");
    }

    public void remove(Integer id) {
        System.out.println("Remove methodu çalıştı ve tamamlandı.");
    }
}

Bu sınıfta ben insert, update, remove methodları çalıştığında bunları loglamak istiyorum.

    public void insert(Person person) {
        System.out.println("Insert methodu çalıştı ve tamamlandı.");
        Logger.log(person.toString() + " eklendi.")
    }

    public void update(Person person) {
        System.out.println("Update methodu çalıştı ve tamamlandı.");
        Logger.log(person.getId() + " id numaralı veri güncellendi.")
        Logger.log("Güncel veri: " + person.toString());
    }

    public void remove(Integer id) {
        System.out.println("Remove methodu çalıştı ve tamamlandı.");
        Logger.log(id + " id numaralı veri silindi.");
    }

Yukarıdaki gibi yaparsam, mesela ìnsert() methodu için konuşursak amacı veri eklemek olan yerde bir de loglama yapıyorum ve Single Responsibility prensibini çöpe atmış oluyorum.

Peki ileride ben Logger sınıfından vazgeçip loglama için başka bir kütüphaneye vs. geçersem ne olacak? Bu sefer de geliştirme yapmış olacağım ancak aynı zamanda buraları da değiştirmem gerekecek ve Open/Closed prensibini de çöpe atmış oldum.

Şimdi bu methodları önceki haline geri getirelim ve AspectJ ile temizinden aşağıdaki gibi Logger sınıfı oluşturalım.

package aop.logging;

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

import aop.model.Person;

@Aspect
public class Logger {

    @After("execution(* aop.repository.PersonDaoImpl.insert(..))")
    public void logInsert(JoinPoint joinPoint) {
        Person person = (Person) joinPoint.getArgs()[0];
        System.out.println(person.toString() + " eklendi.");
    }

    @After("execution(* aop.repository.PersonDaoImpl.update(..))")
    public void logUpdate(JoinPoint joinPoint) {
        Person person = (Person) joinPoint.getArgs()[0];
        System.out.println(person.getId() + " id numaralı veri güncellendi.");
        System.out.println("Güncel veri: " + person.toString());
    }

    @After("execution(* aop.repository.PersonDaoImpl.remove(..))")
    public void logRemove(JoinPoint joinPoint) {
        Integer id = (Integer) joinPoint.getArgs()[0];
        System.out.println(id + " id numaralı veri silindi.");
    }
}

JoinPoint değişkeni ile de görüldüğü gibi PointCut Expression’da belirttiğim methodun değişkenlerini de kullanabiliyorum.

Şimdi de bunu aşağıdaki sınıfı yazarak çalıştıralım ve konsol çıktısına bakalım.

package aop;

import aop.model.Person;
import aop.repository.PersonDao;
import aop.repository.PersonDaoImpl;

public class App {


    public static void main(String[] args) {
        Person person = new Person(1, "Cem", "Asma");

        PersonDao personDao = new PersonDaoImpl();
        personDao.insert(person);

        person.setName("Ali");
        personDao.update(person);

        personDao.remove(1);
    }

}

Konsol Çıktısı;

Insert methodu çalıştı ve tamamlandı.
1 Cem Asma eklendi.
Update methodu çalıştı ve tamamlandı.
1 id numaralı veri güncellendi.
Güncel veri: 1 Ali Asma
Remove methodu çalıştı ve tamamlandı.
1 id numaralı veri silindi.

Mesela ileride ek olarak bir loglama kütüphanesi daha getirebilirim ve tek yapmam gereken kütüphaneyi kurup ardından da Aspect’imi ayarlamak olacak ya da Logger’ı kaldırıp yerine bir şey getireceksem de aynı şey geçerli ki Logger’ı silmem gerekmiyor Aspect annotation’ını kaldırsam yeterli. Sonuç itibariyle Single Responsibility ve Open/Closed prensipleri için endişelenmemize gerek kalmadı.