文章目录
- 1、代理模式基本介绍
- 2、Jdk中的动态代理
- 2.1、场景推导
- 2.2、Jdk动态代理
- 3、静态代理
- 4、代理模式的关键点
- 5、代理模式和适配器模式的比较
- 6、代理模式UML图
1、代理模式基本介绍
代理模式的定义:
- 为其他对象提供一种代理以
控制对这个对象的访问
。 - 在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
简单来说
-
代理模式就是代理对象具备真实对象的功能,并代替真实对象完成相应操作,
并能够在操作执行的前后,对操作进行增强处理
。 -
为真实对象提供代理,然后供其他对象通过代理访问真实对象
2、Jdk中的动态代理
2.1、场景推导
实现一个简单的加减乘除运算功能
interface ICalc{int add(int a,int b);int sub(int a,int b);int mul(int a,int b);int div(int a,int b);
}
class CalcImpl implements ICalc{@Overridepublic int add(int a, int b) {return a+b;}@Overridepublic int sub(int a, int b) {return a-b;}@Overridepublic int mul(int a, int b) {return a*b;}@Overridepublic int div(int a, int b) {return a/b;}
}
class AppTest{public static void main(String[] args) {CalcImpl c = new CalcImpl();System.out.println(c.add(4,2));System.out.println(c.sub(4,2));System.out.println(c.mul(4,2));System.out.println(c.div(4,2));}
}
现在变化
来了,客户要求为每个方法添加日志,记录方法开始和结束的时机
package com.hh.demo.designpattern;interface ICalc{int add(int a,int b);int sub(int a,int b);int mul(int a,int b);int div(int a,int b);
}
class CalcImpl implements ICalc{@Overridepublic int add(int a, int b) {System.out.println("add方法开始!" +"a="+a+"b="+b);int r = a+b;System.out.println("add方法结束!" +"r="+r);return r;}@Overridepublic int sub(int a, int b) {System.out.println("sub方法开始!" +"a="+a+"b="+b);int r = a-b;System.out.println("sub方法结束!" +"r="+r);return r;}@Overridepublic int mul(int a, int b) {System.out.println("mul方法开始!" +"a="+a+"b="+b);int r = a*b;System.out.println("mul方法结束!" +"r="+r);return r;}@Overridepublic int div(int a, int b) {System.out.println("div方法开始!" +"a="+a+"b="+b);int r = a/b;System.out.println("div方法结束!" +"r="+r);return r;}
}
class AppTest{public static void main(String[] args) {CalcImpl c = new CalcImpl();System.out.println(c.add(4,2));System.out.println(c.sub(4,2));System.out.println(c.mul(4,2));System.out.println(c.div(4,2));}
}
梭哈搞定,打完收工!
我们发现,这样完成业务根本不是一个好办法
- 代码在重复,核心业务(加减乘除)和非核心业务(打印日志)在不断的重复。
- 如果Icalc和CalcImpl不是我们自己创建的,是被发现的,那我们手里是没有源代码的,不能直接修改源代码(开闭原则)
- 需求如果再次变化,需要加入开方,求余的过程 ;又或者客户要求 上午需要日志,下午不需要日志!!!
我们尝试使用动态代理
来完成上述功能
2.2、Jdk动态代理
Jdk动态代理:在程序的执行过程中,使用jdk的反射机制,创建代理对象,并动态的指定代理的目标类
先来看看业务逻辑是怎么实现的
package com.hh.demo.designpattern;interface ICalc {int add(int a, int b);int sub(int a, int b);int mul(int a, int b);int div(int a, int b);
}
class CalcImpl implements ICalc {@Overridepublic int add(int a, int b) {int r = a + b;return r;}@Overridepublic int sub(int a, int b) {int r = a - b;return r;}@Overridepublic int mul(int a, int b) {int r = a * b;return r;}@Overridepublic int div(int a, int b) {int r = a / b;return r;}
}
//调用处理器
class MyHandler implements InvocationHandler {//关联private ICalc calculator;public MyHandler(ICalc calculator) {this.calculator = calculator;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println(method.getName()+"开始,参数:"+ Arrays.toString(args));//利用反射机制,调用方法//把method所代表的方法,当作calculator对象的调用,参数是args//Object是为了通用Object res = method.invoke(calculator, args);System.out.println(method.getName()+"结束,结果是:"+ res);return res;//这个返回值会返回到代理对象的方法调用处}
}
public class AppTest {public static void main(String[] args) {ICalc calculator = new CalcImpl();//当前类的字节码获得当前类的类加载器ClassLoader classLoader = AppTest.class.getClassLoader();//创建代理对象,需要传入三个参数ICalc proxy = (ICalc) Proxy.newProxyInstance(classLoader, new Class[]{ICalc.class}, new MyHandler(calculator));//总之,对代理对象的方法的调用,都统统会进入调用处理器中proxy.add(3, 2);proxy.sub(3, 2);proxy.mul(3, 2);proxy.div(3, 2);}/*** add开始,参数:[3, 2]* add结束,结果是:5* sub开始,参数:[3, 2]* sub结束,结果是:1* mul开始,参数:[3, 2]* mul结束,结果是:6* div开始,参数:[3, 2]* div结束,结果是:1** Process finished with exit code 0*/
}
我们先来看看动态代理api
Proxy.newProxyInstance();
里面传三个参数,分别是:
-
第一个参数:
-
实例化一个对象,必然会调用类的构造器。在调用构造器之前,Jvm会加载该类的字节码,而Jvm就是使用类加载器来加载类的字节码,这一步是Jvm自动完成的。
-
简单来说:只要实例化对象,一定要加载类的字节码,加载字节码就一定要类的加载器。
-
使用
动态代理
的api实例化对象是一种不常用的创建对象的方式,但这也是一种实例化,需要我们手动把类的加载器传入
-
使用构造器实例化对象时Jvm会自动找到类加载器。
-
-
第二个参数:
-
第一个参数传入的类加载器,加载的是哪个类的字节码?加载的字节码就是在运行期动态生成的字节码,这个动态生成的字节码是不需要源代码的。
-
字节码确实可以自动生成,那么动态代理api成成的字节码的内容,是根据什么生成的呢?恰恰是根据第二个参数生成的。动态生成代理,会生成一个实现了目标接口的类的字节码,在上面的栗子中就是生成了一个ICalc接口的类的字节码!
-
-
第三个参数:调用处理器 InvocationHandler
- 我们已经知道,动态代理会加载自己动态生成的字节码,且这个字节码是根据某个接口生成的,在上面的例子中就是根据ICalc接口生成的实现了ICalc接口的类的字节码
- 实现一个接口,就要实现其中的抽象方法,那麽动态代理生成的字节码,实现了ICalc接口,必然就要实现其中的add、sub等方法
- 这些方法被实现的方法体是什么内容呢?这恰恰是由第三个参数决定的,MyHandler类的 invoke方法,就是方法体的内容!!
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {第一个参数:动态代理的对象第二个参数:调用的接口方法第三个参数:调用的接口方法的参数}
但是目前这个写法还是有缺点的,太复杂了,对于新手不是很友好,我们来封装一下
package com.hh.demo.designpattern;interface ICalc {int add(int a, int b);int sub(int a, int b);int mul(int a, int b);int div(int a, int b);
}
class CalcImpl implements ICalc {@Overridepublic int add(int a, int b) {int r = a + b;return r;}@Overridepublic int sub(int a, int b) {int r = a - b;return r;}@Overridepublic int mul(int a, int b) {int r = a * b;return r;}@Overridepublic int div(int a, int b) {int r = a / b;return r;}
}
class MyHandler implements InvocationHandler {//关联private Object target;public MyHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println(method.getName()+"开始,参数:"+ Arrays.toString(args));//利用反射机制,调用方法//把method所代表的方法,当作calculator对象的调用,参数是args//Object是为了通用Object res = method.invoke(target, args);System.out.println(method.getName()+"结束,结果是:"+ res);return res;//这个返回值会返回到代理对象的方法调用处}
}
//----------------------------------------------------------------------------------------------------
class MyProxy{//封装:对外隐藏复杂的实现细节,暴露出简单的使用方法public Object getProxy(Object target){//当前类的字节码获得当前类的类加载器ClassLoader classLoader = MyProxy.class.getClassLoader();//获取target所属的类,所实现的接口Class<?>[] interfaces = target.getClass().getInterfaces();//创建代理对象,需要传入三个参数Object proxy = Proxy.newProxyInstance(classLoader,interfaces, new MyHandler(target));return proxy;}
}
public class AppTest {public static void main(String[] args) {ICalc calculator = new CalcImpl();ICalc proxy = (ICalc) new MyProxy().getProxy(calculator);proxy.add(3, 2);proxy.sub(3, 2);proxy.mul(3, 2);proxy.div(3, 2);}/*** add开始,参数:[3, 2]* add结束,结果是:5* sub开始,参数:[3, 2]* sub结束,结果是:1* mul开始,参数:[3, 2]* mul结束,结果是:6* div开始,参数:[3, 2]* div结束,结果是:1** Process finished with exit code 0*/
}
MyProxy类对外隐藏复杂的实现细节,暴露出简单的使用方法。似乎有点代理的意思了
目前看起来似乎挺好的,但是仍然有问题:
目前我们创建的代理对象,只能在真实对象的真实方法调用前后加上日志,无法扩展其他功能,比如,用户不想加日志功能,而是想加缓存功能,或者权限控制…
再次封装代码,我们定义一个接口,用来描述代理类对应方法执行前后需要拓展执行的方法。
称这个接口为Interceptor
拦截器,也可以理解为切面
package com.hh.demo.designpattern;interface ICalc {int add(int a, int b);int sub(int a, int b);int mul(int a, int b);int div(int a, int b);
}
class CalcImpl implements ICalc {@Overridepublic int add(int a, int b) {int r = a + b;return r;}@Overridepublic int sub(int a, int b) {int r = a - b;return r;}@Overridepublic int mul(int a, int b) {int r = a * b;return r;}@Overridepublic int div(int a, int b) {int r = a / b;return r;}
}
class MyHandler implements InvocationHandler {//关联private Object target;private Interceptor interceptor;public MyHandler(Object target,Interceptor interceptor) {this.target = target;this.interceptor = interceptor;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//方法执行前的钩子函数interceptor.before(target, method, args);Object res = method.invoke(target, args);//方法执行后的钩子函数interceptor.after(target, method, args, res);// 返回到代理对象的方法调用处return res;}
}
class MyProxy{public Object getProxy(Object target, Interceptor interceptor){//当前类的字节码获得当前类的类加载器ClassLoader classLoader = MyProxy.class.getClassLoader();//获取target所属的类,所实现的接口Class<?>[] interfaces = target.getClass().getInterfaces();//创建代理对象,需要传入三个参数Object proxy = Proxy.newProxyInstance(classLoader,interfaces, new MyHandler(target,interceptor));return proxy;}
}
interface Interceptor {//前置通知void before(Object target, Method method, Object[] args);//后置通知void after(Object target, Method method, Object[] args, Object returnVal);
}
//----------------------------------------------------------------------------------------------------
//用户制作拦截器的实现类
class LogInterceptor implements Interceptor{@Overridepublic void before(Object target, Method method, Object[] args) {System.out.println(String.format("方法名为:%s,参数为:%s", method.getName(), Arrays.toString(args)));}@Overridepublic void after(Object target, Method method, Object[] args, Object returnVal) {System.out.println(String.format("返回结果为:%s", returnVal.toString()));}
}
public class AppTest {public static void main(String[] args) {ICalc calculator = new CalcImpl();ICalc proxy = (ICalc) new MyProxy().getProxy(calculator,new LogInterceptor());proxy.add(3, 2);proxy.sub(3, 2);proxy.mul(3, 2);proxy.div(3, 2);}/*** 方法名为:add,参数为:[3, 2]* 返回结果为:5* 方法名为:sub,参数为:[3, 2]* 返回结果为:1* 方法名为:mul,参数为:[3, 2]* 返回结果为:6* 方法名为:div,参数为:[3, 2]* 返回结果为:1** Process finished with exit code 0*/
}
这样就简单了很多,应对不同的需求我们就去定制不同的代理类和拦截器,实现不同的需求
可是现在变化
又来了,客户有了新需求:
针对ICalc接口的日志功能,add方法使用中文日志,sub方法使用英文日志,mul方法和div方法不要日志。
这时只能用判断来解决了,针对不同方法有不同的日志。我们来实现一下
//用户制作拦截器的实现类
class LogInterceptor implements Interceptor{@Overridepublic void before(Object target, Method method, Object[] args) {if("add".equals(method.getName())){System.out.println(String.format("方法名为:%s,参数为:%s", method.getName(), Arrays.toString(args)));}else if("sub".equals(method.getName())){System.out.println(String.format("methodName is:%s,parameter is:%s", method.getName(), Arrays.toString(args)));}else{System.out.println(method.getName()+Arrays.toString(args));}}@Overridepublic void after(Object target, Method method, Object[] args, Object returnVal) {if("add".equals(method.getName())){System.out.println(String.format("返回结果为:%s", returnVal.toString()));}else if("sub".equals(method.getName())){System.out.println(String.format("result is:%s", returnVal.toString()));}else{System.out.println(returnVal.toString());}}
}
运行结果:
方法名为:add,参数为:[3, 2]
返回结果为:5
methodName is:sub,parameter is:[3, 2]
result is:1
mul[3, 2]
6
div[3, 2]
1Process finished with exit code 0
可以看到,虽然做虽然满足了客户的需求,但是有很多的 if else ,感觉好像怪怪的!!!
仔细想想,这设计违反了什么设计原则呢?单一职责设计原则
那就拆分呗,设计原则不就是讲究一个分
字吗? 我们针对于四个方法,做四个拦截器,这里写两个作为演示
//用户制作拦截器的实现类
class addInterceptor implements Interceptor{@Overridepublic void before(Object target, Method method, Object[] args) {if("add".equals(method.getName())){System.out.println(String.format("方法名为:%s,参数为:%s", method.getName(), Arrays.toString(args)));}}@Overridepublic void after(Object target, Method method, Object[] args, Object returnVal) {if("add".equals(method.getName())) {System.out.println(String.format("返回结果为:%s", returnVal.toString()));}}
}
class subInterceptor implements Interceptor{@Overridepublic void before(Object target, Method method, Object[] args) {if("sub".equals(method.getName())){System.out.println(String.format("methodName is:%s,parameter is:%s", method.getName(), Arrays.toString(args)));}}@Overridepublic void after(Object target, Method method, Object[] args, Object returnVal) {if("sub".equals(method.getName())){System.out.println(String.format("result is:%s", returnVal.toString()));}}
}
客户端:
public class AppTest {public static void main(String[] args) {//calculator是目标对象ICalc calculator = new CalcImpl();//根据目标对象calculator,动态生成一个代理对象ICalc proxy = (ICalc) new MyProxy().getProxy(calculator,new addInterceptor());proxy.add(3, 2);proxy.sub(3, 6);}/*** 方法名为:add,参数为:[3, 2]* 返回结果为:5** Process finished with exit code 0*/
}
但是问题又来了
现在,getProxy方法只能传addInterceptor 或者subInterceptor,不能同时用啊
有同学可能想到用可变参数解决这个问题,没错,是可以的;
但是换一种思路,既然能根据目标对象动态代理生成一个代理对象,那我是不是可以将这个代理对象再当成一个新的目标对象
动态代理一下?等等,感觉cpu要烧了!!!!
public class AppTest {public static void main(String[] args) {//calculator是目标对象ICalc calculator = new CalcImpl();//根据目标对象calculator,动态生成一个代理对象ICalc proxy = (ICalc) new MyProxy().getProxy(calculator,new addInterceptor());//我们把proxy这个代理对象,再当成一个新的目标对象ICalc proxy2 = (ICalc) new MyProxy().getProxy(proxy,new subInterceptor());//我们发现add方法和sub方法都能拦截proxy2.add(3, 2);proxy2.sub(3, 6);}/*** 方法名为:add,参数为:[3, 2]* 返回结果为:5* methodName is:sub,parameter is:[3, 6]* result is:-3** Process finished with exit code 0*/
}
简单理解就是套娃,贴一张图帮助理解
现在代码总没有问题了吧?是吗?那我说目前代码还有问题呢?
问题是:添加拦截器的顺序是逆向的,对用户不友好
解决方法:
public class AppTest {public static void main(String[] args) {//calculator是目标对象ICalc calculator = new CalcImpl();List<Interceptor> interceptors = new ArrayList<>();interceptors.add(new addInterceptor());interceptors.add(new subInterceptor());for (int i =interceptors.size() - 1; i >=0; i--) {Interceptor interceptor = interceptors.get(i);calculator = (ICalc) MyProxy.getProxy(calculator, interceptor);}calculator.add(1,2);calculator.sub(3,2);}/*** 方法名为:add,参数为:[1, 2]* 返回结果为:3* methodName is:sub,parameter is:[3, 2]* result is:1** Process finished with exit code 0*/
}
但是现在客户端代码很复杂了,对用户不友好呀
我们封装一下倒叙添加拦截器的逻辑
class MyProxy{public static Object getProxy(Object target, Interceptor interceptor){//当前类的字节码获得当前类的类加载器ClassLoader classLoader = MyProxy.class.getClassLoader();//获取target所属的类,所实现的接口Class<?>[] interfaces = target.getClass().getInterfaces();//创建代理对象,需要传入三个参数Object proxy = Proxy.newProxyInstance(classLoader,interfaces, new MyHandler(target,interceptor));return proxy;}//封装倒叙添加拦截器public static Object getProxy2(Object target, List<Interceptor> interceptors){for (int i =interceptors.size() - 1; i >=0; i--) {Interceptor interceptor = interceptors.get(i);target = (ICalc) MyProxy.getProxy(target, interceptor);}return target;}
}
客户端代码:
public class AppTest {public static void main(String[] args) {//calculator是目标对象ICalc calculator = new CalcImpl();List<Interceptor> interceptors = new ArrayList<>();interceptors.add(new addInterceptor());interceptors.add(new subInterceptor());ICalc proxy2 = (ICalc) MyProxy.getProxy2(new CalcImpl(), interceptors);proxy2.add(1,2);proxy2.sub(3,2);}/*** 方法名为:add,参数为:[1, 2]* 返回结果为:3* methodName is:sub,parameter is:[3, 2]* result is:1** Process finished with exit code 0*/
}
现在还有问题,以后,用户要添加拦截器,删除拦截器,必然要修改应用程序代码,要修改 List,这不合理鸭
用户应该是改配置而不是改代码,所以这个interceptors根本不用传
//封装倒叙添加拦截器
public static Object getProxy2(Object target) throws Exception{//拦截器集合不是用户传进来的,是读取配置文件得到的,配置文件放在同一个包下Properties prop = new Properties();InputStream in = MyProxy.class.getResourceAsStream("myconfig.properties");prop.load(in);String str = prop.getProperty("interceptors");String[] split = str.split(",");List<Interceptor> interceptors = new ArrayList<>();for (String hh : split) {interceptors.add((Interceptor)Class.forName(hh).newInstance());}for (int i =interceptors.size() - 1; i >=0; i--) {Interceptor interceptor = interceptors.get(i);target = (ICalc) MyProxy.getProxy(target, interceptor);}return target;
}
客户端:
public static void main(String[] args) throws Exception {ICalc proxy2 = (ICalc) MyProxy.getProxy2(new CalcImpl());proxy2.add(1,2);proxy2.sub(3,2);}
客户端代码变得非常简洁
3、静态代理
业务场景:现在需要做一个图书解析器,解析一本书里面有多少个句子,多少个副词
package com.hh.demo.designpattern;
//图书解析器
class BookParser{//接收一本书的内容,字符串的值,是很大的private String content = "天下大事,分久必合,合久必分...!!";public Integer numberOfSentence(){//每次解析,都有很高的执行代价return content.split("[.!?]").length;}public Integer numberOfVerb() throws InterruptedException {//假设执行了很多逻辑;Thread.sleep(1000);return 80;}public Integer numberOfAdverb() throws InterruptedException {//假设执行了很多逻辑;Thread.sleep(1000);return 220;}
}public class AppTest {public static void main(String[] args) throws InterruptedException {BookParser bp = new BookParser();Integer a = bp.numberOfAdverb();System.out.println("有"+ a + "个副词");Integer a2 = bp.numberOfAdverb();System.out.println("有"+ a2 + "个副词");Integer a3 = bp.numberOfAdverb();System.out.println("有"+ a3 + "个副词");}/*** //每隔一秒出现一个结果* 有220个副词* 有220个副词* 有220个副词** Process finished with exit code 0*/}
现在有个问题,每解析一次就要花费1s, 这是极其不合理的;
我们可以做一个代理,每次调方法时进入代理,代理判断一下这个数字有没有统计过,如果统计过了,直接返回这个值,就不用去调用真实对象,如果没有统计过,就去调真实对象,然后返回值,并将这个值存到缓存中;
//图书解析器
class BookParser{//接收一本书的内容,字符串的值,是很大的private String content = "天下大事,分久必合,合久必分...!!";public Integer numberOfSentence(){//每次解析,都有很高的执行代价return content.split("[.!?]").length;}public Integer numberOfVerb() throws InterruptedException {//假设执行了很多逻辑;Thread.sleep(1000);return 80;}public Integer numberOfAdverb() throws InterruptedException {//假设执行了很多逻辑;Thread.sleep(1000);return 220;}
}
class BookParserProxy extends BookParser{//因为没有定义接口,所以为了与真实对象有相同的方法,继承一下BookParserprivate Integer numberOfSentence;private Integer numberOfVerb;private Integer numberOfAdverb;@Overridepublic Integer numberOfSentence() {if(numberOfSentence == null){numberOfSentence = super.numberOfSentence();}return numberOfSentence;}@Overridepublic Integer numberOfVerb() throws InterruptedException {if(numberOfVerb == null){numberOfVerb = super.numberOfVerb();}return numberOfVerb;}@Overridepublic Integer numberOfAdverb() throws InterruptedException {if(numberOfAdverb == null){numberOfAdverb = super.numberOfAdverb();}return numberOfAdverb;}
}
public class AppTest {public static void main(String[] args) throws InterruptedException {BookParser bp = new BookParserProxy();Integer a = bp.numberOfAdverb();System.out.println("有"+ a + "个副词");Integer a2 = bp.numberOfAdverb();System.out.println("有"+ a2 + "个副词");}/*** //等待一秒后两个结果同时出现,说明什么,说明第二次没有调用真实方法* 有220个副词* 有220个副词** Process finished with exit code 0*/}
这个就叫做静态代理,自己手写的代码,写死的代理类
而动态代理是运行时动态的生成字节码
4、代理模式的关键点
- 代理对象,一定与目标对象有相同的接口。这样才能做代理
- 代理对象中,一定有目标对象
- 代理对象,具有对目标对象的访问权限
其中上面的栗子中充分体现了前两点;第三点也可以体现,只需要改一下前置通知的返回值为boolean,然后做个判断就好啦
类似于现在要做一个权限验证的业务逻辑,再前置通知里面判断,返回true,才能调目标对象。
5、代理模式和适配器模式的比较
- 代理模式中,代理对象和它所包裹的目标对象,必须实现相同的接口;适配器模式种 ,适配器和它所包裹的对象不用实现相同的接口
- 代理模式中,代理对象可以控制它所包裹的目标对象的方法是否执行;适配器模式中,适配器总是调用目标对象的方法,无法控制