1. 代理模式:为什么我们需要它?
在Java开发中,我们经常会遇到这样的场景:你无法直接访问某个对象,或者想在访问对象时添加一些额外操作。这时候代理模式(Proxy Pattern)就派上用场了。作为一个在Java领域摸爬滚打多年的开发者,我发现代理模式是面试中最常被问到的设计模式之一,也是实际项目中最实用的模式之一。
代理模式的核心思想很简单:为其他对象提供一个代理或占位符,以控制对这个对象的访问。听起来有点抽象?让我用一个生活中的例子来解释:假设你想邀请一位明星来参加你的活动,你不会直接去找明星本人,而是会联系他的经纪人。这个经纪人就是明星的代理,他可以帮助处理各种事务,比如筛选邀请、安排行程等。
在Java中,代理模式主要解决以下几个问题:
- 控制对原始对象的访问(比如权限校验)
- 在访问对象时添加额外功能(比如日志记录)
- 延迟加载(当对象创建开销很大时)
- 简化复杂对象的访问方式
2. 代理模式的三种实现方式
2.1 静态代理:最直观的实现
静态代理是最简单直接的代理实现方式。它需要你手动创建一个代理类,这个代理类和被代理类实现相同的接口。
// 接口 public interface Image { void display(); } // 真实对象 public class RealImage implements Image { private String fileName; public RealImage(String fileName) { this.fileName = fileName; loadFromDisk(); } private void loadFromDisk() { System.out.println("Loading " + fileName); } @Override public void display() { System.out.println("Displaying " + fileName); } } // 代理类 public class ImageProxy implements Image { private RealImage realImage; private String fileName; public ImageProxy(String fileName) { this.fileName = fileName; } @Override public void display() { if (realImage == null) { realImage = new RealImage(fileName); } realImage.display(); } }静态代理的优点:
- 实现简单,容易理解
- 不侵入原有代码
- 可以在不修改目标对象的情况下扩展功能
但静态代理也有明显的缺点:
- 每个代理类只能为一个接口服务,如果有多个接口需要代理,就需要创建多个代理类
- 接口一旦增加方法,代理类和目标对象都要修改
2.2 JDK动态代理:运行时生成代理类
JDK动态代理是Java内置的代理机制,它可以在运行时动态创建代理类。这种方式不需要我们手动编写代理类,而是由JVM在运行时生成。
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class DynamicProxyHandler implements InvocationHandler { private Object target; public DynamicProxyHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Before method " + method.getName()); Object result = method.invoke(target, args); System.out.println("After method " + method.getName()); return result; } public static Object createProxy(Object target) { return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new DynamicProxyHandler(target) ); } }使用示例:
Image realImage = new RealImage("test.jpg"); Image proxyImage = (Image) DynamicProxyHandler.createProxy(realImage); proxyImage.display();JDK动态代理的特点:
- 只能代理实现了接口的类
- 生成的代理类继承自Proxy类
- 通过InvocationHandler接口实现方法调用拦截
- 性能比静态代理稍差,但更加灵活
提示:JDK动态代理在实际项目中常用于AOP(面向切面编程)实现,比如Spring框架中的事务管理、日志记录等。
2.3 CGLIB动态代理:无需接口的代理
CGLIB(Code Generation Library)是一个强大的高性能代码生成库,它可以在运行时扩展Java类和实现接口。与JDK动态代理不同,CGLIB不需要目标对象实现接口。
import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; public class CglibProxy implements MethodInterceptor { private Object target; public CglibProxy(Object target) { this.target = target; } public Object getProxyInstance() { 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("Before method " + method.getName()); Object result = method.invoke(target, args); System.out.println("After method " + method.getName()); return result; } }使用示例:
RealImage realImage = new RealImage("test.jpg"); RealImage proxyImage = (RealImage) new CglibProxy(realImage).getProxyInstance(); proxyImage.display();CGLIB代理的特点:
- 可以代理没有实现接口的类
- 通过继承方式实现代理
- 性能比JDK动态代理更好
- 不能代理final类和方法
3. 代理模式的应用场景
3.1 远程代理(Remote Proxy)
远程代理用于在不同的地址空间中代表对象。典型的例子是RPC(远程过程调用)框架,客户端持有的实际上是一个代理对象,这个代理对象负责与远程服务器通信。
// 伪代码示例 public class RemoteImageProxy implements Image { private String fileName; private String host; private int port; public RemoteImageProxy(String fileName, String host, int port) { this.fileName = fileName; this.host = host; this.port = port; } @Override public void display() { // 通过网络与远程服务器通信 Socket socket = new Socket(host, port); ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream()); out.writeObject("display"); out.writeObject(fileName); // 处理响应... } }3.2 虚拟代理(Virtual Proxy)
虚拟代理用于延迟加载开销大的对象。比如在加载大图片时,可以先显示一个缩略图,等真正需要时才加载完整图片。
public class ImageProxy implements Image { private RealImage realImage; private String fileName; private ImageIcon icon; public ImageProxy(String fileName) { this.fileName = fileName; // 先加载缩略图 this.icon = new ImageIcon("thumbnails/" + fileName); } @Override public void display() { if (realImage == null) { // 当真正需要时才加载大图 realImage = new RealImage(fileName); } realImage.display(); } }3.3 保护代理(Protection Proxy)
保护代理用于控制对原始对象的访问权限。比如在文件系统中,不同用户对文件的操作权限不同。
public class ProtectedFileProxy implements File { private RealFile realFile; private User user; public ProtectedFileProxy(RealFile realFile, User user) { this.realFile = realFile; this.user = user; } @Override public void read() { if (user.hasReadPermission()) { realFile.read(); } else { throw new SecurityException("No read permission"); } } @Override public void write() { if (user.hasWritePermission()) { realFile.write(); } else { throw new SecurityException("No write permission"); } } }3.4 智能引用代理(Smart Reference Proxy)
智能引用代理在访问对象时执行额外的操作,比如:
- 记录对象被引用的次数
- 当对象没有被引用时自动释放资源
- 锁定对象以确保其他对象不能修改它
public class SmartReferenceProxy implements Image { private RealImage realImage; private int referenceCount = 0; public SmartReferenceProxy(RealImage realImage) { this.realImage = realImage; } @Override public void display() { referenceCount++; System.out.println("Reference count: " + referenceCount); realImage.display(); } public void close() { if (--referenceCount == 0) { System.out.println("No more references, releasing resource"); realImage.close(); } } }4. 代理模式的性能考量与最佳实践
4.1 性能比较
在实际项目中,选择哪种代理实现方式需要考虑性能因素:
| 代理类型 | 创建速度 | 执行速度 | 内存占用 | 适用场景 |
|---|---|---|---|---|
| 静态代理 | 快 | 快 | 低 | 接口稳定,代理逻辑简单 |
| JDK动态代理 | 慢 | 中等 | 中等 | 需要代理多个接口 |
| CGLIB | 最慢 | 最快 | 高 | 需要代理类而非接口 |
4.2 最佳实践
根据我的项目经验,以下是使用代理模式的一些最佳实践:
接口优先:尽量让被代理的类实现接口,这样你可以选择使用JDK动态代理,它比CGLIB更轻量级。
避免过度使用:代理模式会引入额外的间接层,不要为了使用模式而使用模式,只有在真正需要控制访问或添加额外功能时才使用。
关注线程安全:如果你的代理类有状态(比如缓存),确保它是线程安全的。
考虑性能影响:在性能敏感的场景中,代理调用的开销可能变得显著,需要进行性能测试。
清晰的命名:代理类应该清楚地表明它们的角色,比如UserServiceProxy、RemoteFileProxy等。
4.3 常见问题与解决方案
问题1:动态代理为什么只能代理接口(JDK动态代理)?
这是因为JDK动态代理生成的代理类已经继承了Proxy类,而Java不支持多重继承,所以无法再继承其他类。解决方案是使用CGLIB,它通过生成目标类的子类来实现代理。
问题2:CGLIB代理final方法的问题
CGLIB无法代理final方法,因为它是通过继承实现的。解决方案是:
- 避免在需要被代理的类中使用final方法
- 将这些方法提取到另一个类中
- 使用组合而非继承
问题3:代理导致的栈溢出
当代理方法中又调用了被代理对象的其他方法时,可能会导致无限递归:
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 这里又调用了method,可能导致无限递归 return method.invoke(target, args); }解决方案是直接调用目标方法而非通过反射:
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 直接调用目标对象的方法 return target.doSomething(); }5. 代理模式在流行框架中的应用
5.1 Spring AOP中的代理
Spring框架广泛使用代理模式来实现AOP功能。根据目标对象是否实现接口,Spring会自动选择使用JDK动态代理还是CGLIB。
@Aspect @Component public class LoggingAspect { @Before("execution(* com.example.service.*.*(..))") public void logBefore(JoinPoint joinPoint) { System.out.println("Before method: " + joinPoint.getSignature().getName()); } }Spring代理的特点:
- 默认使用JDK动态代理(如果目标对象实现接口)
- 可以通过配置强制使用CGLIB
- 代理对象由Spring容器管理
5.2 MyBatis中的Mapper代理
MyBatis通过JDK动态代理为Mapper接口创建实现类:
public class MapperProxy<T> implements InvocationHandler { private final SqlSession sqlSession; private final Class<T> mapperInterface; public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 将方法调用转换为SQL执行 return sqlSession.selectOne(method.getName(), args[0]); } }5.3 Hibernate中的延迟加载
Hibernate使用代理模式实现延迟加载(Lazy Loading)。当你访问一个关联对象时,Hibernate返回的是一个代理对象,只有在真正访问其属性时才会从数据库加载数据。
@Entity public class Order { @Id private Long id; @ManyToOne(fetch = FetchType.LAZY) private Customer customer; // ... } // 使用时 Order order = entityManager.find(Order.class, 1L); Customer customer = order.getCustomer(); // 这里返回的是代理对象 String name = customer.getName(); // 此时才会真正加载数据6. 代理模式的变体与扩展
6.1 链式代理(Proxy Chain)
有时候我们需要对同一个方法执行多个代理操作,这时可以使用链式代理:
public class ProxyChain implements InvocationHandler { private List<InvocationHandler> handlers = new ArrayList<>(); private Object target; private int index = 0; public ProxyChain(Object target) { this.target = target; } public void addHandler(InvocationHandler handler) { handlers.add(handler); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (index < handlers.size()) { return handlers.get(index++).invoke(proxy, method, args); } return method.invoke(target, args); } }6.2 动态代理的缓存优化
由于创建动态代理对象开销较大,我们可以使用缓存来优化:
public class ProxyFactory { private static final Map<Class<?>, Object> proxyCache = new ConcurrentHashMap<>(); @SuppressWarnings("unchecked") public static <T> T createProxy(T target) { return (T) proxyCache.computeIfAbsent( target.getClass(), k -> Proxy.newProxyInstance( k.getClassLoader(), k.getInterfaces(), new LoggingInvocationHandler(target) ) ); } }6.3 接口默认方法的代理处理
Java 8引入了接口默认方法,这给动态代理带来了一些挑战。我们需要特别处理默认方法:
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.isDefault()) { // 处理默认方法 return InvocationHandler.invokeDefault(proxy, method, args); } // 正常处理 return method.invoke(target, args); }在实际项目中,我发现代理模式是构建灵活、可扩展系统的强大工具。它不仅能帮助我们实现横切关注点的分离,还能在不修改原有代码的情况下增强功能。掌握各种代理实现方式的优缺点,能够让你在架构设计时做出更合理的选择。