代理模式:AOP 实现——为其他对象提供一种代理以控制对这个对象的访问

代理模式,说白了就是“找人办事”。你不想直接跟目标对象打交道,或者你没法直接访问它,那就找个代理来替你完成。这个代理跟你长得一样,接口也一样,但它在背后偷偷做了很多额外的事情。

我在项目中用过很多次代理模式。最典型的,就是 Spring AOP。你想想看,如果没有代理,你怎么在不修改业务代码的情况下,给每个方法加上日志、事务、权限校验?嗯,代理模式就是干这个的。

代理模式的核心结构

代理模式的结构其实很简单,就三个角色:

  • 抽象主题(Subject):定义真实对象和代理对象的公共接口。
  • 真实主题(RealSubject):真正干活的业务对象。
  • 代理(Proxy):持有真实主题的引用,控制对它的访问,并在调用前后做一些额外操作。

我习惯把代理模式理解为“中间层”。你在调用者和目标对象之间插了一层,这一层可以做任何事情:缓存、延迟加载、权限校验、远程通信……

Spring AOP 中的 JDK 动态代理和 CGLIB 代理

Spring AOP 是代理模式最经典的应用。它有两种实现方式:JDK 动态代理和 CGLIB 代理。

JDK 动态代理

JDK 动态代理只能代理接口。它通过 java.lang.reflect.ProxyInvocationHandler 在运行时动态生成代理类。

我记得有一次,团队里有个同事问:“为什么我的 Service 类加了 @Transactional 注解,事务就是不生效?”我一看,他那个 Service 类没有实现接口。Spring 默认用 JDK 动态代理,而 JDK 动态代理要求目标对象必须实现接口。没有接口,代理就生成不了,事务自然不生效。

// 定义接口
public interface UserService {
    void addUser(String name);
}

// 真实业务类
public class UserServiceImpl implements UserService {
    @Override
    public void addUser(String name) {
        System.out.println("添加用户:" + name);
    }
}

// 实现 InvocationHandler
public class LogInvocationHandler implements InvocationHandler {
    private Object target;

    public LogInvocationHandler(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 userService = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
    userService.getClass().getClassLoader(),
    userService.getClass().getInterfaces(),
    new LogInvocationHandler(userService)
);
proxy.addUser("张三");

CGLIB 代理

CGLIB 代理不需要接口。它通过继承目标类,在运行时生成目标类的子类作为代理。Spring 在目标对象没有实现接口时,会自动切换到 CGLIB 代理。

我个人建议,如果你能确保业务类都实现接口,就用 JDK 动态代理。如果有些类确实没有接口,那就用 CGLIB。不过要注意,CGLIB 代理的是类,所以 final 方法不能被代理,因为子类无法重写 final 方法。

// 不需要接口
public class UserService {
    public void addUser(String name) {
        System.out.println("添加用户:" + name);
    }
}

// 使用 CGLIB 的 MethodInterceptor
public class LogMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("【日志】调用方法:" + method.getName());
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("【日志】方法执行完毕");
        return result;
    }
}

// 使用
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);
enhancer.setCallback(new LogMethodInterceptor());
UserService proxy = (UserService) enhancer.create();
proxy.addUser("李四");

核心区别:JDK 动态代理基于接口,CGLIB 代理基于继承。Spring 默认优先使用 JDK 动态代理,如果目标对象没有实现接口,则使用 CGLIB。

远程代理(RPC)

远程代理,说白了就是让你像调用本地方法一样调用远程服务。你在本地调用代理对象的方法,代理对象把调用信息序列化后通过网络发送到远程服务器,远程服务器执行真正的业务逻辑,再把结果返回。

我在项目中用过 Dubbo 和 gRPC,它们底层都用了远程代理。你配置一个接口,框架自动生成代理对象。你调用代理对象的方法,框架帮你完成网络通信、序列化、反序列化、负载均衡等复杂操作。

嗯,这里要注意:远程代理虽然方便,但性能开销比本地调用大得多。网络延迟、序列化开销、连接池管理,这些都是你需要考虑的。

虚拟代理(延迟加载大图)

虚拟代理,就是“先给你一个假的,等真正需要的时候再加载真的”。最常见的场景是延迟加载大图。

