代理模式之场景模拟:
第一步:新建一个modules
第二步:创建接口,其中包含我们业务所涉及的功能
package spring_CalculatorProxy; public interface Calculator { int add(int i,int j); int sub(int i,int j); int mul(int i,int j); int div(int i,int j); }
第三步:创建接口的实现类并重写其中的方法
package spring_CalculatorProxy; public class CalculatorImpl implements Calculator{ @Override public int add(int i, int j) { //实现日志功能--额外功能 System.out.println("日志,方法:add,参数"+i+","+j); //核心功能--算数运算 int result=i+j; System.out.println("方法内部:result"+result); System.out.println("日志,方法:add,结果"+result); return result; } @Override public int sub(int i, int j) { System.out.println("日志,方法:sub,参数"+i+","+j); int result=i-j; System.out.println("方法内部:result"+result); System.out.println("日志,方法:sub,结果"+result); return result; } @Override public int mul(int i, int j) { System.out.println("日志,方法:mul,参数"+i+","+j); int result=i*j; System.out.println("方法内部:result"+result); System.out.println("日志,方法:mul,结果"+result); return result; } @Override public int div(int i, int j) { System.out.println("日志,方法:div,参数"+i+","+j); int result=i/j; System.out.println("方法内部:result"+result); System.out.println("日志,方法:div,结果"+result); return result; } }
提出场景模拟中存在的问题:
现有代码缺陷:
针对带日志功能的实现类,我们发现有如下缺陷:
1: 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力 2: 附加功能分散在各个业务功能方法中,不利于统一维护
解决思路:
解决这两个问题,核心就是:解耦
,我们需要把附加功能从业务功能代码中抽取出来
但要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决,原因是面向对象是纵向继承机制,我们只能将连续执行的代码进行封装,而不能实现抽取任意的某几行不连续的代码,所以需要引入新的技术
代理模式:
其实代理模式在我们的日常生活中,也是非常常见的,比如:
广告商找明星拍广告需要通过经纪人 合作伙伴找老板谈合作约见面时间需要经过秘书 房产中介是买卖双方的代理
概念:
将非核心逻辑剥离出来以后,封装这些非核心逻辑的类,对象,方法
它是属于23种设计模式中的一种,属于结构型模式,它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用,让不属于目标方法核心逻辑的代码从目标方法中剥离出来—解耦,调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时附加功能能够集中在一起也有利于统一维护
之前我们都是调用目标方法,直接实现功能,如下所示:
使用代理模式之后,如下所示:
目标对象/类/方法:被代理“套用”了非核心逻辑代码,例如我们上述场景模拟中的Calculator
我们现在就是创建目标对象所对应的代理类,每次访问目标对象的时候,都是通过代理对象进行访问,我们就不再直接访问目标对象了,而是通过代理对象对其进行访问,代理对象是间接访问目标对象的功能,代理对象和目标对象实现的功能是相同的,那么如何说明他们实现的功能都是相同的呢?
代理对象去实现目标功能的时候,直接在代理对象中去调用目标对象实现功能的方法
拿我们上述场景模拟中的add方法来说,如果使用代理模式,那么意味着,代理对象中也会产生一个add方法,当我们需要访问add方法时,我们并不会直接访问该方法,而访问的是代理对象中的add方法,通过代理对象间接去访问,怎么个间接法呢?
即为在代理对象中的add方法中直接去调用目标对象的add方法,代理对象能够控制目标对象方法的执行,这样我们就能保证,无论是通过目标对象实现还是通过代理对象实现,最终所实现的结果是不变的
代理模式之静态代理实现:
静态代理的特点就是一对一,当前一个代理类对应一个目标类,当前代理类只能作为目标类的代理,代理对象是为目标对象创建一个访问它的对象,如果需要访问目标对象中的功能时,我们就通过代理对象来访问,既然是这样,那么是不是就可以说明只要是目标对象可实现的事情,代理对象也可以实现,从代码层面来说,目标对象所拥有的方法,代理对象也必须有,因为目标对象都是通过代理对象来进行间接访问的,我们可以通过代理对象去控制目标对象的执行过程从而加入一些额外的操作,这样可以进行代码增强的效果,代码不改变的情况下,通过代理对象间接去访问目标对象中的功能,并且在它实现功能的过程中去加入一些额外的操作
创建代理类:
1:代理类和目标类需要实现相同的功能----代理类和目标类实现相同的接口
2:目标对象是通过代理对象间接访问它----实现访问目标对象所实现的功能
package spring_CalculatorProxy; public class Calculator_proxy implements Calculator{ //声明目标类的实现类或接口 private CalculatorImpl target; //创建目标类的对象---通过该对象调用目标类中的核心功能 public Calculator_proxy() { target=new CalculatorImpl(); } @Override public int add(int i, int j) { System.out.println("日志,方法:add,参数"+i+","+j); //通过创建的目标类对象去调用add方法 int result= target.add(i,j); System.out.println("日志,方法:add,结果"+result); return result; } @Override public int sub(int i, int j) { System.out.println("日志,方法:sub,参数"+i+","+j); int result= target.sub(i,j); System.out.println("日志,方法:sub,结果"+result); return result; } @Override public int mul(int i, int j) { System.out.println("日志,方法:mul,参数"+i+","+j); int result= target.mul(i,j); System.out.println("日志,方法:mul,结果"+result); return result; } @Override public int div(int i, int j) { System.out.println("日志,方法:div,参数"+i+","+j); int result= target.div(i,j); System.out.println("日志,方法:div,结果"+result); return result; } }
目标类:
此时只专注于核心的算术运算功能
package spring_CalculatorProxy; public class CalculatorImpl implements Calculator { @Override public int add(int i, int j) { return i+j; } @Override public int sub(int i, int j) { return i-j; } @Override public int mul(int i, int j) { return i*j; } @Override public int div(int i, int j) { return i/j; } }
编写测试类:
import org.junit.Test; import spring_CalculatorProxy.CalculatorImpl; import spring_CalculatorProxy.Calculator_proxy; public class ProxyTest { @Test public void ProxyTest(){ //通过代理类对象去调用目标方法,内部其实是通过代理类中的目标类的对象去访问目标类中的目标方法 Calculator_proxy calculatorProxy=new Calculator_proxy(); calculatorProxy.add(2,3); calculatorProxy.sub(20,10); calculatorProxy.div(18,6); calculatorProxy.mul(15,3); } }
输出如下所示:
日志,方法:add,参数2,3 日志,方法:add,结果5 日志,方法:sub,参数20,10 日志,方法:sub,结果10 日志,方法:div,参数18,6 日志,方法:div,结果3 日志,方法:mul,参数15,3 日志,方法:mul,结果45
通过代理模式进行功能增强时,不仅只有目标功能前后,还可以通过try-catch-finally中加入额外的操作,但是并不是所有位置都可以,比如目标对象存在10行代码,我们不能说想在第五行添加就在第五行添加,虽然说我们可以在目标对象实现的过程中增加新的功能,但是我们只能在目标对象方法执行之前,执行之后,catch,finally这四个位置进行
引入动态代理:
静态代理确实实现了解耦,但是由于代码不够灵活不便于修改,就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,因此产生了大量重复的代码,日志功能还是分散的,没有统一管理,但如果我们将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现,这就需要使用动态代理技术了
那么动态代理技术的动态体现在哪里呢?
动态生成目标类所对应的代理类,使用动态代理技术就可以不用创建动态代理类,而是通过jdk提供的方法和API动态的为某一个目标类创建它的代理类,和静态代理不同的是,动态代理类起初是不存在的,我们需要通过jdk提供的API在代码运行的过程中,动态创建每个目标类所对应的动态代理类
动态代理的实现:
创建工厂类:
工厂类—>ProxyFactory 并不是真正的代理类,而是动态生成目标类所对应的代理类的一个工具类,该类本身并没什么功能,它并不是某一个目标类所对应的代理类,而是我们可以通过这个类帮助我们动态生成目标类的代理类,之所以为工厂,是因为它的功能是批量地生产代理类
下面为一个生产代理类工厂的创建过程:
由于该工厂的作用是生产代理类,而代理类所代理的对象又为目标类,因此我们需要先创建目标对象,由于我们不知道目标对象的类型,因此设置为Object,再创建有参构造或者set方法为目标对象赋值,如下所示:
package SpringCalculatorProxy; public class ProxyFactory { private Object target; public ProxyFactory(Object target) { this.target = target; } }
接下来我们通过使用jdk中的API中的newProxyInstance去创建代理类的实例,如下所示:
其动态创建代理类的过程是通过反射完成的:
newProxyInstance方法包含三个参数,如下所示:
第一步类加载的过程:
ClassLoader classLoader=this.getClass().getClassLoader();
在 Java 中,每个类都有一个对应的类加载器,用于加载和解析该类的字节码文件。类加载器负责将字节码文件加载到内存中,并生成对应的 Class 对象,以便在程序中使用,我们可通过当前对象去调用getClassLoader()以获取当前对象所属类的类加载器,用代码表示即为this.getClass().getClassLoader(),this.getClass() 返回当前对象所属的类的 Class 对象,getClassLoader() 方法返回的是一个 ClassLoader 对象,它是一个抽象类,具体的实现由不同的类加载器提供。在实际使用中,可以根据需要进行类型转换,或者使用 ClassLoader 类中定义的方法来加载和管理类。
第二步获取目标对象实现的所有接口的class对象的数组:
Class<?>[] interfaces= target.getClass().getInterfaces();
target.getClass().getInterfaces() 用于获取目标对象所实现的接口数组。
在 Java 中,一个类可以实现一个或多个接口,通过实现接口,类可以定义和实现接口中声明的方法。target.getClass() 用于获取目标对象的类的 Class 对象,getInterfaces() 方法是 Class 类的一个方法,用于获取当前类所实现的接口数组。 getInterfaces() 方法返回的是一个 Class<?>[] 数组,其中的元素是 Class 对象,表示目标对象所实现的接口。可以通过遍历数组或者直接访问数组元素来获取具体的接口信息。
第三步执行处理操作:
我们通过创建一个实现了 InvocationHandler 接口的匿名内部类的实例,该实例可以作为参数传递给 Proxy.newProxyInstance 方法,用于创建代理对象。
在 Java 中,InvocationHandler 是一个接口,用于在动态代理中处理代理对象的方法调用。它只有一个方法 invoke,在代理对象的方法被调用时,该方法会被调用。通过重写 invoke 方法,可以在代理对象的方法调用前后进行额外的操作,例如打印日志、权限验证等。
InvocationHandler invocationHandler=new InvocationHandler() { //proxy:代理对象 method:要执行/重写的方法 args:要执行方法的参数列表 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //在调用目标对象的方法执行前进行额外的操作---输出日志信息 System.out.println("日志,方法名:"+method.getName()+",参数:"+ Arrays.toString(args)); //调用目标对象的方法 Object result= method.invoke(target,args); //在调用目标对象的方法执行后进行额外的操作---输出日志信息 System.out.println("日志,方法名:"+method.getName()+",结果:"+ result); return result; } };
这样的日志信息并不完整,在学习静态代理的时候,我们说过进行功能增强时,我们可以通过try-catch-finally
进行额外的操作,那么下面我们就演示一下:
修改ProxyFactory类
中的getProxy方法
中的InvocationHandler中
的方法的代码,添加try-catch-finally代码块,如下所示,IDE中可使用快捷键Ctrl+Alt+t
InvocationHandler invocationHandler = new InvocationHandler() { //代理类中的方法如何重写 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = null; try { System.out.println("日志,方法名:" + method.getName() + ",参数:" + Arrays.toString(args)); result = method.invoke(target, args); System.out.println("日志,方法名:" + method.getName() + ",结果:" + result); } catch (Exception e) { e.printStackTrace(); System.out.println("日志,方法名:" + method.getName() + ",异常:" +e); } finally { System.out.println("日志,方法名:" + method.getName() + ",方法执行完毕"); } return result; } };
完整代码如下所示:
package SpringCalculatorProxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; public class ProxyFactory { private Object target; public ProxyFactory(Object target) { this.target = target; } public Object getProxy() { //获取类加载器 ClassLoader classLoader = this.getClass().getClassLoader(); //2:获取目标对象实现的所有接口的class对象的数组---为了保证目标类和代理类实现的功能是一致的,代理类和目标类实现相同的接口 Class<?>[] interfaces = target.getClass().getInterfaces(); //3:执行处理:设置代理类中的抽象方法如何重写 InvocationHandler invocationHandler = new InvocationHandler() { //代理类中的方法如何重写 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = null; try { System.out.println("日志,方法名:" + method.getName() + ",参数:" + Arrays.toString(args)); result = method.invoke(target, args); System.out.println("日志,方法名:" + method.getName() + ",结果:" + result); } catch (Exception e) { e.printStackTrace(); System.out.println("日志,方法名:" + method.getName() + ",异常:" + e); } finally { System.out.println("日志,方法名:" + method.getName() + ",方法执行完毕"); } return result; } }; return Proxy.newProxyInstance(classLoader,interfaces,invocationHandler); } }
测试类:
package ProxyTest; import SpringCalculatorProxy.Calculator; import SpringCalculatorProxy.CalculatorImpl; import SpringCalculatorProxy.ProxyFactory; import org.junit.jupiter.api.Test; public class ProxyTest { @Test public void ProxyTest(){ ProxyFactory proxyFactory=new ProxyFactory(new CalculatorImpl()); //由于我们不清楚此时代理类所代理的目标对象时什么类型,又不能使用Object类,但我们知道它实现的接口,因此可以通过上转型完成 Calculator Proxy= (Calculator) proxyFactory.getProxy(); Proxy.mul(2,4); } }
输出如下所示:
日志,方法名:mul,参数:[2, 4] 日志,方法名:mul,结果:8 日志,方法名:mul,方法执行完毕
上述是未有异常出现的情况,似乎和为加try-catch-finally时的输出结果差别不大,那么下面我们进行有异常的测试:
在测试类中调用除法运算:
//被除数不能为0,因此这里会抛出ArithmeticException算数异常 Proxy.div(2,0);
测试结果如下:
动态代理有两种:
jdk代理:要求必须有接口,最终生成的代理类和目标类实现相同的接口在com.sun.proxy包下,类名为$proxy2 cglib代理:,生成的代理类最终会继承目标类,并且和目标类在相同的包下
静态代理和动态代理的区别:
静态代理需要代理类和被代理类实现同一个接口或继承同一个父类
,代理类在编译期间就已经确定,代理类中的方法也都是固定的。而动态代理则是在运行时动态生成代理类
,不需要代理类和被代理类实现同一个接口或继承同一个父类,代理类中的方法也可以动态添加或删除