java设计模式之代理模式(上)——基于接口的动态代理(JDK中的Proxy)

简介

代理模式就类似于生活中的各种中介、代理商,在生活中非常常见。先不说代理模式,我们先从中介说起,中介起到了好处呢?比如说,租房中介,如果没有租房中介,那么租客租房都只能直接去练习房东,虽然说可能是要便宜一点,但是租客花了很大一部分时间和精力去和一个房东联系、沟通,很大可能最终只能看一套房(此处不考虑有很多套房土豪房东),而且很有可能房客对这一套房不满意。那么中介的好处是什么呢,有了中介,房客就不用直接和房东联系,而是和中介联系,中介手上有很多房源,房客可以任意的挑选,挑选合适的。而且中介还可以对房东的房源进行一些广告宣传之类的效果增强。
又例如代理商,如果没有代理商,我们购买东西直接从厂家购买,虽然可能是便宜一些。但是随之而来也会带来一些问题,比如去厂家负责生产产品,但是他没对产品进行宣传,所以可能有质量好的商品我们发现不了,而且对于厂家来说,商品的售后也是问题。

在软件行业,也存在这样类似的中介,此种模式成为代理模式。代理模式为了解决的问题是:在不修改被代理的原类基础上,对原类的一些方法进行增强,或者进行一些同一的操作。概念描述起来比较抽象,但是其实这个思想是很好理解的,下面我们来写一个简单的例子。

简单例子(静态代理)

就那代理商来示例,我们的角色有:厂家、代理商、客户端,各自的功能不需要再赘述。

无代理

先来用代码实现没有代理商的形式
厂家

public class Producer
{
    public void production()
    {
        System.out.println("厂家生产商品");
    }

    public void sales(double price)
    {
        System.out.println("商家售卖商品,单价"+price);
        System.out.println("用户花费"+price+",得到了商品");
    }
}

客户端

public class Client
{
    public static void main(String[] args)
    {
        Producer producer = new Producer();
        producer.sales(300);
    }
}

写的有些简陋,能体现业务功能就行了。逻辑很简单,输出也很简单,就是控制台打印:

商家售卖商品,单价300.0
用户花费300.0,得到了商品

但是存在的问题就是,厂家没有对商品进行宣传,也没有对商品的售后进行处理,这是有问题的。但是实际上一般来说,厂家的主要核心功能是生产商品,宣传和售后是不需要关心的。

有代理

此时代理商就出现了,代理商可以对厂家的功能进行增强,比如在销售之前进行宣传,在销售之后提供售后服务等等。代码如下

被代理对象与代理对象都要实现的接口

/**
 * @author ql
 * @date 2020/12/9
 * @time 18:20
 *  销售的接口,要使用代理模式,要求被代理的目标和代理对象必须实现相同的接口
 **/
public interface Salesable
{
    void sales(double price);
}

厂家

/**
 * @author ql
 * @date 2020/12/9
 * @time 18:22
 *  厂家,
 **/
public class Producer implements Salesable
{

    public void production()
    {
        System.out.println("厂家生产商品");
    }

    @Override
    public void sales(double price)
    {
        System.out.println("商家售卖商品,单价"+price);
        System.out.println("用户花费金额,得到了商品");
    }
}

代理商

/**
 * @author ql
 * @date 2020/12/9
 * @time 18:23
 *  代理商,也必须的实现需要被代理对象相同的接口
 **/
public class Agent implements Salesable
{
    private Salesable producer = new Producer();

    @Override
    public void sales(double price)
    {
        // 代理商在销售之前进行宣传
        propaganda();
        // 代理商赚20%差价
        producer.sales(price * 0.8);
        // 代理商提供售后服务
        afterSales();
    }

    public void propaganda()
    {
        System.out.println("代理商宣传商品");
    }

    public void afterSales()
    {
        System.out.println("代理商提供售后服务");
    }
}

客户端使用

public class Client
{
    public static void main(String[] args)
    {
        Agent agent = new Agent();
        agent.sales(300);
    }
}

输出

代理商宣传商品
商家售卖商品,单价240.0
用户花费金额,得到了商品
代理商提供售后服务

如此一来,客户端使用不再直接与厂家联系,而是和代理商进行联系,代理商能帮厂家进行宣传,还能提供一些售后服务。对Producer的sales方法进行了增强,但是又没有Producer的任何代码,这就是代理模式起到的作用。

动态代理(基于接口)

