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


面试考察点

  1. 概念理解:面试官不仅仅是想知道代理模式的定义,更是想知道你是否理解它的核心思想——"不直接访问目标对象,通过代理间接访问",以及为什么要这么设计。

  2. 技术深度:能否区分静态代理和动态代理,能否讲清楚 JDK 动态代理和 CGLIB 的底层原理差异。这块能答好的候选人不多,答好了就是加分项。

  3. 框架关联:面试官特别想看你是否能把代理模式和 Spring AOP、MyBatis Mapper、RPC 远程调用等实际框架联系起来,证明你不止停留在理论层面。

核心答案

一句话定义:代理模式就是给目标对象提供一个 "替身",由替身控制对目标对象的访问。客户端不直接和目标对象打交道,而是通过代理对象间接访问。

打个生活中的比方:你要租房,不直接找房东,而是找中介。中介就是 "代理",房东就是 "目标对象"。中介可以在租房前后加一些额外操作(比如带看房、签合同、收中介费),而你只需要和中介打交道就行。

上面的图展示了代理模式的核心结构。关键角色有三个:

  • Subject(公共接口):定义了代理和目标对象的共同行为,客户端面向接口编程
  • RealSubject(目标对象):真正干活的那个,也就是 "房东"
  • Proxy(代理对象):持有目标对象的引用,在调用目标方法前后可以增加额外逻辑,也就是 "中介"

核心思路就是:代理和目标对象实现同一个接口,代理内部持有目标的引用,客户端只跟代理打交道。

深度解析

一、静态代理

静态代理的 "静态" 体现在:代理类在编译期就确定了,需要手动编写代理类。

// 公共接口
public interface UserService {
    void addUser(String name);
}

// 目标对象
public class UserServiceImpl implements UserService {
    @Override
    public void addUser(String name) {
        System.out.println("添加用户:" + name);
    }
}

// 代理对象(手动编写)
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("[日志] 添加用户完成");      // 后置增强
    }
}

// 使用
UserService target = new UserServiceImpl();
UserService proxy = new UserServiceProxy(target);
proxy.addUser("犬小哈");

输出:

[日志] 开始添加用户...
添加用户:犬小哈
[日志] 添加用户完成

静态代理的缺点很明显——一个接口就得写一个代理类,如果项目里有 50 个 Service,你就得写 50 个代理类,这谁顶得住?而且接口一旦增加方法,目标类和代理类都得改。

二、JDK 动态代理

动态代理的 "动态" 体现在:代理类在运行时由 JVM 动态生成,不需要手动编写。JDK 提供了 java.lang.reflect.Proxy 来实现。

public class JdkProxyFactory {

    private Object target;

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

    public Object getProxyInstance() {
        return Proxy.newProxyInstance(
            target.getClass().getClassLoader(), // 目标类的类加载器
            target.getClass().getInterfaces(),  // 目标类实现的接口
            new InvocationHandler() {           // 调用处理器
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    System.out.println("[JDK 代理] 开始执行:" + method.getName());
                    Object result = method.invoke(target, args); // 反射调用目标方法
                    System.out.println("[JDK 代理] 执行完成:" + method.getName());
                    return result;
                }
            }
        );
    }
}

// 使用
UserService target = new UserServiceImpl();
UserService proxy = (UserService) new JdkProxyFactory(target).getProxyInstance();
proxy.addUser("犬小哈");

JDK 动态代理的核心原理:

  • 通过 Proxy.newProxyInstance() 在运行时动态生成一个代理类的 .class 文件
  • 这个代理类实现了目标类所实现的所有接口
  • 代理类内部通过 InvocationHandlerinvoke() 方法来转发请求给目标对象

JDK 动态代理有一个限制:目标类必须实现接口。如果目标类没有实现任何接口,JDK 动态代理就用不了。这个限制在面试中经常被问到。

三、CGLIB 动态代理

CGLIB(Code Generation Library)采用的方式不同——它通过字节码技术生成目标类的子类来实现代理,不需要目标类实现接口。

public class CglibProxyFactory implements MethodInterceptor {

    public Object getProxyInstance(Class<?> targetClass) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(targetClass);   // 设置父类(目标类)
        enhancer.setCallback(this);            // 设置回调
        return enhancer.create();              // 生成子类代理对象
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("[CGLIB 代理] 开始执行:" + method.getName());
        Object result = proxy.invokeSuper(obj, args); // 调用父类方法
        System.out.println("[CGLIB 代理] 执行完成:" + method.getName());
        return result;
    }
}

