代理模式:静态代理、动态代理与AOP实现原理
代理模式,说白了就是「找人替你办事」。你想想看,生活中我们经常需要代理——买房找中介、打官司找律师、上网用VPN。在软件世界里,代理模式干的也是同样的事:一个对象不想直接暴露自己,或者想在干活前后加点私货,就找个代理替它出面。
我做了十几年架构,代理模式几乎每天都在用。尤其是做微服务和AOP的时候,没有代理模式,很多功能根本没法优雅实现。今天我就把静态代理、动态代理(JDK和CGLIB)、AOP原理,以及C++里的代理实现,一次性给你讲透。
1. 代理模式的核心思想
先看一张图,把代理模式的整体结构刻在脑子里:
核心就三句话:
- 抽象接口:定义业务方法
- 真实对象:真正干活的类
- 代理对象:替真实对象接活,可以在前后加逻辑
客户端只跟代理交互,不知道背后是谁在干活。这就是「控制访问」的精髓。
2. 静态代理——最朴素的实现
静态代理,就是在编译期就把代理类写好。每个真实对象对应一个代理类,一对一。
我记得刚入行时,项目经理让我给所有Service方法加日志。我傻乎乎地每个类都写一个代理……那叫一个酸爽。后来才知道,这种硬编码的方式就是静态代理。
Java 静态代理示例
// 1. 抽象接口
public interface UserService {
void addUser(String name);
void deleteUser(int id);
}
// 2. 真实对象
public class UserServiceImpl implements UserService {
@Override
public void addUser(String name) {
System.out.println("添加用户:" + name);
}
@Override
public void deleteUser(int id) {
System.out.println("删除用户:" + id);
}
}
// 3. 代理对象
public class UserServiceProxy implements UserService {
private UserService target;
public UserServiceProxy(UserService target) {
this.target = target;
}
@Override
public void addUser(String name) {
System.out.println("[日志] 开始添加用户");
target.addUser(name);
System.out.println("[日志] 添加用户结束");
}
@Override
public void deleteUser(int id) {
System.out.println("[日志] 开始删除用户");
target.deleteUser(id);
System.out.println("[日志] 删除用户结束");
}
}
静态代理的痛点:每个方法都要手动写代理逻辑,接口一改,代理类也得改。10个接口就要写10个代理类,维护成本爆炸。
3. 动态代理——JDK Proxy
静态代理太笨重,于是Java在1.3版本引入了动态代理。你只需要写一个InvocationHandler,就能给任意接口生成代理对象。
说白了,动态代理就是在运行时「凭空捏造」一个代理类出来。我当年第一次看到Proxy.newProxyInstance时,觉得这简直是黑魔法。
JDK动态代理示例
import java.lang.reflect.*;
public class LogHandler implements InvocationHandler {
private Object target;
public LogHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("[日志] 调用方法:" + method.getName());
Object result = method.invoke(target, args);
System.out.println("[日志] 方法调用结束");
return result;
}
}
// 使用方式
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new LogHandler(target)
);
proxy.addUser("张三");
注意:JDK动态代理只能代理接口,不能代理类。如果目标对象没有实现接口,JDK Proxy就无能为力了。我曾经在这个坑里摔过一次——接手一个老项目,全是具体类,没有接口,想加日志都加不了。
4. CGLIB动态代理——代理类也能玩
JDK Proxy只能代理接口,那没有接口的类怎么办?CGLIB就是来填这个坑的。
CGLIB的原理是:通过字节码技术,生成目标类的子类作为代理。说白了,它不是在接口层面做文章,而是直接继承目标类,重写它的方法。
CGLIB动态代理示例
import net.sf.cglib.proxy.*;
public class CglibProxy implements MethodInterceptor {
private Object target;
public Object getProxy(Object target) {
this.target = target;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(this);
return enhancer.create();
}
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
System.out.println("[CGLIB] 前置处理");
Object result = proxy.invoke(target, args);
System.out.println("[CGLIB] 后置处理");
return result;
}
}
// 使用方式
UserServiceImpl target = new UserServiceImpl();
UserServiceImpl proxy = (UserServiceImpl)
new CglibProxy().getProxy(target);
proxy.addUser("李四");
CGLIB的局限:不能代理final类,也不能代理final方法。因为它是通过继承实现的,final类不能被继承,final方法不能被重写。另外,CGLIB在生成代理类时性能开销比JDK Proxy大,但运行时性能差不多。
5. JDK Proxy vs CGLIB 对比
| 对比维度 | JDK Proxy | CGLIB |
|---|---|---|
| 原理 | 基于接口,生成实现类 | 基于继承,生成子类 |
| 目标要求 | 必须实现接口 | 不能是final类 |
| 生成速度 | 快(反射生成) | 慢(字节码操作) |
| 运行速度 | 较慢(反射调用) | 较快(直接调用) |
| 适用场景 | 面向接口编程 | 无接口的类 |
6. AOP实现原理——代理模式的高级应用
AOP(面向切面编程),说白了就是「在不修改源码的情况下,给方法加逻辑」。日志、事务、权限校验……这些横切关注点,用AOP处理最合适。
Spring AOP的底层就是代理模式:
- 如果目标对象实现了接口 → 用JDK Proxy
- 如果目标对象没有实现接口 → 用CGLIB
我参与过一个支付系统的重构,原来每个支付方法里都手动写日志和事务,代码重复率高达60%。后来用AOP一把梭,核心业务代码减少了40%,维护起来舒服多了。
AOP核心概念
// 切面:日志切面
@Aspect
@Component
public class LogAspect {
// 切入点:匹配所有Service方法
@Pointcut("execution(* com.example.service.*.*(..))")
public void servicePointcut() {}
// 前置通知
@Before("servicePointcut()")
public void before(JoinPoint joinPoint) {
System.out.println("调用方法:" + joinPoint.getSignature().getName());
}
// 后置通知
@After("servicePointcut()")
public void after(JoinPoint joinPoint) {
System.out.println("方法结束:" + joinPoint.getSignature().getName());
}
// 环绕通知
@Around("servicePointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed();
long elapsed = System.currentTimeMillis() - start;
System.out.println("耗时:" + elapsed + "ms");
return result;
}
}
AOP的本质:Spring在启动时,扫描到@Aspect注解,解析切入点表达式,然后为匹配的Bean生成代理对象。代理对象在调用目标方法时,会先执行切面逻辑,再调用真实方法。整个过程对业务代码完全透明。
7. C++中的代理模式实现
C++没有Java那样的反射机制,也没有动态代理。但C++有模板、虚函数和智能指针,照样能实现代理模式。
我个人习惯在C++中用两种方式实现代理:
- 基于虚函数的静态代理:和Java静态代理类似,手动写代理类
- 基于模板的通用代理:用模板和函数包装器实现类似动态代理的效果
C++静态代理示例
#include <iostream>
#include <memory>
// 抽象接口
class Image {
public:
virtual void display() = 0;
virtual ~Image() = default;
};
// 真实对象
class RealImage : public Image {
private:
std::string filename;
public:
RealImage(const std::string& f) : filename(f) {
loadFromDisk();
}
void display() override {
std::cout << "显示图片: " << filename << std::endl;
}
private:
void loadFromDisk() {
std::cout << "从磁盘加载: " << filename << std::endl;
}
};
// 代理对象
class ProxyImage : public Image {
private:
std::unique_ptr<RealImage> realImage;
std::string filename;
public:
ProxyImage(const std::string& f) : filename(f) {}
void display() override {
// 懒加载:第一次使用时才创建真实对象
if (!realImage) {
realImage = std::make_unique<RealImage>(filename);
}
std::cout << "[代理] 记录日志" << std::endl;
realImage->display();
}
};
int main() {
ProxyImage proxy("photo.jpg");
// 第一次调用,触发加载
proxy.display();
// 第二次调用,直接使用缓存
proxy.display();
return 0;
}
C++模板通用代理
#include <iostream>
#include <functional>
#include <memory>
// 通用代理模板
template<typename T>
class Proxy {
private:
std::shared_ptr<T> target;
std::function<void()> beforeHook;
std::function<void()> afterHook;
public:
Proxy(std::shared_ptr<T> t) : target(t) {}
void setBeforeHook(std::function<void()> hook) {
beforeHook = hook;
}
void setAfterHook(std::function<void()> hook) {
afterHook = hook;
}
// 通过重载->操作符实现方法拦截
T* operator->() {
if (beforeHook) beforeHook();
// 这里无法直接拦截方法调用,需要配合其他机制
return target.get();
}
};
// 使用示例
class Service {
public:
void doWork() {
std::cout << "执行核心业务" << std::endl;
}
};
int main() {
auto service = std::make_shared<Service>();
Proxy<Service> proxy(service);
proxy.setBeforeHook([](){
std::cout << "[日志] 方法开始" << std::endl;
});
proxy->doWork();
return 0;
}
C++代理的局限:C++没有Java那样的动态代理机制,无法在运行时动态生成代理类。但通过模板和函数对象,我们仍然可以实现灵活的代理逻辑。如果追求更强大的AOP效果,可以考虑使用AspectC++这样的AOP扩展。
8. 避坑指南
我曾经踩过几个代理模式的坑,分享给你:
- JDK Proxy强制转换异常:如果目标对象没有实现接口,用JDK Proxy会抛出ClassCastException。解决办法是改用CGLIB,或者在设计时就坚持面向接口编程。
- CGLIB代理final方法失效:CGLIB无法重写final方法,如果目标类有final方法,代理不会生效。我建议在需要代理的类中,避免使用final关键字。
- 循环代理问题:AOP中如果切面方法内部又调用了同一个类的其他方法,可能会触发无限递归。解决办法是用AopContext.currentProxy()获取当前代理对象。
- 性能陷阱:动态代理在频繁创建和销毁的场景下性能较差。如果代理对象需要频繁创建,可以考虑使用对象池或者静态代理。
9. 总结
代理模式是设计模式中应用最广泛的一个,没有之一。从简单的日志记录,到复杂的AOP框架,再到RPC远程调用,处处都有代理的身影。
我个人建议:
- 如果项目是面向接口编程,优先用JDK Proxy
- 如果遇到没有接口的遗留代码,用CGLIB
- 如果做大型项目,直接用Spring AOP,它帮你封装好了这一切
- C++项目中,根据需求选择静态代理或模板代理
记住一句话:代理模式的核心不是「代理」本身,而是「控制」。控制访问、控制行为、控制生命周期——这才是代理模式的灵魂。
公众号:蓝海资料掘金营,微信deep3321