Java代理模式及源码实现

简介: 代理模式是设计模式中非常常用的一种设计模式,在Java和安卓源码中都有涉及。使用代理模式可以对一些事务进行日志处理,权限控制等.

代理模式简介


代理模式是设计模式中非常常用的一种设计模式,在Java和安卓源码中都有涉及。使用代理模式可以对一些事务进行日志处理,权限控制等。

为其他对象提供一种代理以控制对这个对象的访问。代理对象起到中介作用,可去掉功能服务或增加额外的服务。


代理模式的几种实现方式


代理模式一般有三种:

  • 使用继承
  • 使用聚合
  • 动态代理(重点)
    继承和聚合的方式相对于动态代理来说可以认为是静态代理 ,因为这两种方式的代理类是静态实现的,而动态代理类是根据参数的不同动态实现的。


代理的实现


继承实现代理

使用继承的方式实现代,理非常简单。其实原理就是子类继承父类,子类是父类的代理。

构建以下场景:

比如我们有一公共汽车Bus,在汽车启动前需要确认司机身份,司机身份确认后才能开始行驶,并记录汽车行驶时间,那么我们该如何构建类的关系呢?如果再有一辆小轿车也要做相同的事情呢?如果再有火车,马车。。。

最简单的方法就是:定义一个汽车类Bus.java,它有一个move方法,在move调用前进行身份验证,记录时间这些工作。再定义一个轿车类Car.java,它有一个move方法,在move方法调用前进行身份验证,记录这些工作。再定义。。。很明显,我们这样会使代码无限增多,维护性差,扩展性差。那么我们就定义一个接口Moveable.java,所有类型的车的类都要实现这个接口。

Moveable.java

public interface Moveable {
    void move();
}

Bus.java

public class Bus implements Moveable {
    @Override
    public void move() {
        //预处理逻辑
        System.out.println("确认司机身份中...");
        System.out.println("启动...");
        long startTime = System.currentTimeMillis();
        //核心逻辑
        System.out.println("moving");
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("车辆行驶时间:" + (endTime - startTime) + "毫秒");
    }
}

我们实现了一Bus.java类,但足以说明问题了。

可以发现,此时我们的身份验证和时间记录是写死在move方法中的,很明显这是内聚过度,导致了move的核心逻辑和预处理(身份验证)耦合性太强。

这时我们需要把身份验证和move的核心逻辑进行解耦,新建一个代理类ExtendsProxy.java

public class ExtendsProxy extends Bus {
    @Override
    public void move() {
        //预处理逻辑
        System.out.println("确认司机身份中...");
        System.out.println("启动...");
        long startTime = System.currentTimeMillis();
        super.move();//调用被 代理对象的方法
        long endTime = System.currentTimeMillis();
        System.out.println("车辆行驶时间:" + (endTime - startTime) + "毫秒");
    }
}

这时在Bus.java的move方法中就不需要做预处理操作了,Bus.java如下:

