Java Developer phải biết về AOP (Trợ thủ cho OOP) trong Spring Framework

Tram Ho

Lập trình theo kiểu Aspect Oriented Programming (AOP) Sử Dụng Spring Framework

ASPECT ORIENTED PROGRAMMING

AOP (Aspect-Oriented Programming) là một kiểu lập trình đã có từ lâu nhưng vẫn còn khá mới mẻ với nhiều anh em. Nó tách rời các khía cạnh không liên quan đến logic chính của chương trình, như ghi log, quản lý giao dịch, bảo mật, để làm cho mã lập trình rõ ràng và tái sử dụng dễ dàng hơn. Spring Framework là một open-source phổ biến được sử dụng để triển khai AOP trong Java. Declarative transaction là một chức năng quan trọng của Spring Framework, cho phép quản lý giao dịch một cách tập trung và không xâm nhập vào code gốc.

ASPECT ORIENTED PROGRAMMING (AOP) VÀ OBJECT ORIENTED PROGRAMMING (OOP)

Một điểm quan trọng khác cần được nhấn mạnh là AOP được xem như là một bổ sung cho OOP, không phải là một thay thế cho nó. Để bắt đầu với AOP, người lập trình cần có kiến thức về OOP. AOP thường được coi là phù hợp cho các ứng dụng phức tạp và thường dành cho những người có kinh nghiệm trong lập trình Java.

Để làm quen với OOP, bạn có thể tìm hiểu qua sách giáo trình hoặc từ trường học. Nếu bạn gặp khó khăn trong việc hiểu các khái niệm như danh từ, tính từ, hoặc trả lời các câu hỏi “ai”, “cái gì”, “làm gì”, khi tìm kiếm các class, bạn có thể đọc lại các bài viết về OOP mà đã được trình bày trước đó để có thêm sự giúp đỡ.

Dù bạn học OOP theo cách nào, kết quả cuối cùng của OOP là chia ứng dụng thành các phần nhỏ với các chức năng riêng biệt, như các hộp đen (black box). Các hộp đen này giúp tái sử dụng và quản lý ứng dụng dễ dàng hơn, và còn nhiều lợi ích khác mà không thể đề cập đầy đủ ở đây.

HẠN CHẾ CỦA OBJECT ORIENTED PROGRAMMING

Lấy ví dụ về transaction trong việc truy cập database. Trong Java, truy cập database đòi hỏi nhiều buớc: tạo ra Connection, bắt đầu transation, commit, clean up connection… Để đơn giản hoá, ta hãy tạo ra 1 class hay black box chuyên làm việc này.

Giả sử trong 1 ứng dụng, có 1 class (hay blackbox) XuatNhapHang làm chức năng xuất nhập hàng và nó chứa 1 method làm chức năng nhập hang.

Hình dung mục đích sử dụng blackbox này là dễ dàng. Khi cần truy cập cơ sở dữ liệu, người sử dụng chỉ cần gọi các phương thức công khai của blackbox mà không cần quan tâm đến cách nó hoạt động bên trong. Sự thay đổi bên trong của blackbox không ảnh hưởng đến người sử dụng, đó là sự đẹp của việc tạo ra blackbox. Mọi người có vẻ vui vẻ và độc lập với công việc của mình. Tuy nhiên, thực tế không hoàn toàn như vậy. Giả sử trong tương lai, ứng dụng yêu cầu theo dõi mọi hoạt động liên quan đến truy cập cơ sở dữ liệu. Mỗi khi có truy cập database, tên người sử dụng và thời gian sẽ được lưu trữ. Chúng ta có thể tạo ra một blackbox đơn giản chuyên làm công việc này.

Method nhapHang hay những nơi trong ứng dụng đã truy cập database đều phải sửa đổi như sau

