首页 > 编程语言 > Java设计模式之代理模式详解
2021
06-19

Java设计模式之代理模式详解

一、代理模式

代理模式就是有一个张三,别人都没有办法找到他,只有他的秘书可以找到他。那其他人想和张三交互,只能通过他的秘书来进行转达交互。这个秘书就是代理者,他代理张三。

再看看另一个例子:卖房子

卖房子的步骤:

1.找买家

2.谈价钱

3.签合同

4.和房产局签订一些乱七八糟转让协议

一般卖家只在签合同的时候可能出面一下,其他的1,2,4都由中介去做。那你问这样有什么用呢?

首先,一个中介可以代理多个卖房子的卖家,其次,我们可以在不修改卖家的代码的情况下,给他实现房子加价、打广告等等夹带私货的功能。

而Java的代理模式又分为静态代理和动态代理

二、静态代理

静态代理中存在着以下的角色:

  • 抽象角色:一般使用接口或者抽象类实现(一般是真实角色和代理角色抽象出来的共同部分,比如卖房子的人和中介都有公共的方法卖房子)
  • 真实角色:被代理的角色(表示一个具体的人,比如卖房子的张三)
  • 代理角色:代理真实角色的中介,一般在代理真实角色后,会做一些附属的操作
  • 客户:使用代理角色来进行一些操作(买房子的)

代码实现:

//接口(抽象角色)
public interface Singer{
	// 歌星要会唱歌
	void sing();
}

实体类男歌手

//具体角色,男歌手
public class MaleSinger implements Singer{
    private String name;
    public MaleSinger(String name) {
        this.name = name;
    }
    @Override
    public void sing() {
        System.out.println(this.name+"男歌手在唱歌");
    }
}

歌手的经纪人

//代理角色
public class Agent implements Singer{
    private MaleSinger singer; //代理角色要有一个被代理角色
    public Agent(MaleSinger singer) {
        this.singer = singer;
    }
    @Override
    public void sing() {
        System.out.println("协商出场费,做会场工作");
        //一定是被代理角色歌手去唱歌
        singer.sing();
        System.out.println("会场收拾,结算费用");
    }
}

客户

//客户
public class Client {
    public static void main(String[] args) {
        MaleSinger singer=new MaleSinger("周杰伦");
        Agent agent=new Agent(singer);
        agent.sing();//通过代理来运行唱歌
    }
}

可以看到抽象角色就包含了具体角色和代理角色公共的方法sing()。然后通过歌手的经纪人在歌手唱歌的前后可以任意增加自己想要增加的代码。从而达到不修改歌手类方法的同时给唱歌增加新功能的目的。

说白了。代理就是在不修改原来的代码的情况下,给源代码增强功能。

小结

静态代理模式的主要优点有:

  • 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用(经纪人保护周杰伦,外界不能直接接触周杰伦)
  • 代理对象可以扩展目标对象的功能(本来只能唱歌,现在又多了协商出场费,做会场工作等等功能)
  • 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性

其主要缺点是:

  • 代理模式会造成系统设计中类的数量增加(多了个代理类)
  • 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢(每次都要先找中介才能找到周杰伦)
  • 增加了系统的复杂度(一开始只是个独立的流浪歌手,然后有了经纪人后就十分复杂了)

三、动态代理

静态代理中,比如上述的例子,我们所写的经纪人只能服务malesinger,不能再服务其他的类型的歌手,这很不现实。因为经纪人肯定能去服务不止一种歌手,甚至可能连歌手都不是,去服务跳舞的了。如果静态代理中要实现这个结果,那我们要手动编写好多个agent类,十分繁琐而复杂。所以就出现了动态代理,动态代理可以自动生成代理人的代码。

JDK原生的动态代理

核心类:InvocationHandler类Proxy类

我们重新写一下Singer接口,给他多一个跳舞的方法

//歌手接口
public interface Singer2 {
    void sing();
    void dance();
}

当然对应的男歌手实现类也要改变

//男歌手实现类
public class MaleSinger2 implements Singer2 {
    private String name;

    public MaleSinger2(String name) {
        this.name = name;
    }

    @Override
    public void sing() {
        System.out.println(this.name+"在唱歌");
    }

