Java代理模式与装饰模式的应用与实现

发布时间 - 2026-01-10 00:00:00    点击率:
该用InvocationHandler而非继承当需不修改目标类即实现权限校验、日志记录等访问控制;JDK动态代理仅支持接口,调用method.invoke(target,args)不可省略,Spring中应启用AOP而非手动创建代理。

代理模式:什么时候该用 InvocationHandler 而不是继承?

代理模式的核心是「控制访问」,不是「增强行为」。如果你需要在不修改目标类的前提下,对方法调用做权限校验、日志记录、延迟加载或远程通信封装,Proxy.newProxyInstance() + InvocationHandler 是标准解法。

常见错误是把代理当成装饰器来用——比如只为加个缓存就写个代理,结果绕过类型安全、丢失泛型信息、还无法处理 final 方法。

  • 只适用于接口:JDK 动态代理只能代理接口,不能代理具体类;要代理类得用 CGLIB(但会生成子类,final 类/方法直接失败)
  • invoke() 中必须显式调用 method.invoke(target, args),漏掉这句就等于拦截后没转发,目标逻辑不会执行
  • 如果目标对象本身是 Spring Bean,别手动 new Proxy,应通过 @EnableAspectJAutoProxy(proxyTargetClass = true)@Scope("prototype") 配合 AOP 使用,否则可能破坏 Spring 的生命周期管理
public class LoggingInvocationHandler implements InvocationHandler {
    private final Object target;
    public LoggingInvocationHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before " + method.getName());
        Object result = method.invoke(target, args); // 必须调用!
        System.out.println("After " + method.getName());
        return result;
    }
}

装饰模式:为什么 InputStream 体系是教科书级实现?

装饰模式的关键是「职责叠加」,每个装饰器都持有被装饰对象的引用,并在自身方法中组合调用它。Java 标准库的 InputStream 子类就是典型:你可以在 FileInputStream 上套一层 BufferedInputStream,再套一层 GZIPInputStream,每一层只关心自己的逻辑,不侵入下层。

容易踩的坑是混淆「构造时传入」和「运行时替换」:装饰器一旦构建完成,内部引用的对象就不能动态切换;想换底层流,得重建整个装饰链。

立即学习“Java免费学习笔记(深入)”;

  • 所有装饰器必须和被装饰类实现同一接口(如 InputStream),否则无法无缝替换
  • 装饰器通常不重写全部方法,只覆盖关心的几个(如 read()),其余直接委托给被装饰对象
  • 注意关闭顺序:应从最外层装饰器开始 close(),它内部会自动触发内层的 close();但如果某层装饰器自己打开了资源(如 BufferedOutputStream 的缓冲区),必须确保其 close() 被调用,否则资源泄露
public class CountingInputStream extends InputStream {
    private final InputStream in;
    private long bytesRead = 0;
    public CountingInputStream(InputStream in) {
        this.in = in;
    }
    @Override
    public int read() throws IOException {
        int b = in.read();
        if (b != -1) bytesRead++;
        return b;
    }
    public long getBytesRead() { return bytesRead; }
}

代理 vs 装饰:一个接口,两种意图

两者代码结构相似(都持有一个目标对象并转发调用),但设计意图完全不同。判断依据不是“谁包着谁”,而是「谁拥有对象生命周期」和「谁决定是否调用目标」。

代理模式中,代理对象通常由框架创建(如 Spring AOP),客户端拿到的是代理,根本不知道真实对象存在;而装饰模式中,客户端主动组装装饰链,清楚每一层的作用,且能随时拆解某一层。

  • 代理关注「访问控制」:SecurityManager.checkPermission() 在调用前抛异常,目标方法根本不执行
  • 装饰关注「功能增强」:BufferedInputStream 把多次小读取合并成一次大读取,但最终仍会调用到底层 read()
  • Spring 的 @Transactional 是代理(事务开启/提交完全绕过业务方法逻辑);而 Collections.unmodifiableList() 返回的是装饰器(它把所有修改操作转为抛异常,但查询操作照常委托)

手写代理易忽略的线程与泛型问题

手动实现静态代理或简单动态代理时,最容易被忽略的是泛型擦除和线程安全性。JDK 代理返回的 Object 需要强制转型,而泛型信息在运行时已不存在,强转失败会在运行时报 ClassCastException,而不是编译期报错。