Như các bạn đã thấy chúng ta đã phải chỉnh sửa code chức năng của chúng ta chỉ để thêm 1 tính năng hỗ trợ. Sự thay đổi rộng khắp trong toàn bộ ứng dụng là một vấn đề gây không hài lòng cho nhiều người. Người lập trình kỳ cựu đồng ý rằng trong thực tế, yêu cầu của ứng dụng thường thay đổi theo thời gian. Mỗi khi có yêu cầu mới, một hoặc nhiều blackbox mới được tạo ra và mã nguồn sẽ phải thay đổi rộng khắp trong ứng dụng để sử dụng các blackbox mới này. Thực hiện thay đổi mã nguồn rộng khắp trong ứng dụng là một công việc tốn kém và yêu cầu kiểm thử toàn bộ hệ thống. Khi có lỗi xuất hiện trong quá trình này, thường rất khó tìm kiếm vì người lập trình thường coi nhẹ việc “sửa một chút thì không sao…”.

Để giảm thiểu rủi ro và khó khăn trong việc thay đổi mã nguồn rộng khắp, cần thực hiện quy trình kiểm thử và xác nhận chất lượng kỹ lưỡng. Điều này bao gồm thực hiện các bộ kiểm thử đầy đủ và kiểm tra kỹ lưỡng tất cả các thành phần bị ảnh hưởng bởi thay đổi. Đồng thời, duy trì tính linh hoạt và mô-đun trong thiết kế và triển khai ứng dụng cũng có thể giúp giảm thiểu tác động của các thay đổi rộng khắp trong mã nguồn.

Một vấn đề quan trọng là làm sao tạo được một kiểu lập trình linh hoạt và thích ứng với sự phức tạp của ứng dụng. Mô hình này phải cho phép xây dựng nhanh chóng ứng dụng khi nó còn đơn giản ở giai đoạn ban đầu và có khả năng thay đổi nhanh chóng để đáp ứng yêu cầu mới. OOP cho phép tạo ra các blackbox, và điều này được coi là không thể thiếu trong lập trình. Tuy nhiên, vấn đề không nằm ở blackbox chính, mà nằm ở cách sử dụng chúng. Sự kết nối mạch (hard-wired) blackbox vào mã nguồn bằng cách gọi trực tiếp các phương thức public của chúng là vấn đề.

Có cách nào để sử dụng blackbox mà không cần gọi trực tiếp chúng trong mã nguồn không? Có cần phải có một cách để gọi gián tiếp các phương thức public của blackbox, gọi là kết nối linh hoạt (soft-wired), khi sử dụng chúng. Đây là một khía cạnh mà OOP bỏ sót, và AOP ra đời để đáp ứng nhu cầu này. AOP cho phép chúng ta tách rời các khía cạnh không liên quan đến logic chính của ứng dụng, như quản lý giao dịch, ghi log, bảo mật, và áp dụng chúng một cách tập trung thông qua việc gắn kết gián tiếp (indirectly) các phương thức public của blackbox.

VẬY LÀM THẾ NÀO ĐỂ NỐI MỀM 1 ỨNG DỤNG?

Đây cũng chính là câu hỏi mà AOP cần phải trả lời . Java cho phép là đuợc việc này.

Nối mềm (soft-wiring) cho phép sử dụng một lớp (hoặc blackbox) mà không cần gọi trực tiếp các phương thức công khai của nó trong mã nguồn. Điều này đã được thực hiện từ lâu bởi các container như EJB, portlet và servlet. Lấy ví dụ về EJB container, nó có sự tương đồng với khái niệm AOP. Để sử dụng một EJB, bạn phải gọi phương thức create… thay vì sử dụng từ khóa new. Trong thực tế, EJB chạy trong một container. Container tạo ra trực tiếp một phiên bản của EJB và chuyển nó cho người sử dụng khi phương thức create… được gọi. Mỗi khi người sử dụng gọi phương thức của EJB, container sẽ chặn cuộc gọi này (intercept) và sau đó chuyển nó cho phiên bản của EJB. Trước khi chuyển giao cho EJB, container thường thực hiện nhiều công việc khác… Container có thể bắt đầu một giao dịch hoặc kiểm tra quyền của người gọi, tuỳ thuộc vào cấu hình của EJB trong tệp ejb-jar.xml. Nếu coi EJB là một blackbox, sự kết nối giữa EJB và người sử dụng mặc dù là gián tiếp nhưng chưa được coi là nối mềm. Container có thể chứa sẵn một blackbox bên trong nó để thực hiện công việc bắt đầu giao dịch hoặc kiểm tra quyền của người gọi. Việc kết nối giữa EJB và những blackbox bên trong của container mới thực sự là nối mềm. EJB hoặc người viết EJB hoàn toàn không biết về sự tồn tại của một blackbox có sẵn bên trong container để thực hiện công việc giao dịch. Người viết EJB chỉ cần thông báo với container thông qua tệp cấu hình ejb-jar.xml rằng cần bắt đầu một giao dịch hoặc thực hiện những công việc khác mỗi khi phương thức của nó được gọi. Dựa trên ý tưởng này, Spring Framework là một container nhẹ và có thể chứa các đối tượng Java thông thường mà không phức tạp như EJB.