    @Override
    public void dance() {
        System.out.println(this.name+"在跳舞");
    }
}

然后我们直接进入客户,测试。

import com.hj.Agent2;
import com.hj.MaleSinger2;
import com.hj.Singer2;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Client2 {
    public static void main(String[] args) {
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");//设置用于输出jdk动态代理产生的类
        //简单例子,把所有东西放到一段来解释
        System.out.println("实例1------------------------------------------------");
        MaleSinger2 maleSinger = new MaleSinger2("周杰伦");
        //新建代理实例
        //newProxyInstance(ClassLoader loader, 类加载器,不懂的可以去看https://blog.csdn.net/Doraemon_Nobita/article/details/115702012?spm=1001.2014.3001.5501
        //Class<?>[] interfaces, 实现的接口,注意是个数组
        //InvocationHandler h 处理函数)
        Singer2 agent = (Singer2) Proxy.newProxyInstance(Client2.class.getClassLoader(),
                new Class[]{Singer2.class}, new InvocationHandler() {//匿名内部类的方式实现InvocationHandler接口,对这个看不懂的可以参考https://blog.csdn.net/Doraemon_Nobita/article/details/115506705?spm=1001.2014.3001.5501
                    @Override
                    // 这个invoke就是我们调用agent.sing()后调用的方法
                    // invoke(Object proxy, 代理对象
                    // Method method, method是方法,即我们要调用的方法(是唱歌还是跳舞,在调用的时候会是sing()还是dance())
                    // Object[] args 参数列表,可能你需要传参)
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("协商出场费,做会场工作");
                        //关于invoke的讲解,详情可以参考:https://blog.csdn.net/Doraemon_Nobita/article/details/115702012?spm=1001.2014.3001.5501 调用指定的方法的那部分。
                        //invoke方法的参数,一个是Object类型,也就是调用该方法的对象,
                        //第二个参数是一个可变参数类型,也就是给这个方法的传参,外层的这个已经给我们封装成args了,直接用就是了
                        Object invoke = method.invoke(maleSinger,args);//通过反射获取到的method名我们再invoke激活一下,传入要调用该方法的对象。这里用maleSinger
                        System.out.println("会场收拾,结算费用");
                        return invoke;
                    }
                });
        agent.sing();//可以调用到maleSinger的sing()
        agent.dance();//调用到maleSinger的dance()
        System.out.println("实例2------------------------------------------------");
        //这个简单例子不行啊,我还每次必须写死这里是maleSinger,以后想换别的还得改这里。动态代理岂是如此不便之物。
        //所以我们直接实现一下InvocationHandler接口,取名为Agent2
        MaleSinger2 JayZ=new MaleSinger2("周杰伦");
        MaleSinger2 JJ =new MaleSinger2("林俊杰");
        Singer2 agentJJ=(Singer2) Proxy.newProxyInstance(Client2.class.getClassLoader(),
                new Class[]{Singer2.class}, new Agent2(JJ));
        Singer2 agentJayZ=(Singer2) Proxy.newProxyInstance(Client2.class.getClassLoader(),
                new Class[]{Singer2.class}, new Agent2(JayZ));
        //可以看到现在代理人创建就十分方便了
        agentJJ.dance();
        agentJJ.sing();
        agentJayZ.sing();
        agentJayZ.dance();
    }
}

在第一个例子中,可以看到我们需要利用Proxy类的newProxyInstance()方法就可以生成一个代理对象。而newProxyInstance()的参数又有类加载器、实现的接口数组、以及InvocationHandler对象。在这里使用匿名内部类来实现InvocationHandler接口。实现该接口需要实现他的invoke方法,这个方法就是我们代理对象调用原方法的时候会使用到的方法。区别于反射中的invoke方法,它有三个参数分别是代理对象,调用的方法,方法的参数数组。这里代理对象我们不管,调用的方法则是通过反射获取到的我们使用该代理调用sing()方法或者dance()方法的方法名。通过反射中的invoke方法,可以运行这个指定的对象里方法名的方法。

