Hiểu sâu về Reflection và Dynamic Proxy trong Java
Giới thiệu về Reflection
Reflection là gì?
Reflection (Phản chiếu) là một trong những đặc điểm của ngôn ngữ lập trình Java, cho phép chương trình Java trong quá trình chạy có thể truy cập thông tin về chính nó và thao tác với các thuộc tính bên trong của lớp hoặc đối tượng.
Thông qua cơ chế Reflection, chúng ta có thể truy cập vào các thuộc tính, phương thức, constructor của đối tượng Java trong quá trình chạy.
Các ứng dụng của Reflection
Reflection được sử dụng chủ yếu trong các trường hợp sau:
- Phát triển các framework chung - Reflection là ứng dụng quan trọng nhất của việc phát triển các framework chung. Nhiều framework (ví dụ như Spring) được cấu hình thông qua các tệp XML (ví dụ như cấu hình JavaBean, Filter, v.v.), để đảm bảo tính chung của framework, chúng có thể cần tải các đối tượng hoặc lớp khác nhau dựa trên tệp cấu hình, và gọi các phương thức khác nhau, lúc này cần sử dụng Reflection - tải đối tượng cần tải trong quá trình chạy.
- Động lực hóa - Trong lập trình hướng khía cạnh (AOP), cần chặn các phương thức cụ thể, thường chọn cách động lực hóa. Lúc này, cần sử dụng công nghệ Reflection để thực hiện.
- Chú thích - Chú thích chính nó chỉ đóng vai trò đánh dấu, nó cần sử dụng cơ chế Reflection để gọi trình thông dịch chú thích dựa trên đánh dấu chú thích, thực hiện hành vi. Nếu không có cơ chế Reflection, chú thích không hữu ích hơn chú thích.
- Tính năng mở rộng - Ứng dụng có thể sử dụng Reflection để tạo các phiên bản đối tượng có thể mở rộng bằng cách sử dụng tên đầy đủ của nó.
Nhược điểm của Reflection
- Tốn kém về hiệu năng - Do Reflection liên quan đến việc phân tích động các loại, nên không thể thực hiện một số tối ưu hóa của máy ảo Java. Do đó, hiệu năng của các hoạt động Reflection thường kém hơn so với các hoạt động không sử dụng Reflection, nên nên tránh sử dụng trong các đoạn mã cần tần suất gọi trong các ứng dụng nhạy cảm về hiệu năng.
- Phá vỡ tính đóng gói - Khi gọi phương thức bằng Reflection, có thể bỏ qua kiểm tra quyền truy cập, do đó có thể phá vỡ tính đóng gói và gây ra vấn đề về bảo mật.
- Tiết lộ thông tin nội bộ - Do Reflection cho phép mã thực thi các hoạt động không hợp lệ trong mã không sử dụng Reflection, ví dụ như truy cập vào các trường và phương thức riêng tư, do đó việc sử dụng Reflection có thể gây ra các tác động phụ không mong muốn, dẫn đến lỗi chức năng và có thể phá vỡ tính di động của mã. Mã Reflection phá vỡ tính trừu tượng, do đó có thể thay đổi hành vi theo cách không mong muốn khi nâng cấp nền tảng.
Cơ chế Reflection
Quá trình tải lớp
Quá trình tải lớp hoàn chỉnh như sau:
- Trong quá trình biên dịch, trình biên dịch Java biên dịch tệp
.java
thành tệp.class
. Tệp.class
là tệp nhị phân chứa mã máy chỉ JVM mới có thể nhận dạng. - Trình tải lớp trong JVM đọc tệp bytecode, trích xuất dữ liệu nhị phân và tải lên bộ nhớ, phân tích cú pháp và thông tin trong tệp
.class
. Trình tải lớp sẽ sử dụng tên đầy đủ của lớp để lấy luồng byte của lớp này; sau đó, nó sẽ chuyển đổi cấu trúc lưu trữ tĩnh này thành cấu trúc dữ liệu thời gian chạy của vùng nhớ phương thức. - Sau khi quá trình tải lớp kết thúc, JVM bắt đầu giai đoạn kết nối (bao gồm xác minh, chuẩn bị, khởi tạo). Sau một loạt các hoạt động này, biến của lớp sẽ được khởi tạo.
Đối tượng Class
Để sử dụng Reflection, trước tiên cần có đối tượng Class tương ứng với lớp mà ta muốn thao tác. Trong Java, dù có bao nhiêu đối tượng của một lớp được tạo ra, chúng đều tương ứng với cùng một đối tượng Class. Đối tượng Class này được tạo ra bởi JVM và cho phép ta truy cập vào toàn bộ cấu trúc của lớp.
Reflection thực chất là việc ánh xạ các thành phần của lớp Java thành các đối tượng Java.
Ví dụ, nếu có đoạn mã sau:
Các bước thực hiện như sau:
- Khi JVM tải lớp, nếu gặp
new User()
thì JVM sẽ tải lớpUser.class
. - JVM sẽ tìm kiếm tệp
User.class
trên đĩa và tải nó vào bộ nhớ JVM. - JVM sẽ tạo một đối tượng
java.lang.Class
proxy cho lớpUser
và lưu trữ nó trong vùng nhớ của JVM. Lưu ý rằng một lớp chỉ có một đối tượngClass
tương ứng.
Gọi phương thức bằng Reflection
Gọi phương thức bằng Reflection được thực hiện bằng cách sử dụng phương thức Method.invoke
.
Mã nguồn của phương thức Method.invoke
:
Phương thức Method.invoke
thực tế sẽ gọi phương thức invoke
của giao diện MethodAccessor
. Nó có hai cài đặt cụ thể đã có:
NativeMethodAccessorImpl
: Sử dụng phương thức native để triển khai gọi phản chiếu.DelegatingMethodAccessorImpl
: Sử dụng mô hình ủy quyền để triển khai gọi phản chiếu.
Mỗi đối tượng Method
sẽ tạo ra một cài đặt ủy quyền (DelegatingMethodAccessorImpl) khi gọi phản chiếu lần đầu tiên. Cài đặt ủy quyền này sẽ ủy quyền cho một cài đặt cụ thể (NativeMethodAccessorImpl), nơi thực sự gọi phương thức. Cài đặt cụ thể này rất dễ hiểu. Khi chúng ta đã vào bên trong máy ảo Java, chúng ta đã có địa chỉ cụ thể của phương thức mà đối tượng Method
đang chỉ đến. Lúc này, việc gọi phản chiếu chỉ đơn giản là chuẩn bị các tham số và gọi phương thức đích.
【Ví dụ】In ra thông tin gọi phương thức Method.invoke
Đầu tiên, nó gọi DelegatingMethodAccessorImpl
và sau đó gọi NativeMethodAccessorImpl
và cuối cùng gọi phương thức thực tế.
Tại sao gọi DelegatingMethodAccessorImpl
làm trung gian, thay vì gọi trực tiếp cài đặt native?
Thực tế, Java còn có một cài đặt tạo mã bytecode động (dynamic bytecode) khác, sử dụng chỉ thị invoke để gọi phương thức đích. So với cài đặt native, cài đặt tạo mã bytecode động chạy nhanh hơn gấp 20 lần. Điều này bởi vì cài đặt tạo mã bytecode động không cần thông qua quá trình chuyển đổi từ Java sang C++ rồi lại chuyển đổi từ C++ sang Java. Tuy nhiên, việc tạo bytecode rất tốn thời gian, chỉ cần gọi một lần thì cài đặt native lại nhanh hơn 3-4 lần.
Với mục tiêu làm việc hiệu quả, Java Virtual Machine (JVM) đã đặt một ngưỡng là 15 (có thể điều chỉnh bằng cách sử dụng -Dsun.reflect.inflationThreshold), khi số lần gọi phản chiếu vượt qua ngưỡng này, JVM sẽ bắt đầu tạo bytecode động và chuyển đổi đối tượng ủy quyền từ cài đặt native sang cài đặt bytecode động. Quá trình này được gọi là Inflation.
【Ví dụ】Chạy java -verbose:class MethodDemo02
Kết quả:
Có thể thấy, từ lần thứ 16 trở đi, chỉ sử dụng DelegatingMethodAccessorImpl
và không sử dụng cài đặt cục bộ NativeMethodAccessorImpl
nữa.
Hiệu suất của việc gọi phản chiếu
Việc gọi phản chiếu của phương thức có thể gây ra một số hiệu suất kém, chủ yếu do ba nguyên nhân:
- Mảng Object gây ra bởi phương thức có tham số biến đổi
- Tự động đóng gói và mở gói các kiểu dữ liệu cơ bản
- Và quan trọng nhất là việc nội tuyến hóa phương thức
Class.forName
sẽ gọi phương thức cục bộ, trong khi Class.getMethod
sẽ duyệt qua các phương thức công khai của lớp đó. Nếu không tìm thấy, nó sẽ tiếp tục duyệt qua các phương thức công khai của lớp cha. Cả hai hoạt động này đều tốn thời gian.
Lưu ý, các hoạt động tìm kiếm phương thức, ví dụ như
getMethod
, sẽ trả về một bản sao của kết quả tìm kiếm. Do đó, chúng ta nên tránh sử dụnggetMethods
hoặcgetDeclaredMethods
trả về một mảngMethod
trong mã nguồn quan trọng để giảm sự tiêu tốn không cần thiết của bộ nhớ heap. Trong thực tế, chúng ta thường lưu kết quả củaClass.forName
vàClass.getMethod
trong ứng dụng.
Dưới đây chỉ tập trung vào hiệu suất của việc gọi phản chiếu chính nó.
Đầu tiên, vì Method.invoke
là một phương thức có tham số biến đổi, ở mức bytecode, tham số cuối cùng của nó sẽ là một mảng Object (các bạn có thể sử dụng javap
để kiểm tra). Trình biên dịch Java sẽ tạo ra một mảng Object có độ dài bằng số lượng tham số được truyền vào và lưu trữ các tham số trong mảng này.
Thứ hai, vì mảng Object không thể lưu trữ các kiểu dữ liệu cơ bản, trình biên dịch Java sẽ tự động đóng gói các tham số kiểu dữ liệu cơ bản.
Cả hai thao tác này không chỉ gây ra hiệu suất kém mà còn có thể chiếm dụng bộ nhớ heap, làm tăng tần suất của việc thu gom rác (GC). (Nếu bạn quan tâm, bạn có thể thử sử dụng tham số máy ảo -XX:+PrintGC
.)
Vậy làm thế nào để giảm bớt sự tiêu tốn này?
Sử dụng Reflection
Gói java.lang.reflect
Gói java.lang.reflect
trong Java cung cấp các tính năng reflection. Các lớp trong gói java.lang.reflect
không có phương thức khởi tạo công khai (public
).
Các giao diện và lớp chính trong gói java.lang.reflect
bao gồm:
- Giao diện
Member
: Phản ánh thông tin định danh về một thành viên đơn (trường hoặc phương thức) hoặc một hàm tạo. - Lớp
Field
: Cung cấp thông tin về các trường của một lớp và cung cấp giao diện để truy cập vào các trường của lớp. - Lớp
Method
: Cung cấp thông tin về các phương thức của một lớp và cung cấp giao diện để truy cập vào các phương thức của lớp. - Lớp
Constructor
: Cung cấp thông tin về các hàm tạo của một lớp và cung cấp giao diện để truy cập vào các hàm tạo của lớp. - Lớp
Array
: Cung cấp các phương thức để tạo và truy cập mảng Java động. - Lớp
Modifier
: Cung cấp các phương thức tĩnh và hằng số để giải mã các quyền truy cập và sửa đổi của lớp và thành viên. - Lớp
Proxy
: Cung cấp các phương thức tĩnh để tạo lớp proxy và các phiên bản lớp động.
Lấy đối tượng Class
Có ba cách để lấy đối tượng Class
:
(1) Phương thức tĩnh Class.forName
【Ví dụ】Sử dụng phương thức tĩnh Class.forName
để lấy đối tượng Class
Sử dụng tên đầy đủ của lớp để phản ánh lớp đối tượng. Ứng dụng phổ biến là: sử dụng phương thức này để tải trình điều khiển cơ sở dữ liệu trong phát triển JDBC.
(2) Tên lớp + .class
【Ví dụ】Sử dụng tên lớp + .class
để lấy đối tượng Class
(3) Phương thức getClass
của đối tượng Object
Lớp Object
có phương thức getClass
, vì tất cả các lớp đều kế thừa từ lớp Object
. Do đó, có thể sử dụng lớp Object
để lấy đối tượng Class
.
【Ví dụ】Sử dụng phương thức getClass
của đối tượng Object
để lấy đối tượng Class
Kiểm tra xem một đối tượng có phải là một thể hiện của một lớp nào đó không
Có hai cách để kiểm tra xem một đối tượng có phải là một thể hiện của một lớp nào đó không:
- Sử dụng từ khóa
instanceof
- Sử dụng phương thức
isInstance
của đối tượngClass
(đây là một phương thức Native)
【Ví dụ】
Tạo thể hiện
Có hai cách chính để tạo thể hiện đối tượng bằng cách sử dụng reflection:
- Sử dụng phương thức
newInstance
của đối tượngClass
. - Sử dụng phương thức
newInstance
của đối tượngConstructor
.
【Ví dụ】
Tạo thể hiện mảng
Mảng là một kiểu đặc biệt của kiểu trong Java và nó có thể được gán cho một tham chiếu đối tượng. Trong Java, sử dụng Array.newInstance
để tạo thể hiện mảng.
【Ví dụ】Sử dụng reflection để tạo mảng
Trong đó, lớp Array là lớp java.lang.reflect.Array
. Nguyên mẫu của Array.newInstance
là:
Field
Đối tượng Class
cung cấp các phương thức sau để lấy thành viên (Field) của đối tượng:
getFiled
- Lấy thành viên công khai (public) của lớp dựa trên tên.getDeclaredField
- Lấy thành viên đã khai báo của lớp dựa trên tên. Tuy nhiên, không thể lấy được thành viên của lớp cha.getFields
- Lấy tất cả các thành viên công khai (public) của lớp.getDeclaredFields
- Lấy tất cả các thành viên đã khai báo của lớp.
Ví dụ:
Method
Lớp Class
cung cấp các phương thức sau để lấy các phương thức (method) của đối tượng:
getMethod
- Trả về phương thức cụ thể của lớp hoặc giao diện. Tham số đầu tiên là tên phương thức, các tham số sau là các đối tượng Class tương ứng với các tham số của phương thức.getDeclaredMethod
- Trả về phương thức khai báo cụ thể của lớp hoặc giao diện. Tham số đầu tiên là tên phương thức, các tham số sau là các đối tượng Class tương ứng với các tham số của phương thức.getMethods
- Trả về tất cả các phương thức public của lớp hoặc giao diện, bao gồm cả các phương thức public của lớp cha.getDeclaredMethods
- Trả về tất cả các phương thức đã khai báo của lớp hoặc giao diện, bao gồm các phương thức public, protected, default và private, nhưng không bao gồm các phương thức được kế thừa.
Sau khi có một đối tượng Method
, có thể sử dụng phương thức invoke
để gọi phương thức đó.
Nguyên mẫu của phương thức invoke
là:
【Ví dụ】
Constructor
Lớp Class
cung cấp các phương thức sau để lấy các constructor (hàm tạo) của đối tượng:
getConstructor
- Trả về constructor public cụ thể của lớp. Tham số là các đối tượng Class tương ứng với các tham số của constructor.getDeclaredConstructor
- Trả về constructor cụ thể của lớp. Tham số là các đối tượng Class tương ứng với các tham số của constructor.getConstructors
- Trả về tất cả các constructor public của lớp.getDeclaredConstructors
- Trả về tất cả các constructor của lớp.
Sau khi có một đối tượng Constructor
, có thể sử dụng phương thức newInstance
để tạo một thể hiện của lớp.
【Ví dụ】
Vượt qua giới hạn truy cập
Đôi khi, chúng ta cần truy cập vào các thành viên, phương thức riêng tư (private). Có thể sử dụng Constructor/Field/Method.setAccessible(true)
để vượt qua giới hạn truy cập trong Java.
Dynamic Proxy
Dynamic Proxy là một cơ chế tiện lợi để xây dựng proxy và xử lý động các cuộc gọi phương thức của proxy trong thời gian chạy. Nhiều tình huống trong lập trình sử dụng cơ chế tương tự, chẳng hạn như đóng gói các lệnh gọi RPC, lập trình hướng khía cạnh (AOP).
Có nhiều cách để thực hiện dynamic proxy, ví dụ như dynamic proxy được cung cấp bởi JDK sử dụng cơ chế phản chiếu đã được đề cập ở trên. Có các phương pháp triển khai khác, chẳng hạn như sử dụng cơ chế hoạt động mã byte hiệu suất cao như ASM, cglib (dựa trên ASM), Javassist, v.v.
Static Proxy
Static Proxy thực chất là mô hình proxy trong design pattern.
Mô hình proxy cung cấp một proxy cho các đối tượng khác để kiểm soát việc truy cập vào đối tượng đó.
Subject xác định giao diện chung của RealSubject và Proxy, điều này cho phép Proxy được sử dụng ở bất kỳ đâu mà RealSubject được sử dụng.
RealSubject xác định thực thể thực sự mà Proxy proxy.
Proxy lưu trữ một tham chiếu cho phép Proxy truy cập vào thực thể và cung cấp một giao diện giống như giao diện của Subject, điều này cho phép Proxy thay thế thực thể.
Giải thích:
Mặc dù mô hình static proxy có nhiều ưu điểm trong việc truy cập vào tài nguyên không thể truy cập và tăng cường chức năng kinh doanh của giao diện hiện có, nhưng việc sử static proxy một cách lớn sẽ làm tăng kích thước của lớp trong hệ thống và khó bảo trì; và vì chức năng của Proxy và RealSubject cơ bản là giống nhau, Proxy chỉ đóng vai trò là một trung gian, sự tồn tại của proxy này trong hệ thống dẫn đến cấu trúc hệ thống trở nên phình to và lỏng lẻo.
Dynamic Proxy JDK
Để giải quyết vấn đề của static proxy, đã có ý tưởng tạo ra một dynamic proxy:
Trong trạng thái chạy, nơi cần proxy, dựa trên Subject và RealSubject, tạo động một Proxy, sau khi sử dụng xong, nó sẽ bị hủy, điều này giúp tránh vấn đề dư thừa các lớp Proxy trong hệ thống.
Java dynamic proxy dựa trên mô hình proxy cổ điển, giới thiệu một InvocationHandler
, InvocationHandler
chịu trách nhiệm quản lý tất cả cuộc gọi phương thức của Proxy.
Các bước dynamic proxy:
- Lấy danh sách giao diện trên RealSubject;
- Xác định tên lớp của lớp Proxy được tạo ra, mặc định là:
com.sun.proxy.$ProxyXXXX
; - Dựa trên thông tin giao diện cần triển khai, tạo mã byte của lớp Proxy trong mã nguồn;
- Chuyển đổi mã byte tương ứng thành đối tượng lớp;
- Tạo một đối tượng handler của
InvocationHandler
để xử lý tất cả cuộc gọi phương thức của Proxy; - Sử dụng đối tượng class của Proxy để khởi tạo một đối tượng proxy, sử dụng đối tượng handler đã tạo.
Nhìn vào trên, chúng ta có thể thấy rằng, cách triển khai dynamic proxy JDK dựa trên việc triển khai giao diện, làm cho Proxy và RealSubject có cùng chức năng.
Tuy nhiên, thực tế còn một cách suy nghĩ khác: thông qua kế thừa. Nghĩa là: cho phép Proxy kế thừa RealSubject, điều này làm cho cả hai đều có chức năng giống nhau, Proxy còn có thể triển khai đa hình bằng cách ghi đè các phương thức trong RealSubject. CGLIB được thiết kế dựa trên cách suy nghĩ này.
Trong cơ chế dynamic proxy của Java, có hai lớp (giao diện) quan trọng, một là giao diện InvocationHandler
, một là lớp Proxy
, lớp này và một giao diện là những gì chúng ta cần để triển khai dynamic proxy.
Giao diện InvocationHandler
Giao diện InvocationHandler được định nghĩa như sau:
Mỗi lớp dynamic proxy cần phải triển khai giao diện InvocationHandler này và mỗi đối tượng proxy được liên kết với một Handler. Khi chúng ta gọi một phương thức thông qua đối tượng proxy, cuộc gọi phương thức sẽ được chuyển tiếp cho phương thức invoke của giao diện InvocationHandler.
Chúng ta hãy xem xét phương thức invoke trong giao diện InvocationHandler
:
Giải thích các tham số:
- proxy - Đối tượng thực sự của proxy.
- method - Đối tượng Method của phương thức cần gọi trên đối tượng thực sự.
- args - Các tham số được truyền cho phương thức cần gọi trên đối tượng thực sự.
Nếu bạn chưa hiểu rõ, chúng ta sẽ đi vào chi tiết các tham số này thông qua một ví dụ cụ thể.
Lớp Proxy
Lớp Proxy được sử dụng để tạo đối tượng dynamic proxy. Nó cung cấp nhiều phương thức, nhưng phương thức chúng ta sử dụng nhiều nhất là phương thức newProxyInstance
:
Phương thức này được sử dụng để tạo một đối tượng dynamic proxy.
Giải thích các tham số:
- loader - Một đối tượng ClassLoader, xác định lớp nào sẽ tải đối tượng proxy được tạo ra.
- interfaces - Một mảng đối tượng
Class<?>
, proxy cho một tập hợp các giao diện mà đối tượng proxy sẽ triển khai. Nếu chúng ta cung cấp một tập hợp giao diện, đối tượng proxy sẽ được coi là triển khai các giao diện này (đa hình), cho phép chúng ta gọi các phương thức trong tập hợp giao diện này. - h - Một đối tượng InvocationHandler, proxy cho đối tượng Handler mà đối tượng proxy sẽ liên kết với.
Ví dụ về dynamic proxy JDK
Sau khi đã giới thiệu hai giao diện (lớp) trên, chúng ta sẽ xem xét một ví dụ để hiểu rõ hơn về mô hình dynamic proxy của chúng ta:
Đầu tiên, chúng ta định nghĩa một giao diện Subject, và khai báo hai phương thức cho nó:
Tiếp theo, chúng ta định nghĩa một lớp để triển khai giao diện này, đây là đối tượng thực sự của chúng ta, lớp RealSubject:
Tiếp theo, chúng ta sẽ định nghĩa một lớp dynamic proxy, như đã đề cập trước đó, mỗi lớp dynamic proxy đều phải triển khai giao diện InvocationHandler, vì vậy lớp dynamic proxy của chúng ta cũng không ngoại lệ:
Cuối cùng, hãy xem xét lớp Client của chúng ta:
Trước tiên, hãy xem xét đầu ra của bảng điều khiển:
com.sun.proxy.$Proxy0
Before method
Call Method: public abstract void com.hnv99.javacore.reflect.InvocationHandlerDemo$Subject.hello(java.lang.String)
Hello World
After method
Before method
Call Method: public abstract java.lang.String com.hnv99.javacore.reflect.InvocationHandlerDemo$Subject.bye()
Goodbye
After method
Result is: Over
Trước tiên, chúng ta hãy xem xét phần “com.sun.proxy.$Proxy0”, chúng ta thấy rằng điều này được in ra bởi câu lệnh System.out.println(subject.getClass().getName());
, vậy tại sao tên lớp proxy trả về lại như vậy?
Có thể tôi nghĩ rằng đối tượng proxy trả về sẽ là một đối tượng kiểu Subject hoặc là một đối tượng kiểu InvocationHandler, nhưng kết quả lại không phải như vậy. Đầu tiên, chúng ta hãy giải thích tại sao chúng ta có thể chuyển đổi nó thành một đối tượng kiểu Subject?
Lý do là: trên tham số thứ hai của phương thức newProxyInstance này, chúng ta cung cấp một tập hợp các giao diện, vì vậy đối tượng proxy sẽ triển khai các giao diện này, trong trường hợp này là Subject, vì vậy chúng ta có thể chuyển đổi nó thành kiểu Subject.
Đồng thời, chúng ta nhớ rằng đối tượng proxy được tạo ra bằng cách sử dụng Proxy.newProxyInstance
là một đối tượng động được tạo ra trong quá trình chạy của JVM, nó không phải là một đối tượng kiểu InvocationHandler hoặc là một đối tượng kiểu giao diện mà chúng ta đã định nghĩa, mà là một đối tượng được tạo ra trong quá trình chạy và được đặt tên theo cách này, bắt đầu bằng $, proxy là trung gian và số cuối cùng proxy cho số thứ tự của đối tượng.
Tiếp theo, chúng ta hãy xem xét hai câu lệnh sau:
Ở đây, chúng ta gọi phương thức của đối tượng thực sự thông qua đối tượng proxy, lúc này chương trình sẽ chuyển hướng đến phương thức invoke của đối tượng Handler liên kết với nó để thực hiện, và đối tượng Handler của chúng ta nhận một tham số kiểu RealSubject, proxy cho đối tượng thực sự mà chúng ta muốn, vì vậy lúc này phương thức invoke sẽ được gọi.
Chúng ta thấy rằng khi gọi phương thức thực sự của đối tượng thông qua đối tượng proxy, chúng ta có thể thêm một số hoạt động của riêng mình trước và sau phương thức đó, đồng thời chúng ta cũng thấy rằng đối tượng method trong phương thức invoke của chúng ta có dạng như sau:
Chính xác là các phương thức trong giao diện Subject của chúng ta, điều này cũng chứng minh rằng khi chúng ta gọi phương thức thông qua đối tượng proxy, thực tế là chúng ta đang gọi thông qua các proxy, không phải tự gọi.
Tổng kết về Dynamic Proxy JDK
Lớp proxy và lớp ủy nhiệm cùng triển khai cùng một giao diện, chủ yếu là thông qua lớp proxy triển khai InvocationHandler
và ghi đè phương thức invoke
để triển khai dynamic proxy và phương thức invoke
được xử lý trong phương thức.
Đặc điểm của dynamic proxy JDK:
- Ưu điểm: So với mô hình static proxy, không cần mã hóa cứng giao diện, tỷ lệ tái sử dụng mã cao.
- Nhược điểm: Yêu cầu lớp proxy triển khai giao diện
InvocationHandler
.
Dynamic Proxy CGLIB
CGLIB cung cấp một giải pháp khác so với dynamic proxy JDK. Nhiều framework, ví dụ như Spring AOP, sử dụng dynamic proxy CGLIB.
CGLIB hoạt động dựa trên ASM, một framework mã bytecode Java mạnh mẽ để thực hiện tăng cường mã bytecode.
Các bước làm việc của dynamic proxy CGLIB:
- Tạo tệp bytecode nhị phân cho lớp proxy;
- Tải bytecode nhị phân và tạo đối tượng
Class
(ví dụ: sử dụng phương thứcClass.forName()
); - Sử dụng cơ chế phản chiếu để lấy hàm tạo và tạo đối tượng lớp proxy.
Đặc điểm của dynamic proxy CGLIB:
Ưu điểm: Sử dụng tăng cường mã bytecode, hiệu năng cao hơn so với cách dynamic proxy JDK. Có thể tăng cường lớp hoặc giao diện trong thời gian chạy, và lớp ủy nhiệm không cần triển khai giao diện.
Nhược điểm: Không thể proxy cho lớp final
hoặc phương thức final
.