public class Bus implements Moveable {
    @Override
    public void move() {
        System.out.println("moving");
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

我们来测试一下:

public class Client {
    public static void main(String[] args) {        
        //新建代理类对象
        Moveable m = new ExtendsProxy();
        //调用代理方法
        m.move();       
    }
}

结果如下:

确认司机身份中...
启动...
moving
车辆行驶时间:175毫秒

非常好!很开心我们实现了一个代理。可是突然又增加了需求,我们不仅要代理Bus了,还要代理小轿车Car,那怎么办呢?有的人说:好办,再新建一个类继承Car.java,用同样的方式实现代理不就可以了吗?这样确实是可以的,但是相信你的leader会不开森,要是代理一百个类,那你还再写一百个代理 类么?这时你突然醒悟过来:继承的方式实现代理有很大的局限性,扩展性不好,那么我们是坚决摒弃这种做法的。

聚合实现代理

聚合方式是通过把被代理对象当作参数传入代理对象中。这种方式是很常用的方式。

我们实现UnionProxy.java,它也需要实现Moveable接口。为什么要实现Moveable接口呢?统一接口,统一调用。

public class UnionProxy implements Moveable {
    private Moveable m;
    public UnionProxy(Moveable m) {
        this.m = m;
    }
    @Override
    public void move() {
        if (m != null) {
            //预处理逻辑
            System.out.println("确认司机身份中...");
            System.out.println("启动...");
            long startTime = System.currentTimeMillis();
            //调用被 代理对象的方法
            m.move();
            long endTime = System.currentTimeMillis();
            System.out.println("车辆行驶时间:" + (endTime - startTime) + "毫秒");
        }
    }
}

这时如果我们再想要代理其实实现了Movealbe的类,比如轿车,马车之类 的,那么我们做的是直接把它们的对象传到UnionProxy的构造参数中即可,解决了继承方式的弊端,真棒!

但是,但是,但是,问题又来了,现在Leader要求,先启动汽车,再确认身份,顺序颠倒了,那么怎么办呢。因为我们的预处理逻辑是硬编码的,要想添加新的逻辑,那么只能再写一个UnionProxy2.java,在move方法中重新定义预处理逻辑,这样好不好呢?没错,正如你所想的那样,肯定是不好的,因为如果我们有一万种预处理逻辑,就要再写一万个代理类吗?我们希望有一种代理类,既可以动态得传入被代理对象,又可以动态得生成代理方法,这就是动态代理!


动态代理


动态代理在JDK中提供了API,我们可以很方便得使用它,下面我们来了解一下动态代理是如何使用的吧。

JDK中在java.long.reflect中提供了两个类:

  • Proxy.java 代理类,用来生成代理对象
  • InvocationHandler.java 实现这个接口,添加预处理逻辑等
    使用流程如下:
  1. 新建xxxHandler实现InvocationHandler接口,添加预处理逻辑
  2. 创建被代理类对象,并获取被代理类对象的ClassLoader和Intefaces
  3. 创建xxxHandler对象
  4. 调用Proxy.newInstance(loader, interfaces, handler);来创建代理类对象
  5. 调用代理类对象的代理方法
    我们这里创建DDdogerHandler.java实现InvocationHandler接口
    DDdogerHandler.java

public class DDdogerHandler implements InvocationHandler {
    //被代理对象
    private Object target;  
    public DDdogerHandler(Object target) {
        super();
        this.target = target;
    }
    /**
     * o  被代理对象
     * m  被代理的方法
     * args 被代理方法的参数
     */
    @Override
    public Object invoke(Object o, Method m, Object[] args) throws Throwable {
        // 预处理逻辑
        System.out.println("确认司机身份中...");
        System.out.println("启动...");
        long startTime = System.currentTimeMillis();
        m.invoke(target);//调用被代理方法,并传入被代理对象作为参数
        long endTime = System.currentTimeMillis();
        System.out.println("车辆行驶时间:" + (endTime - startTime) + "毫秒");
        return null;
    }
}

接下来创建代理对象并调用:

public static void main(String[] args) {
        Moveable target;
        // 新建被代理对象
        target = new Bus();
//      target = new Car();
        // 新建handler对象
        DDdogerHandler handler = new DDdogerHandler(target);
        /**
         * 参数: loader 类加载器 
         * interfaces 被代理对象实现的接口 
         * h InvocationHandler对象
         */
        Moveable m = (Moveable) Proxy.newProxyInstance(target.getClass().getClassLoader(), 
                target.getClass().getInterfaces(),
                handler);
        m.move();
    }

运行结果如下:

确认司机身份中...
启动...
The bus is moving
车辆行驶时间:72毫秒

我们先来分析一下,如何使用JDK提供的API实现对任意对象添加任意的代理

正如之前所说:如果我们要对其它类比如Car.java实现先验证司机身份再启动的,最后记录行驶时间的代理,那我们应该怎么做呢?

因为代理的预处理工作不变,所以只需要把传入把Bus对象换成Car对象即可,如下:

public static void main(String[] args) {
        Moveable target;
        // 新建被代理对象
//      target = new Bus();
        target = new Car();
        // 新建handler对象
        DDdogerHandler handler = new DDdogerHandler(target);
        /**
         * 参数: loader 类加载器 
         * interfaces 被代理对象实现的接口 
         * h InvocationHandler对象
         */
        Moveable m = (Moveable) Proxy.newProxyInstance(target.getClass().getClassLoader(), 
                target.getClass().getInterfaces(),
                handler);
        m.move();
    }

运行结果如下:

确认司机身份中...
启动...
The car is moving
车辆行驶时间:770毫秒

怎么样,很简单吧。那么我们如果想要实现先启动,再验证司机身份的预处理逻辑该怎么做呢?那我们只需要把参数传入到handler对象中,根据参数来定义预处理逻辑的顺序即可。这里不再详述。

看来动态代理确实很方便啊,那它内部究竟是如何实现的呢?

下面我们来手动实现动态代理吧~


手动实现动态代理

动态代理,顾名思义,就是动态得生成代理类,再动态得把代理类编译成class文件,最后生成代理类对象并调用的一个过程。所以我们的实现思路如下:

  1. 首先要声明一段源码,要知道源码其实就是一些字符,把这些字符写到文件中就是源码文件了
  2. 编译源码,JDK提供了相关的Complier API,生成字节码文件
  3. 通过反射通过字节码文件生成代理对象
    PS:为了与JDK的API区别开来,以下类使用不同的命名方式。
    首先要有一个代理类DDdogerProxy.java,它有一个方法newInstance()来生成代理类对象。生成对象的过程就是上面的3步。
    话不多说,先上源码!

public class DDdogerProxy {
    /**
     * @param infc 被代理类实现的接口Class对象
     * @return
     * @throws Exception
     */
    public static Object newInstance(Class infc) throws Exception {
        //1 生成源码文件
        //1.1 声明一段源码,这里使用的是UnionProxy的源码进行稍加整理
        //其中Moveable需要根据传入的参数infc来确定
        //构建的代理类名取为$Proxy0,与jdk保持一致,当然也可以换成别的名字
        String sourceStr = "package com.dddoger.proxy;\r\n" + 
                "\r\n" + 
                "public class $Proxy0 implements "+ infc.getName() +" {\r\n" + 
                "   private "+ infc.getName() +" m;\r\n" + 
                "   public $Proxy0("+ infc.getName() +" m) {\r\n" + 
                "       this.m = m;\r\n" + 
                "   }\r\n" + 
                "   @Override\r\n" + 
                "   public void move() {\r\n" + 
                "       if (m != null) {\r\n" + 
                "           //预处理逻辑\r\n" + 
                "           System.out.println(\"确认司机身份中...\");\r\n" + 
                "           System.out.println(\"启动...\");\r\n" + 
                "           long startTime = System.currentTimeMillis();\r\n" + 
                "           \r\n" + 
                "           //调用被 代理对象的方法\r\n" + 
                "           m.move();\r\n" + 
                "           \r\n" + 
                "           long endTime = System.currentTimeMillis();\r\n" + 
                "           System.out.println(\"车辆行驶时间:\" + (endTime - startTime) + \"毫秒\");\r\n" + 
                "       }\r\n" + 
                "   }\r\n" + 
                "}";
        //1.2 定义文件名,相对路径,位于工程目录下/bin/com/dddoger/proxy/$Proxy0.java
        String filename = System.getProperty("user.dir") +"/bin/com/dddoger/proxy/$Proxy0.java";
        File file = new File(filename);
        //1.3 这里使用的commons-io方便操作,生成源文件$Proxy0.java
        FileUtils.writeStringToFile(file, sourceStr);
        //2 编译源码文件
        //2.1 获取系统编译器
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        //2.2 通过cmopiler对象获取文件管理者对象
        StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
        //2.3 获取文件对象
        Iterable fileObjs = fileMgr.getJavaFileObjects(filename);
        //2.4 建立编译任务
        CompilationTask task = compiler.getTask(null, fileMgr, null, null, null, fileObjs);
        //2.5 开始编译任务
        task.call();
        //3 加载class文件到内存
        //3.1 获取类加载器
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        //3.2 加载指定的类到内存,我们只需要加载代理类$Proxy
        Class clazz = loader.loadClass("com.dddoger.proxy.$Proxy0");
        //4 使用构造器创建代理对象并返回
        //4.1 获取被代理类的构造器,参数为被代理类对象
        Constructor constructor = clazz.getConstructor(infc);
        //4.2 创建代理对象,并传入参数
        Object proxy = constructor.newInstance(new Bus());
        return proxy;
    }
}

注释已经写得很详细了,相信大家理解整体的思路。

然后来测试一下看:

public static void main(String[] args) throws Exception {
        Moveable target;
        //被代理对象
        target = new Bus();
        //生成代理对象,因为代理类是实现了Movealbe接口的,
        //也就是说代理类和被代理类都实现了同一接口
        Moveable m = (Moveable)DDdogerProxy.newInstance(Moveable.class);
        m.move();
    }

运行结果:

确认司机身份中...
启动...
The Bus is moving
车辆行驶时间:533毫秒

再说明一下中间过程,这样更清晰一些。

在执行完FileUtils.writeStringToFile(file, sourceStr);之后,会生成源码文件$Proxy0.java,使用Navigator可以看到:3.png


$Proxy0.java内容如下:


package com.dddoger.proxy;
public class $Proxy0 implements com.dddoger.proxy.Moveable {
    private com.dddoger.proxy.Moveable m;
    public $Proxy0(com.dddoger.proxy.Moveable m) {
        this.m = m;
    }
    @Override
    public void move() {
        if (m != null) {
            //预处理逻辑
            System.out.println("确认司机身份中...");
            System.out.println("启动...");
            long startTime = System.currentTimeMillis();
            //调用被 代理对象的方法
            m.move();
            long endTime = System.currentTimeMillis();
            System.out.println("车辆行驶时间:" + (endTime - startTime) + "毫秒");
        }
    }
}

经过执行task.call();后会生成$Proxy0.class4.png


字节码文件,需要先加载到内存中,再创建对象。

写到这里,动态代理还没有完全实现,因为我位的代理逻辑是硬编码的,需要把这些代理逻辑解耦,该怎么办呢?别忘了,JDK中有一个InvocationHandler的类,叫做事务处理器,代理逻辑是放在这里面的。把InvocationHandler对象传到DDdogerProxy作为参数,再反射调用InvocationHandler的方法不就可以了吗。

DDdogerInvocationHandler.java

public interface DDdogerInvocationHandler {
    /**
     * @param proxy 代理对象
     * @param m 被代理方法
     * @throws Exception 
     */
    void invoke(Object proxy, Method m) throws Exception;
}

实现该接口,并添加代理逻辑

BusHandler.java

public class BusHandler implements DDdogerInvocationHandler {
    private Object target;  
    public BusHandler(Object target) {
        super();
        this.target = target;
    }   
    @Override
    public void invoke(Object proxy, Method m) throws Exception {
        // 预处理逻辑
        System.out.println("确认司机身份中...");
        System.out.println("启动...");
        long startTime = System.currentTimeMillis();
        m.invoke(target);
        long endTime = System.currentTimeMillis();
        System.out.println("车辆行驶时间:" + (endTime - startTime) + "毫秒");
    }
}

我们的代理类DDdogerProxy.java也要做一些修改,添加handler参数,并且动态得生成代理逻辑。

修改后的DDdogerProxy.java如下:

public class DDdogerProxy {
    /**
     * @param infc 被代理类实现的接口Class对象
     * @return
     * @throws Exception
     */
    public static Object newInstance(Class infc, DDdogerInvocationHandler h) throws Exception {
        //1 生成源码文件
        //1.1 声明一段源码,这里使用的是UnionProxy的源码进行稍加整理
        //其中Moveable需要根据传入的参数infc来确定
        //构建的代理类名取为$Proxy0,与jdk保持一致,当然也可以换成别的名字
        String methodStr = "";
        //循环获取被代理类的方法
        for(Method m : infc.getMethods()) {
            methodStr += 
                    "   @Override\r\n" + 
                    "   public void " + m.getName() + "() {\r\n" + 
                    "       try{\r\n" +
                    "           Method md = " + infc.getName() + ".class.getMethod(\"" + m.getName() + "\");" +
                    "           h.invoke(this, md);\r\n" + 
                    "       }catch(Exception e){e.printStackTrace();}\r\n" +
                    "   }";
        }
        String sourceStr = "package com.dddoger.proxy;\r\n" + 
                "import java.lang.reflect.Method;\r\n" + 
                "import com.dddoger.proxy.manual.DDdogerInvocationHandler;\r\n" + 
                "public class $Proxy0 implements "+ infc.getName() +" {\r\n" + 
                "   private DDdogerInvocationHandler h;\r\n" +
                "   public $Proxy0(DDdogerInvocationHandler h) {\r\n" + 
                "       this.h = h;\r\n" + 
                "   }\r\n" + 
                    methodStr +
                "}";
        //1.2 定义文件名,相对路径,位于工程目录下/bin/com/dddoger/proxy/$Proxy0.java
        String filename = System.getProperty("user.dir") +"/bin/com/dddoger/proxy/$Proxy0.java";
        File file = new File(filename);
        //1.3 这里使用的commons-io方便操作,生成源文件$Proxy0.java
        FileUtils.writeStringToFile(file, sourceStr);
        //2 编译源码文件
        //2.1 获取系统编译器
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        //2.2 通过cmopiler对象获取文件管理者对象
        StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
        //2.3 获取文件对象
        Iterable fileObjs = fileMgr.getJavaFileObjects(filename);
        //2.4 建立编译任务
        CompilationTask task = compiler.getTask(null, fileMgr, null, null, null, fileObjs);
        //2.5 开始编译任务
        task.call();
        //3 加载class文件到内存
        //3.1 获取类加载器
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        //3.2 加载指定的类到内存,我们只需要加载代理类$Proxy
        Class clazz = loader.loadClass("com.dddoger.proxy.$Proxy0");
        //4 使用构造器创建代理对象并返回
        //4.1 获取被代理类的构造器,参数为被代理类对象
        Constructor constructor = clazz.getConstructor(DDdogerInvocationHandler.class);
        //4.2 创建代理对象,并传入参数
        Object proxy = constructor.newInstance(h);
        return proxy;
    }
}

主要修改有以下几点:

  1. newInstance方法中添加了事务处理器DDdogerInvocationHandler的h参数,DDdogerInvocationHandler在它的invoke方法中有预处理逻辑并反射调用了代理方法。
  2. 代理类$Proxy0.java要动态实现Moveable接口的所有方法,在方法内部使用反射调用事务处理器的方法。

for(Method m : infc.getMethods()) {
    methodStr += 
        "   @Override\r\n" + 
        "   public void " + m.getName() + "() {\r\n" + 
        "       try{\r\n" +
        "           Method md = " + infc.getName() + ".class.getMethod(\"" + m.getName() + "\");" +
        "           h.invoke(this, md);\r\n" + 
        "       }catch(Exception e){e.printStackTrace();}\r\n" +
        "   }";
}
  1. 生成$Proxy0的对象使用的参数也要对应改变

//4.1 获取被代理类的构造器,参数为被代理类对象
Constructor constructor = clazz.getConstructor(DDdogerInvocationHandler.class);
//4.2 创建代理对象,并传入参数
Object proxy = constructor.newInstance(h);

再测试一下:

target = new Bus();
BusHandler handler = new BusHandler(target);
//也就是说代理类和被代理类都实现了同一接口
Moveable m = (Moveable)DDdogerProxy.newInstance(Moveable.class, handler);
m.move();

输出结果:

确认司机身份中...
启动...
The Bus is moving
车辆行驶时间:137毫秒

这时我们的动态代理就基本实现了,只不过我们实现的事务处理器的invoke方法少一个args参数,返回值也为void,但是不影响我们整体的,我们暂时不再过多讨论。


目录
相关文章
|
2天前
|
监控 前端开发 Java
Java基于B/S医院绩效考核管理平台系统源码 医院智慧绩效管理系统源码
医院绩效考核系统是一个关键的管理工具,旨在评估和优化医院内部各部门、科室和员工的绩效。一个有效的绩效考核系统不仅能帮助医院实现其战略目标,还能提升医疗服务质量,增强患者满意度,并促进员工的专业成长
9 0
|
2天前
|
Java 云计算
Java智能区域医院云HIS系统SaaS源码
云HIS提供标准化、信息化、可共享的医疗信息管理系统,实现医患事务管理和临床诊疗管理等标准医疗管理信息系统的功能。优化就医、管理流程,提升患者满意度、基层首诊率,通过信息共享、辅助诊疗等手段,提高基层医生的服务能力构建和谐的基层医患关系。
16 2
|
2天前
|
Java
从源码出发:JAVA中对象的比较
从源码出发:JAVA中对象的比较
10 3
|
3天前
|
前端开发 Java 关系型数据库
Java医院绩效考核系统源码B/S架构+springboot三级公立医院绩效考核系统源码 医院综合绩效核算系统源码
作为医院用综合绩效核算系统,系统需要和his系统进行对接,按照设定周期,从his系统获取医院科室和医生、护士、其他人员工作量,对没有录入信息化系统的工作量,绩效考核系统设有手工录入功能(可以批量导入),对获取的数据系统按照设定的公式进行汇算,且设置审核机制,可以退回修正,系统功能强大,完全模拟医院实际绩效核算过程,且每步核算都可以进行调整和参数设置,能适应医院多种绩效核算方式。
21 2
|
5天前
|
传感器 人工智能 前端开发
JAVA语言VUE2+Spring boot+MySQL开发的智慧校园系统源码(电子班牌可人脸识别)Saas 模式
智慧校园电子班牌,坐落于班级的门口,适合于各类型学校的场景应用,班级学校日常内容更新可由班级自行管理,也可由学校统一管理。让我们一起看看,电子班牌有哪些功能呢?
46 4
JAVA语言VUE2+Spring boot+MySQL开发的智慧校园系统源码(电子班牌可人脸识别)Saas 模式
|
5天前
|
分布式计算 Java API
Java8 Lambda实现源码解析
Java8的lambda应该大家都比较熟悉了,本文主要从源码层面探讨一下lambda的设计和实现。
|
6天前
|
存储 Java
0基础java初学者都能做的打字通小游戏? 内含源码解读和细致讲解!!
0基础java初学者都能做的打字通小游戏? 内含源码解读和细致讲解!!
17 2
0基础java初学者都能做的打字通小游戏? 内含源码解读和细致讲解!!
|
6天前
|
SQL Java 分布式数据库
实现HBase表和RDB表的转化(附Java源码资源)
该文介绍了如何将数据从RDB转换为HBase表,主要涉及三个来源:RDB Table、Client API和Files。文章重点讲解了RDB到HBase的转换,通过批处理思想,利用RDB接口批量导出数据并转化为`List<Put>`,然后导入HBase。目录结构包括配置文件、RDB接口及实现类、HBase接口及实现类,以及一个通用转换器接口和实现。代码中,`RDBImpl`负责从RDB读取数据并构造`Put`对象,`HBaseImpl`则负责将`Put`写入HBase表。整个过程通过配置文件`transfer.properties`管理HBase和RDB的映射关系。
22 3
实现HBase表和RDB表的转化(附Java源码资源)
|
8天前
|
消息中间件 缓存 Java
java基于云部署的SaaS医院云HIS系统源码 心理CT、B超 lis、电子病历
云HIS系统是一款满足基层医院各类业务需要的健康云产品。该产品能帮助基层医院完成日常各类业务,提供病患预约挂号支持、病患问诊、电子病历、开药发药、会员管理、统计查询、医生工作站和护士工作站等一系列常规功能,还能与公卫、PACS等各类外部系统融合,实现多层机构之间的融合管理。
42 12
|
12天前
|
人工智能 监控 Java
java互联网+智慧工地云平台SaaS源码
智慧工地以施工现场风险预知和联动预控为目标,将智能AI、传感技术、人像识别、监控、虚拟现实、物联网、5G、大数据、互联网等新一代科技信息技术植入到建筑、机械、人员穿戴设施、场地进出关口等各类设备中,实现工程管理与工程施工现场的整合
24 0