而第二个例子中,为了实现可以代理任何类,我们实现InvocationHandler接口,并把取类名为Agent2。下面是Agent2的代码。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class Agent2 implements InvocationHandler {
    private Object object;//想代理谁都可以,随便啊

    public Agent2(Object object) {
        this.object = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("协商出场费,做会场工作");
        //一定是歌手去唱歌
        Object invoke = method.invoke(object,args);
        System.out.println("会场收拾,结算费用");
        return invoke;
    }
}

可以看出,这里和第一个例子的实现是差不多的,只不过我们使用Object类来代替了之前的写死的MaleSinger类,这样我们就可以代理任何的类型了,只要这个类型需要我们在前后加"协商出场费,做会场工作"、“会场收拾,结算费用”。那可以看到第二个例子中,林俊杰和周杰伦的代理人可以很方便地创建出来,哪怕后面再实现了一个FemaleSinger类,也可以直接生成他的代理人。

加了

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");//设置用于输出jdk动态代理产生的类这句代码以后,我们就可以在项目中找到JDK自动生成的代理类代码:

在这里插入图片描述

打开可以看到就是自动生成的一段帮我们写代理的方法。

在这里插入图片描述

可以看到就是调用了h.invoke,这个h就是我们传参为InvocationHandler的对象,调用了我们自己写的invoke方法。

cglib动态代理

我们需要在maven配置文件中导入相应的包。在pom.xml文件里增加如下代码:

<dependencies>
    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>3.3.0</version>
    </dependency>
</dependencies>

使用方法和JDK的动态代理类似,只是我们不需要再实现接口了,定义一个普通类CglibMaleSinger.java

public class CglibMaleSinger {
    public CglibMaleSinger(String name) {
        this.name = name;
    }

    private String name;

    public CglibMaleSinger() {//注意这里一定要有无参构造器,不然之后会报错Superclass has no null constructors but no arguments were given
    }

    public void sing(){
        System.out.println(this.name+"要去唱歌了");
    }
    public void dance(){
        System.out.println(this.name+"要去跳舞了");
    }
}

然后直接在客户端测试:

import com.hj.CglibMaleSinger;
import net.sf.cglib.core.DebuggingClassWriter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CglibClient {
    public static void main(String[] args) {
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"./class");//用于输出生成的代理class文件,"./class"表示存储在class文件夹中
        CglibMaleSinger JayZ=new CglibMaleSinger("周杰伦");
        Enhancer enhancer = new Enhancer();//定义一个增强器enhancer
        enhancer.setSuperclass(CglibMaleSinger.class);//设置其超类,我们要代理哪个类就传哪个类
        //MethodInterceptor是拦截器,就是把我的方法拦截住然后再去增强
        enhancer.setCallback(new MethodInterceptor() {//设置方法拦截器
            // o 是指被增强的对象,指自己
            // method是拦截后的方法,把父类的方法拦截,增强后写在了子类里
            // objs 参数
            // methodProxy 父类的方法(拦截前的方法对象)
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                System.out.println("谈出场费");
                method.invoke(JayZ,objects);
                System.out.println("出场费谈完了");
                return null;
            }
        });
        CglibMaleSinger cglibMaleSinger = (CglibMaleSinger)enhancer.create();
        cglibMaleSinger.sing();
        cglibMaleSinger.dance();
    }
}

和JDK的动态代理使用方法基本一致,只是invoke方法变成了intercept方法而已。

加上System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"存储路径");语句后,在你自己设置的存储路径下会出现一个包含生成的class文件的文件夹。

在这里插入图片描述

点开hj文件夹下的.class文件

在这里插入图片描述

可以看到是继承了我们的CglibMaleSinger类,并且重写了我们的方法,重写内容中调用了intercept()方法。

在这里插入图片描述

小结

Java动态代理只能够对接口进行代理,不能对普通类进行代理(因为所有生成的代理类的父类为Proxy,java不支持多重继承)CGLIB可以代理普通类Java动态代理使用Java原生的反射API进行操作,在生成类上比较高效;而CGLIB使用ASM框架直接对字节码(.class)改了,所以运行的时候是要比Java原生的效率要高些。

到此这篇关于Java设计模式之代理模式详解的文章就介绍到这了,更多相关Java代理模式内容请搜索自学编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持自学编程网!

编程技巧