Trở lại ví dụ xuất nhập hàng ở trên, nếu class XuatNhapHang chạy bên trong spring framework thì method nhapHang ở trên có thể viết lại như sau

Không hề có 1 đọan mã nguồn nào bên trong method nhapHang nói đến việc tạo ra transaction hay theo doi truy cap . Bản thân các blackbox transaction và theo dõi try cập cũng đuợc chạy trong spring. Nguời viết class XuatNhapHang chỉ việc truyền đạt cho spring, qua 1 file cấu hình, rằng mỗi khi method nhapHang đuợc gọi , hãy gọi blackbox transaction và theo dõi truy cập truớc khi chuyển nó đến nhapHang . Bằng cách nối mềm này, việc bao gồm hay loại bỏ những blackbox đuợc thực hiện qua việc thay đổi file cấu hình, chứ không cần phải thay đổi mã nguồn của class XuatNhapHang . Đây chính là lập luận mạnh nhứt trong việc cho ra đời AOP . Những nguời ủng hộ AOP trong ví dụ này lí luận rằng class XuatNhap hay method nhapHang chỉ nên lo lắng việc xuất nhập hàng . Việc bắt đầu 1 transaction hay theo dõi truy cập dữ liệu , đành rằng phải có do yêu cầu của ứng dụng, cũng không nên chen lấn 1 cách thô bạo vào trong mã nguồn của class XuatNhap, hay bất kì vào trong những blackbox nào khác khi chúng cũng cần truy cập dữ liệu . Có như vậy thì 1 blackbox mới giữ đuợc tính độc lập của nó với những thay đổi của yêu cầu của ứng dụng, dẫn đến việc tái sử dụng blackbox đuợc triệt để hơn.

Những khía cạnh như transaction hay theo dõi truy cập, nếu cần phải ‘chen lấn ‘ thì mã nguồn không phải là chỗ tốt để làm . Có những chỗ tốt hơn chẳng hạn như dùng file cấu hình, và spring framework làm đúng như vậy . Công việc của spring framework là đón đầu các cú gọi và thực hiện các chức năng thể hiện qua file cấu hình . Đây cũng chính là nguyên tắc cơ bản của AOP: thay các cú gọi trực tiếp, bằn các cú gọi qua file cấu hình

KHÓ KHĂN KHI ÁP DỤNG ASPECT ORIENTED PROGRAMMING

Đúng, có thể bạn không cảm thấy hứng thú với AOP sau khi đọc đến đây. Thực tế, các tệp cấu hình cho Spring hoặc các framework AOP khác như AspectJ không đơn giản chút nào. Gọi trực tiếp method chỉ mất một dòng và dễ theo dõi logic của ứng dụng. Liệu việc thay đổi tệp cấu hình có an toàn hơn việc thay đổi mã nguồn hay không? Điều này vẫn còn đang được tranh luận. Những người ủng hộ AOP cho rằng nó là cách làm đúng. Còn những người còn nghi ngờ thì lưỡng lự.

Các framework AOP khác nhau có cách tạo tệp cấu hình khác nhau. Không giống như EJB có thể chạy trong bất kỳ container EJB nào, ứng dụng viết cho một framework AOP không chạy được trong các framework AOP khác. Các nỗ lực để đồng nhất các framework AOP cho đến nay vẫn chưa kết thúc. Việc container đón đầu các cú gọi có thể ảnh hưởng đến tốc độ chạy của ứng dụng. Ngoài ra, không phải mọi lớp trong ứng dụng đều cần được quản lý bởi framework AOP, giống như không phải mọi lớp đều trở thành EJB. Nếu ứng dụng được thiết kế kém, có thể dẫn đến tăng số lượng lớp mà AOP container phải quản lý cùng với các tệp cấu hình.

