![AOP: Pemrograman Berorientasi Aspect](https://images.ctfassets.net/7wjlcr9d5ii8/6Wpso3jPUY77S5a2gJmEnM/c59ae4b7339c02c95cd527c7ba50d07d/_8dfc7e8f-6691-4ddf-9309-a90bdbd25adf.jpg?w=1024&h=1024&fl=progressive&q=100&fm=jpg&fit=thumb&f=faces)
Aspect Oriented Programming (AOP) merupakan paradigma pemrograman dimana kita bisa memisahkan logic tertentu secara terpusat dan menyisipkannya ke dalam objek tanpa harus mengubah objek tersebut secara langsung. Misalnya pada sebuah method yang melakukan sebuah action, kita ingin menyisipkan behavior tambahan pada method tersebut tanpa harus menulisnya pada method tersebut secara langsung. Contoh kasus yang sering menggunakan AOP meliputi setup config, caching, logging, error handling, validation, dan lainnya. Contohnya adalah anotasi @Transactional
pada Spring Data yang secara "magic" menyisipkan begin transaction, commit atau rollback tanpa perlu kita tulis secara manual. Atau anotasi @BeforeEach
dan @AfterEach
pada JUnit yang membuat kita bisa menyisipkan setup tertentu pada setiap test method saat dieksekusi tanpa harus menulisnya satu-persatu pada tiap method. Juga anotasi @ControllerAdvice
pada Spring web untuk menyisipkan logic catch exception secara terpusat. Dan masih ada beberapa contoh lainnya. Ada beberapa cara untuk mengimplementasinya, tapi yang paling umum adalah menggunakan AspectJ dan Spring AOP.
Terdapat 3 terminology pada AOP. Pertama, Pointcut yaitu ekspresi atau kondisi yang menjadi penanda kapan method tersebut akan disisipkan, seperti annotasi tertentu atau pola tertentu. Kedua, Advice yaitu logic yang akan kita sisipkan ketika method tersebut memenuhi syarat pada Pointcut. Terakhir, Aspect yaitu konfigurasi Advice dan Pointcut tersebut.
Ini adalah framework native AOP pada Java. Di sini behavior tambahan tersebut disisipkan secara native ke dalam object saat compile. Jadi secara teknis behavior yang kita buat itu benar-benar disisipkan ke dalam method yang digunakan. Ketika menggunakan AspectJ kita tidak bisa menggunakan compiler standar dari Java seperti “javac”, melainkan menggunakan compiler bawaan dari AspectJ, yaitu “ajc”. Kelemahan menggunakan AspectJ adalah sering bermasalah dengan library annotation processor lainnya seperti Lombok, Mapstruct, dan sejenisnya karena tidak compatible dengan compilernya. Berikut ini contoh setup AOP menggunakan AspectJ.
AspectJ pom.xml
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.9</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.14.0</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>
Cara kedua adalah menggunakan Spring AOP. Sebenarnya Spring AOP ini juga membawa beberapa konfigurasi dari AspectJ, hanya saja implementasinya sangat berbeda. Setup project-nya jauh lebih gampang. Di sini behavior tambahan itu disisipkan lewat Proxy, bukan disisipkan secara native. Untuk lebih jelasnya gimana cara kerja Proxy bisa baca Proxy Design Pattern pada tulisan sebelumnya. Bedanya di sini Spring menggunakan library untuk membuat objek Proxy agar di-generate ketika runtime, bukan dibuat manual. Keunggulannya kita tetap bisa menggunakan “javac” compiler tanpa harus setup “ajc” compiler seperti AspectJ sehingga tetap compatible dengan library annotation processor lain. Kekurangannya adalah ini hanya bisa diimplementasi pada Spring Bean saja, seperti class yang memiliki annotasi @Component
, @Service
, @Controller
, @RestController
, @Repository
, @Configuration
, atau objek yang dibuat lewat @Bean
method. Selain itu secara teori menggunakan Proxy untuk menyisipkan behavior tersebut saat runtime tentu lebih lambat dibandingkan disisipkan secara native saat compile. Berikut ini adalah contoh setup AOP menggunakan Spring AOP.
Spring AOP pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
Misalkan kita memiliki class Calculator seperti berikut dan ingin menyisipkan beberapa behavior tambahan pada masing-masing method tanpa harus copy-paste behavior tersebut secara langsung ke dalam method.
Calculator
public class Calculator{
public int add(int x, int y){
int result = x + y;
System.out.println("x + y = " + result);
return result;
}
public int subtract(int x, int y){
int result = x - y;
System.out.println("x - y = " + result);
return result;
}
public int multiply(int x, int y){
int result = x * y;
System.out.println("x * y = " + result);
return result;
}
public int divide(int x, int y){
int result = x / y;
System.out.println("x / y = " + result);
return result;
}
}
Untuk yang menggunakan Spring AOP, perlu tambahkan annotasi @Component
pada class Calculator biar jadi Spring Bean. Kita juga perlu siapkan anotasi "Interception" seperti berikut sebagai Pointcut Annotation.
Interception
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Interception{
String printValue();
}
Sekarang kita akan coba bikin AOP menggunakan Pointcut annotation "Interception". Secara umum AOP memiliki 5 jenis Advice, yaitu Before, After, After Throwing, After Returning, dan Around. Masing-masing Advice tersebut di-konfigurasi pada class Aspect yang bertugas melakukan konfigurasi penyisipan behavior. Contohnya seperti berikut:
InterceptionConfig
@Aspect
public class InterceptionConfig{
}
Di sini kita perlu tambahkan anotasi @Aspect
untuk menandakan bahwa disinilah aspect tersebut dikonfigurasi. Oh ya, kalau menggunakan Spring AOP, begini saja tidak cukup. Kita juga perlu menjadikan objek tersebut Spring Bean seperti penjelasan sebelumnya. Kita bisa menambahkan anotasi @Configuration
pada class InterceptionConfig. Sekarang kita tinggal bikin Pointcut dan Advice yang ingin kita sisipkan ke dalam method Calculator.
Ini adalah Advice yang diimplementasi pada @BeforeEach
di JUnit. Sesuai namanya, ini artinya kita akan menyisipkan behavior tertentu sebelum method tersebut dieksekusi. Sekarang kita akan bikin method add
dan subtract
pada Calculator untuk melakukan print value “penambahan” dan “pengurangan” sebelum penambahan dan pengurangan itu dilakukan.
Calculator
@Interception(printValue = "penambahan")
public int add(int x, int y){
int result = x + y;
System.out.println("x + y = " + result);
return result;
}
@Interception(printValue = "pengurangan")
public int subtract(int x, int y){
int result = x - y;
System.out.println("x - y = " + result);
return result;
}
public int multiply(int x, int y){
int result = x * y;
System.out.println("x * y = " + result);
return result;
}
InterceptionConfig
@Before("@annotation(interception) && execution(* *(..))")
public void before(Interception interception){
System.out.println(interception.printValue());
}
Di sini kita tambahkan annotasi @Before
dengan Pointcut @annotation(interception)
sesuai nama annotasi pada paramater. Itu artinya setiap method yang memiliki annotasi @Interception
akan disisipkan println
sebelum dieksekusi. Juga sebaiknya tambahkan Pointcut execution(* *(..))
secara spesifik agar Pointcut yang dieksekusi adalah “execution”, karena selain “execution” juga ada Pointcut “call” untuk menghindari eksekusi dua kali. Tapi untuk Spring AOP ini tidak perlu karena Spring AOP hanya support Pointcut “execution”.
Contoh code
Calculator calculator = new Calculator();
calculator.add(2, 1);
calculator.subtract(2, 1);
calculator.multiply(2, 1);
Sekarang setiap kita eksekusi add
dan subtract
method maka akan muncul print “penambahan” dan “pengurangan” setiap sebelum eksekusi.
Ini Advice yang diimplementasi pada @AfterEach
di JUnit. Ini kebalikan dari @Before
, yaitu menyisipkan behavior tertentu setelah method tersebut di-eksekusi. Baik ketika method tersebut kena exception maupun tidak. Contohnya seperti berikut, kita ingin print “executed” setelah eksekusi.
InterceptionConfig
@After("@annotation(interception) && execution(* *(..))")
public void after(Interception interception){
System.out.println("executed");
}
Sekarang setelah kita tambahkan code di atas, akan muncul “executed” setiap selesai eksekusi.
Ini Advice yang diimplementasi pada @ControllerAdvice
di Spring Web. Bedanya dengan @After
adalah ini hanya akan dieksekusi ketika terjadi error. Misalnya saat melakukan pembagian dengan 0, maka kita ingin print “ada error”.
Calculator
@Interception(printValue = "pembagian")
public int divide(int x, int y){
int result = x / y;
System.out.println("x / y = " + result);
return result;
}
InterceptionConfig
@AfterThrowing("@annotation(interception) && execution(* *(..))")
public void afterThrowing(Interception interception){
System.out.println("ada error");
}
Contoh code
Calculator calculator = new Calculator();
calculator.divide(2, 0);
Sekarang setiap terjadi exception, maka akan muncul “ada error” pada console.
Ini adalah kebalikan dari @AfterThrowing
, yaitu hanya akan dieksekusi ketika method tersebut tidak ada error. Misalnya kita ingin print “tak ada error” setiap eksekusi tanpa ada exception.
InterceptionConfig
@AfterReturning("@annotation(interception) && execution(* *(..))")
public void afterReturning(Interception interception){
System.out.println("tak ada error");
System.out.println();
}
Contoh code
Calculator calculator = new Calculator();
calculator.divide(2, 2);
Sekarang jika tidak terjadi exception, maka akan muncul “tak ada error” pada console.
Ini diimplementasi oleh @Transactional
pada Spring Data. Ini adalah gabungan @After
dan @Before
di atas. Di sini kita perlu tambahkan parameter “ProceedingJoinPoint” untuk eksekusi method aslinya. Jadi method aslinya akan dieksekusi di tengah-tengah sisipan. Misalnya kita ingin print value “kalkulasinya dimulai” sebelum eksekusi dan print “kalkulasinya selesai” setelah eksekusi pada method multiply
. Biar ga conflict dengan anotasi sebelumnya, kita akan tambahkan anotasi baru.
InterceptionAround
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface InterceptionAround{
}
Calculator
@InterceptionAround
public int multiply(int x, int y){
int result = x * y;
System.out.println("x * y = " + result);
return result;
}
InterceptionConfig
@Around("@annotation(interceptionAround) && execution(* *(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint, InterceptionAround interceptionAround) throws Throwable{
System.out.println("kalkulasinya dimulai");
Object proceed = proceedingJoinPoint.proceed();
System.out.println("kalkulasinya selesai");
System.out.println();
return proceed;
}
Contoh code
Calculator calculator = new Calculator();
calculator.multiply(2, 2);
Kita perlu eksekusi proceedingJoinPoint.proceed()
untuk mengeksekusi method aslinya dan return hasilnya. Sekarang akan muncul value “kalkulasinya dimulai” sebelum eksekusi dan print “kalkulasinya selesai” setelah eksekusi method multiply
.
Kita telah menerapkan AOP menggunakan AspectJ dan Spring AOP menggunakan Pointcut annotation. Selain menggunakan annotation, kita juga bisa menggunakan Pointcut parameter argument tertentu atau menggunakan pola tertentu. Tapi cara yang paling umum, mudah di-maintain, dan gampang dipelajari adalah menggunakan annotation. Pendekatan AOP sering diimplementasi oleh framework untuk mempermudah pengembangan seperti Spring dan JUnit. Menggunakan AspectJ lebih cepat karena bekerja secara native, tapi setup-nya lebih ribet dan sering ga compatible dengan library annotation processor lain. Menggunakan Spring AOP lebih simple dan compatible dengan library annotation processor lain, tapi lebih lambat karena berbasis Proxy yang dibuat secara runtime. Code di atas gw share di github gw menggunakan AspectJ dan Spring AOP.