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

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

Bài viết được sự cho phép của tác giả Edward Thiên Hoàng

ASPECT ORIENTED PROGRAMMING

Trong khoảng 5 năm trở lại đây, một khuynh huớng lập trình mới xuất hiện. Nó được gọi là AOP để phân biệt với kiểu lập trình OOP đã có sẵn. Nguời viết muốn cung cấp cho bạn đọc một kiến thức cơ bản về kiểu lập trình mới mẻ này, và sử dụng springframework, một open-source phổ biến, để minh hoạ. Nếu bạn muốn tìm hiểu chi tiết về cách sử dụng springframework, thì bài viết này không nhằm mục đích đó, mà chỉ trình bày khái quát về những thuật ngữ và nguyên tắc trong việc sử dụng 1 chức năng phổ biết của spring: declarative transaction.

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

Một điểm quan trọng nữa cần đuợc nêu ra, AOP đuợc xem là cái bổ sung cho OOP, chỗ 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. AOP khônng phải là cái thay thế OOP. Nguời lập trình bắt buộc phải quen thuộc với OOP để bắt đầu với AOP. Cũng vì lí do này mà AOP thuờng đuợc xem là dành cho các bạn đã có kinh nghiệm, chứ không dành cho các bạn mới bắt đầu làm quen với Java. Bạn có thể làm quen với OOP qua lí thuyết và sách vở từ truờng học. Và nếu bạn thấy vẫn còn lạc lối trong việc tìm kiếm những danh từ, tính từ, hay trả lời những câu hỏi ai, cái gì, làm gì… đại loại như vậy, trong việc tìm kiếm class, thì bạn có thể đọc lại những bài viết về OOP mà nguời viết đã trình bày truớc đây xem có giúp ích hơn không. Dù học OOP theo kiểu nào thì kết quả cuối cùng của OOP là chia ứng dụng thành nhiều phần nhỏ với những chức năng riêng biệt, theo kiểu các hộp đen (black box). Các hộp đen này làm cho việc tái sử dụng và bao quản chúng đuợc dễ dàng hơn….và còn thêm nhiều ưu điểm khác nữa không kể ra ở đâ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.

public class DBTransaction {
public DBTransaction () {
//mã nguồn tạo ra Connection và bắt đầu 1 transaction
}

public Connection getConnection () {
//mã nguồn return Connection đã tạo ra
}

public void commit () {
//mã nguồn commit transaction và clean up resources
}
}

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.

public void nhapHang ( int monHang, int soLuong )
{
     //sử dụng blackbox để bắt đầu 1 transaction
     DBTransation tx = new DBTransactiơn ();

     // ...mã nguồn truy cập database để nhập hàng...

}
public class DBTruyCapTheoDoi {
     public DBTruyCapTheoDoi (Connection con, String tenNguoi) {
          //mã nguồn lưu lại trong database nguời sử dụng và thời gian
          .....
     }
}

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

public void nhapHang ( int monHang, int soLuong )
{
     //sử dụng blackbox để bắt đầu 1 transaction
     DBTransation tx = new DBTransactiơn ();
     //theo doi truy cap
     DBTruyCapTheoDoi (tx.getConnection (), tenNguoi);

     // ...mã nguồn truy cập database để nhập hàng...

}

Sửa đổi không nhiều nhưng nó rộng khắp trong toàn ứng dụng. Đây chính là điểm làm cho nhiều nguời không hài lòng. Những nguời lập trình kì cựu đều đồng ý với nhau rằng, trong thực tế, đòi hỏi của ứng dụng luôn thay đổi theo thời gian. Cứ mỗi 1 yêu cầu mới của ứng dụng thì một (hay nhiều) blackbox đuợc tạo thành, và mã nguồn sẽ bị thay đổi rộng khắp trong ứnng dụng để sử dụng blackbox mới này. Thay đổi mã nguồn rộng khắp ứng dụng là điều tối kị vì nó đòi hỏi phải testing lại toàn bộ . Những bug xuất hiện lúc này thuờng rất khó tìm vì nguời lập trình thuờng cho rằng “mình sửa có 1 chút thì không sao…” .

