我们在生活场景中处处都是代理:租房买房找中介、购买商品找商城、吃饭点外卖等。今天就和勾勾一起来学习代理设计模式,并通过代码理解静态代理和动态代理。
代理设计模式
代理模式(Proxy Pattern)就是当一个对象A对另一个对象B的访问无法或者不想直接访问时,可以在两个对象添加代理对象C,A通过访问C而间接的访问B对象。
代理模式一般包含3个角色:
- 抽象主题角色(ISubject):它可以是一个接口或者抽象类,主要是为了声明真实主题和代理主题的共同方法。
- 真实主题角色(RealSubject):它是一个实现或者继承抽象主题的类,重写了抽象主题中定义的方法,也被称为被代理类或者目标类。
- 代理类(Proxy):它具有真实主题的代理权,即内部有RealSubject的引用,客户端调用代理对象的方法也即是调用真实主题的方法,但是代理可以在真实主题的方法前后进行增强。
举个实际点的例子,假如我们作为消费者想购买一台手机,我们不会直接去找手机生产商去买,而是通过一些手机代理商(类似很多手机销售店或者网上商店)购买。在这个例子里面手机生产厂家是真实主题角色,也即是真正生产手机贩卖手机的角色,手机销售店即是代理角色,而我们作为消费者就可以通过代理角色访问到真实角色了。像一些店铺平时也会做满减活动,我们可以把它当作方法的增强。
通过上述的学习我们可以总结,代理模式的作用:一是为了保护真实主题对象,二是增强真实主题对象。
代理一般分为两类:静态代理和动态代理。
下面我们就通过代码实现上述买手机的例子。
首先我们先建一个抽象主题的接口,定义一个sell()的规范:
public interface Phone { double sell(int amount); }
再定义了两个真实主题:华为手机、小米手机。不要过多纠结这个amount,我们就认为它是1.
public class HWPhone implements Phone { @Override public double sell(int amount) { return 4688.0; } } public class XMPhone implements Phone { @Override public double sell(int amount) { return 2688.0; } }
我们首先通过静态代理实现代理模式。
静态代理
静态代理开发需要手动创建代理对象。那么我们接下来就创建代理对象。
public class PhoneProxy implements Phone{ private Phone phone; public PhoneProxy(Phone phone) { this.phone = phone; } @Override public double sell(int amount) { double price = phone.sell(amount); //对方法进行增强 System.out.println("满减活动"); return price; } }
接下来我们写个main方法,将其作为客户端消费者去买手机:
public static void main(String[] args) { PhoneProxy proxy = new PhoneProxy(new HWPhone()); double hwprice = proxy.sell(1); System.out.println("消费者通过华为手机代理商购买一台手机的价格为:" + hwprice); PhoneProxy proxy0 = new PhoneProxy(new XMPhone()); double xmprice = proxy0.sell(1); System.out.println("消费者通过小米手机代理商购买一台手机的价格为:" + xmprice); }
通过上面代码我们可以发现静态代理实现起来是比较简单的,只需要都实现抽象主题接口就可以,但是依赖性比较强扩展起来不是很方便。
因此我们可以动态代理,动态代理分为JDK动态代理和CGLib动态代理。
动态代理-JDK
JDK动态代理是利用JDK的反射机制,创建代理对象,并且动态的指定要代理的目标类。
JDK动态代理使用java.lang.reflect包下的3个类:InvocationHandler、Method、Proxy。
- InvocationHandler:调用处理器,定义了invoke方法。它用来表示代理具体要做什么,我们需要创建类实现InvocationHandler并且重写invoke方法。invoke需要3个参数:proxy:代理对象,无需赋值method:目标类中的方法。args:目标类中方法需要的参数。
public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }
- Method,目标类中的方法,通过method.invoke(目标类,args)可以执行目标类的具体的方法。
- Proxy:核心对象,用于创建代理对象。它通过调用newProxyInstance方法创建代理对象,我把这个方法的源码拿出来了,删除了无关紧要的代码,只看其中的关键代码,newProxyInstance它需要3个参数:ClassLoader:代理对象的类加载器interfaces:代理对象实现的接口InvocationHandler:我们自己创建的实现了InvocationHandler接口的类
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException { final Class<?>[] intfs = interfaces.clone(); /* * 查找或者生成代理对象 */ Class<?> cl = getProxyClass0(loader, intfs); /* * 获取InvocationHandler的构造器 */ final Constructor<?> cons = cl.getConstructor(constructorParams); final InvocationHandler ih = h; /* * 根据构造器生成实例对象并返回 */ return cons.newInstance(new Object[]{h}); }
了解了基本的概念之后,你可能会有点晕,那我们接下来使用JDK动态代理实现上述的功能,你可以在写完代码之后再来了解上面的知识。
首先创建MyInvocationHandler类实现InvocationHandler接口,重写invoke方法,在invoke方法中完成代理对象的真正业务逻辑:
public class PhoneProxyByJdk implements InvocationHandler { Object target = null; public PhoneProxyByJdk(Object target) { this.target = target; } /** * 获取代理类 * @param clazz * @param <T> * @return */ public <T> T getProxy(Class clazz) { return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object res = method.invoke(target, args); //方法增强 System.out.println("满减活动"); return res; } }
创建main方法模拟客户端消费者,消费者明确知道买哪个手机,创建这个厂家的对象并告诉InvocationHandler我要买这个厂家的手机,这个时候再使用Proxy创建代理对象,通过代理调用目标方法即可实现相同的效果。
public static void main(String[] args) { //打印JDK动态代理字节码 System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles" , "true"); /** * 消费者明确知道要买哪个手机,要找哪个代理商 */ PhoneProxyByJdk phoneProxyByJdk = new PhoneProxyByJdk(new HWPhone()); Phone proxy = phoneProxyByJdk.getProxy(Phone.class); double price = proxy.sell(1); System.out.println("消费者通过华为手机代理商购买一台手机的价格为:" + price); }
通过上面代码我们可以看到,JDK动态代理无需创建多个代理类,使用比较灵活。
我们分析其实现原理,我们在工作空间中打印了字节码文件:
我们看到JDK动态代理对象实现了Phone目标接口,重写了目标方法。
动态代理-CGLib
CGLib是ASM框架生成的字节码,使用字节码技术生成代理类,效率比较高。
我们接下来继续使用CGLib动态代理实现动态代理。CGLib动态代理是第三方的工具库。
可以通过链接查看CGLib类库:http://cglib.sourceforge.net/apidocs/index.html
我们先引入CGLib的依赖:
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>
创建MyMethodInterceptor类实现MethodInterceptor接口,重写了intercept方法,在这个方法里完成了代理类的所有逻辑。
public class PhoneProxyByCGLib implements MethodInterceptor { /** * 获取代理类 * @param clazz * @param <T> * @return */ public <T> T getProxy(Class clazz) { //字节码增强类 Enhancer enhancer = new Enhancer(); //设置超类 enhancer.setSuperclass(clazz); //设置回调类 enhancer.setCallback(this); return (T) enhancer.create(); } @Override public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable { Object res = proxy.invokeSuper(target, args); System.out.println("满减活动"); return res; } }
依然用main方法模拟客户端:
public static void main(String[] args) { //打印CGlib生成的字节码文件 System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "E:/"); PhoneProxyByCGLib phoneProxyByCGLib = new PhoneProxyByCGLib(); Phone hwPhone = phoneProxyByCGLib.getProxy(HWPhone.class); double price = hwPhone.sell(1); System.out.println("消费者通过华为手机代理商购买一台手机的价格为:" + price); }
那么接下来我们分析下CGLib的原理,在main方法中我们打印了CGLib生成的字节码文件:
找到HWPhone的代理对象字节码:
这里我们看到了CGLib动态代理生成的代理对象是继承了我们的目标类,重写了目标类的方法,因此我们在调用目标类的方法时使用的是invokeSuper方法。
如果被代理的对象是个接口,CGLib动态代理生成的代理类是实现目标接口。
总结
代理模式降低了系统之间的耦合性,可以保护目标对象并且可以增强目标对象,但是当类比较多的时候,也增加了代码的复杂性。
静态代理需要手动实现代理,代码编写简单,但是扩展性不好。
动态代理采用动态生成代码的方式,虽然复杂一点,但是使用起来更为灵活,动态代理有JDK动态代理和CGLib动态代理。
CGLib和JDK动态代理的对比:
- JDK动态代理实现了被代理对象的接口,CGLib动态代理继承了被代理对象。
- JDK动态代理是通过反射机制实现的,CGLib动态代理是通过增强字节码实现的,CGLib动态代理的执行效率更高
- CGLib动态代理更复杂一点,生成代理类比JDK动态代理效率低一点。
动态代理在源码如Spring和Mybatis中是比较重要的,也是比较常见的面试题,希望这篇文章对你有帮助。