代理模式是很多框架中非常重要且常用的设计模式,分为静态代理和动态代理,下面我们从一个简单的例子一起走进代理模式。
需求:现在需要实现加减乘除功能
新增一个功能接口
interface Calculator{
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 MyCalculator implements Calculator{
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public int sub(int a, int b) {
return a - b;
}
@Override
public int mul(int a, int b) {
return a * b;
}
@Override
public int div(int a, int b) {
return a / b;
}
}
客户端
public class negtive {
public static void main(String[] args) {
Calculator c = new MyCalculator();
System.out.println(c.add(2, 3));
System.out.println(c.sub(10, 3));
System.out.println(c.mul(8, 3));
System.out.println(c.div(99, 3));
}
}
现在增加日志系统
```java
class MyCalculator implements Calculator{
@Override
public int add(int a, int b) {
System.out.println("初始值为:a=" + " " + a + ",b= " + b);
int r = a + b;
System.out.println("结果为:" + r);
return r;
}
}
可见,代码的侵入性很强,严重违反开闭原则,并不适合真正的开发。那如何解决这个问题呢?可能有些同学想到策略模式,开的多套不同的策略以此解决问题。确实方法是可行的,但是不是太过于复杂了呢?下面我们来介绍一种高效的方式------动态代理。
什么是动态代理呢?实现动态代理有两种方式:JDK动态代理和CGlib动态代理。
JDK动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
CGlib动态代理利用ASM(开源的Java字节码编辑库,操作字节码)开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
两者各自优势不同,如感兴趣请读者自行查阅资料。
我们这里探索JDK自有一套动态代理API使用方法
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
- 第1个参数:ClassLoader(动态代理的对象的类加载器)
我们都知道,要实例化一个对象,是需要调用类的构造器的,在程序运行期间第一次调用构造器时,就会引起类的加载,加载类的时候,就是jvm会自动找到类加载器,拿着ClassLoader去加载类的字节码的,只有字节码被加载到了内存中,才能进一步去实例化出类的对象。简单来说,就是只要涉及实例化类的对象,就一定要加载类的字节码,而加载字节码就必须使用类加载器!下面我们使用的是动态代理的api来创建一个类的对象,这是一种不常用的实例化类对象的方式,尽管不常用,但毕竟涉及实例化类的对象,那就一定也需要加载类的字节码,也就一定需要类加载器,所以我们手动把类加载器传入! - 第2个参数:Class[](需要调用其方法的接口)
我们已经知道,下面的代码,是用来实例化一个对象的,实例化对象,就一定是实例化某一个类的对象,问题是,到底是哪个类呢?类在哪里?字节码又在哪里?
比如我们new String() JVM会加载String.class、new Date() 会加载Date.class,而动态代理的这个类,其实并不在硬盘上,而是在内存中!是由动态代理在内存中"动态生成的!要知道,这个在内存中直接生成的字节码,会去自动实现下面方法中的第2个参数中,所指定的接口!所以,利用动态代理生成的代理对象,就能转成Calculator接口类型!那么这个代理对象就拥有add、sub、mul、div方法!
动态代理JVM原理:
- 第3个参数:InvocationHandler(调用方法时的处理程序)
我们已经知道,下面的代理对象porxy所属的类,实现了alculator接口,所以,这个代理对象就拥有add、 sub、 mul、div方法!我们就可以通过代理对象调用add、 sub、 mul、div方法!注意,每次对代理对象任何方法的调用,都不会进入真正的实现方法中。而是统统进入第3个参数的invoke方法中!
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
- Object proxy:代理对象
- Method:代理对象调用的方法
- Object[] args:调用方法的参数
有了以上JDK代理知识,现在我们进行JDK动态代理将日志融合:
写一个InvocationHandler的实现类。在实现类的内部关联Calculator,用于调用Calculator的方法。
public class MyHandler implements InvocationHandler {
private Calculator calculator ;
public MyHandler(Calculator c){
this.calculator = c;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("调用"+method.getName()+", 参数是"+ Arrays.toString(args));
// 调用真实对象的真实方法
int res = (int) method.invoke(calculator, args);
System.out.println("结果是 "+res);
return res;
}
}
客户端
public class Postive {
public static void main(String[] args) {
Calculator c = new MyCalculator();
ClassLoader loader = postive.class.getClassLoader();
Calculator proxy = (Calculator)Proxy.newProxyInstance(loader, new Class[]{Calculator.class}, new MyHandler(c));
proxy.add(22,33);
proxy.sub(55,22);
proxy.div(10,2);
proxy.mul(50,5);
}
}
输出
调用add, 参数是[22, 33]
结果是 55
调用sub, 参数是[55, 22]
结果是 33
调用div, 参数是[10, 2]
结果是 5
调用mul, 参数是[50, 5]
结果是 250
进行封装
class MyProxy{
public static Object getProxy(Object target) {
// 获取类加载器
ClassLoader loader = Postive.class.getClassLoader();
// 获取代理目标实现接口字节码
Class[] interfaces = target.getClass().getInterfaces();
// 生成代理对象
Object proxy = Proxy.newProxyInstance(loader, interfaces, new MyHandler(target));
return proxy;
}
}
客户端
/*===================客户端=============*/
public class Postive{
public static void main(String[] args) {
Calculator c = new MyCalculator();
Calculator proxy = (Calculator)MyProxy.getProxy(c);
proxy.add(22,33);
proxy.sub(55,22);
proxy.div(10,2);
proxy.mul(50,5);
}
}
现在,我们可以已经可以看到代理模式的雏形了。下面我们继续推进。
仔细想想上面的例子,如果我们不想加日志功能呢?
我们把增加的功能抽象出来
编写一个拦截器接口
interface Interceptor {
void before(Method method, Object[] args);
void after(Method method, Object res);
}
用户实现接口
class A implements Interceptor {
@Override
public void before(Method method, Object[] args) {
System.out.println(method.getName()+"前置处理"+"=>参数是:"+Arrays.toString(args));
}
@Override
public void after(Method method, Object args) {
System.out.println(method.getName()+"后置处理结果是=>"+args);
}
}
修改处理器handler
class MyHandler implements InvocationHandler {
private Object obj;
private Interceptor interceptor;
// 将拦截器传入构造器
MyHandler(Object obj, Interceptor interceptor) {
this.obj = obj;
this.interceptor = interceptor;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 前置通知
interceptor.before(method, args);
// 调用真实对象的真实方法
int res = (int) method.invoke(obj, args);
// 后置通知
interceptor.after(method,res);
return res;
}
}
class MyProxy {
public static Object getProxy(Object target, Interceptor interceptor) {
// 获取类加载器
ClassLoader loader = Proxy1.class.getClassLoader();
// 获取代理目标实现接口字节码
Class[] interfaces = target.getClass().getInterfaces();
// 生成代理对象
Object proxy = Proxy.newProxyInstance(loader, interfaces, new MyHandler(target, interceptor));
return proxy;
}
}
客户端
public class Postive{
public static void main(String[] args) {
Calculator c = new MyCalculator();
Calculator proxy = (Calculator) MyProxy.getProxy(c, new A());
proxy.add(22, 33);
}
}
测试结果
add前置处理=>参数是:[22, 33]
add后置处理结果是=>55
那如果需求变了呢?比如加法用中文日志,减法用英文日志,乘法用日文。这时我们很容易想到用if else来实现需求。
但这样做会造成大量代码冗余,且违反了单一职责原则,下面我们来尝试解决这个问题。
未完,待更---------------------------------
============================
代理模式的定义:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。