Câu hỏi đặt ra là làm sao tạo đuợc một kiểu lập trình uyển chuyển với sự phức tạp của ứng dụng . Mô hình này phải cho phép tạo dựng nhanh chóng ứng dụng khi nó ở trong giai đọan đầu đơn giản, và thay đổi nhanh chóng để thích ứng kịp thời với đòi hỏi mới . OOP cho phép tạo ra những blackbox và điều này vẫn đuợc xem là không thể thiếu trong việc lập trình . Bản thân những blackbox không phải là vấn đề, mà chính việc sử dụng chúng mới là vấn đề . Việc gọi trực tiếp những public method của 1 blackbox khi phải sử dụnng nó trong mã nguồn, tạm gọi là nối cứng (hard wired) blackbox vào mã nguồn. Có cách nào để sử dụng 1 blackbox mà khônng cần gọi trực tiếp nó trong mã nguồn hay không? Cần có 1 cách nào đó để gọi gián tiếp những public method, hay tạm gọi là nối mềm (soft wired) khi sử dụng những blackbox. Đây là chỗ OOP bỏ sót, và AOP ra đời để đáp ứng nhu cầu này .

Đến đây bạn có thể thấy rằng, OOP là buớc đầu tiên cần phải có để tạo thành các blackbox. AOP là buớc kế tiếp để nối mềm những blackbox tạo thành 1 ứng dụng hoàn chỉnh . Việc nối mềm đòi hỏi nguời thiết kế phải có kinh nghiệm trong việc chia ứng dụng thành nhiều lớp (layer) để ứng dụng đuợc tạo ra và chạy 1 cách hữu hiệu . Điều này sẽ đuợc nói kĩ hơn sau đây. Một rắc rối nữa của việc nối mềm là nó thuờng đòi hỏi tạo nhiều cấu hình (configuration) phụ trợ .

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 là cho phép sử dụng 1 class (hay blackbox) mà không cần gọi trực tiếp public methods của nó trong mã nguồn, và bạn có thể ngạc nhiên nếu biết rằng điều này thực sự đã đuợc làm từ lâu . Nó đuợc làm bởi các container chẳng hạn như của EJB, portlet, servlet. Lấy EJB container làm ví dụ, vì nó rất gần với khái niệm AOP . Muốn sử dụng 1 EJB, bạn phải gọi create… chứ không bao giờ sửng dụng new . EJB trong thực tế chạy bên trong 1 container. Container trực tiếp tạo ra instance của EJB và chuyển cho nguời sử dụng khi method create… được gọi . Mỗi lần nguời sử dụng gọi method của EJB, container sẽ đón đầu (intercept) cú gọi này, rồi mới chuyển giao cho instance của EJB . Truớc khi chuyển giao cho EJB, thuờng container có thể sẽ làm nhiều việc khác nữa …. Container có thể bắt đầu 1 transaction, hay kiểm tra xem nguời gọi có đuợc phép gọi hay không, tùy theo cách cấu hình của EJB đó bên trong file chúng tôi . Nếu xem EJB là 1 blackbox thì việc nối giữa EJB và nguời sử dụng nó mặc dù là gián tiếp nhưng chưa đuợc xem là nối mềm . Bản thân EJB container có thể chứa sẵn bên trong nó 1 blackbox làm công việc bắt đầu 1 transaction, hay 1 blackbox khác làm công việc kiểm tra mức độ cho phép nguời gọi . Việc nối kết giữa EJB và những blackbox bên trong của container mới chính xác là mềm … Bản thân EJB hay nguời viết nó hoàn toàn không biết đến có tồn tại 1 blackbox có sẵn bên trong container làm công việc transaction . Nguời viết EJB chỉ việc thông tin với container qua file cấu hình chúng tôi rằng phải bắt đầu 1 transaction hay làm thêm những chuyện khác mỗi khi method của nó đuợc gọi . Cũng dựa trên ý tuởng này, spring framework chính là 1 container thuộc loại nhẹ và nó có thể chứa đựng những Java object thông thuờng chứ 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

public void nhapHang ( int monHang, int soLuong )
{
     // ...mã nguồn truy cập database để nhập hàng...
}

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

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

