Java的动态代理在实践中有着广泛的使用场景,比如最场景的Spring AOP、Java注解的获取、日志、用户鉴权等。本篇文章带大家了解一下代理模式、静态代理以及基于JDK原生动态代理。

代理模式

无论学习静态代理或动态代理,我们都要先了解一下代理模式。

先看百度百科的定义:

代理模式的定义:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

直接看定义可能有些难以理解,我们就以生活中具体的实例来说明一下。

我们都去过超市购买过物品,超市从厂商那里购买货物之后出售给我们,我们通常并不知道货物从哪里经过多少流程才到超市。

在这个过程中,等于是厂商“委托”超市出售货物,对我们来说是厂商(真实对象)是不可见的。而超市(代理对象)呢,作为厂商的“代理者”来与我们进行交互。

同时,超市还可以根据具体的销售情况来进行折扣等处理,来丰富被代理对象的功能。

通过代理模式,我们可以做到两点:

1、隐藏委托类的具体实现。

2、实现客户与委托类的解耦,在不改变委托类代码的情况下添加一些额外的功能(日志、权限)等。

代理模式角色定义

在上述的过程中在编程的过程中我们可以定义为三类对象:

  • Subject(抽象主题角色):定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法。比如:广告、出售等。
  • RealSubject(真实主题角色):真正实现业务逻辑的类。比如实现了广告、出售等方法的厂家(Vendor)。
  • Proxy(代理主题角色):用来代理和封装真实主题。比如,同样实现了广告、出售等方法的超时(Shop)。

以上三个角色对应的类图如下:

静态代理实例

静态代理是指代理类在程序运行前就已经存在,这种情况下的代理类通常都是我们在Java代码中定义的。

下面我们就以具体的实例来演示一下静态代理。

首先定义一组接口Sell,用来提供广告和销售等功能。然后提供Vendor类(厂商,被代理对象)和Shop(超市,代理类),它们分别实现了Sell接口。

Sell接口定义如下:

/** * 委托类和代理类都实现了Sell接口 * @author sec * @version 1.0 * @date 2020/3/21 9:30 AM **/public interface Sell {    /**     * 出售     */    void sell();    /**     * 广告     */    void ad();}

Vendor类定义如下:

/** * 供应商 * @author sec * @version 1.0 * @date 2020/3/21 9:30 AM **/public class Vendor implements Sell{    @Override    public void sell() {        System.out.println("Shop sell goods");    }    @Override    public void ad() {        System.out.println("Shop advert goods");    }}

Shop类定义如下:

/** * 超市,代理类 * @author sec * @version 1.0 * @date 2020/3/21 9:30 AM **/public class Shop implements Sell{    private Sell sell;    public Shop(Sell sell){        this.sell = sell;    }    @Override    public void sell() {        System.out.println("代理类Shop,处理sell");        sell.sell();    }    @Override    public void ad() {        System.out.println("代理类Shop,处理ad");        sell.ad();    }}

其中代理类Shop通过聚合的方式持有了被代理类Vendor的引用,并在对应的方法中调用Vendor对应的方法。在Shop类中我们可以新增一些额外的处理,比如筛选购买用户、记录日志等操作。

下面看看在客户端中如何使用代理类。