Có lẽ chính vì những lý do này, AOP vẫn chưa phát triển nhanh chóng như OOP. Điểm khó khăn cuối cùng của AOP là khái niệm trừu tượng hóa nó để truyền đạt. AOP đưa ra những khái niệm mới lạ, khó nắm bắt và có thể làm nản lòng những người mới bắt đầu. Trong phần tiếp theo, người viết sẽ trình bày những khái niệm cơ bản của AOP và nguyên tắc Inversion of Control hoặc Dependency Injection của Spring Framework trong triển khai AOP.

NHỮNG KHÁI NIỆM TRONG ASPECT ORIENTED PROGRAMMING

Khái niệm đầu tiên thường gặp trong AOP là “concern” (mối quan tâm) hoặc “aspect” (khía cạnh) – tương đương với blackbox trong OOP. Ví dụ, trong ứng dụng quản lý cửa hàng, các yêu cầu bao gồm: quản lý tài chính, xuất nhập hàng, kết toán cuối tháng, cuối năm, quản lý nhân viên… Các yêu cầu này có thể thay đổi theo thời gian. Sử dụng OOP, chúng ta có thể tạo ra các blackbox như xuất nhập hàng, transaction, hay theo dõi truy cập, những blackbox này tương ứng với các khía cạnh trong ứng dụng mà người thiết kế phải quan tâm.

Tuy nhiên, có những khía cạnh, như transaction, cần “chen lấn” vào các khía cạnh khác. Những khía cạnh này được gọi là “crosscutting aspect” – tức là những khía cạnh cắt ngang. Thực tế cho thấy, các điểm chen lấn của crosscutting aspect thường là những điểm đặc biệt trong mã nguồn. Ví dụ, khía cạnh transaction chỉ chen vào khi bắt đầu method nhapHang để khởi tạo một transaction, và lại chen vào lần nữa khi method nhapHang kết thúc để commit transaction. Các điểm đặc biệt như vậy có thể là bắt đầu hoặc kết thúc một method, khi xảy ra ngoại lệ. Spring chỉ cho phép chen vào các điểm đặc biệt này, trong khi AspectJ cho phép chen vào gần như bất kỳ điểm nào trong mã nguồn.

Cần cân nhắc kỹ lưỡng khi chen lấn tuỳ tiện và thường được khuyến nghị tránh làm điều này. Các điểm chen lấn được gọi là “joinpoint” (điểm nối). Tập hợp các điểm nối này được gọi là “pointcut” (cắt điểm). Blackbox XuatNhapHang, do bị chen lấn, được gọi là “target object” (đối tượng mục tiêu). Blackbox transaction, triển khai transaction aspect, thực hiện công việc chen lấn, được gọi là “advice” (lời khuyên). Vì có nhiều điểm chen lấn đặc biệt như đã đề cập, nên có nhiều loại advice:

  • Around advice: là loại chen vào truớc khi cú gọi đuợc chuyển tới method và sau khi method thực hiện xong. Transaction advice chính là loại này
  • Before advice: chen vào truớc khi cú gọi đuợc chuyển tới method
  • Throws advice: chen vào khi bản thân method thows excpetion
  • After returning advice: chen vào sau khi method thực hiện và không có exception

Ta có thể hình dung như sau: mỗi khi ứng dụng gặp điểm “chen lấn”, container sẽ đón đầu cú gọi và thực thi mã nguồn của advice cho điểm “chen lấn” đó, sau đó mới chuyển giao cho method.

Như đã đề cập, để thực sự lập trình theo kiểu AOP, bạn cần làm quen với một framework AOP như Spring hay AspectJ, đặc biệt là cách tạo file cấu hình cho mỗi framework.

Quay trở lại Spring Framework, nó được đề cập ở đây vì chức năng quản lý transaction được sử dụng phổ biến. Trên Spring, chúng ta sử dụng khái niệm Declarative Transaction, tương tự như trong EJB, nhưng đơn giản và dễ sử dụng hơn nhiều. Để sử dụng chức năng này, bạn chỉ cần hiểu về khái niệm AOP như đã trình bày ở trên. Khi sử dụng Spring, một khái niệm quan trọng là Inversion of Control (IoC) hoặc còn gọi là Dependency Injection, sẽ được giải thích chi tiết bên dưới.

