简介
代理模式就类似于生活中的各种中介、代理商,在生活中非常常见。先不说代理模式,我们先从中介说起,中介起到了好处呢?比如说,租房中介,如果没有租房中介,那么租客租房都只能直接去练习房东,虽然说可能是要便宜一点,但是租客花了很大一部分时间和精力去和一个房东联系、沟通,很大可能最终只能看一套房(此处不考虑有很多套房土豪房东),而且很有可能房客对这一套房不满意。那么中介的好处是什么呢,有了中介,房客就不用直接和房东联系,而是和中介联系,中介手上有很多房源,房客可以任意的挑选,挑选合适的。而且中介还可以对房东的房源进行一些广告宣传之类的效果增强。
又例如代理商,如果没有代理商,我们购买东西直接从厂家购买,虽然可能是便宜一些。但是随之而来也会带来一些问题,比如去厂家负责生产产品,但是他没对产品进行宣传,所以可能有质量好的商品我们发现不了,而且对于厂家来说,商品的售后也是问题。
在软件行业,也存在这样类似的中介,此种模式成为代理模式。代理模式为了解决的问题是:在不修改被代理的原类基础上,对原类的一些方法进行增强,或者进行一些同一的操作。概念描述起来比较抽象,但是其实这个思想是很好理解的,下面我们来写一个简单的例子。
简单例子(静态代理)
就那代理商来示例,我们的角色有:厂家、代理商、客户端,各自的功能不需要再赘述。
无代理
先来用代码实现没有代理商的形式
厂家
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();
}
}
控制台输出
如此一来,我们就实现了在不修改原本代码的情况下,对目标对象的方法进行增强,还可以统一的对参数做校验、返回值处理、增加日志等等操作。
其实如果功力深厚,可以尝试自己来写一个Proxy和InvocationHandler,会更清晰的理解其中的执行过程(主要是利用反射实现),。我现在有这个想法,有个大概的雏形,但是实现起来还是有点障碍。等后面技术更上一层之后,再来不上。
水平有限,仅代表个人理解,如果什么地方有误,还请指出