上面的做法虽然基本达到要求,但是还存在问题

  • 因为代理对象需要和目标对象实现相同的接口,所以如果目标类实现了多个接口,那么代理类也会随之增加
  • 如果接口中的方法增加了或者减少了,那么目标对象和代理对象都需要维护

基于这些问题,动态代理产生了,动态代理不需要和目标对象实现相同的接口,可以方便灵活的使用。

例子1、代理自定义类

现在我们只需要Salesable接口和实现了该接口的Producer实现类,即可对Producer进行代理,对一些方法进行增强。
基于接口的动态代理
Salesable、Produer都没有变,还是上面实例的代码,下面是客户端使用的代码

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

/**
 * @author ql
 * @date 2020/12/10
 * @time 9:18
 **/
public class Client
{
    public static void main(String[] args)
    {
        // 需要被代理的目标对象,
        Producer target = new Producer();

        /*
            Proxy是java.lang.reflect下的一个类,提供用于创建动态代理类和实例的静态方法,
            newProxyInstance方法就是用于获取代理对象,这个方法有三个参数
                ClassLoader loader:类加载器,用于加载被代理的目标对象,就提供目标对象的类加载器即可
                Class<?>[] interfaces:Class类型数组,这是需要被增强的接口,其实就是我们需要被增强的方法,因为在接口中定义了方法,将这种代理方式称为基于接口的代理就是这个原因
                InvocationHandler h:具体怎么增强的逻辑,需要我们自己去写,因为是我们自己决定要怎么增强的。这是一个接口,其中只有一个方法,这个方法里就是我们增强的逻辑
         */
        Salesable proxyInstance = (Salesable)Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new InvocationHandler()//这里采用了匿名内部类的方式来做的
        {
            /**
             *  这个方法就是增强的具体逻辑,并不是被代理对象中的所有方法都能被增强,只有被代理对象接口中的方法才能被增强,所以只是在执行被代理对象接口中的方法时,才会
             *  会经过这个方法,我们就可以在这个方法中,对我们需要增强的方法进行增强
             * @param proxy 只是代理对象的引用,我也没太理解这个参数的含义
             * @param method 可以被增强的所有方法,这些方法来自于newProxyInstance的第二个参数,Class<?>[] interfaces
             * @param args 被增强方法的参数,
             * @return 需要和被增强方法的返回值类型相同
             * @throws Throwable
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
            {
                // 在此处对需要被代理的方法进行增强,
                System.out.println("对方法进行增强");
                // 对方法进行增强,意味着我们还是需要执行原本的方法,所以还需要将被代理对象原本的方法执行一遍
                Object invoke = method.invoke(target, args);
                // 按照具体要求,我们甚至可以对这个参数或者返回值做一些处理,例如
                // Object invoke = method.invoke(proxy, args[0] * 0.8 );
                // Object invoke = method.invoke(proxy, args);
                return invoke;
            }
        });

        // 使用代理对象进行销售
        proxyInstance.sales(300);

    }

}

控制台输出

对方法进行增强
商家售卖商品,单价300.0
用户花费金额,得到了商品

在代码中我写了详细的注释,解释了每个方法、参数的含义,可以试着理解一下。就我个人经验,第一遍写下来是懵的,可以先照着代码敲一遍,将能理解的理解。然后在不看实例的情况下,自己来实现一遍这个代理,在写的过程中,就会去理解每一步的含义。

上面的实例中,只是笼统的将接口中所有的方法都增强了,那如果我们需要对指定的方法增强呢,其他方法都是正常调用呢?其实这个也是能办到的,只是在一开始的实例中,我怕理解起来困难,没有实现。
其实如果搞清楚了逻辑也很简单,因为在具体的增强逻辑中,参数列表里有一个Method类型的参数,这个参数就是在执行具体方法的时候,方法的类型,里面携带有方法的信息,我们将这个方法信息里的名字取出来,如果是我们需要增强的方法,就给予增强,否则就调用原来的方法。代码如下:

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

/**
 * @author ql
 * @date 2020/12/10
 * @time 9:18
 **/
public class Client01
{
    public static void main(String[] args)
    {
        // 需要被代理的目标对象,
        Producer target = new Producer();

        /*
            Proxy是java.lang.reflect下的一个类,提供用于创建动态代理类和实例的静态方法,
            newProxyInstance方法就是用于获取代理对象,这个方法有三个参数
                ClassLoader loader:类加载器,用于加载被代理的目标对象,就提供目标对象的类加载器即可
                Class<?>[] interfaces:Class类型数组,这是需要被增强的接口,其实就是我们需要被增强的方法,因为在接口中定义了方法,将这种代理方式称为基于接口的代理就是这个原因
                InvocationHandler h:具体怎么增强的逻辑,需要我们自己去写,因为是我们自己决定要怎么增强的。这是一个接口,其中只有一个方法,这个方法里就是我们增强的逻辑
         */
        Salesable proxyInstance = (Salesable)Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new InvocationHandler()//这里采用了匿名内部类的方式来做的
        {
            /**
             *  这个方法就是增强的具体逻辑,并不是被代理对象中的所有方法都能被增强,只有被代理对象接口中的方法才能被增强,所以只是在执行被代理对象接口中的方法时,才会
             *  会经过这个方法,我们就可以在这个方法中,对我们需要增强的方法进行增强
             * @param proxy 只是代理对象的引用,我也不太理解这个参数放在这里的作用
             * @param method 可以被增强的所有方法,这些方法来自于newProxyInstance的第二个参数,Class<?>[] interfaces
             * @param args 被增强方法的参数,
             * @return 需要和被增强方法的返回值类型相同
             * @throws Throwable
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
            {
                String name = method.getName();
                if ("sales".equals(name))
                {
                    // 在此处对需要被代理的方法进行增强,
                    System.out.println("对方法进行增强");
                    // 对方法进行增强,意味着我们还是需要执行原本的方法,所以还需要将被代理对象原本的方法执行一遍
                    Object invoke = method.invoke(target, args);
                    // 按照具体要求,我们甚至可以对这个参数或者返回值做一些处理,例如
                    // Object invoke = method.invoke(proxy, args[0] * 0.8 );
                    // Object invoke = method.invoke(proxy, args);
                    return invoke;
                }
                else 
                {
                    System.out.println("此处不对方法增强,正常调用");
                    return method.invoke(target,args);
                }
            }
        });

        // 使用代理对象进行销售
        proxyInstance.sales(300);

    }

}

稍微修改一下原本的代码即可,测试我就不测试,测试也很简单,在Salesable接口中增添一个方法,然后在Producer中将其实现,使用上面的实例代码,用代理对象去调用新增的那个方法就可以了。

例子2、代理JDK中的类String

上面的例子足以说明代理模式的作用了,但是可能一切都是自己模拟的情况下,第一次接触的同学不太好理解,下面我们来写一个更真实的例子:对JDK中的类进行代理

我的目标是:String。我现在的需求是:在对字符串进行求长度时,将原来的长度减一,和末尾下标一样,其他方法不变,不进行增强。
(ps:我设计这个例子也是事先进行了考察的,因为String中的大部分方法包括length都是实现的接口CharSequence中的,所以才能代理),代码如下:

代理String

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

/**
 * @author ql
 * @date 2020/12/10
 * @time 11:14
 **/
public class Test01
{
    public static void main(String[] args)
    {
        // 需要代理的目标对象
        String target = "abc";

        // 实现代理的具体逻辑,并且返回一个代理对象
        CharSequence proxyInstance = (CharSequence)Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new InvocationHandler()
                {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
                    {
                        String name = method.getName();
                        if ("length".equals(name))// 是我们需要增强的方法
                        {
                            System.err.println("当前方法名为length,是目标方法,进行增强");
                            Object invoke = method.invoke(target, args);
                            return (int)invoke - 1;

                        }
                        else // 不是我们需要增强的方法
                        {
                            System.err.println("当前方法名是"+method.getName()+",不是目标方法,不进行处理");
                            return method.invoke(target, args);
                        }
                    }
                });

        // 使用代理对象调用方法,先调用目标方法
        int length = proxyInstance.length();
        System.out.println(length);

        // 调用非目标方法
        proxyInstance.toString();

    }
}

控制台输出
代理String输出
如此一来,我们就实现了在不修改原本代码的情况下,对目标对象的方法进行增强,还可以统一的对参数做校验、返回值处理、增加日志等等操作。

其实如果功力深厚,可以尝试自己来写一个Proxy和InvocationHandler,会更清晰的理解其中的执行过程(主要是利用反射实现),。我现在有这个想法,有个大概的雏形,但是实现起来还是有点障碍。等后面技术更上一层之后,再来不上。

水平有限,仅代表个人理解,如果什么地方有误,还请指出

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值