INVERSION OF CONTROL (IOC) VÀ DEPENDENCY INJECTION (DI)

IoC (Inversion of Control) thường được thực hiện bởi các loại container như Servlet, Portlet, hay EJB.

Lấy ví dụ với EJB container, người sử dụng không tạo instance của EJB trực tiếp, mà container tạo ra và chuyển giao instance đến người sử dụng khi cần. Khi và cách nào instance được tạo ra nằm ngoài sự kiểm soát của người sử dụng. Container có thể tạo instance trước và đợi cho đến khi người sử dụng yêu cầu, hoặc tạo instance ngay lập tức khi cần. Thuật ngữ IoC cũng thể hiện nguyên tắc này: container lấy lại sự kiểm soát từ người sử dụng (trong lập trình thông thường, người sử dụng giành kiểm soát bằng cách sử dụng từ khóa “new” để tạo instance). Khi instance của EJB được tạo ra, trong trường hợp Session Bean, container luôn gọi phương thức setSessionContext(SessionContext sc) để cung cấp SessionContext cho EJB sử dụng.

Tương tự, trong Servlet container, khi instance của Servlet được tạo ra, container luôn gọi phương thức init(ServletConfig sc) để cung cấp ServletConfig cho Servlet sử dụng. Cả hai trường hợp đều cho thấy container “inject” (cung cấp) những cái mà instance cần. Cái mà được cung cấp như SessionContext hay ServletConfig được gọi là dependency (phụ thuộc).

Thuật ngữ Dependency Injection cũng bắt nguồn từ đây. Tuy có một sự khác biệt giữa cách sử dụng thuật ngữ Dependency Injection và định nghĩa quan hệ giữa các class trong UML. Dựa trên tài liệu “Mastering UML with Rational Rose”, instance của EJB sử dụng SessionContext, do đó instance mới chính là dependency phụ thuộc vào SessionContext. Nếu có thể giải thích tại sao có sự khác biệt này, thì chúng ta sẽ chia sẻ thông tin với tất cả mọi người. Tuy nhiên, không nên dành quá nhiều thời gian để tranh luận về các định nghĩa và thuật ngữ ở đây.

ASPECT ORIENTED PROGRAMMING TRONG SPRING FRAMEWORK

Muốn sử dụng Spring, ít nhiều bạn phải làm quen với Spring container . Class tiêu biểu cho spring container là ApplicationContext . Nguời sử dụng phải trực tiếp tạo ra instance của spring container truớc khi có thể sử dụng những object mà nó chứa . Có nhiều cách tạo ra spring container. Cách thông thuờng nhứt là (những ví dụ theo sau đuợc dựa trên tài liệu spring-reference.pdf vversion 1.2.8)

Spring container tạo ra theo cách này sẽ đọc những file config duới dạng xml để load tất cả các Java ojbect mà nó cần quản lí .
Như ví dụ sau

ExampleBean và ExampleBeanTwo có thể là những java object thông thuờng chứ không có gì đặc biệt . Muốn truy cập chúng qua container rất dễ dàng

Theo mặc định thì container sẽ tạo ra singleton instance cho mỗi bean, có nghĩa là chỉ 1 instance của 1 bean đuợc tái sử dụng cho những lần gọi sau . Ta có thể sử dụng Dependency Injection của container như sau

Bản thân ExampleBean phải chứa những setter method để container gọi ngay sau khi tạo ra chúng và truớc khi chuyển cho nguời sử dụng .

Nguyên tắc sử dụng spring container chỉ đơn giản như vậy thôi . Nó cho phép định nghĩa qua file cấu hình những logic phức tạp hơn mà bài viết không trình bày hết ra đây . Tới đây bạn có thể thắc mắc như vậy thì sức mạnh của spring nằm ở đâu ? Tiện lợi chỉ đơn giản như vậy thì có đáng đuợc sử dụng không ?