Tới đây có lẽ khá đủ cho nguời mới làm quen với AOP. Để thực sự có thể lập trình theo kiểu AOP, bạn cần phải thông thạo với 1 AOP framework như spring hay aspectJ, đặc biệt là cách tạo file cấu hình cho mỗi framework.

Trở lại spring framework, nó đuợc đề cập ở đây vì 1 chức năng phổ biến : declarative transaction, giống 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, chỉ cần khái niệm của AOP ở trên là đủ . Khi sử dụng spring, 1 khái niệm luôn gặp phải là Inversion of Control (IoC) hay còn gọi là Dependency Injection, nó sẽ đuợc nói rõ duới đây .

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

IoC (tạm dịch là đảo nguợc kiểm soát) thuờng được thực hiện bởi các loại container như servlet, portlet, hay EJB.

Lấy ví dụ EJB container, nguời sử dụng không trực tiếp tạo ra instance của EJB, mà container tạo ra nó và chuyển giao nguời sử dụng khi cần tới . Khi nào instance đuợc tạo ra nằm ngoài sự kiểm soát của nguời sử dụng . Container có thể tạo instance ra truớc và chờ đến khi nguời sử dụng, hoặc tạo ra ngay vào lúc đuợc cần tới . Tên gọi IoC cũng nhằm chỉ lí do này: container giành sự kiểm soát từ nguời sử dụng (trong lập trình thông thuờng nguời sử dụng giành kiểm soát bằng cách gọi new để tạo ra instance). Khi instance của EJB đuợc tạo ra và trong truờng hợp session bean, container luôn gọi method setSessionContext(SessionContext sc) để cho EJB sử dụng SessionContext của nó .

Tuơng tự trong servlet container, khi instance của servlet đuợc tạo ra, container luôn gọi method init(ServletConfig sc) để servlet sử dụng . Trong cả 2 truờng hợp, container đuợc xem là nạp (injection) cho instance cái mà nó cần . Cái đuợc cần như SessionContext hay ServletConfig gọi là dependency .

Tên gọi Dependency Injection cũng từ đây mà ra. Có 1 điểm bất đồng giữa cách gọi tên Dependency Injection và định nghĩa về mối quan hệ giữa class trong UML . Dựa trên tài liệu “Mastering UML with Rational Rose” thì instance của EJB sử dụng SessionContext nên instance mới chính là dependency phụ thuộc vào SessionContext . Nếu bạn giải thích đuợc tại sao có sự bất đồng này thì làm ơn cho mọi nguời cùng biết . Dù sao thì chúng ta không nên mất quá nhiều thời giờ cho vấn đề định nghĩa và tên gọi ở đâ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 chúng tôi vversion 1.2.8)

ApplicationContext ac = new ClassPathXmlApplicationContext( new String[] {"applicationContext.xml", "applicationContext-part2.xml"});

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

ExampleBean eb = (ExampleBean)ac.getBean("exampleBean"); ExampleBeanTwo eb2 = (ExampleBeanTwo)ac.getBean("anotherExample");

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

public class ExampleBean {      private AnotherBean beanOne;      private YetAnotherBean beanTwo;      private int i;      public void setBeanOne(AnotherBean beanOne) {           this.beanOne = beanOne;      }      public void setBeanTwo(YetAnotherBean beanTwo) {           this.beanTwo = beanTwo;      }      public void setIntegerProperty(int i) {           this.i = i;      } }

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 ?

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:

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:

XuatNhapHang xnh = (XuatNhapHang)ac.getBean("xuatNhapBean"); xnh.nhapHang(...);

Bằng cách extends cấu hình của txProxyTemplate qua việc sử dụng attribute parent, mỗi khi 1 method của class XuatNhapHang đuợc gọi, 1 transaction sẽ tự động bắt đầu và kết thúc . Transaction sẽ bị rollback trong truờng hợp exception MyException xảy ra . Spring còn giúp mô hình JDBC operations như những java object qua việc xử dụng class MappingSqlQuery, SqlUpdate, StoredProcedure .

Sưu tầm