Bài viết được sự cho phép của tác giả Nguyễn Hữu Khanh
AOP là gì?
Trong khi xây dựng các chương trình ứng dụng, có rất nhiều những vấn đề liên quan đến phần mềm mà chúng ta cần quan tâm. Chẳng hạn, chúng ta xây dựng một hệ thống đăng ký tạo tài khoản cho một ngân hàng. Ngoài công việc chính cho phép người dùng có thể tạo tài khoản (core concern), hệ thống còn phải đảm bảo các vấn đề khác (cross-cutting concern) như chứng thực người dùng, kiểm tra ràng buộc, quản lý transaction, xử lý ngoại lệ, ghi log, debug, đo hiệu năng của ứng dụng,…
Logger logger = Logger.getLogger(...); TransactionManager tm = getTransactionManager(); public void addAccount(Account account) { logger.info("Creating (" + account + ") Account"); try { tm.beginTransaction(); db.add(account); tm.commit(); } catch (Exception) { tm.rollback(); logger.error("Account creation failed"); } }
Như bạn thấy, logic của chương trình của chúng ta phải làm rất nhiều việc như ghi log, mở/ đóng transaction, xử lý ngoại lệ, … Khi có nhiều phương thức tương tự vậy trong ứng dụng, code chúng ta bị liên kết chặt vào nhau, duplicate code, phân mảnh nhiều nơi, khó khăn khi sửa đổi thay thêm logic mới, … Để giải quyết vấn đề này, chúng ta có thể sử dụng kỹ thuật AOP.
AOP là từ viết tắt của Aspect Oriented Programming, dịch ra tiếng Việt là “phương pháp lập trình hướng khía cạnh”. AOP là một kỹ thuật lập trình cho phép phân tách chương trình thành cách module riêng rẽ, không phụ thuộc nhau. Khi hoạt động, chương trình sẽ kết hợp các module lại để thực hiện các chức năng nhưng khi sửa đổi chức năng thì chỉ cần sửa đổi trên một module cụ thể.
AOP còn được gọi là Aspect Oriented Software Development (AOSD) là một nguyên tắc thiết kế, giúp tách rời các yêu cầu hay các vấn đề được quan tâm (separation of concerns) trong chương trình thành các thành phần độc lập và từ đó tăng tính uyển chuyển cho chương trình. Trong Separation of concerns, người ta cho rằng những vấn đề tương tự nhau nên được giải quyết trong một unit of code. Khi lập trình thủ tục (functional programming), một unit of code là một function/ method, còn trong lập trình hướng đối tượng (OOP) thì unit of code là một class.
Trong AOP, chương trình của chúng ta được chia thành 2 loại concern:
- Core concern/ Primary concern: là requirement, logic xử lý chính của chương trình.
- Cross-cutting concern: là những logic xử lý phụ cần được thực hiện của chương trình khi core concern được gọi như security, logging, tracing, monitoring, …
Một số thuật ngữ khác trong AOP:
- Joinpoint: là một điểm trong chương trình, là những nơi có thể được chèn những cross-cutting concern. Chẳng hạn chúng ta cần ghi log lại sau khi chạy method nào đó thì điểm ngay sau method đó được thực thi gọi là một Jointpoint. Một Jointpoint có thể là một phương thức được gọi, một ngoại lệ được throw ra, hay một field được thay đổi.
- Pointcut: có nhiều cách để xác định Joinpoint, những cách như thế được gọi là Pointcut. Nó là các biểu thức được sử dụng để kiểm tra nó có khớp với các Jointpoint để xác định xem Advice có cần được thực hiện hay không.
- Advice: những xử lý phụ (crosscutting concern) được thêm vào xử lý chính (core concern), code để thực hiện các xử lý đó được gọi Advice. Advice được chia thành các loại sau:
- Before: được thực hiện trước join point.
- After: được thực hiện sau join point.
- Around: được thực hiện trước và sau join point.
- After returning : được thực hiện sau join point hoàn thành một cách bình thường.
- After throwing : được thực hiện sau khi join point được kết thúc bằng một Exception.
- Aspect: tương tự như một Java class. Một Aspect đóng gói toàn bộ cross-cutting concern và có thể chứa các JointPoint, PointCut, Advice.
- Target Object : là những đối tượng mà advice được áp dụng.
Xem thêm việc làm JavaScript hấp dẫn lương cao tại TopDev!
Một vài cross-cutting concern thường thấy trong ứng dụng:
- Logging
- Monitor
- Access control
- Error handling
- Transaction management
- Session management
- Input/output validation
Weaving là gì?
Về cơ bản Weaving (đan/ dệt) là quá trình liên kết các thành phần aspect và non-aspect của một chương trình để tạo ra đầu ra mong muốn.
Có một vài cách khác nhau giữa các hệ thống AOP về cách tạo ra Weaving. Có thể chia làm các loại Weaving: Compile-time weaving (static weaving), Load-Time Weaving và Run-time weaving (dynamic weaving).
- Compile-time weaving :
- Pre-Compile Weaving : sử dụng bộ tiền xử lý (pre-processor) để combine code của aspect và code non-aspect lại với nhau trước khi code được biên dịch thành byte code Java (.class).
- Post-Compile Weaving / Binary weaving : cách này dùng để inject code của aspect vào những tập tin .class của Java đã được compile.
- Load-Time Weaving : cách này dùng để inject code của aspect khi class cần sử dụng aspect được load vào JVM, nghĩa là trong khi ứng dụng đang chạy.
- Run-time weaving: thực hiện weaving và unweaving code của aspect và non-aspect tại run-time.
Static weaving
Static weaving là quá trình combine code của aspect và code non-aspect lại với nhau trước khi code được biên dịch thành Java byte code (.class) bằng cách sử dụng bộ tiền xử lý (pre-processor). Do đó, code gốc chỉ được thay đổi một lần tại thời gian biên dịch (compile). Hiệu suất của code được combine này tương đương với code được viết theo truyền thống.
Hạn chế của phương pháp Static weaving là khó khăn trong việc xác định code của aspect sau này hoặc thực hiện thay đổi đối với code của aspect. Mỗi khi code của aspect bị thay đổi, tất cả tất cả code sử dụng aspect phải được biên dịch lại.
Dynamic weaving
Dynamic weaving khắc phục một số hạn chế gặp phải khi weaving được thực hiện tại thời gian biên dịch (Compile-time weaving/ Static weaving). Có thể tránh được yêu cầu biên dịch lại (recompilation), triển khai lại (redeployment) và khởi động lại (restart) bằng cách thực hiện quy trình weaving trong thời gian chạy (run-time weaving).
Có một chút khác biệt giữa load-time và run-time weaving.
- Load-time weaving chỉ đơn giản là trì hoãn quá trình weaving cho đến khi các lớp được nạp bởi class loader. Cách tiếp cận này yêu cầu sử dụng một weaving class loader hoặc thay thế class loader bằng một loader khác. Hạn chế là load-time tăng lên và thiếu quyền truy cập vào các aspect trong khi chạy.
- Run-time weaving là quá trình weaving và unweaving tại run-time. Cách tiếp cận này yêu cầu các cơ chế mới để can thiệp vào việc tính toán chạy.
Các AOP Framework khác nhau có cách thực hiện dynamic weaving khác nhau. Trong khi AspectWerkz sử dụng sửa đổi byte code thông qua chức năng cấp JVM và kiến trúc “hotswap” để thực hiện weaving các lớp tại run-time, thì Spring AOP Framework dựa trên các proxy thay vì các class loader hoặc dựa vào các đối số JVM.
Dynamic weaving cho phép tăng tốc các giai đoạn thiết kế và kiểm thử trong phát triển phần mềm, vì các aspect mới có thể được thêm vào hoặc các aspect hiện tại có thể được thay đổi mà không cần biên dịch lại và triển khai lại các ứng dụng. Tuy nhiên, một nhược điểm lớn là hiệu suất giảm, vì weaving xảy ra tại run-time.
Cài đặt AOP trong Java như thế nào?
Một vài ý tưởng để implement AOP trong chương trình của chúng ta:
- Class-weaving : như đã đề cập ở phần trên.
- Proxy-based : bạn có thể tưởng tượng nó như là một ví dụ sử dụng >Decorator Pattern. Sử dụng công cụ mã hóa byte code của một số thư viện như JDK proxy, CGLib proxy, chúng ta có thể chặn các lệnh gọi hàm và thêm code của riêng mình để được thực thi trước.
- JDK proxy : đây là cách đơn giản nhất, nhưng chỉ có thể xử lý với các phương thức public được gọi, không thể xử lý các cuộc gọi nội bộ (các cuộc gọi bắt nguồn từ chính lớp đó).
- CGLib proxy : yêu cầu chỉnh sửa byte code bị giới hạn và có thể xử lý các lời gọi phương thức private, nhưng vẫn không thể xử lý truy cập thuộc tính trực tiếp.
- Sử dụng các thư viện sau: Google Guice, AspectJ, Spring AOP.
Ví dụ tự xây dựng một AOP framework
Trong ví dụ bên dưới chúng ta sẽ tự viết code để tạo AOP Framework sử dụng JDK Proxy mà không cần sử dụng bất cứ một thư viện thứ ba nào (third-party libraries).
Để xử dụng JDK Proxy chúng ta thực hiện các bước sau:
- Tạo Invocation Handler: class này phải implemenet java.lang.reflect.InvocationHandler. InvocationHandler là interface được thực hiện bởi trình xử lý lời gọi (invocation handler) của một proxy instance. Khi một phương thức được gọi trên một proxy instance, lời gọi phương thức được mã hóa và gửi đến phương thức gọi của invocation handler của nó.
- Tạo Proxy Instance : sử dụng phương thức Proxy.newProxyInstance() được cung cấp bởi factory method java.lang.reflect.Proxy.
Ví dụ: chúng ta cần thêm một vài xử lý trước và sau khi các phương thức trong class AccountService được gọi.
Account.java
package com.gpcoder.aop.account; import lombok.AllArgsConstructor; import lombok.Data; @Data @AllArgsConstructor public class Account { private String owner; private String currency; private int balance; }
AccountService.java
package com.gpcoder.aop.account; public interface AccountService { void addAccount(Account account); void removeAccount(Account account); int getSize(); }
AccountServiceImpl.java
package com.gpcoder.aop.account;
import java.util.ArrayList;
import java.util.List;
public class AccountServiceImpl implements AccountService {
private List<Account> accounts = new ArrayList<>();
@Override
public void addAccount(Account account) {
System.out.println("addAccount: " + account);
accounts.add(account);
}
@Override
public void removeAccount(Account account) {
System.out.println("removeAccount: " + account);
accounts.remove(account);
}
@Override
public int getSize() {
System.out.println("getSize: " + accounts.size());
return accounts.size();
}
}
AbstractHandler.java
package com.gpcoder.aop.handler; import java.lang.reflect.InvocationHandler; public abstract class AbstractHandler implements InvocationHandler { private Object targetObject; public void setTargetObject(Object targetObject) { this.targetObject = targetObject; } public Object getTargetObject() { return targetObject; } }
BeforeHandler.java
package com.gpcoder.aop.handler; import java.lang.reflect.Method; /** * The class BeforeHandler provides a template for the before execution */ public abstract class BeforeHandler extends AbstractHandler { /** * Handles before execution of actual method. */ public abstract void handleBefore(Object proxy, Method method, Object[] args); /* * (non-Javadoc) * * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, * java.lang.reflect.Method, java.lang.Object[]) */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { handleBefore(proxy, method, args); return method.invoke(getTargetObject(), args); } }
BeforeHandlerImpl.java
package com.gpcoder.aop.handler.impl; import java.lang.reflect.Method; import com.gpcoder.aop.handler.BeforeHandler; /** * This class provides implementation before actual execution of method. */ public class BeforeHandlerImpl extends BeforeHandler { @Override public void handleBefore(Object proxy, Method method, Object[] args) { // Provide your own cross cutting concern System.out.println("Handling before actual method execution"); } }
AfterHandler.java
package com.gpcoder.aop.handler; import java.lang.reflect.Method; public abstract class AfterHandler extends AbstractHandler { /** * Handles after the execution of method. */ public abstract void handleAfter(Object proxy, Method method, Object[] args); @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = method.invoke(getTargetObject(), args); handleAfter(proxy, method, args); return result; } }
AfterHandlerImpl.java
package com.gpcoder.aop.handler.impl; import java.lang.reflect.Method; import com.gpcoder.aop.handler.AfterHandler; /** * This class provides an implementation of business logic which will be * executed after the actual method execution. */ public class AfterHandlerImpl extends AfterHandler { @Override public void handleAfter(Object proxy, Method method, Object[] args) { // Provide your own cross cutting concern System.out.println("Handling after actual method execution"); System.out.println("---"); } }
ProxyFactory.java
package com.gpcoder.aop.handler; import java.lang.reflect.Proxy; import java.util.List; /** * A factory for creating Proxy objects. */ public class ProxyFactory { private ProxyFactory() { throw new UnsupportedOperationException(); } public static Object getProxy(Object targetObject, List<AbstractHandler> handlers) { if (handlers != null && !handlers.isEmpty()) { Object proxyObject = targetObject; for (AbstractHandler handler : handlers) { handler.setTargetObject(proxyObject); proxyObject = Proxy.newProxyInstance(targetObject.getClass().getClassLoader(), targetObject.getClass().getInterfaces(), handler); } return proxyObject; } return targetObject; } }
AspectOrientedProgrammingInJdkExample.java
package com.gpcoder.aop; import java.util.ArrayList; import java.util.List; import com.gpcoder.aop.account.Account; import com.gpcoder.aop.account.AccountService; import com.gpcoder.aop.account.AccountServiceImpl; import com.gpcoder.aop.handler.AbstractHandler; import com.gpcoder.aop.handler.ProxyFactory; import com.gpcoder.aop.handler.impl.AfterHandlerImpl; import com.gpcoder.aop.handler.impl.BeforeHandlerImpl; /** * This class to verify an AOP example using JDK proxy. */ public class AspectOrientedProgrammingInJdkExample { public static void main(String[] args) { List<AbstractHandler> handlers = new ArrayList<>(); handlers.add(new BeforeHandlerImpl()); handlers.add(new AfterHandlerImpl()); AccountService proxy = (AccountService) ProxyFactory.getProxy(new AccountServiceImpl(), handlers); Account account = new Account("gpcoder", "USD", 100); proxy.addAccount(account); proxy.getSize(); proxy.removeAccount(account); proxy.getSize(); } }
Output của chương trình:
Handling before actual method execution addAccount: Account(owner=gpcoder, currency=USD, balance=100) Handling after actual method execution --- Handling before actual method execution getSize: 1 Handling after actual method execution --- Handling before actual method execution removeAccount: Account(owner=gpcoder, currency=USD, balance=100) Handling after actual method execution --- Handling before actual method execution getSize: 0 Handling after actual method execution ---
Lợi ích của AOP là gì?
- Tăng hiệu quả của Object-orented programming (OOP).
- AOP không phải dùng để thay thế OOP mà để bổ sung cho OOP, nơi mà OOP còn thiếu sót trong việc tạo những ứng dụng thuộc loại phức tạp.
- Tăng cường tối đa khả năng tái sử dụng của mã nguồn.
- Đảm bảo >Single responsibility principle: mỗi một module chỉ làm cái mà nó cần phải làm.
- Tuân thủ nguyên tắc “You aren’t gonna need it – >YAGNI” – chúng ta chỉ cài đặt những thứ chúng ta thực sự cần, không bao giờ làm trước.
- Module hóa ở mức tiến trình/ chức năng.
- Code gọn gàng hơn do tách biệt phần xử lý chính và phần xử lý liên quan.
- Chức năng chính của chương trình không cần biết đến các chức năng phụ khác.
- Các chức năng phụ có thể được thêm thắt, bật tắt tại thời điểm run-time tùy theo yêu cầu.
- Các thay đổi nếu có đối với các chức năng phụ sẽ không ảnh hưởng đến chương trình chính.
- Hệ thống sẽ uyển chuyển và giảm thiểu tính phụ thuộc lẫn nhau của các module.
Xem thêm:
Xem thêm công việc CNTT hấp dẫn trên TopDev