Cần nhắc lại 1 nguyên tắc làm việc của mọi dạng container là đón đầu những cú gọi . Spring làm việc này bằng cách dựa trên file cấu hình để tạo ra những object phụ trợ khác vào lúc runtime . Cái return từ method getBean đuợc xem là 1 proxy đuợc tạo ra vào lúc runtime của java object mà spring quản lí . Đằng sau proxy này là 1 tập họp các phụ trợ object thực hiện các chức năng theo yêu cầu của file cấu hình truớc khi chuyển cú gọi tới cho java object . Chẳng hạn như các phụ trợ object có thể chạy mã nguồn của advice, hoặc bắt đầu 1 transaction, một chức năng đã được làm sẵn trong Spring, sẽ đuợc nói tới duới đây .

Trong việc truy cập database, ứng dụng có thể sử dụng các phuơng pháp khác nhau như: trực tiếp sử dụng SQL, Hybernate, EJB Entity … Để đơn giản hoá vấn đề, ta lấy truờng sử dụng SQL để cập nhật database .

Trong Java, connection thường tạo ra duới dạng connection pool hay DataSource, là 1 tập họp những connection để xử dụng chung cho toàn bộ ứng dụng . Giả sử apache datasource đuợc xử dụng ở đây . Để container có thể tạo ra datasource cho oracle, ta xử dụng cấu hình duới đây

Nếu datasource đã đuợc tạo ra xử dụng JNDI, ta có thể dùng cấu hình

Bản thân datasource sẽ đuợc xử dụng bởi 1 transaction manager, 1 object quản lí transaction có sẵn trong spring như sau:

Trở lại class XuatNhapHang ở trên, muốn spring tự động bắt đầu và kết thúc 1 transaction mỗi khi 1 method của class này đuợc gọi , cần có những cấu hình cho pointcut hay advice khá phức tạp theo kiểu AOP . Rất may là spring đã gói gọn trong 1 class TransactionProxyFactoryBean đơn giản sau

Chúng ta sẽ kiểm tra kĩ lưỡng cấu hình này:

_ abstract=”true” có nghĩa là ứng dụng không đuợc phép gọi method getBean(“txProxyTemplate”) . Cấu hình này chỉ đuợc dùng để các cấu hình khác extends . Giống như tính chất inheritance trong OOP

_ property name=”transactionManager” ref=”txManager” cho biết class TransactionProxyFactoryBean sẽ đuợc nạp bean txManager bởi container.

_ prop key=”*”: áp dụng cho mọi method của những class extends cấu hình này

_ PROPAGATION_REQUIRED: bắt đầu 1 transaction nếu chưa có hoặc tiếp tục 1 transaction có sẵn

_ MyException: nếu method throw MyException, thì dấu – đằng truớc sẽ làm transaction bị rollback, và dấu + sẽ commit transaction truớc khi throw MyException.

Cấu hình của class XuatNhapHang sẽ extends cấu hình này như sau:

Ứng dụng có thể xử dụng class XuatNhapHang như sau:

Bằng cách mở rộng cấu hình của txProxyTemplate thông qua thuộc tính parent, mỗi khi một phương thức của lớp XuatNhapHang được gọi, một giao dịch sẽ tự động bắt đầu và kết thúc. Trong trường hợp xảy ra ngoại lệ MyException, giao dịch sẽ bị rollback. Spring cũng hỗ trợ mô hình JDBC operations như là các đối tượng Java thông qua việc sử dụng các lớp MappingSqlQuery, SqlUpdate, StoredProcedure.

Bài viết kết thúc ở đây với mục đích giúp bạn làm quen với các khái niệm của AOP. Vì AOP là một phần bổ sung cho các thiếu sót của OOP, nó không phù hợp cho người mới bắt đầu lập trình. Spring framework, mặc dù triển khai AOP đơn giản hơn so với AspectJ, vẫn có độ phức tạp với nhiều chức năng có thể sử dụng và người viết chỉ tóm tắt chức năng phổ biến nhất của nó là declarative transaction. Dù sao, bạn vẫn cần tham khảo tài liệu hướng dẫn về Spring trước khi sử dụng nó một cách hiệu quả. Chúc bạn may mắn trong hành trình lập trình của mình.

Chia sẻ bài viết ngay

Nguồn bài viết : Viblo