疯狂创客圈:如果说Java是一个武林,这里的聚集一群武痴, 交流着编程的各种招式
QQ群链接:疯狂创客圈QQ群
在Java编程中,代理模式是一种极为重要的设计模式。
什么是代理模式呢?简单的说,当不想直接访问,或者无法直接访问某个方法时,可以通过一个代理对象来间接访问,这便是代理的模式。
为什么在这里讲解代理模式呢?
Java中的代理模式,与Java的反射技术关系非常的密切,故与反射技术一起分析和研究。
打个比方,我们前面设计了一个简单的宠物类Pet类。
有两个方法: sayHello和sayAge,赘述如下:
现在问题来了: 如果在调用sayHello 和sayHi时,需要在文件中加上访问日志,该如何实现呢?
假如,要求实现的访问日志的实例如下:
2018-09-23 03:03:11 嗨,大家好!我是小狗-1
2018-09-23 03:03:11 嗨,我是小狗-1,我的年龄是:91
2018-09-23 03:03:11 嗨,大家好!我是小猫-1
2018-09-23 03:03:11 嗨,我是小猫-1,我的年龄是:4
访问日志,是一种非常普遍、非常常见的日常开发需求。
在我们的日常开发中,往往需要进行访问的统计,统计网站页面或者API接口的详细访问日志,以便进行后续的性能分析和产品优化。这种访问统计,小部分是保存在文件中,更多的是保存在数据库中,而大型的网站的访问日志,则是保存在分布式的文件系统中。
为了简单起见,本实例将日志保存在文件中。 按照日期为单位,每天在log目录下生成一个以当天为文件名称的log文件。
如何实现这个日志的功能呢?
有一种愚笨蠢的方式,就是直接修改原来的代码,加上日志的功能。为什么说这个方法愚笨蠢呢? 这明细不符合面向对象设计的原则:对修改封闭,对扩展开放。去修改原来的老的代码,一是可能会引入新的问题。更何况,老的代码可能来自其他的项目和工程,手上未必有老的代码。
稍微高级一点的办法,是使用静态代理的模式,对原来的代码进行继承和扩展。
首先说下静态代理的简单做法:
(1)首先,继承老的代码,增加中间层。
(2)在中间层中,加入扩展的功能。
(3)在中间层中,组合老的对象实例,调用老的代码。
(4)中间层代码,必须继承原来的接口。
保持接口的一致,利用面向对象的多态机制和向上转型功能,这样,能保障客户端代码不需要改变。
在思路上非常的简单,按照上面的思路,实现前面讲的案例:设计一个静态代理类,给Pet类增加日志的能力,设计UML图如下:
新设计一个PetLoger类,作为Pet类的代理,包含一个原Pet对象的组合,并且扩展了日志的能力。
PetLoger类代理对象控制对Pet原对象的引用,对客户端的调用,隐藏Pet原对象的具体信息。
PetLoger类的代码如下:
package com.crazymakercircle.Proxy;
import com.crazymakercircle.common.pet.IPet;
import com.crazymakercircle.common.pet.Pet;
import com.crazymakercircle.util.FileLogger;
class PetLogger extends Pet implements IPet {
IPet pet;
public PetLogger(IPet pet) {
this.pet = pet;
}
@Override
public IPet sayHello() {
pet.sayHello();
String logInfo = "嗨,大家好!我是" + pet.getName();
FileLogger.info(logInfo);
return this;
}
@Override
public IPet sayAge() {
pet.sayAge();
String logInfo = "嗨,我是" + pet.getName() + ",我的年龄是:" + pet.getAge();
FileLogger.info(logInfo);
return this;
}
}
在PetLogger 代理类的sayHello和sayAge方法中,进行了日志能力的增强,调用fileLogger,向文件中进行日志的记录。
客户端的调用代码如下:
package com.crazymakercircle.Proxy;
import com.crazymakercircle.common.pet.Cat;
import com.crazymakercircle.common.pet.Dog;
public class PetLoggerDemo
{
public static void main(String[] args) {
PetLogger petLogger=new PetLogger(new Dog());
petLogger.sayHello();
petLogger.sayAge();
petLogger=new PetLogger(new Cat());
petLogger.sayHello();
petLogger.sayAge();
}
}
客户端程序,除了对象的新建需要稍微的调整之外,其他的代码,都不需要改变。
运行客户端的程序,在日志文件中,进行了日志的保存。日志保存的结果,如下所示:
2018-09-23 03:03:11 嗨,大家好!我是小狗-1
2018-09-23 03:03:11 嗨,我是小狗-1,我的年龄是:91
2018-09-23 03:03:11 嗨,大家好!我是小猫-1
2018-09-23 03:03:11 嗨,我是小猫-1,我的年龄是:4
新设计的PetLogger,调用了一个通用的日志类FileLogger,向日志文件,追加日志。
日志类FileLogger的代码,逻辑上比较简单。
由于与本例核心逻辑不相干,这里不赘述,欢迎来疯狂创客圈QQ群共享下载。
通过上面的实例,对静态代理有一个比较清晰的了解。
静态代理模式,原理上由3大组成部分:
(1)抽象接口类(abstract subject)
该类的主要职责是声明目标类与代理类的共同接口方法,该类既可以是一个抽象类也可以是一个接口。
(2) 真实目标类(real subject)
该类也称为被委托类或被代理类,该类定义了代理所表示的真实对象,由其执行具体业务逻辑方法,而客户端则通过代理类间接地调用真实目标类中定义的方法。
(3)代理类(proxy subject)
该类也称为委托类或代理类,该类持有一个对真实目标类(realSubject)的引用,在其所实现的接口方法中调用真实目标类中相应的接口方法执行,以此起到代理的作用。同时,代理类执行扩展的业务逻辑,实现能力的扩展。
前面讲到,静态代理模式,完美的符合了面向对象的一项基本原则:对修改封闭,对扩展开放。
然而,这种模式有一个显而易见的缺点。
下面做一个假设, 假设一种新的场景:需要对属性的合理性进行校验。静态代理模式的缺点就一览无余了。
在实际的开发中,客户会一个接着一个,不断提出新的要求。这也符合互联网开发的迭代思维,是在一次又一次的迭代开发中,产品的功能一步一步丰富和完善的。
这里假设一种新的场景:
在解决完了日志的能力扩展之后,领导或者客户又提出了一项新的能力扩展,需要对宠物的年龄进行属性校验。
校验的逻辑是:对于在合理范围之外的宠物年龄,要求进行年龄的保密,不暴露给外界。
为了进行属性校验,先进行两项准备工作:
(1)定义一个注解,用来进行年龄的配置注解的代码如下:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface AgeRange {
int min();
int max();
}
(2)第二项准备工作是: 给Pet 类的age成员,加上年龄的注解标记
public class Pet implements IPet{
.........
@AgeRange(max = 15,min = 2)
protected int age;
..........
}
接下来,万事具备,只欠东风了。
为了实现对年龄进行校验,可以使用静态代理模式。
参考前面的案例,再一次对Pet类进行扩展,新增一个AgaChecker 代理类,在 AgaChecker 代理类的sayAge方法中,对age的属性值进行校验。
AgaChecker 代理类的代码如下:
class AgaChecker extends PetLogger implements IPet {
IPet pet;
public AgaChecker(IPet pet) {
super(pet);
this.pet = pet;
}
@Override
public IPet sayAge() {
Field field = null;
try {
field = Pet.class.getDeclaredField("age");
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
AgeRange anno = field.getAnnotation(AgeRange.class);
if (this.getAge() > anno.max() || this.getAge()
在上面的 AgaChecker 代理类的sayAge方法中, 首先取得了注解的min和max值,然后对age 进行校验:如果不在合理范围之类, 不在输出宠物的年龄,实现了保密的目的。
在这个代码中, 应用了注解的技术、 反射的技术、静态代理的技术。
几项Java的比较牛逼的技术,都全面的得到了引用。 然后,通过新技术的应用,优美的解决了客户的问题。
正在心里洋洋得意之时,客户或者领导不断的暗示你:这个不是终点,后面几轮迭代,还需要对持续Pet类进行扩展和改进,还需要扩展性能统计能力、安全控制能力、异常处理能力 ........
在这种场景下,估计作为开发者的你,也是头都大了。
难道每一种场景,都去增加一个代理类吗? 显然,静态代理模式的缺点,已经昭然若揭的出来了。
静态代理的一个显著的缺点是:
一个静态代理类,只服务于一种场景的能力增强。反过来说,每一次能力的增强,需要设计一个新的代理类。
如果能力增强的场景很多,就需要设计较多的代理类型。在这种情况下,若采用静态代理,则会出现新增静态代理类多,造成代码量大,从而导致代码冗余、重复的问题。
如何解决这个问题呢?
上帝告诉我们:办法总比问题多。 其办法之一就是:动态代理。
动态代理则与静态代理相反,不需要手工实现代理类。
动态代理通过反射机制,动态地生成代理类型。在编码阶段,不需要编写代理类。这个代理类,由JDK通过反射技术,在执行阶段动态生成,所以也叫动态代理。
通过JDK 的方式,实现动态代理的代码很简单,大致如下3步:
(1)构造一个能力扩展对象,能力扩展对象需要实现InvocationHandler接口,实现该接口的invoke抽象方法。能力扩展的代码逻辑,写在此invoke方法中。 InvocationHandler接口是JDK的预定义接口,位于 java.lang.reflect 包中。
(2)在JDK生成运行时代理对象前,需要取得真实目标对象(real subject)的接口,作为运行时代理类需要实现的接口。
(3)将第一步构造的InvocationHandler接口的能力扩展对象、第二步获取的真实目标对象的接口、还加上一个类加载器,三者凑齐一起,作为入口参数,传入java.lang.reflect.Proxy类的newProxyInstance方法中,获取JDK 在运行阶段生成动态代理对象,然后,大功告成。
本例中,将以上的第二步、第三个步骤进行了封装,写成了一个通用的获取代理对象的方法,具体如下:
public static Object newProxyInstance(IPet targetObject, InvocationHandler handler ) {
Class targetClass = targetObject.getClass();
ClassLoader loader = targetClass.getClassLoader();
//被代理类实现的接口
Class>[] targetInterfaces = ReflectionUtil.getInterfaces(targetClass);
Object proxy = Proxy.newProxyInstance(loader, targetInterfaces, handler);
return proxy;
}
为了代码的复用,将以上的定义的newProxyInstance方法,放置在一个与反射相关的Util类——ReflectionUtil类中,供后续调用。
顺便说一下,上面这个方法,后面会反复用到,用来取得动态代理类型对象。
如果这个三步暂时不好理解,看两个例子就清楚了。
按照上面的三步,来实现动态代理实例,实现前面日志记录的能力。
其第一步,构造一个能力扩展对象LogHandler,实现InvocationHandler接口,实现该接口的invoke抽象方法,代码如下:
package com.crazymakercircle.Proxy;
import com.crazymakercircle.common.pet.IPet;
import com.crazymakercircle.util.FileLogger;
import com.crazymakercircle.util.Logger;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
class LogHandler implements InvocationHandler {
IPet pet;
public LogHandler(IPet pet) {
this.pet = pet;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (!(proxy instanceof IPet)) {
Logger.info("proxy is not IPet, need not file log");
}
IPet pet = this.pet;
if (method.getName().equals("sayHello")) {
// method.invoke(pet, args);
pet.sayHello();
String logInfo = "嗨,大家好!我是" + pet.getName();
FileLogger.info(logInfo);
} else if (method.getName().equals("sayAge")) {
pet.sayAge();
// method.invoke(pet, args);
String logInfo = "嗨,我是" + pet.getName() + ",我的年龄是:" + pet.getAge();
FileLogger.info(logInfo);
} else {
return method.invoke(pet, args);
}
return null;
}
}
动态代理方案,简化了编程。不需要继承抽象接口,编写一个一个的新的代理类型。 只需要将扩展的代码逻辑,写在InvocationHandler 实现类的invoke方法中即可。
完成了能力扩展实例的设计,只需要通过java.lang.reflect.Proxy类,传入前面说的3个参数,获取所需要的动态代理对象即可。
下面的代码,使用前面封装好的ReflectionUtil类中的newProxyInstance方法,取得动态代理对象,然后直接调用。
调用动态代理对象的代码如下:
package com.crazymakercircle.Proxy;
import com.crazymakercircle.common.pet.Cat;
import com.crazymakercircle.common.pet.Dog;
import com.crazymakercircle.common.pet.IPet;
import com.crazymakercircle.util.ReflectionUtil;
import java.lang.reflect.InvocationHandler;
public class PetLoggerProxyDemo {
public static void main(String[] args) {
//生成代理类的class文件
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// 创建被代理类的委托类,之后想要调用被代理类的方法时,
// 都会委托给这个类的invoke方法
IPet dog=new Dog();
InvocationHandler handler = new LogHandler(dog);
IPet petProxy =(IPet) ReflectionUtil.newProxyInstance(dog,handler);
petProxy.sayHello();
petProxy.sayAge();
IPet cat=new Cat();
// 创建被代理类的委托类,之后想要调用被代理类的方法时,
// 都会委托给这个类的invoke方法
handler = new LogHandler(cat);
petProxy =(IPet) ReflectionUtil.newProxyInstance(cat,handler);
petProxy.sayHello();
petProxy.sayAge();
}
}
动态代理,是本质上是通过java.lang.reflect.Proxy的newProxyInstance方法,生成一个动态代理类的实例。
这里对 java.lang.reflect.Proxy的newProxyInstance方法,做一个详细的介绍。该方法的定义如下:
public static Object newProxyInstance(ClassLoader loader,
Class>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
....
}
我们已经知道,此方法会产生一个动态代理类,并且创建一个动态代理类的对象,并且返回。
此方法的参数中,最重要的是第二个和第三个参数。
第二个参数是动态代理类需要实现的抽象接口,此接口也是真实目标类的所实现的接口。
第三个参数是一个对真实目标类进行能力增强的InvocationHandler 对象。在此类中, 实现扩展的业务逻辑,比方日志的记录、属性的检查,或者其他的能力增强的逻辑。
JVM执行时,所产生的动态代理类,到底长成啥样呢?是否能看到动态扩展类的class字节码呢?
答案是肯定的。
需要一个额外的操作。在生成动态扩展类之前,加上一句系统属性设置的代码,就可以将JVM在运行时生成的动态扩展类的class文件,保存在当前的工程目录下。
设置系统属性的代码,具体如下:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
这句代码告诉JVM 在将生成的字节码保存下来,默认的保存为项目根路径的com/sun/proxy/下。
以前面的日志动态代理实例为例,运行程序,可以发现 在com/sun/proxy/路径下生成了$Proxy0.class字节码文件。如果IDE有反编译的能力,可以在IDE中直接打开,后可以看到它的源码如下:
package com.sun.proxy;
import com.crazymakercircle.common.pet.IPet;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy3 extends Proxy implements IPet {
private static Method m1;
private static Method m6;
private static Method m3;
private static Method m2;
private static Method m4;
private static Method m5;
private static Method m0;
public $Proxy3(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return ((Boolean)super.h.invoke(this, m1, new Object[]{var1})).booleanValue();
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final IPet sayHello() throws {
try {
return (IPet)super.h.invoke(this, m6, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String getName() throws {
try {
return (String)super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final IPet sayAge() throws {
try {
return (IPet)super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int getAge() throws {
try {
return ((Integer)super.h.invoke(this, m5, (Object[])null)).intValue();
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return ((Integer)super.h.invoke(this, m0, (Object[])null)).intValue();
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
m6 = Class.forName("com.crazymakercircle.common.pet.IPet").getMethod("sayHello", new Class[0]);
m3 = Class.forName("com.crazymakercircle.common.pet.IPet").getMethod("getName", new Class[0]);
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m4 = Class.forName("com.crazymakercircle.common.pet.IPet").getMethod("sayAge", new Class[0]);
m5 = Class.forName("com.crazymakercircle.common.pet.IPet").getMethod("getAge", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
通过代码可以看出,这个动态代理类,其实只做了两件简单的事情:
(1)实现了抽象接口类的每一个抽象方法
上面的例子,是实现了IPet接口的动态代理类。除了实现了Ipet接口的sayHello、sayAge等四个方法,还有额外的三个方法。
这个额外的三个方法,是从java.lang.Object中继承来的equals()、hashCode()、toString()方法。这个3方法,都在代理类中,生成了对应的代理实现。
(2)在每一个代理实现的方法中,其实代码很简单。仅仅是统一调用了InvocationHandler对象的invoke()方法。并且将调用的参数,进行了二次传递。
而实际上,InvocationHandler对象的invoke()方法,正是我们进行能力增强的方法。
使用动态代理,实现多种能力的增强,就不需要再继承抽象接口,实现很多的能力增强类了。
比如,使用动态代理的方法,进行年龄属性的合理性检查,为以下两步:
(1)实现InvocationHandler 的invoke方法,加入能力增强的代码,进行age的合理性检查
(2)通过Proxy.newProxyInstance方法,取得动态代理对象,可以使用。
实现属性检查的能力增强,第一步的代码如下:
package com.crazymakercircle.Proxy;
import com.crazymakercircle.anno.AgeRange;
import com.crazymakercircle.common.pet.IPet;
import com.crazymakercircle.common.pet.Pet;
import com.crazymakercircle.util.Logger;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
class AgeCheckerHandler implements InvocationHandler {
IPet pet;
public AgeCheckerHandler(IPet pet) {
this.pet = pet;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("sayAge")) {
Field field = null;
try {
field = Pet.class.getDeclaredField("age");
} catch (NoSuchFieldException e) {
e.printStackTrace();
return null;
}
AgeRange anno = field.getAnnotation(AgeRange.class);
if (pet.getAge() > anno.max() || pet.getAge() < anno.min()) {
Logger.debug("sorry,my age is secret ");
return null;
}
return pet.sayAge();
// return method.invoke(pet, args);
} else {
return method.invoke(pet, args);
}
}
}
在invoke方法中,首先对调用的方法进行判断,只对sayAge进行增强。对年龄进行边界的校验,边界之外的直接保密。
除了sayAge之前的其他的方法,还是直接执行目标对象的原方法。
这里执行原目标对象的方法,使用的是反射的方式:
return method.invoke(pet, args);
总结起来,使用动态代理,主要通过InvocationHandler 的invoke方法来调用具体的被代理方法。动态代理可以使我们的代码逻辑更加简洁,不需要写一大堆的代理类,不再需要关心到底代理谁。
最后,说明一下,动态代理的,可以直接通过JDK 实现,也可以使用第三方的字节码生成工具cglib等来实现。
使用第三方库进行动态代理,原理与JDK的方法,基本相同,这里不做展开。