Class Proxy trong Java và các ứng dụng
Bài đăng này đã không được cập nhật trong 5 năm
Bài viết này nói về lớp Proxy (java.lang.reflect.Proxy)
trong ngôn ngữ lập trình Java.
- Thuật ngữ Proxy ở đây không phải là thuật ngữ Proxy thường dùng trong mạng máy tính, mà là lớp
java.lang.reflect.Proxy
được cài đặt sẵn trong bộ thư viện chuẩn của ngôn ngữ lập trình Java. - Bài viết dựa trên cách hiểu của mình, nên nếu có điểm nào sai, hãy comment để mình biết và sửa chữa
1. Sơ lược về lớp Proxy
-
Lớp Proxy trong Java có chức năng chính gần tương tự như Proxy Design Pattern, là điều khiển việc thực hiện method của một Object khác bằng cách tạo ra một Object đại diện cho Object ban đầu - Gọi là Proxy.
-
Lớp Proxy cung cấp các phương thức tĩnh để tạo ra các lớp proxy động (dynamic proxy class) và thực thể của nó, nó cũng là lớp cha của tất cả các Lớp Proxy động tạo ra bởi các phương thức nay.
-
Đến đây chúng ta có một số thuật ngữ cần hiểu:
- Dynamic proxy class - lớp proxy động, hay gọi đơn giản là proxy class: là một lớp implement nhiều Interface khác nhau, tuy nhiên khác với bình thường, đó là các việc class này được tạo ra ở thời điểm runtime của chương trình.
- Proxy interface: là các Interface được implement bởi một proxy class.
- Proxy instance là một thực thể của proxy class.
-
Ví dụ một phương thức thường dùng của lớp
Proxy
để tạo thực thể cho một lớp proxy động:
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
-
Method này bao gồm 3 tham số, theo thứ tự như sau:
- Một
ClassLoader
. Các kiến thức về class loader nằm ngoài phạm vi của bài viết này. Ở đây chúng ta sử dụngnull
để sử dụng class loader mặc định. - Một mảng các đối tượng
Class
, mỗi một đối tượng là một Interface cần implement (lưu ý trong Java có một class tên làClass
). - Một Invocation Handler. Đây là gì? Nhớ lại mục đích chính của Proxy là điều khiển việc thực thi của một Object ban đầu khác, bằng cách tạo một lớp bọc (Wrap) bên ngoài lớp gốc. Tham số này chính là thực thể của lớp gốc đó sau khi đã implement interface
InvocationHandler
.
- Một
-
Để dễ hiểu hơn chúng ta xét ví dụ cụ thể sau:
Object proxy = Proxy.newProxyInstance(null, new Class[] { Comparable.class }, handler);
-
Hãy hiểu đoạn lệnh
Proxy.newProxyInstance(...)
thực hiện công việc như sau, lớp Proxy cung cấp phương thức tĩnhnewProxyInstance
để tạo ra một lớp proxy động$Proxy0
nào đó ở giai đoạn runtime, sau đó lớp$Proxy0
này tạo một thực thể của nó, gán vào biếnproxy
. Để ý vế trái phép gán, ta có thể thấy biến này có kiểu Object. Vì sao lớp$Proxy0
trên lại tạo ra một instance có kiểu là Object? Lớp này được tạo ra ở giai đoạn runtime, ở giai đoạn lập trình, hay compile, nó chưa được tạo ra, nên bạn sẽ không thể biết được lớp này cụ thể là lớp tên gì, hay lớp nào cả, vậy nên đơn giản nhất, dùng một biến kiểu Object để tham chiếu tới thực thể vừa tạo ra này. Lớp$Proxy0
này, sẽ implement các interface trong mảngnew Class[] {...}
.- Nếu không muốn nhận trực tiếp thực thể tạo ra từ phương thức
newProxyInstance
, lớpProxy
còn có phương thứcgetProxyClass
sẽ trả về đối tượngClass
được tạo ra!.static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)
- Nếu không muốn nhận trực tiếp thực thể tạo ra từ phương thức
-
Thực thể
proxy
được tạo ra bao gồm:- Tất cả các method trong các Interface mà lớp gốc sẽ implement (các method của các phần tử trong mảng kiểu
Class
- tham số thứ 2 của methodnewProxyInstance
, trong ví dụ trên là lớpComparable
, nên sẽ bao gồm methodcompareTo()
). - Tất cả các method được định nghĩa trong lớp Object
(toString(), equals(),...)
.
- Tất cả các method trong các Interface mà lớp gốc sẽ implement (các method của các phần tử trong mảng kiểu
-
Còn tham số
handler
? Như đã nêu ở trên, tham số này chính là lớp gốc sau khi đã implement interfaceInvocationHandler
. Lớp này thường được định nghĩa dạng như sau:class FoobarHandler implements InvocationHandler { private Object target; public FoobarHandler(Object t) { target = t; } @Override public Object invoke(Object proxy, Method m, Object[] args) throws Throwable { // do something here //... //... //invoke actual method return m.invoke(target, args); } }
-
Khi có bất kì phương thức nào gọi trên đối tượng
proxy
, phương thức đó sẽ không được thực thi ngay lập tức, mà hàmpublic Object invoke
sẽ được thực thi.- Phương thức gốc được gọi ban đầu sẽ được "đóng gói" lại, truyền vào phương thức
public Object invoke
củahandler
dưới dạng tham sốMethod m
. - Tham số của phương thức ban đầu cũng sẽ được "đóng gói" và truyền vào dưới dạng tham số thứ 3
Object[] args
- Tham số đầu tiên
Object proxy
là proxy củahandler
này.
- Phương thức gốc được gọi ban đầu sẽ được "đóng gói" lại, truyền vào phương thức
-
Phương thức gốc sẽ được thực thi bằng lệnh
m.invoke(target, args)
. Lưu ý phân biệt tên methodinvoke
của interfaceInvocationHandler
và methodm.invoke
của đối tượngMethod m
.
2. Thử tạo một đối tượng Proxy
Đặt ra một bài toán đơn giản như sau:
- Bạn Dẫu cần viết một chương trình thực hiện việc tìm kiếm nhị phân. Vì đi đâu cũng nhanh như gió lốc, nên Dẫu quyết định sử dụng phương thức
Arrays.binarySearch
có sẵn để thực hiện cho nhanh. Tuy nhiên thầy giáo lại bắt Dẫu phải nêu ra phương thức này chạy cụ thể như thế nào, bằng việc in ra màn hình trực tiếp từng bước chạy của chương trình. Nếu tự viết hàm tìm kiếm thì Dẫu có thểSystem.out.println()
để in ra các bước chạy, tuy nhiên Dẫu lại không biết tự viết nên đành phải dùng phương thức có sẵn, nhưng dùng phương thức có sẵn thì làm thế nào để in ra được từng bước chạy?? May mắn là Dẫu biết cách dùng Proxy, nên Dẫu viết chương trình như sau:
Lớp DebugHandler là lớp implement interface InvocationHandler:
class DebugHandler implements InvocationHandler {
// original Object
private Object target;
public DebugHandler(Object t) {
target = t;
}
@Override
public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
// print implicit argument
System.out.print(target);
// print method name
System.out.print("." + m.getName() + "(");
// print explicit arguments
if(args != null) {
for(int i=0; i<args.length; i++) {
System.out.print(args[i]);
if(i<args.length - 1) System.out.println(", ");
}
}
System.out.println(")");
//invoke actual method
return m.invoke(target, args);
}
}
Lớp ProxyTest chứa hàm main thực thi chương trình chính:
public class ProxyTest {
public static void main(String[] args) {
Object[] elements = new Object[1000];
// wrap 1000 Integers from 1 to 1000 by 1000 proxies and assign them to the Object array named "elements"
for(int i=0; i<elements.length; i++) {
Integer value = i+1;
// wrap
InvocationHandler handler = new DebugHandler(value);
// create proxy for the wrapped handler
Object proxy = Proxy.newProxyInstance(null, new Class[] { Comparable.class }, handler);
elements[i] = proxy;
}
// construct a random Integer to be found by Binary Search
Integer key = new Random().nextInt(elements.length) + 1;
System.out.println("Key: " + key);
// search for the key
int result = Arrays.binarySearch(elements, key);
// print match if found
if(result >= 0) System.out.println(elements[result].toString());
}
}
Kết quả chạy chương trình: (chương trình có thể tạo kết quả khác tùy vào giá trị Key được khởi tạo ngẫu nhiên)
Key: 140
500.compareTo(140)
250.compareTo(140)
125.compareTo(140)
187.compareTo(140)
156.compareTo(140)
140.compareTo(140)
140.toString()
140
Giải thích:
- Ở lớp
DebugHandler
:- Đối tượng kiểu Object
target
: Khi chương trình chính gọiInvocationHandler handler = new DebugHandler(value);
để tạo handler cho các proxy, thì các số nguyên Integer sẽ được gán vào cho Objecttarget
này. - Constructor
DebugHandler
: dễ hiểu, nó thực hiện việc gán đối tượng ban đầu vào biếntarget
khi khởi tạo proxy. - Phương thức
invoke
: là phương thức Override lại của InterfaceInvocationHandler
. Khi có bất kì lời gọi phương thức nào xảy ra trên đối tượng proxy (ví dụ khi đối tượng proxyelements[0]
được gọielements[0].compareTo()
), phương thức đó sẽ bị "đóng gói lại" truyền vào phương thứcinvoke
của interfaceInvocationHandler
dưới dạng tham số thứ 2Method m
, các tham số cho phương thức ban đầu (nếu có) cũng sẽ được "đóng gói", truyền vào dưới dạng tham số thứ 3Object[] args
sau đó thực thi hàminvoke
này, cuối hàminvoke
sẽ thực hiện lời gọi phương thức ban đầum.invoke(target, args)
.
- Đối tượng kiểu Object
- Chương trình main trong lớp
ProxyTest
tạo 1000 thực thể proxy kiểuObject
, 1000 proxy này wrap - đại diện cho 1000 đối tượng Integer có giá trị từ 1 đến 1000. Các thực thể proxy này được gán vào mảngObject[] elements
. - Khởi tạo giá trị
key
là một số ngẫu nhiên cần tìm kiếm trong khoảng 1 đến 1000. - Gọi phương thức
Arrays.binarySearch(elements, key)
thực hiện tìm kiếm phần tử thuộc mảngelements
"bằng" vớikey
.- Chúng ta đều biết, tìm kiếm nhị phân phải thực hiện phép so sánh, và trong phương thức
Arrays.binarySearch
, phép so sánh thực hiện bằng cách gọi hàmcompareTo()
có dạng như thế này:if (elements[i].compareTo(key) < 0) . . .
- Lời gọi compareTo lên các phần tử proxy trong mảng elements, sẽ được "đóng gói" lại và truyền đến phương thức
invoke
của lớpDebugHandler
dưới dạng tham số như đã viết ở trên. Tuy nhiên để ý 1 điều, lớp Object của Java không có phương thứccompareTo()
, vậy làm sao đối tượng proxy ban đầu biếtcompareTo()
là gì để nó đóng gói? Câu trả lời nằm ở tham số thứ 2 củanewProxyInstance
ở chương trình main. Nó đã được implements lại interfaceComparable
trong thời điểm runtime của chương trình. Phương thứcinvoke
thực thi theo nội dung trong code, in ra đối tượngtarget
ở đây là giá trị sẽ được so sánh vớikey
, tên phương thứcMethod m
, tham sốkey
và cho ra kết quả:500.compareTo(140)
,... - Dòng cuối cùng trong kết quả
140.toString()
là từ câu lệnh cuối cùng:
Tương tự như trên, phương thứcif(result >= 0) System.out.println(elements[result].toString());
toString()
được gọi lên phần tử proxy tìm thấy trong mảng sau khi chạy tìm kiếm nhị phân, nó gọi lại phương thứcinvoke
của lớpDebugHandler
và in ra nội dung gọi hàm. - Dòng kết quả cuối cùng
140
là kết quả trả về sau khi thực hiệninvoke()
, hay nói cách khác, thực hiệntoString()
và được in ra bởiSystem.out.println
.
- Chúng ta đều biết, tìm kiếm nhị phân phải thực hiện phép so sánh, và trong phương thức
3. Khi nào nên sử dụng Proxy
- Dẫn hướng các lời gọi phương thức đến các remote servers.
- Log lại các nội dung khi gọi hay kết thúc một phương thức dùng cho mục đích debug. (như trường hợp ví dụ trên).
- Khi một lớp implement nhiều Interface khác nhau, và xuất hiện trường hợp 2 hoặc nhiều Interface có các phương thức giống hoàn toàn với nhau. Có thể tìm hiểu qua link :https://stackoverflow.com/questions/13730419/two-interfaces-specify-methods-with-the-same-signature-but-specified-to-have-di.
- Thực hiện chức năng Lazy loading để tăng hiệu suất làm việc của chương trình. Lazy loading của Framework Hibernate là một ví dụ điển hình.
- ...
4. Tài liệu tham khảo
- Sách:
- Cay S. Horstmann (2016), Core Java Volume I - Fundamentals (tenth edition), Prentice Hall.
- Internet:
All rights reserved