什么是代理模式?应用场景有哪些?

#中间件#面试八股

什么是代理模式?应用场景有哪些?

结论先行:代理模式(Proxy Pattern)给目标对象套一个同类型的替身,替身负责控制访问——加日志、加事务、加缓存、加权限,而目标对象的代码一行不改。Java 里有三种落地方式:静态代理、JDK 动态代理(基于接口 + 反射)、CGLIB(基于继承 + 字节码生成)。

1. 结构:调用方以为自己在调真实对象

   Client ──► Proxy(替身) ──转发──► RealSubject(真实业务逻辑)
                 │                          ▲
                 │ 前置/后置增强             │
                 └──────────────────────────┘

   约束:Proxy 与 RealSubject 实现同一个 Subject 接口,调用方无感

关键:代理与真实对象实现同一个接口,所以调用方无感。这就是"对扩展开放、对修改关闭"最经典的落地。

2. 三种实现方式的差别

维度静态代理JDK 动态代理CGLIB
生成时机手写,编译期运行时 Proxy.newProxyInstance运行时生成子类字节码
前提条件无目标类必须实现接口目标类不能是 final,方法不能 final
代理数量一个接口一个类通用,一个 handler 代理所有通用
性能最快反射调用,略慢直接调用,较快
典型使用者早期手写Spring AOP(有接口时)Spring AOP(无接口时)

3. 静态代理:写死,类会膨胀

interface UserService {
    User findById(long id);
}

class UserServiceImpl implements UserService {
    public User findById(long id) { return queryFromDb(id); }
}

// 手写代理:每个方法都要重复写日志和计时
class UserServiceProxy implements UserService {
    private final UserService target;
    UserServiceProxy(UserService target) { this.target = target; }

    public User findById(long id) {
        long start = System.nanoTime();
        try {
            return target.findById(id);
        } finally {
            System.out.println("findById 耗时 " + (System.nanoTime() - start) + " ns");
        }
    }
}

接口一多,代理类就成倍增长,且增强逻辑到处重复——于是有了动态代理。

4. JDK 动态代理:一个 handler 打天下

import java.lang.reflect.*;

public class TimingHandler implements InvocationHandler {
    private final Object target;

    public TimingHandler(Object target) { this.target = target; }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.nanoTime();
        try {
            return method.invoke(target, args);      // 反射调用真实方法
        } catch (InvocationTargetException e) {
            throw e.getTargetException();            // 剥掉反射包装,抛出原始异常
        } finally {
            System.out.println(method.getName() + " 耗时 " + (System.nanoTime() - start) + " ns");
        }
    }
}

// 使用:返回的对象实现了 UserService 接口
UserService proxy = (UserService) Proxy.newProxyInstance(
        UserService.class.getClassLoader(),
        new Class<?>[]{ UserService.class },
        new TimingHandler(new UserServiceImpl()));

proxy.findById(42L);

三个参数分别是:类加载器、要实现的接口数组、调用处理器。注意异常必须用 getTargetException() 还原,否则业务代码 catch 具体异常会失效。

5. CGLIB:没有接口时的兜底

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class);        // 生成子类
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
    long start = System.nanoTime();
    try {
        return proxy.invokeSuper(obj, args);          // 调父类方法
    } finally {
        System.out.println(method.getName() + " 耗时 " + (System.nanoTime() - start));
    }
});
UserServiceImpl proxy = (UserServiceImpl) enhancer.create();

原理是运行期生成 UserServiceImpl$$EnhancerByCGLIB 子类并重写方法。因此 final 类、final 方法、private 方法都代理不了。

6. 应用场景

场景代理角色做什么真实案例
远程代理(Remote)把本地调用编码成网络请求RPC 客户端 stub、Dubbo、Feign
虚拟代理(Virtual)延迟创建重对象Hibernate 懒加载、图片占位
保护代理(Protection)调用前校验权限@PreAuthorize、接口鉴权
缓存代理命中直接返回,未命中原调用@Cacheable
同步/事务代理方法前后开闭事务@Transactional
日志/监控代理记录耗时、埋点AOP 切面、链路追踪

7. 常见追问

Q:JDK 动态代理为什么必须基于接口? 生成的代理类已经 extends Proxy,Java 单继承,所以只能靠实现接口来"看起来是目标类型"。

Q:Spring AOP 什么时候用 JDK、什么时候用 CGLIB? Spring Boot 2.x 起默认 proxyTargetClass=true,统一用 CGLIB;有接口也会用 CGLIB,除非显式配置 spring.aop.proxy-target-class=false。

Q:为什么同类方法自调用,事务/缓存注解不生效? 因为增强发生在代理对象上。this.b() 走的是原始对象,绕过了代理,自然没有增强。改成注入自身代理、拆到另一个 bean,或用 AopContext.currentProxy()。

Q:代理和装饰器的区别? 结构几乎一样,意图不同:代理控制访问(常常自己创建目标),装饰器叠加功能(目标由外部传入,可无限层叠)。

小结

  • 代理 = 同类型替身 + 控制访问,目标对象零侵入。
  • 静态代理手写、易膨胀;JDK 动态代理需接口、靠反射;CGLIB 靠继承、怕 final。
  • 记住三大高频场景:RPC stub、Spring AOP(事务/缓存)、权限校验。
  • 追问常落在"自调用失效"和"JDK 与 CGLIB 的选择"上。
笔记加载中…