设计模式之动态代理
Posted mingorun
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式之动态代理相关的知识,希望对你有一定的参考价值。
在了解什么是动态代理模式,以及怎么实现动态代理模式之前,我们先明确一个场景,那就是当我们需要对一类实现类增加一些额外处理对其中的某些方法做一些增强时。譬如增加日志打印,参数校验,鉴权等。这些功能很重要,但是又具有一定的独立性(与业务代码不相关)。针对这种情况, 我们就可以采用动态代理模式来实现。
动态代理顾名思义,就是动态的对方法实现代理。要真正理解动态代理,需要先理解什么是动态,什么是代理。一方面动态和静态是相对,通过比较静态代理和动态代理可以对动态代理有直观感受;另一方面,静态代理和装饰者模式看着神似,比较静态代理模式和装饰者模式可以让我们对代理有更清晰的认识。综上,本文接下来以以下顺序安排
1) 装饰者模式
① 什么是装饰者模式
② 装饰者模式实现
2) 静态代理模式
① 什么是静态代理模式
② 静态代理实现
3) 动态代理模式
① 动态代理的特点
② 动态代理的实现
4) 动态代理模式的常见实现
① 动态代理的CGlib 实现
② 动态代理的 javassist 实现
1) 装饰者模式
① 什么是装饰者模式
装饰者模式: 是一种不修改原类文件同时也不使用继承方式的情况下,通过创建一个包装对象,来实现对原对象功能增强的设计模式。它需要有以下要素(特点):
- 包装类和原类需实现共同的接口
- 包装对象中含有原对象的引用
- 使用时由包装对象接收客户端参数进行调用
② 装饰者模式实现
我们以一个上课的例子来看看具体实现。作为学生每学期都有些课程(公共接口) 需要去上(takeClass),现在假设学校加课堂授课管理,每个学生都需要签到,缺课8次不能参加考试。代码如下:
/** * 公共接口 */ public interface Course void takeClass();
/** * 普通课程 */ public class OtherCourse implements Course public void takeClass() System.out.println("start a normal class..");
/** 程序繁琐的课 */ public class StrictClass implements Course // 被包装对象 private Course course; public StrictClass(Course course) this.course = course; public void takeClass() if(!isAbsenceOver8Times()) signInBeforeClass(); course.takeClass(); private boolean isAbsenceOver8Times() // 取数据计算缺课次数; 下面瞎写个返回结果的逻辑 System.out.println("check in for student‘s record"); if(new Random().nextBoolean()) return false; return true; private void signInBeforeClass() System.out.println("every student should sign before get sited.."); // 记录签到数据
以上便是公共接口,被装饰对象 和 装饰对象 的类定义,测试代码中执行如下语句便可以看到装饰模式的效果
// 客户端通过调用 StrictClass 在不改变原OtherCourse的内容下增加了新的功能
Course course = new StrictClass(new OtherCourse()); course.takeClass();
可以看到,由于包装类StrictClass 中包含了对OtherCourse 的应用,因此作为被包装对象,每次新建包装对象时都需要作为参数传入;虽然包装类与被包装类实现了相同的接口,在很多场合可以不做修改的直接使用包装类来实现功能增强,但是对于实现不同接口的原类型而言,就需要有针对的定义不同的包装类,这会让包装类数量变得庞大,最终难以维护。
第一个问题,我们可以来看看静态代理模式, 第二个问题动态代理模式中将得到解决。
2) 静态代理模式
① 什么是静态代理模式
静态代理模式直观理解就是 静态的封装的被代理对象,当客户端需要访问原对象时,需要通过代理对象的来访问。静态的意思指的被代理对象被硬编码在代理对象中(代理类需要关联被代理类)。而动态代理可以代理任意类型。
② 静态代理模式的实现
静态代理模式的实现与装饰者模式非常接近,不同是, 装饰模式的被装饰对象时客户端传入的,而静态的代理模式由于对客户端限制了被代理对象的访问,不需要传入被代理对象作为参数,参考如下代码:
// 被装饰(代理)对象 private Course course; //装饰者模式 public StrictClass(Course course) this.course = course; // 静态代理模式 public StrictClass() this.course = new OtherCourse();
这样在客户端调用的时候的差别也就很直观了:
// 装饰者模式,客户端需要同时获取到装饰对象和装饰者对象 Course course = new StrictClass(new OtherCourse()); course.takeClass();
// 静态代理模式, 客户端感知不到被代理对象的存在 Course course1 = new StrictClass(); course1.takeClass();
3) 动态代理模式
① 动态代理模式的特点
从静态代理的实现来看,它做的事与装饰者模式是一样的,不同的是它对客户端隐藏了被代理对象。从代码复用程度,灵活性上来看它还不如装饰者模式。譬如如果公共接口有多个实现类, 静态代理就需要在代码中分别关联并提供这些类的get方法,而装饰者模式只需要在客户端创建需要使用的被装饰类即可。因此静态代理其实用的很少, 与之相对的动态代理则很不一样。
动态代理模式:动态代理模式为其它对象提供一个代理以控制对这个对象的直接访问,且不论是何对象,均可通过代理类实现间接访问。
② 动态代理模式的实现
还是采用前面的例子,其中接口和被代理对象都不变(可以不用接口了),可以看到接收代理对象的属性变成了Object 类型
// JDK 动态代理实现方式
public class CourseHandler implements InvocationHandler private Object course; // 传入被代理对象,获取代理对象 public Object getCourseProxy(Object course) this.course = course; Object o = Proxy.newProxyInstance(course.getClass().getClassLoader(), course.getClass().getInterfaces(), this); return o; public Object invoke(Object proxy, Method method, Object[] args) throws Throwable if(!isAbsenceOver8Times()) signInBeforeClass(); return method.invoke(course, args); return null; private boolean isAbsenceOver8Times() // 取数据计算缺课次数; System.out.println("check in for student‘s record"); if(new Random().nextBoolean()) return false; return true; private void signInBeforeClass() System.out.println("every student should sign before get sited.."); // 记录签到数据
客户端在调用时只需要获取代理对象,并将代理对象的类型转化成被代理对象类型,直接调用需要被代理的方法即可。至于希望原对象被怎样代理,可以在实现InvocationHandler的实现类中的invoke方法里添加对应的代理方式即可。以下是客户端调用方式
// 动态代理模式 客户端调用 CourseHandler courseHandler = new CourseHandler(); Course courseProxy = (Course) courseHandler.getCourseProxy(new OtherCourse()); courseProxy.takeClass();
4) 动态代理模式的常见实现
除了使用JDK自带的动态代理方式外,常见的动态代理还有CGlib实现的动态代理和javassis 动态代理。这两种动态代理的实现都需要引入第三方jar依赖。
① 动态代理模式的CGlib 实现
CGlib 是一个开源的实现动态代理的三方库,使用CGlib 需要引入以下依赖:
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.2.12</version> </dependency>
在自定义动态代理策略上, 使用CGlib与使用JDK动态代理有些类似, 先看看CGlib 的动态代理的实现
public class CourseMethodHandler implements MethodInterceptor private Object course; public Object getCourseProxy(Object course) this.course = course; return Enhancer.create(course.getClass(), course.getClass().getInterfaces(), this); public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable if(!isAbsenceOver8Times()) signInBeforeClass(); // 这里原方法的实现可以通过使用第二个参数method 来调用(反射机制) // 如果利用反射机制,需要将被代理类作为第一个参数传给method.invoke的第一个参数 // method.invoke(course,objects) // 还可以通过使用methodProxy调用父类方法来实现; 速度更快,推荐使用该方式 Object invoke = methodProxy.invokeSuper(o, objects); return invoke; return null; private boolean isAbsenceOver8Times() // 取数据计算缺课次数; System.out.println("check in for student‘s record"); if(new Random().nextBoolean()) return false; return false; private void signInBeforeClass() System.out.println("every student should sign before get sited.."); // 记录签到数据
这里CGlib 自定义代理方法的实现需要实现MethodInterceptor的接口,然后重写intercept方法,在intercept方法中回调被代理对象中的原方法, 这里就有两种调用方式了。一种是使用jdk的反射也即method.invoke,这个就和直接使用JDK动态代理是一样的了,另一个是使用
methodProxy.invokeSuper() 方式, 该方式传入对象 intercept 方法的第一个传参,invokeSuper方法定义如下:
/** * Invoke the original (super) method on the specified object. * @param obj the enhanced object, must be the object passed as the first 也就是this对象 * argument to the MethodInterceptor * @param args the arguments passed to the intercepted method; you may substitute a different * argument array as long as the types are compatible * @see MethodInterceptor#intercept * @throws Throwable the bare exceptions thrown by the called method are passed through * without wrapping in an <code>InvocationTargetException</code> */ public Object invokeSuper(Object obj, Object[] args) throws Throwable try init(); FastClassInfo fci = fastClassInfo; return fci.f2.invoke(fci.i2, obj, args); catch (InvocationTargetException e) throw e.getTargetException();
然后在客户端的调用就几乎与JDK动态代理是一样的了
CourseMethodHandler courseMethodHandler = new CourseMethodHandler(); OtherCourse oc = (OtherCourse) courseMethodHandler.getCourseProxy(new OtherCourse()); oc.takeClass();
② 动态代理的 javassist 实现
javassist是一个开源的字节码生成工具,用它可以直接在运行时动态的生成类,仅需要使用javassist 提供的高级API及java反射机制可以从无到有的创建任意想要的java类(参考官方文档)。下面来看看javassist 实现动态代理:
public interface IHello void sayHello(String toWho); // 需要被代理类 public class Hello implements IHello public void sayHello(String toWho) System.out.println("hello, " + toWho); // 实现InvocationHandler 接口,这里 javassist 创建动态代理在动态类后 也是用的jdk的动态代理机制 public class CustomHandler implements InvocationHandler private Object obj; public CustomHandler(Object obj) this.obj = obj; public Object invoke(Object proxy, Method method, Object[] args) throws Throwable System.out.println("pre method"); Object result = method.invoke(obj, args); System.out.println("post method"); return result;
重点在于ProxyFactory ,ProxyFactory的newProxyInstance 方法用代码创建了一个名叫 HelloProxyClass 的代理类,在该代理类中调用sayHello 方法得到的将是"增强"了的原方法。
public class ProxyFactory public static Object newProxyInstance(ClassLoader loader, Class<?> interfaceClass, InvocationHandler h) throws Throwable ClassPool pool = ClassPool.getDefault(); // 1.创建代理类:public class NewProxyClass CtClass proxyCc = pool.makeClass("HelloProxyClass"); /* 2.给代理类添加字段:private InvocationHandler h; */ CtClass handlerCc = pool.get(InvocationHandler.class.getName()); CtField handlerField = new CtField(handlerCc, "h", proxyCc); // CtField(CtClass fieldType, String fieldName, CtClass addToThisClass) handlerField.setModifiers(AccessFlag.PRIVATE); proxyCc.addField(handlerField); /* 3.添加构造函数:public NewProxyClass(InvocationHandler h) this.h = h; */ CtConstructor ctConstructor = new CtConstructor(new CtClass[] handlerCc , proxyCc); ctConstructor.setBody("$0.h = $1;"); // $0代表this, $1代表构造函数的第1个参数 proxyCc.addConstructor(ctConstructor); /* 4.为代理类添加相应接口方法及实现 */ CtClass interfaceCc = pool.get(interfaceClass.getName()); // 4.1 为代理类添加接口:public class NewProxyClass implements IHello proxyCc.addInterface(interfaceCc); // 4.2 为代理类添加相应方法及实现 CtMethod[] ctMethods = interfaceCc.getDeclaredMethods(); for (int i = 0; i < ctMethods.length; i++) String methodFieldName = "m" + i; // 新的方法名 // 4.2.1 为代理类添加反射方法字段 // 如:private static Method m1 = Class.forName("chapter9.javassistproxy3.IHello").getDeclaredMethod("sayHello2", new Class[] Integer.TYPE ); /* 构造反射字段声明及赋值语句 */ String classParamsStr = "new Class[0]"; // 方法的多个参数类型以英文逗号分隔 if (ctMethods[i].getParameterTypes().length > 0) // getParameterTypes获取方法参数类型列表 for (CtClass clazz : ctMethods[i].getParameterTypes()) classParamsStr = (("new Class[0]".equals(classParamsStr)) ? clazz.getName() : classParamsStr + "," + clazz.getName()) + ".class"; classParamsStr = "new Class[] " + classParamsStr + ""; String methodFieldTpl = "private static java.lang.reflect.Method %s=Class.forName(\"%s\").getDeclaredMethod(\"%s\", %s);"; String methodFieldBody = String.format(methodFieldTpl, "m" + i, interfaceClass.getName(), ctMethods[i].getName(), classParamsStr); // 为代理类添加反射方法字段. CtField.make(String sourceCodeText, CtClass addToThisClass) CtField methodField = CtField.make(methodFieldBody, proxyCc); proxyCc.addField(methodField); System.out.println("methodFieldBody: " + methodFieldBody); /* 4.2.2 为方法添加方法体 */ /* 构造方法体. this.h.invoke(this, 反射字段名, 方法参数列表); */ String methodBody = "$0.h.invoke($0, " + methodFieldName + ", $args)"; // 如果方法有返回类型,则需要转换为相应类型后返回,因为invoke方法的返回类型为Object if (CtPrimitiveType.voidType != ctMethods[i].getReturnType()) // 对8个基本类型进行转型 // 例如:((Integer)this.h.invoke(this, this.m2, new Object[] paramString, new Boolean(paramBoolean), paramObject )).intValue(); if (ctMethods[i].getReturnType() instanceof CtPrimitiveType) CtPrimitiveType ctPrimitiveType = (CtPrimitiveType) ctMethods[i].getReturnType(); methodBody = "return ((" + ctPrimitiveType.getWrapperName() + ") " + methodBody + ")." + ctPrimitiveType.getGetMethodName() + "()"; else // 对于非基本类型直接转型即可 methodBody = "return (" + ctMethods[i].getReturnType().getName() + ") " + methodBody; methodBody += ";"; /* 为代理类添加方法. CtMethod(CtClass returnType, String methodName, CtClass[] parameterTypes, CtClass addToThisClass) */ CtMethod newMethod = new CtMethod(ctMethods[i].getReturnType(), ctMethods[i].getName(), ctMethods[i].getParameterTypes(), proxyCc); newMethod.setBody(methodBody); proxyCc.addMethod(newMethod); System.out.println("Invoke method: " + methodBody); // 创建的代理类的字节码文件 写如 D:/tmp 路径 proxyCc.writeFile("D:/tmp"); // 5.生成代理实例. 将入参InvocationHandler h设置到代理类的InvocationHandler h变量 @SuppressWarnings("unchecked") Object proxy = proxyCc.toClass().getConstructor(InvocationHandler.class).newInstance(h); return proxy;
被创建动态创建的代理类反编译后如下:
public class HelloProxyClass implements IHello private InvocationHandler h; private static Method m0 = Class.forName("proxy.Dynamic.javassist.IHello").getDeclaredMethod("sayHello", String.class); public HelloProxyClass(InvocationHandler var1) this.h = var1; public void sayHello(String var1) this.h.invoke(this, m0, new Object[]var1);
参考:https://juejin.im/post/5bca012fe51d450e97055186
以上是关于设计模式之动态代理的主要内容,如果未能解决你的问题,请参考以下文章