Java - Aspect-Oriented Programming
09 Jun 2017Basitç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ı.