Proxy vs. Decorator Patterns and DIY Spring AOP Implementation in Java
This article explains the proxy and decorator design patterns using a coffee analogy, provides complete Java code examples for static and dynamic proxies, discusses their differences, and shows how to implement a Spring‑like AOP mechanism with JDK InvocationHandler and cglib, while also warning about their limitations.
Introduction
The author, a senior architect, starts with a coffee analogy to introduce the proxy pattern and its relationship to Aspect‑Oriented Programming (AOP). The goal is to demonstrate how to implement proxy and decorator patterns in pure Java without relying on Spring.
Proxy and Decorator Patterns
A proxy replaces another object and provides the same interface, while a decorator adds extra behavior by wrapping the original object. The article uses a "bitter coffee" example to illustrate both patterns.
First, a simple Coffee interface is defined:
public interface Coffee {
/**
* Print the material of the coffee.
*/
void printMaterial();
}A default implementation BitterCoffee prints "咖啡" (coffee):
public class BitterCoffee implements Coffee {
@Override
public void printMaterial() {
System.out.println("咖啡");
}
}The basic client code creates a bitter coffee and calls printMaterial() :
public class Main {
public static void main(String[] args) {
Coffee coffee = new BitterCoffee();
coffee.printMaterial();
}
}To add sugar, a SugarDecorator is introduced (decorator pattern):
public class SugarDecorator implements Coffee {
/** The coffee being decorated */
private final Coffee coffee;
public SugarDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public void printMaterial() {
System.out.println("糖");
this.coffee.printMaterial();
}
}Client code now decorates the bitter coffee:
public class Main {
public static void main(String[] args) {
Coffee coffee = new BitterCoffee();
coffee = new SugarDecorator(coffee);
coffee.printMaterial();
}
}For the proxy approach, a new class CoffeeWithSugar implements Coffee and internally delegates to a BitterCoffee instance:
public class CoffeeWithSugar implements Coffee {
private final Coffee coffee;
public CoffeeWithSugar() {
this.coffee = new BitterCoffee();
}
@Override
public void printMaterial() {
System.out.println("糖");
this.coffee.printMaterial();
}
}The client simply creates a CoffeeWithSugar object:
public class Main {
public static void main(String[] args) {
Coffee coffee = new CoffeeWithSugar();
coffee.printMaterial();
}
}The article highlights the subtle difference: the decorator wraps an existing object, while the proxy creates a new object that already contains the extra behavior.
AOP and Spring
The author points out that Spring AOP is essentially a proxy‑based implementation of the proxy pattern. To reproduce the same effect without Spring, a manual dynamic proxy is required.
First, an SMSService interface and its implementation are defined:
public interface SMSService {
void sendMessage();
}
public class SMSServiceImpl implements SMSService {
@Override
public void sendMessage() {
System.out.println("【梦云智】您正在执行重置密码操作,您的验证码为:1234,5分钟内有效,请不要将验证码转发给他人。");
}
}To count the cost of each SMS, a custom InvocationHandler is written:
public class MoneyCountInvocationHandler implements InvocationHandler {
/** The target object */
private final Object target;
/** Accumulated money */
private Double moneyCount;
public MoneyCountInvocationHandler(Object target) {
this.target = target;
this.moneyCount = 0.0;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = method.invoke(target, args);
moneyCount += 0.07; // each SMS costs 0.07
System.out.println("发送短信成功,共花了:" + moneyCount + "元");
return result;
}
}The client creates a JDK dynamic proxy:
public class Main {
public static void main(String[] args) {
SMSService smsService = new SMSServiceImpl();
smsService = (SMSService) Proxy.newProxyInstance(
Main.class.getClassLoader(),
new Class[]{SMSService.class},
new MoneyCountInvocationHandler(smsService));
smsService.sendMessage();
smsService.sendMessage();
}
}The author notes that JDK dynamic proxies only work for interfaces. When a concrete class (e.g., SMSServiceImpl ) is injected directly, JDK proxies cannot intercept it. This limitation is solved by cglib, which creates a subclass at runtime and weaves the advice into overridable methods.
cglib can proxy concrete classes, but it cannot intercept final methods because they cannot be overridden.
Conclusion
The article summarises that understanding the intent behind design patterns and AOP helps developers build their own lightweight frameworks. It also reminds readers that both JDK dynamic proxies and cglib have trade‑offs, and that a solid grasp of these mechanisms is essential for backend development.
Reading and truly understanding the author's ideas is the key to mastering a framework; only then can you design your own.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.