Proxy pattern đặt một đối tượng thay thế đứng trước đối tượng thật, cùng giao diện, để kiểm soát việc truy cập: chặn, ghi log, cache, kiểm tra quyền, hoặc trì hoãn khởi tạo. Trong Java, Proxy pattern xuất hiện ở ba tầng: tự viết tay như lớp bọc thông thường, sinh lúc chạy bằng java.lang.reflect.Proxy (JDK dynamic proxy), và sinh lớp con lúc chạy bằng CGLIB, thứ nằm sau @Transactional, @Cacheable và @Async của Spring. Bài này đi từ khái niệm đến code chạy thật (biên dịch cho Java 17) với Spring Boot 3.0, kèm những cái bẫy khiến annotation "không có tác dụng" trong dự án thật.
Bài trước, Decorator pattern: học từ java.io và áp dụng cho dịch vụ, đã cho thấy một chồng lớp bọc cùng giao diện. Proxy pattern có hình dạng gần như giống hệt nhưng ý định khác, và bài này chỉ rõ điểm khác đó. Nếu cần nhắc lại cách tách phụ thuộc bằng interface, xem Nguyên lý SOLID trong Java qua ví dụ thực tế.
Proxy pattern giải quyết vấn đề gì
Để thấy vì sao cần Proxy pattern, hãy hình dung ProductService truy vấn cơ sở dữ liệu. Bạn muốn ghi log mọi lời gọi, cache kết quả đọc, và kiểm tra quyền trước khi sửa. Nhét tất cả vào ProductServiceImpl làm lớp này lẫn nghiệp vụ với các mối quan tâm hạ tầng, vi phạm Single Responsibility. Ngược lại, bắt mọi nơi gọi tự bọc log và cache sẽ lặp code. Proxy pattern cho phép client vẫn gọi ProductService như cũ, còn một đối tượng trung gian làm phần hạ tầng rồi mới chuyển lời gọi cho đối tượng thật.
Theo refactoring.guru, Proxy pattern cung cấp một "substitute or placeholder" cho đối tượng khác và kiểm soát truy cập vào nó, cho phép chạy logic trước hoặc sau khi yêu cầu tới đối tượng gốc. Các biến thể thường gặp:
| Loại proxy | Việc làm | Ví dụ quen thuộc |
|---|---|---|
| Virtual proxy | Trì hoãn tạo đối tượng nặng tới khi thật sự cần | Lazy loading của quan hệ trong ORM |
| Protection proxy | Chỉ chuyển yêu cầu khi người gọi đủ quyền | Method security trong Spring Security |
| Remote proxy | Đại diện cho đối tượng nằm trên máy khác | Client RPC, Feign, JAX-WS |
| Logging proxy | Ghi lại mỗi lời gọi rồi chuyển tiếp | Aspect ghi log |
| Caching proxy | Lưu kết quả của lời gọi lặp lại | @Cacheable |
| Smart reference | Theo dõi người dùng và giải phóng đối tượng khi không còn ai dùng | Đếm tham chiếu, connection pool |
Cấu trúc Proxy pattern và khác biệt với Decorator
Cấu trúc của Proxy pattern gồm bốn phần: Service Interface mà cả proxy và đối tượng thật cùng cài đặt, Service là đối tượng thật, Proxy giữ tham chiếu tới Service và chuyển tiếp lời gọi, và Client chỉ biết giao diện nên nhận proxy hay đối tượng thật đều dùng như nhau. Hình dạng này trùng với Decorator, nên khác biệt nằm ở ý định và vòng đời:
- Decorator thêm hành vi và do client lắp ráp, xếp chồng nhiều lớp theo ý muốn.
- Proxy pattern kiểm soát truy cập và thường tự quản lý vòng đời đối tượng thật: tự tạo nó (lazy), tự quyết định có gọi hay không (quyền, cache).
- Adapter đổi giao diện; Proxy pattern giữ nguyên giao diện.
- Facade cũng ngăn cách client với phần phức tạp, nhưng Facade đưa ra giao diện đơn giản hơn và khác, còn Proxy pattern dùng chung giao diện với đối tượng thật nên hai bên hoán đổi được. Facade là chủ đề của bài kế tiếp.
Trong thực tế ranh giới giữa hai mẫu này mờ. Một lớp bọc cache có thể gọi là Decorator hay Proxy pattern tuỳ bạn nhìn ý định nào. Đừng tốn thời gian tranh luận nên gọi là Proxy hay Decorator; hãy chọn tên phản ánh đúng ý định để người đọc code hiểu nhanh.
Cài đặt Proxy pattern bằng tay
Cách trực tiếp nhất là viết một lớp cài cùng interface. Ví dụ cache cho thao tác đọc tên sản phẩm:
interface ProductService {
String findName(long id);
}
class CachingProductProxy implements ProductService {
private final ProductService real;
private final Map<Long, String> cache = new ConcurrentHashMap<>();
CachingProductProxy(ProductService real) { this.real = real; }
public String findName(long id) {
return cache.computeIfAbsent(id, real::findName);
}
}Với một interface chỉ có một phương thức thì viết proxy bằng tay là đủ và rõ ràng nhất. Vấn đề của cách viết tay xuất hiện khi interface có mười phương thức và bạn muốn cùng một logic (log, đo thời gian, kiểm tra quyền) áp dụng cho tất cả, hoặc muốn áp dụng cho hàng chục interface khác nhau. Lúc đó cần dùng Proxy pattern ở dạng proxy sinh động lúc chạy.
JDK dynamic proxy: Proxy pattern sinh lúc chạy
Từ Java 1.3, java.lang.reflect.Proxy tạo một lớp cài đặt các interface bạn chỉ định ngay trong lúc chạy, và chuyển mọi lời gọi tới một InvocationHandler. Theo Javadoc của Proxy, Proxy.newProxyInstance(loader, interfaces, handler) trả về đối tượng cài đặt các interface đó, và lời gọi một phương thức interface luôn được chuyển tới invoke(Object proxy, Method method, Object[] args). Giá trị invoke trả về trở thành kết quả lời gọi.
interface ProductService {
String findName(long id);
void rename(long id, String name) throws java.io.IOException;
}
static class LoggingCachingHandler implements InvocationHandler {
private final Object target;
private final Map<String, Object> cache = new HashMap<>();
LoggingCachingHandler(Object target) { this.target = target; }
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getDeclaringClass() == Object.class) {
System.out.println(" [proxy] nhận " + method.getName() + " (khai báo ở Object)");
}
String key = method.getName() + Arrays.toString(args);
if (method.getName().equals("findName") && cache.containsKey(key)) {
System.out.println(" [proxy] cache hit " + key);
return cache.get(key);
}
// args là null khi phương thức không có tham số (ví dụ hashCode)
String hienThi = args == null ? "()" : Arrays.toString(args);
System.out.println(" [proxy] -> " + method.getName() + hienThi);
try {
Object result = method.invoke(target, args);
if (method.getName().equals("findName")) cache.put(key, result);
return result;
} catch (InvocationTargetException e) {
System.out.println(" [proxy] !! " + e.getCause());
throw e.getCause(); // bỏ lớp bọc phản chiếu để client thấy đúng ngoại lệ
}
}
}
ProductService proxy = (ProductService) Proxy.newProxyInstance(
ProductService.class.getClassLoader(),
new Class<?>[] { ProductService.class },
new LoggingCachingHandler(real));Lưu ý: LoggingCachingHandler dùng HashMap không an toàn đa luồng, chỉ là code minh hoạ; bản dùng thật cần ConcurrentHashMap hoặc một thư viện cache. Một handler duy nhất phục vụ mọi phương thức, và dùng lại được cho interface khác miễn là target cài đặt interface đó. Biên dịch bằng javac --release 17 rồi chạy java JdkProxyDemo cho output thật (tên lớp dạng $Proxy38 ở phần Spring bên dưới có thể khác số thứ tự giữa các bản JDK, như Javadoc đã cảnh báo, nên đừng dựa vào nó):
1) Gọi findName hai lần:
[proxy] -> findName[7]
[impl] findName(7) chạm DB
kết quả: Sản phẩm #7
[proxy] cache hit findName[7]
kết quả: Sản phẩm #7
2) Ngoại lệ checked khai báo sẵn đi qua proxy nguyên vẹn:
[proxy] -> rename[7, ]
[proxy] !! java.io.IOException: tên rỗng
client bắt được IOException: tên rỗngLần gọi thứ hai không chạm đối tượng thật. Chú ý khối catch (InvocationTargetException e): Method.invoke bọc mọi ngoại lệ của đích trong InvocationTargetException. Nếu bạn quên throw e.getCause(), client sẽ nhận ngoại lệ phản chiếu thay vì IOException gốc, và nếu đó là checked exception không khai báo thì nhận UndeclaredThrowableException, như đoạn sau cho thấy.
Những quy tắc của JDK dynamic proxy, kiểm chứng bằng code
Javadoc nêu nhiều quy tắc; mình kiểm chứng từng cái trong cùng chương trình (đây là phần output các mục 3 đến 7; mục 5 kiểm tra getClass không bị handler chặn). Với hashCode(), tham số args truyền vào handler là null vì phương thức không có tham số, nên code in ra hashCode():
3) Thông tin về lớp proxy:
Proxy.isProxyClass: true
tên lớp bắt đầu bằng $Proxy: true
là final: true
kế thừa Proxy: true
instanceof ProductServiceImpl: false
4) toString/hashCode/equals cũng đi qua handler:
[proxy] nhận hashCode (khai báo ở Object)
[proxy] -> hashCode()
5) getClass không bị chặn:
proxy.getClass() có interface: [interface ProductService]
6) Ngoại lệ checked không khai báo -> UndeclaredThrowableException:
java.lang.reflect.UndeclaredThrowableException <- lỗi checked không khai báo
7) Proxy chỉ cho interface: thử proxy một lớp cụ thể:
IllegalArgumentException: ProductServiceImpl is not an interface- Chỉ proxy được interface. Truyền một lớp cụ thể vào danh sách interface ném
IllegalArgumentException, như dòng 7. Đây là giới hạn cốt lõi dẫn tới nhu cầu CGLIB. - Lớp proxy là final, kế thừa
java.lang.reflect.Proxy. Vì Java chỉ có đơn kế thừa, proxy không thể đồng thời là con của lớp của bạn. Hệ quả:proxy instanceof ProductServiceImpllàfalse, và ép kiểu về lớp cụ thể gâyClassCastException. Hãy luôn khai báo biến bằng kiểu interface. hashCode,equals,toStringcũng đi qua handler, vớiMethodcó lớp khai báo làObject. Handler ngây thơ chuyển tiếpequalssangtargetsẽ cho kết quả so sánh sai (target không bằng proxy). Dòng 4 minh hoạ handler của ta thấyhashCode.- Ngoại lệ checked phải khớp khai báo. Nếu
invokeném checked exception không có trongthrowscủa phương thức interface, proxy némUndeclaredThrowableException(dòng 6). Các phương thứcObjectkhác nhưgetClasshaywaitkhông bị ghi đè (mục 5 trong output ở trên). - Tên lớp proxy không xác định. Javadoc chỉ đảm bảo tên bắt đầu bằng
$Proxylà dành riêng; đừng phụ thuộc vào tên cụ thể.
CGLIB: Proxy pattern bằng lớp con khi không có interface
Khi lớp đích không cài interface nào, JDK dynamic proxy bó tay. Giải pháp là sinh lớp con của chính lớp đích lúc chạy và ghi đè các phương thức để chèn logic. Đây là việc của CGLIB (Code Generation Library), thư viện mã nguồn mở mà Spring đóng gói lại bên trong spring-core. Vì là lớp con, proxy CGLIB là một lớp đích về mặt kiểu, nên instanceof OrderService trả true; đồng thời nó kế thừa các giới hạn của kế thừa, mà phần dưới nêu cụ thể.
Tài liệu Spring ghi rằng constructor của đối tượng bị proxy không bị gọi hai lần, vì instance proxy CGLIB được tạo qua Objenesis. CGLIB cũng đã được đóng gói sẵn trong spring-core nên bạn không phải thêm thư viện riêng vào classpath.
Spring AOP chọn loại Proxy pattern nào
Theo tài liệu Spring Framework 6.0 về cơ chế proxy: nếu đối tượng đích cài ít nhất một interface thì dùng JDK dynamic proxy và proxy tất cả interface của nó; nếu không có interface nào thì tạo proxy CGLIB, một lớp con sinh lúc chạy. Có thể ép dùng CGLIB bằng thuộc tính proxy-target-class trong XML hoặc proxyTargetClass trong @EnableAspectJAutoProxy. Tài liệu lõi mô tả hành vi mặc định là ưu tiên proxy theo interface; còn với Spring Boot, output thật bên dưới cho thấy cấu hình mặc định của Boot 3.0.0 đã chọn CGLIB ngay cả khi bean có interface (Boot đặt spring.aop.proxy-target-class=true mặc định).
Để khỏi dựa vào trí nhớ, mình dựng một project Maven nhỏ (Spring Boot 3.0.0, Java 17 target, spring-boot-starter-aop) với một aspect @Around ghi log và bốn bean:
OrderServicecài interfaceOrders;PricingServicekhông cài interface nào;Reportercó một phương thứcfinalvà một phương thức thường;- một
CommandLineRunnerin loại proxy của từng bean.
Với cấu hình mặc định của Spring Boot 3.0.0, output thật:
=== 1. Loại proxy ===
OrderService (có interface Orders): lớp thật = OrderService$$SpringCGLIB$$0 | JDK proxy=false | CGLIB proxy=true
PricingService (không interface): lớp thật = PricingService$$SpringCGLIB$$0 | JDK proxy=false | CGLIB proxy=true
OrderService là Orders? true
OrderService là OrderService (lớp cụ thể)? trueĐiều đáng chú ý: OrderService có interface nhưng vẫn bị CGLIB proxy. Đó là hành vi mặc định của Spring Boot 3.0.0 mà mình đã đo, tức thuộc tính spring.aop.proxy-target-class đang bật. Tắt nó bằng --spring.aop.proxy-target-class=false và chạy lại:
=== 1. Loại proxy ===
OrderService (có interface Orders): lớp thật = $Proxy38 | JDK proxy=true | CGLIB proxy=false
PricingService (không interface): lớp thật = PricingService$$SpringCGLIB$$0 | JDK proxy=false | CGLIB proxy=true
OrderService là Orders? true
OrderService là OrderService (lớp cụ thể)? falseHai chế độ cho thấy Proxy pattern trong Spring không cố định một dạng. Lúc này OrderService là JDK proxy ($Proxy38) và không còn là OrderService về mặt kiểu. Hệ quả có thật: trong lần chạy đầu tiên với chế độ này mình khai báo tham số OrderService orders cho CommandLineRunner và ngữ cảnh Spring không khởi động được, báo "Bean named 'orderService' is expected to be of type 'demo.OrderService' but was actually of type 'jdk.proxy2.$Proxy38'". Phải đổi sang tiêm theo interface Orders thì mới chạy. Để tái hiện lỗi này, đổi tham số Orders orders trong App.demo(...) của code/s6-08/spring-demo thành OrderService orders rồi chạy với cờ --spring.aop.proxy-target-class=false; thông báo lỗi được lưu ở output-spring-jdk-inject-class-error.txt. Bài học: tiêm phụ thuộc theo interface, code đó chạy đúng ở cả hai chế độ proxy.
Đây là kết quả đo ở Spring Boot 3.0.0; với phiên bản khác hãy kiểm tra lại bằng cách tương tự.
Bẫy lớn nhất của Proxy pattern: self-invocation bỏ qua proxy
Điểm yếu cố hữu của Proxy pattern là nó chỉ chặn được lời gọi đi qua nó. Khi một phương thức trong đối tượng thật gọi phương thức khác của chính nó bằng this, lời gọi đó đi thẳng vào đối tượng thật và không qua proxy. Tài liệu Spring Framework 6.0 giải thích: lời gọi qua this đi thẳng tới đối tượng đích chứ không qua proxy, nên "self-invocation is not going to result in the advice associated with a method invocation getting a chance to run". Trong project demo, placeOrderAndNotify gọi this.notifyCustomer(id) bên trong:
=== 2. Advice chạy khi gọi từ ngoài ===
[advice] vào OrderService.placeOrder(..)
kết quả: đã đặt DH2001
=== 3. Self-invocation bỏ qua advice ===
[advice] vào OrderService.placeOrderAndNotify(..)
kết quả: đã đặt DH2002 + đã báo khách DH2002Ở mục 3, advice chỉ in đúng một dòng cho placeOrderAndNotify. Hai lời gọi bên trong (placeOrder và notifyCustomer) đều không có dòng [advice] dù aspect khớp cả hai. Nếu aspect là @Transactional hay @Cacheable, nghĩa là transaction không được mở, cache không được dùng, và không có lỗi nào báo cho bạn. Kết quả giống nhau cả ở chế độ JDK proxy (output thứ hai của mình cho đúng như vậy), vì bản chất là this không phải proxy.
Phiên bản tài liệu Spring hiện hành về cơ chế proxy xếp các cách xử lý theo thứ tự ưu tiên: refactor để self-invocation không xảy ra (cách "tốt nhất" theo lời tài liệu, ví dụ đưa phần cần advice sang bean khác); tiêm một tham chiếu tới chính bean đó (self injection) và gọi qua nó thay cho this; và cuối cùng là AopContext.currentProxy(), cách bị khuyến cáo mạnh là không nên dùng vì gắn code vào Spring AOP và đòi bật exposeProxy. Trang tài liệu 6.0.0 mà bài dẫn ở trên chỉ nêu hai cách đầu và cuối (refactor và currentProxy); self injection là mục bổ sung ở bản sau, và cũng là kỹ thuật phổ biến trong cộng đồng Spring. AspectJ weaving lúc biên dịch hoặc lúc nạp lớp không gặp vấn đề này vì chèn advice thẳng vào bytecode thay vì qua proxy.
Giới hạn của Proxy pattern bằng CGLIB: final và private
Vì proxy kiểu CGLIB ghi đè phương thức trong lớp con, tài liệu Spring hiện hành liệt kê (bản 6.0.0 chỉ nêu riêng phương thức final): lớp final không thể proxy vì không kế thừa được; phương thức final và private không thể advise vì không ghi đè được; phương thức không nhìn thấy được, như package-private ở lớp cha khác package, cũng coi như private. Mục 5 của demo kiểm chứng điều này:
=== 5. Phương thức final không bị advise ===
kết quả: final: không qua advice
[advice] vào Reporter.normalMethod()
kết quả: thường: có qua advicePhương thức final chạy mà không có dòng advice, còn phương thức thường thì có. Nguy hiểm ở chỗ nó "chạy được" nhưng im lặng bỏ qua log, bảo mật hoặc transaction.
Proxy pattern ở những thư viện bạn đang dùng
- Spring AOP và
@Transactional. Đây là Proxy pattern phổ biến nhất trong Spring: bean của bạn được thay bằng proxy; proxy mở transaction, gọi phương thức thật, commit hoặc rollback. - Hibernate lazy loading. Quan hệ lazy là virtual proxy, một dạng Proxy pattern: một đối tượng giữ khoá chính và chỉ truy vấn khi bạn gọi thuộc tính đầu tiên.
- Spring Data repository. Interface
JpaRepositorybạn khai báo không có cài đặt; Spring sinh JDK proxy lúc chạy. - Mockito. Mock là proxy sinh lúc chạy ghi nhận và giả lập lời gọi; lý do bạn không mock được lớp
finaltrong các cấu hình cũ. - Client HTTP khai báo kiểu Feign hay Retrofit. Interface Java được proxy hoá thành lời gọi mạng, một remote proxy.
Khi nào không nên dùng Proxy pattern sinh động
- Chỉ có một hay hai phương thức, logic đơn giản. Lớp proxy viết tay rõ ràng hơn, stack trace ngắn hơn, dễ debug hơn reflection.
- Đường nóng cần hiệu năng tối đa. Mỗi lời gọi qua JDK proxy đi qua
Method.invoke. Chi phí này nhỏ so với truy vấn DB hay gọi mạng, nhưng không miễn phí; hãy đo bằng JMH trước khi quyết định, đừng đoán. - Logic cần ngữ nghĩa tường minh. Một aspect áp dụng ngầm khiến người đọc không thấy hành vi ở chỗ gọi. Với logic quan trọng như retry thanh toán, Decorator tường minh như bài trước dễ hiểu hơn.
- Cần advise phương thức private, final hoặc self-invocation. Dùng AspectJ weaving, hoặc thiết kế lại để tách bean.
Lỗi thường gặp khi dùng Proxy pattern
@Transactionaltrên phương thức private hoặc final, hoặc gọi quathis. Không có transaction, không có lỗi. Kiểm tra bằngAopUtils.isAopProxy(bean)và đọc log transaction.- Ép kiểu proxy về lớp cụ thể. Với JDK proxy sẽ
ClassCastException; tiêm theo interface. - Quên
throw e.getCause()trong handler của Proxy pattern động. Client nhậnInvocationTargetExceptionhoặcUndeclaredThrowableExceptionthay vì lỗi nghiệp vụ. - So sánh
equalsgiữa proxy và đối tượng thật. Hai đối tượng khác nhau; so sánh theo khoá nghiệp vụ. - Lazy proxy bị dùng ngoài phiên làm việc. Gọi thuộc tính của proxy ORM sau khi đóng session gây lỗi; nạp dữ liệu cần thiết trong phạm vi transaction.
Checklist áp dụng Proxy pattern
- Xác định ý định: kiểm soát truy cập (Proxy pattern) hay thêm hành vi (Decorator)?
- Một hai phương thức: viết Proxy pattern bằng tay. Nhiều phương thức dùng chung logic: JDK dynamic proxy hoặc AOP.
- Luôn dùng và tiêm theo interface; coi proxy như kiểu dữ liệu không thể ép về lớp cụ thể.
- Không dựa vào self-invocation khi áp dụng Proxy pattern; tách bean nếu cần advice cho lời gọi nội bộ.
- Tránh phương thức final và private cho đối tượng bị advise.
- Kiểm tra bằng code như demo trên: in
AopUtils.isJdkDynamicProxyvàisCglibProxy, đừng giả định.
Bài tiếp theo
Proxy pattern giữ nguyên giao diện, còn mẫu tiếp theo làm điều ngược lại: đưa ra một giao diện đơn giản che cả một hệ thống con phức tạp. Bài kế tiếp trong series: Facade pattern: một cửa vào cho hệ thống phức tạp.