// 使用(注意:UserServiceImpl 不需要实现任何接口)
UserService proxy = (UserService) new CglibProxyFactory().getProxyInstance(UserServiceImpl.class);
proxy.addUser("犬小哈");

CGLIB 也有一个限制:因为是基于继承的,所以不能代理 final 修饰的类和方法

四、JDK 动态代理 vs CGLIB 对比

这是面试中的 "高频考点",必须记住:

对比项JDK 动态代理CGLIB
实现方式基于接口,代理类实现目标接口基于继承,代理类继承目标类
是否需要接口✅ 必须❌ 不需要
final 类/方法不影响❌ 无法代理
性能生成代理快,调用稍慢生成代理慢,调用更快
Spring 默认策略目标有接口时优先使用目标无接口时使用

Spring 在 4.x 之后默认使用 CGLIB(spring.aop.proxy-target-class=true),之前版本默认使用 JDK 动态代理。Spring Boot 2.x 起默认也是 CGLIB。

五、实际应用场景

这块面试官特别爱问,因为能答出 "理论" 的人多,能结合实际场景的人少。

1. Spring AOP

Spring AOP 是代理模式最经典的应用。@Transactional@Async@Cacheable 这些注解背后的原理全都是动态代理。比如 @Transactional

@Service
public class OrderService {

    @Transactional
    public void createOrder() {
        // 业务逻辑
    }
}

Spring 会为 OrderService 生成一个代理对象,createOrder() 执行前后自动加上事务开启和提交/回滚的逻辑。你自己完全不用写事务管理的代码,代理对象帮你搞定了。

2. MyBatis Mapper 代理

@Mapper
public interface UserMapper {
    User selectById(Long id);
}

你只写了个接口,连实现类都没有,为什么能直接调用?因为 MyBatis 用 JDK 动态代理给接口生成了代理对象,invoke() 方法里根据映射文件执行 SQL 并返回结果。

3. RPC 远程调用

Dubbo、Feign 这类 RPC 框架,你调用一个远程方法就像调用本地方法一样:

@FeignClient(name = "user-service")
public interface UserClient {
    @GetMapping("/user/{id}")
    User getById(@PathVariable Long id);
}

背后的原理也是动态代理。代理对象的 invoke() 里帮你在网络层完成序列化、发送请求、反序列化响应这一整套流程。

4. 日志 / 权限 / 性能监控

在方法执行前后记录日志、校验权限、统计耗时,都可以通过代理统一处理,而不是在每个方法里重复写。

面试高频追问

  1. JDK 动态代理和 CGLIB 的区别是什么?

    • JDK 基于接口,CGLIB 基于继承。JDK 要求目标类必须实现接口,CGLIB 不能代理 final 类。详见上方对比表格。
  2. Spring AOP 用的是哪种代理?

    • 默认情况下,目标类实现了接口就用 JDK 动态代理,没实现接口就用 CGLIB。Spring Boot 2.x 起默认全部走 CGLIB。
  3. 为什么 JDK 动态代理必须实现接口?

    • 因为 JDK 生成的代理类继承了 Proxy 类,Java 不支持多继承,所以只能通过实现接口来代理。
  4. CGLIB 为什么不能代理 final 方法?

    • CGLIB 通过生成子类来代理,final 方法无法被子类重写,自然就无法增强。

常见面试变体

  • "手写一个动态代理的例子"
  • "Spring AOP 的底层原理是什么?"
  • "MyBatis 的 Mapper 接口没有实现类,为什么能直接调用?"
  • "动态代理和装饰器模式有什么区别?"

记忆口诀

核心思想:不碰真人找中介,代理帮你加逻辑。

JDK vs CGLIB:JDK 靠接口,CGLIB 靠继承;一个要接口,一个怕 final

应用场景:AOP 切面、Mapper 代理、RPC 调用、日志权限。

总结

代理模式的核心就是 "找个替身控制访问"。面试时先说概念,再对比静态/动态代理的区别,重点展开 JDK 动态代理和 CGLIB 的原理差异,最后结合 Spring AOP、MyBatis、RPC 三个实际场景举例。这条线拉完,面试官基本没有追问的空间了。