/** * 静态代理类测试方法 * @author sec * @version 1.0 * @date 2020/3/21 9:33 AM **/public class StaticProxy {    public static void main(String[] args) {        // 供应商---被代理类        Vendor vendor = new Vendor();        // 创建供应商的代理类Shop        Sell sell = new Shop(vendor);        // 客户端使用时面向的是代理类Shop。        sell.ad();        sell.sell();    }}

在上述代码中,针对客户看到的是Sell接口提供了功能,而功能又是由Shop提供的。我们可以在Shop中修改或新增一些内容,而不影响被代理类Vendor。

静态代理的缺点

静态代理实现简单且不侵入原代码,但当场景复杂时,静态代理会有以下缺点:

1、当需要代理多个类时,代理对象要实现与目标对象一致的接口。要么,只维护一个代理类来实现多个接口,但这样会导致代理类过于庞大。要么,新建多个代理类,但这样会产生过多的代理类。

2、当接口需要增加、删除、修改方法时,目标对象与代理类都要同时修改,不易维护。

于是,动态代理便派上用场了。

动态代理

动态代理是指代理类在程序运行时进行创建的代理方式。这种情况下,代理类并不是在Java代码中定义的,而是在运行时根据Java代码中的“指示”动态生成的。

相比于静态代理,动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类的函数。

基于JDK原生动态代理实现

实现动态代理通常有两种方式:JDK原生动态代理和CGLIB动态代理。这里,我们以JDK原生动态代理为例来进行讲解。

JDK动态代理主要涉及两个类:java.lang.reflect.Proxy和java.lang.reflect.InvocationHandler。

InvocationHandler接口定义了如下方法:

/** * 调用处理程序 */public interface InvocationHandler {     Object invoke(Object proxy, Method method, Object[] args); }

顾名思义,实现了该接口的中介类用做“调用处理器”。当调用代理类对象的方法时,这个“调用”会转送到invoke方法中,代理类对象作为proxy参数传入,参数method标识了具体调用的是代理类的哪个方法,args为该方法的参数。这样对代理类中的所有方法的调用都会变为对invoke的调用,可以在invoke方法中添加统一的处理逻辑(也可以根据method参数对不同的代理类方法做不同的处理)。

Proxy类用于获取指定代理对象所关联的调用处理器。

下面以添加日志为例来演示一下动态代理。

import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.util.Date;public class LogHandler implements InvocationHandler {    Object target;  // 被代理的对象,实际的方法执行者    public LogHandler(Object target) {        this.target = target;    }    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        before();        Object result = method.invoke(target, args);  // 调用 target 的 method 方法        after();        return result;  // 返回方法的执行结果    }    // 调用invoke方法之前执行    private void before() {        System.out.println(String.format("log start time [%s] ", new Date()));    }    // 调用invoke方法之后执行    private void after() {        System.out.println(String.format("log end time [%s] ", new Date()));    }}

客户端编写程序使用动态代理代码如下:

import java.lang.reflect.Proxy;/** * 动态代理测试 * * @author sec * @version 1.0 * @date 2020/3/21 10:40 AM **/public class DynamicProxyMain {    public static void main(String[] args) {        // 创建中介类实例        LogHandler logHandler = new LogHandler(new Vendor());        // 设置该变量可以保存动态代理类,默认名称$Proxy0.class        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");        // 获取代理类实例Sell        Sell sell = (Sell) (Proxy.newProxyInstance(Sell.class.getClassLoader(), new Class[]{Sell.class}, logHandler));        // 通过代理类对象调用代理类方法,实际上会转到invoke方法调用        sell.sell();        sell.ad();    }}

执行之后,打印日志如下:

调用方法sell之【前】的日志处理Shop sell goods调用方法sell之【后】的日志处理调用方法ad之【前】的日志处理Shop advert goods调用方法ad之【后】的日志处理

经过上述验证,我们发现已经成功为我们的被代理类统一添加了执行方法之前和执行方法之后的日志。

在上述实例中为了看一下生成的动态代理类的代码,我们添加了下面的属性设置(在生产环境中需要去掉该属性)。

// 设置该变量可以保存动态代理类,默认名称$Proxy0.classSystem.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

那么,我们可以执行main方法之后,还生成了一个名字为$Proxy0.class类文件。通过反编译可看到如下的代码:

package com.sun.proxy;import com.choupangxia.proxy.Sell;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.lang.reflect.UndeclaredThrowableException;public final class $Proxy0 extends Proxy implements Sell {    private static Method m1;    private static Method m2;    private static Method m4;    private static Method m3;    private static Method m0;    public $Proxy0(InvocationHandler var1) throws  {        super(var1);    }    public final boolean equals(Object var1) throws  {        try {            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});        } catch (RuntimeException | Error var3) {            throw var3;        } catch (Throwable var4) {            throw new UndeclaredThrowableException(var4);        }    }    public final String toString() throws  {        try {            return (String)super.h.invoke(this, m2, (Object[])null);        } catch (RuntimeException | Error var2) {            throw var2;        } catch (Throwable var3) {            throw new UndeclaredThrowableException(var3);        }    }    public final void ad() throws  {        try {            super.h.invoke(this, m4, (Object[])null);        } catch (RuntimeException | Error var2) {            throw var2;        } catch (Throwable var3) {            throw new UndeclaredThrowableException(var3);        }    }    public final void sell() throws  {        try {            super.h.invoke(this, m3, (Object[])null);        } catch (RuntimeException | Error var2) {            throw var2;        } catch (Throwable var3) {            throw new UndeclaredThrowableException(var3);        }    }    public final int hashCode() throws  {        try {            return (Integer)super.h.invoke(this, m0, (Object[])null);        } catch (RuntimeException | Error var2) {            throw var2;        } catch (Throwable var3) {            throw new UndeclaredThrowableException(var3);        }    }    static {        try {            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));            m2 = Class.forName("java.lang.Object").getMethod("toString");            m4 = Class.forName("com.choupangxia.proxy.Sell").getMethod("ad");            m3 = Class.forName("com.choupangxia.proxy.Sell").getMethod("sell");            m0 = Class.forName("java.lang.Object").getMethod("hashCode");        } catch (NoSuchMethodException var2) {            throw new NoSuchMethodError(var2.getMessage());        } catch (ClassNotFoundException var3) {            throw new NoClassDefFoundError(var3.getMessage());        }    }}

可以看到$Proxy0(代理类)继承了Proxy类,并且实现了被代理的所有接口,以及equals、hashCode、toString等方法。

由于动态代理类继承了Proxy类,所以每个代理类都会关联一个InvocationHandler方法调用处理器。

类和所有方法都被public final修饰,所以代理类只可被使用,不可以再被继承。

每个方法都有一个Method对象来描述,Method对象在static静态代码块中创建,以“m+数字”的格式命名。

调用方法的时候通过super.h.invoke(this,m1,(Object[])null);调用。其中的super.h.invoke实际上是在创建代理的时候传递给Proxy.newProxyInstance的LogHandler对象,它继承InvocationHandler类,负责实际的调用处理逻辑。

小结

关于代理和动态代理相关的内容,我们就讲这么多。了解了代理模式可以让我们的系统设计的更加具有可扩展性。而动态代理的应用就更广了,各类框架及业务场景都在使用。有了两个基础,就能够更好的学习其他框架。

关于CGLIB动态代理的内容,我们下篇文章再来聊一聊。

本文首发来自微信公众号:程序新视界。一个软实力、硬技术同步学习的平台。

java动态代理_Java代理模式及动态代理详解相关推荐

  1. java 静态 编译_Java中的动态和静态编译实例详解

    Java中的动态和静态编译实例详解 首先,我们来说说动态和静态编译的问题. Q: java和javascript有什么区别? 总结了一下:有以下几点吧: 1.首先从运行环境来说java代码是在JVM上 ...

  2. Java指令全集_Java的JVM字节码指令集详解

    本文详细介绍了如何使用javap查看java方法中的字节码.以及各种字节码的含义,并且配以完善的案例,一步步,从头到尾带领大家翻译javap的输出.在文末还附有JVM字节码指令集表. 本文不适合没有J ...

  3. java mod %区别_Java中 % 与Math.floorMod() 区别详解

    %为取余(rem),Math.floorMod()为取模(mod) 取余取模有什么区别呢? 对于整型数a,b来说,取模运算或者取余运算的方法都是: 1.求 整数商: c = a/b; 2.计算模或者余 ...

  4. java switch 值_Java switch多值匹配操作详解

    这篇文章主要介绍了Java switch多值匹配操作详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 我们都知道 switch 用来走流程分支,大 ...

  5. java 分割数据_java 分割csv数据的实例详解

    java 分割csv数据的实例详解 实际需要解析的csv中很多都是从excel中转过来的,数据中本身包含了逗号的数据导致split的过程中发现数据对不上,因此,基于这种特性,重新写了一个csv数据的分 ...

  6. java 接口函数_Java函数式接口Supplier接口实例详解

    这篇文章主要介绍了Java函数式接口Supplier接口实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 JDK提供了大量常用的函数式接口以丰 ...

  7. java foreach标签_Java中Velocity foreach循环标签详解

    Java中Velocity foreach循环标签详解 Java Velocity中foreach循环可以很容易的遍历数组或者集合. 定义 #foreach( $elem in $allElems) ...

  8. java supplier接口_Java函数式接口Supplier接口实例详解

    这篇文章主要介绍了Java函数式接口Supplier接口实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 JDK提供了大量常用的函数式接口以丰 ...

  9. linux java内存分析_Java内存分析利器MAT使用详解

    这是一篇阅读MAT helper的笔记.Heap dump是Java进程在特定时间的一个内存快照.通常在触发heap dump之前会进行一次full gc,这样dump出来的内容就包含的是被gc后的对 ...

  10. Java是如何实现Future模式的?万字详解!

    1 Future是什么? 先举个例子,我们平时网购买东西,下单后会生成一个订单号,然后商家会根据这个订单号发货,发货后又有一个快递单号,然后快递公司就会根据这个快递单号将网购东西快递给我们.在这一过程 ...

最新文章

  1. 第二阶段第七次站立会议
  2. 弹出硬盘时应用程序阻碍停用设备_7GB硬盘空间被Win 10“偷走”?微软系统的这些“潜规则”你掌握了吗?...
  3. 实战:SQL sever如何实现同一列上下行运算?
  4. java有var吗_java – Var和Var之间的区别
  5. Go 模块--开始使用Go Modules
  6. 1 2014年12月电大远程网络教育计算机统考 最 新 题 库,2014年12月份电大远程网络教育计算机应用基础统考题库试卷6...
  7. 最近wampserver总是开不开,一直离线,重启就好,各位有何看法?
  8. CCF NOI1023 最大跨度
  9. C#并行和多线程编程
  10. 如何根据IP地址获取局域网内的主机名称
  11. Activiti会签
  12. jQuery的文档处理
  13. 【U8】固定资产模块卡片管理栏目设置中没有“凭证号”栏目
  14. 114.114.114.114和8.8.8.8,哪个DNS好?
  15. B 站,真香 ! ! !
  16. centos 7 使用certbot解决域名证书续签最佳实践
  17. 720°VR全景网站制作
  18. redis数据库指令
  19. 路由器或交换机密码忘了——带你修改路由器+交换机密码
  20. 实现正负值及多条Y轴 Echarts柱状图

热门文章

  1. R语言ggplot2可视化分面图(faceting)、在所有的分面中添加相同的参考基准曲线(overlay a base or reference plot to all facets )
  2. R语言使用ggplot2包使用geom_dotplot函数绘制分组点图(配置显示的分组)实战(dot plot)
  3. R语言保存图片为特定dpi值(分辨率)的图像
  4. R语言可视化绘图基础知识详解
  5. R单样本t检验(ONE-SAMPLE T-TEST)
  6. R构建SVM回归模型
  7. java中的常用的文件流_Java 基础(四)| IO 流之使用文件流的正确姿势
  8. R Learnilng 十八讲7-12
  9. 什么是RNA-Seq (RNA Sequencing)
  10. java将输出结果写入csv文件_如何在Java中将数据写入.csv文件?