另一个隐性问题是 InvocationHandler 实例是否线程安全。如果它内部维护了状态(比如计数器、缓存 map),多个线程同时调用代理对象的方法,就会出现竞态条件。

  • 避免在 InvocationHandler 中保存非线程安全的可变状态;若必须缓存,用 ConcurrentHashMap 或加锁
  • 使用 Proxy.newProxyInstance() 时,ClassLoader 参数建议传目标接口的类加载器(interface.getClass().getClassLoader()),避免跨 ClassLoader 导致 ClassCastException
  • 如果目标接口有泛型方法(如 T get(String key)),代理无法保留类型参数,返回值需按实际类型强转,IDE 可能提示 unchecked warning,这是正常现象
代理和装饰的边界在真实项目里常常模糊。真正难的不是写出来,而是每次加一层包装时,问一句:我是在控制访问,还是在叠加职责?答错了,后面维护的人就得花三倍时间理清调用链。


# java  # ssl  # proxy  # stream  # 延迟加载  # 动态代理  # 标准库  # 为什么  # red 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: 如何在宝塔面板创建新站点?  Laravel如何发送系统通知?(Notification渠道示例)  Laravel如何实现API速率限制?(Rate Limiting教程)  如何在沈阳梯子盘古建站优化SEO排名与功能模块?  通义万相免费版怎么用_通义万相免费版使用方法详细指南【教程】  百度输入法全感官ai怎么关 百度输入法全感官皮肤关闭  Laravel PHP版本要求一览_Laravel各版本环境要求对照  什么是javascript作用域_全局和局部作用域有什么区别?  如何注册花生壳免费域名并搭建个人网站?  高端企业智能建站程序:SEO优化与响应式模板定制开发  Win11怎样安装网易有道词典_Win11安装词典教程【步骤】  JavaScript Ajax实现异步通信  详解Oracle修改字段类型方法总结  Android Socket接口实现即时通讯实例代码  美食网站链接制作教程视频,哪个教做美食的网站比较专业点?  Laravel如何编写单元测试和功能测试?(PHPUnit示例)  香港服务器建站指南:免备案优势与SEO优化技巧全解析  Laravel如何配置和使用队列处理异步任务_Laravel队列驱动与任务分发实例  详解jQuery中的事件  Laravel怎么生成URL_Laravel路由命名与URL生成函数详解  ,怎么在广州志愿者网站注册?  JS中使用new Date(str)创建时间对象不兼容firefox和ie的解决方法(两种)  香港服务器租用每月最低只需15元?  Windows10如何删除恢复分区_Win10 Diskpart命令强制删除分区  如何快速使用云服务器搭建个人网站?  如何撰写建站申请书?关键要点有哪些?  如何快速配置高效服务器建站软件?  Laravel如何与Docker(Sail)协同开发?(环境搭建教程)  微信小程序 配置文件详细介绍  今日头条AI怎样推荐抢票工具_今日头条AI抢票工具推荐算法与筛选【技巧】  如何使用 Go 正则表达式精准提取括号内首个纯字母标识符(忽略数字与嵌套)  edge浏览器无法安装扩展 edge浏览器插件安装失败【解决方法】  消息称 OpenAI 正研发的神秘硬件设备或为智能笔,富士康代工  Laravel如何使用Blade组件和插槽?(Component代码示例)  Laravel怎么实现前端Toast弹窗提示_Laravel Session闪存数据Flash传递给前端【方法】  Laravel如何优化应用性能?(缓存和优化命令)  Laravel如何与Pusher实现实时通信?(WebSocket示例)  历史网站制作软件,华为如何找回被删除的网站?  如何快速搭建高效服务器建站系统?  Windows10电脑怎么设置虚拟光驱_Win10右键装载ISO镜像文件  PHP正则匹配日期和时间(时间戳转换)的实例代码  Java解压缩zip - 解压缩多个文件或文件夹实例  Laravel用户密码怎么加密_Laravel Hash门面使用教程  千库网官网入口推荐 千库网设计创意平台入口  Laravel如何使用Eloquent进行子查询  如何在七牛云存储上搭建网站并设置自定义域名?  香港服务器WordPress建站指南:SEO优化与高效部署策略  Linux后台任务运行方法_nohup与&使用技巧【技巧】  高端建站如何打造兼具美学与转化的品牌官网?  mc皮肤壁纸制作器,苹果平板怎么设置自己想要的壁纸我的世界?