Có lẽ trong các bạn, ai cũng đã từng biết và làm việc với kiểu lập trình hướng đối tượng, Object Oriented Programming (OOP) nhưng với lập trình hướng khía cạnh, Aspect Oriented Programming (AOP), thì có lẽ không nhiều bạn biết. Trong bài viết này, mình sẽ trình bày những khái niệm cơ bản của lập trình hướng khía cạnh để các bạn có thể hiểu rõ hơn về nó các bạn nhé!
Mình không đi lý thuyết dài dòng, nói nôm na thì lập trình hướng khía cạnh là kiểu lập trình cho phép chúng ta có thể thêm những đoạn code xử lý mới vào các ứng dụng đã tồn tại mà không cần phải chỉnh sửa code của các ứng dụng đó.
Ví dụ giả sử mình có một ứng dụng, trong ứng dụng này có 1 class có nội dung như sau:
1 2 3 4 5 6 7 8 9 10 11 12 |
package com.huongdanjava; public class Application { public int sum(int a, int b) { return a + b; } public int sub(int a, int b) { return a - b; } } |
Giờ mình muốn thêm vào một đoạn code trong tất cả các phương thức của class Application để mỗi khi ứng dụng gọi các phương thức này, một đoạn log message sẽ được in ra console. Với cách của lập trình hướng đối tượng, mình phải chỉnh sửa từng phương thức của class Application để đạt được đều mình muốn. Lập trình hướng khía cạnh định nghĩa cách dễ dàng hơn để làm điều này mà không cần phải chỉnh sửa code của class Application.
Với lập trình hướng khía cạnh, chúng ta sẽ tìm cách để mỗi khi các phương thức trong class Application được gọi, chương trình phải gọi đoạn code để in ra log message của chúng ta trước.
Chúng ta có nhiều cách để thực hiện ý tưởng của lập trình hướng khía cạnh, một trong số đó là sử dụng các thư viện có sẵn như Spring AOP, AspectJ,… Để các bạn hiểu rõ hơn, trong bài viết này, mình sẽ sử dụng thư viện AspectJ để giải quyết bài toán mà mình đã đặt ra các bạn nhé!
Mình sẽ tạo một Maven project để làm ví dụ như sau:
Thêm dependency của thư viện AspectJ:
1 2 3 4 5 |
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.8.13</version> </dependency> |
Mình sẽ thêm một class với phương thức main() để sử dụng class Application như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package com.huongdanjava.aspectjexample; public class Example { public static void main(String[] args) { Application application = new Application(); int a = 10; int b = 3; System.out.println(application.sum(a, b)); System.out.println(application.sub(a, b)); } } |
Kết quả:
Giờ mình sẽ sử dụng AspectJ để thêm đoạn code ghi log mỗi khi các phương thức sum() và sub() trong class Application được gọi như sau:
Đầu tiên, mình sẽ tạo một class mới và sử dụng các annotation @Aspect và annotation @Before của AspectJ như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package com.huongdanjava.aspectjexample; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect public class ApplicationAspect { @Before("execution (* com.huongdanjava.aspectjexample.Application.*(..))") public void allMethods(JoinPoint joinPoint) { System.out.println("Method name: " + joinPoint.getSignature().getName()); } } |
Trong phương thức ở trên, mình đã định nghĩa một đoạn code giúp chúng ta có thể in ra đoạn log message với nội dung là tên của phương thức trong class Application mà chương trình của chúng ta đang gọi. Ở đây, annotation @Aspect dùng để khai báo cho AspectJ việc chúng ta cần chèn code vào các class khác và chúng sẽ được khai báo trong class này. Mình sẽ nói rõ chi tiết từng phần về annotation @Before và giá trị của nó, về tham số của phương thức allMethod(),… trong những bài viết sau các bạn nhé!
Tiếp theo, mình sẽ thêm một số Maven plugin để chạy ứng dụng của chúng ta với AspectJ.
Tại sao mình không chạy ứng dụng một cách bình thường, chọn Example class main rồi chọn Run “Example.main()”? Đó là bởi vì, để cho đơn giản thôi các bạn, chúng ta có thể kiểm tra kết quả với chỉ một câu lệnh Maven 🙂
Với lập trình hướng khía cạnh, chúng ta có 3 cách để thực hiện việc chèn code vào class đang tồn tại, đó là:
- Compile-Time Weaving: đây là cách đơn giản nhất. Lúc compile source code, chúng ta sẽ có code của class mà chúng ta muốn chèn code và code mà chúng ta muốn chèn. Do đó, chúng ta chỉ cần cấu hình để combine 2 source code này lại với nhau mà thôi.
- Post-Compile Weaving: cách này thì dùng để chèn code vào những tập tin .class của Java đã được compile nha các bạn.
- Load-Time Weaving: cách này dùng để chèn code khi class mà chúng ta muốn chèn được load vào JVM.
Cụ thể mỗi cách, mình sẽ hướng dẫn các bạn trong những bài viết sau, trong bài viết này mình sử dụng cách đơn giản nhất, compile-time weaving.
Để có thể insert đoạn code ghi log message trước khi gọi các phương thức của class Application, chúng ta sẽ sử dụng một Maven plugin để làm điều này. Nếu các bạn chạy theo cách bình thường thì IDE chỉ build class của chúng ta qua tập tin .class để chạy mà không có sự can thiệp của AspectJ. Do đó, kết quả sẽ không như chúng ta mong đợi.
Plugin đó tên là aspectj-maven-plugin và mình sẽ khai báo nó như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version>1.10</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> <executions> <execution> <goals> <goal>compile</goal> </goals> <configuration> <complianceLevel>1.8</complianceLevel> </configuration> </execution> </executions> </plugin> |
Sau khi đã build code ứng dụng của chúng ta sử dụng AspectJ plugin thì mình sẽ sử dụng một Maven plugin khác để chạy ứng dụng. Plugin đó tên là exec-maven-plugin và mình sẽ khai nó như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>1.6.0</version> <executions> <execution> <phase>package</phase> <goals> <goal>java</goal> </goals> </execution> </executions> <configuration> <mainClass>com.huongdanjava.aspectjexample.Example</mainClass> </configuration> </plugin> |
Lúc này, khi chạy Maven command với câu lệnh:
1 |
mvn clean install exec:java |
Các bạn sẽ thấy output như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/bin/java -Dmaven.multiModuleProjectDirectory=/Users/khanh/Working/Code/huongdanjava.com/aspectj-example "-Dmaven.home=/Applications/IntelliJ IDEA CE.app/Contents/plugins/maven/lib/maven3" "-Dclassworlds.conf=/Applications/IntelliJ IDEA CE.app/Contents/plugins/maven/lib/maven3/bin/m2.conf" "-javaagent:/Applications/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=55055:/Applications/IntelliJ IDEA CE.app/Contents/bin" -Dfile.encoding=UTF-8 -classpath "/Applications/IntelliJ IDEA CE.app/Contents/plugins/maven/lib/maven3/boot/plexus-classworlds-2.5.2.jar" org.codehaus.classworlds.Launcher -Didea.version=2017.2.6 clean install exec:java objc[1816]: Class JavaLaunchHelper is implemented in both /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/bin/java (0x10ace74c0) and /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/libinstrument.dylib (0x10adaf4e0). One of the two will be used. Which one is undefined. [INFO] Scanning for projects... [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building aspectj-example 1.0-SNAPSHOT [INFO] ------------------------------------------------------------------------ [INFO] [INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ aspectj-example --- [INFO] Deleting /Users/khanh/Working/Code/huongdanjava.com/aspectj-example/target [INFO] [INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ aspectj-example --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] Copying 0 resource [INFO] [INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ aspectj-example --- [INFO] Changes detected - recompiling the module! [WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent! [INFO] Compiling 3 source files to /Users/khanh/Working/Code/huongdanjava.com/aspectj-example/target/classes [INFO] [INFO] --- aspectj-maven-plugin:1.10:compile (default) @ aspectj-example --- [INFO] Showing AJC message detail for messages of types: [error, warning, fail] [WARNING] bad version number found in /Users/khanh/.m2/repository/org/aspectj/aspectjrt/1.8.13/aspectjrt-1.8.13.jar expected 1.8.9 found 1.8.13 <unknown source file>:<no line information> [INFO] [INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ aspectj-example --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory /Users/khanh/Working/Code/huongdanjava.com/aspectj-example/src/test/resources [INFO] [INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ aspectj-example --- [INFO] Nothing to compile - all classes are up to date [INFO] [INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ aspectj-example --- [INFO] No tests to run. [INFO] [INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ aspectj-example --- [INFO] Building jar: /Users/khanh/Working/Code/huongdanjava.com/aspectj-example/target/aspectj-example-1.0-SNAPSHOT.jar [INFO] [INFO] --- exec-maven-plugin:1.6.0:java (default) @ aspectj-example --- Method name: sum 13 Method name: sub 7 [INFO] [INFO] --- maven-install-plugin:2.4:install (default-install) @ aspectj-example --- [INFO] Installing /Users/khanh/Working/Code/huongdanjava.com/aspectj-example/target/aspectj-example-1.0-SNAPSHOT.jar to /Users/Khanh/.m2/repository/com/huongdanjava/aspectj-example/1.0-SNAPSHOT/aspectj-example-1.0-SNAPSHOT.jar [INFO] Installing /Users/khanh/Working/Code/huongdanjava.com/aspectj-example/pom.xml to /Users/Khanh/.m2/repository/com/huongdanjava/aspectj-example/1.0-SNAPSHOT/aspectj-example-1.0-SNAPSHOT.pom [INFO] [INFO] --- exec-maven-plugin:1.6.0:java (default-cli) @ aspectj-example --- Method name: sum 13 Method name: sub 7 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.805 s [INFO] Finished at: 2017-12-28T20:21:00+07:00 [INFO] Final Memory: 20M/252M [INFO] ------------------------------------------------------------------------ Process finished with exit code 0 |
Rõ ràng, như các bạn thấy, plugin exec-maven-plugin đã giúp chúng ta chạy ứng dụng sử dụng class Example và làm được điều mà chúng ta mong muốn:
1 2 3 4 5 |
[INFO] --- exec-maven-plugin:1.6.0:java (default-cli) @ aspectj-example --- Method name: sum 13 Method name: sub 7 |
Tất nhiên, sử dụng Maven plugin exec-maven-plugin chỉ để cho mình đơn giản phần ví dụ cho bài viết này. Trong các ứng dụng thật sự, build thành gói jar để chạy thì chúng ta không cần phải sử dụng plugin này nữa.