我曾经做过一个图片浏览应用,用户打开一个包含几百张高清图片的页面。如果一次性加载所有图片,页面直接卡死。我的做法是:先用虚拟代理对象占位,显示一个缩略图或加载中的图标。只有当用户滚动到某张图片的位置时,才真正加载原图。

// 图片接口
public interface Image {
    void display();
}

// 真实图片
public class HighResolutionImage implements Image {
    private String filename;

    public HighResolutionImage(String filename) {
        this.filename = filename;
        loadFromDisk();
    }

    private void loadFromDisk() {
        System.out.println("加载高清图片:" + filename);
        // 模拟耗时加载
        try { Thread.sleep(2000); } catch (InterruptedException e) {}
    }

    @Override
    public void display() {
        System.out.println("显示图片:" + filename);
    }
}

// 虚拟代理
public class ImageProxy implements Image {
    private String filename;
    private HighResolutionImage realImage;

    public ImageProxy(String filename) {
        this.filename = filename;
    }

    @Override
    public void display() {
        if (realImage == null) {
            realImage = new HighResolutionImage(filename);
        }
        realImage.display();
    }
}

提示:虚拟代理的核心思想是“延迟初始化”。它适用于创建开销大的对象,比如大图、大文件、数据库连接等。只有真正需要使用时,才去创建真实对象。

保护代理(权限控制)

保护代理,就是控制谁可以访问目标对象。在调用真实对象的方法之前,代理先检查调用者是否有权限。

我记得在一个后台管理系统中,不同角色有不同的操作权限。管理员可以删除用户,普通用户只能查看。我用保护代理来实现这个逻辑:代理对象在调用真实对象的 deleteUser 方法之前,先检查当前用户的角色。如果不是管理员,直接抛出异常或返回错误提示。

public class UserServiceProxy implements UserService {
    private UserService target;
    private String currentUserRole;

    public UserServiceProxy(UserService target, String currentUserRole) {
        this.target = target;
        this.currentUserRole = currentUserRole;
    }

    @Override
    public void deleteUser(int userId) {
        if (!"ADMIN".equals(currentUserRole)) {
            throw new SecurityException("没有删除用户的权限");
        }
        target.deleteUser(userId);
    }
}

注意:保护代理只能做应用层的权限控制。真正的安全防护需要结合框架级的安全机制,比如 Spring Security。保护代理更多是作为一种轻量级的权限校验手段,适用于简单场景。

代理模式的知识体系

下面这张图展示了代理模式的核心知识结构,包括它的定义、三种主要类型以及在实际项目中的应用场景。

代理模式知识体系 代理模式 静态代理 vs 动态代理 编译期确定代理类 运行时动态生成代理类 三种应用场景 远程代理(RPC) 虚拟代理(延迟加载) 保护代理(权限控制) Spring AOP 实现 JDK 动态代理 CGLIB 代理 基于接口 vs 基于继承

代理模式与其他模式的区别

模式 核心目的 典型场景
代理模式 控制对目标对象的访问 AOP、RPC、延迟加载、权限控制
装饰器模式 动态增强对象的功能 IO 流、日志装饰
适配器模式 转换接口,让不兼容的类能一起工作 电源适配器、接口兼容
外观模式 提供统一的简化接口 子系统封装、门面接口

代理模式和装饰器模式长得有点像,但目的完全不同。代理模式重在“控制访问”,装饰器模式重在“增强功能”。你想想看,代理模式是“我替你做,但我要先检查你有没有资格让我做”,装饰器模式是“你本来就能做,我帮你做得更好”。

个人经验:我曾经在项目中把代理模式和装饰器模式搞混过。后来我总结了一个简单的判断方法:如果你在调用前后做的是“控制性”的操作(权限、日志、事务),那就是代理模式;如果你做的是“增强性”的操作(加功能、改行为),那就是装饰器模式。

代理模式在实际项目中无处不在。Spring AOP 用它实现声明式事务、日志、权限校验;RPC 框架用它实现远程调用;前端框架用它实现图片的延迟加载。说白了,只要你想在不修改原有代码的情况下,给对象调用加上一层控制逻辑,代理模式就是你的首选。

公众号:蓝海资料掘金营,微信deep3321