【结构型模式三】代理模式
一、前情提要
我们在策略模式实现了使用一个策略上下文类,将调用这些操作类的父类的运算方法的过程再做封装,这样客户端连这些操作类的父类都无需知道,就可以直接通过策略上下文类调用运算方法,使得各个运算的实现和调用逻辑完全分离。
客户端代码长这样:
public int computeByStrategy(int num1, String operator, int num2) throws Exception {
OperationContext operationContext = new OperationContextImpl();
return operationContext.execute(num1,operator,num2);
}
二、计算器又要加功能
现在如果要增加一个限制,不是所有人都能直接调用操作运算方法,只有答对口令“芝麻开门”的人才能调用。这个需求就可以使用代理模式来实现。
三、代理模式
代理模式(Proxy)为其他对象提供一种代理,以控制对这个对象的访问。
引入代理就是为了在访问一个对象时引入一定的间接性,因为这种间接性,可以附加多种用途。
代理模式的角色
使用静态代理实现在进行运算操作前验证口令的类图:
使用JDK动态代理实现在进行运算操作前验证口令的类图:
Subject(抽象主题):被代理类的父接口或父类,定义了可被代理的方法。
RealSubject(真实主题):被代理类,真正实现业务逻辑的类;
Proxy(代理):用来代理和封装真实主题;
代理模式的结构比较简单,其核心是代理类,为了让客户端能够一致性地对待真实对象和代理对象,在代理模式中引入了抽象层。
四、代理模式的种类
如果根据字节码的创建时机来分类,可以分为静态代理和动态代理:
- 所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和真实主题角色的关系在运行前就确定了。
- 而动态代理的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以在运行前并不存在代理类的字节码文件
1. 静态代理
特点:代理类实现和被代理类相同的接口,并保存一个被代理类对象的引用,重写接口中的方法。
2. JDK动态代理
- (1) JDK动态代理类实现了InvocationHandler接口。
- (2) JDK动态代理的基础是反射(method.invoke(对象,参数))
- (3) 创建代理对象需要
- 1)得到目标对象的类加载器
- 2)得到目标对象的实现接口
- 3)一个实现了invocationHandler接口的对象
Proxy.newProxyInstance(classLoader, interfaces, this)
3. CGLib动态代理
CGLib是针对类来实现代理的,他的原理是对指定的目标生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。
注意:jdk的动态代理只可以为实现接口的类去完成操作,而cglib它可以为没有实现接口的类去做代理,也可以为实现接口的类去做代理。
五、使用静态代理来实现验证口令后运算
首先实现一个OperationContext的代理类,这个代理类也实现了OperationContext接口的execute方法,但要调用这个方法,首先得答对口令"芝麻开门"才能构造OperationContextImpl对象,从而调用execute方法。
public class OperationContextProxy implements OperationContext {
public static final String PASSWORD = "芝麻开门";
OperationContext operationContext;
public OperationContextProxy(String password) throws Exception {
if (isPasswordRight(password)) {
System.out.println("口令正确,计算结果为:");
if (operationContext == null) {
operationContext = new OperationContextImpl();
}
}else {
throw new Exception("口令错误!");
}
}
private boolean isPasswordRight(String password) {
return password.equals(PASSWORD);
}
@Override
public int execute(int num1, String operator, int num2) throws Exception {
return operationContext.execute(num1,operator,num2);
}
}
六、客户端代码
public class Client {
public int computeByProxy(int num1, String operator, int num2) throws Exception {
OperationContext operationContextProxy = new OperationContextProxy("芝麻开门");
return operationContextProxy.execute(num1,operator,num2);
}
public static void main(String[] args) {
try {
System.out.println(computeByProxy(2,"/",1,"芝麻开门"));
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 运行结果:
口令正确,计算结果为:
2
这样客户端就只能在答对口令为"芝麻开门"的情况下构造代理对象,从而调用操作运算方法了。
通过静态代理,我们达到了功能增强的目的,而且没有侵入原代码,这是静态代理的一个优点。
七、静态代理的缺点
虽然静态代理实现简单,且不侵入原代码,但是,当场景稍微复杂一些的时候,静态代理的缺点也会暴露出来。
1、 当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:
-
只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大
-
新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类
2、 当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护。
7.1 如何改进?
当然是让代理类动态的生成啦,也就是动态代理。
7.2 为什么类可以动态的生成?
这就涉及到Java虚拟机的类加载机制了,推荐翻看《深入理解Java虚拟机》7.3节 类加载的过程。
Java虚拟机类加载过程主要分为五个阶段:加载、验证、准备、解析、初始化。其中加载阶段需要完成以下3件事情:
-
通过一个类的全限定名来获取定义此类的二进制字节流
-
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
-
在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据访问入口
由于虚拟机规范对这3点要求并不具体,所以实际的实现是非常灵活的,关于第1点,获取类的二进制字节流(class字节码)就有很多途径:
-
从ZIP包获取,这是JAR、EAR、WAR等格式的基础
-
从网络中获取,典型的应用是 Applet
-
运行时计算生成,这种场景使用最多的是动态代理技术,在 java.lang.reflect.Proxy 类中,就是用了 ProxyGenerator.generateProxyClass 来为特定接口生成形式为
*$Proxy
的代理类的二进制字节流 -
由其它文件生成,典型应用是JSP,即由JSP文件生成对应的Class类
-
从数据库中获取等等
所以,动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到JVM中使用。
但是如何计算?如何生成?情况也许比想象的复杂得多,我们需要借助现有的方案。
7.3 常见的字节码操作类库
-
Apache BCEL (Byte Code Engineering Library):是Java classworking广泛使用的一种框架,它可以深入到JVM汇编语言进行类操作的细节。
-
ObjectWeb ASM:是一个Java字节码操作框架。它可以用于直接以二进制形式动态生成stub根类或其他代理类,或者在加载时动态修改类。
-
CGLIB(Code Generation Library):是一个功能强大,高性能和高质量的代码生成库,用于扩展JAVA类并在运行时实现接口。
-
Javassist:是Java的加载时反射系统,它是一个用于在Java中编辑字节码的类库; 它使Java程序能够在运行时定义新类,并在JVM加载之前修改类文件。
-
…
7.4 实现动态代理的思考方向
为了让生成的代理类与目标对象(真实主题角色)保持一致性,从现在开始将介绍以下两种最常见的方式:
-
通过实现接口的方式 -> JDK动态代理
-
通过继承类的方式 -> CGLIB动态代理
注:使用ASM对使用者要求比较高,使用Javassist会比较麻烦
八、JDK动态代理实践
- JDK动态代理主要涉及两个类:
java.lang.reflect.Proxy
和java.lang.reflect.InvocationHandler
; - 首先我们要编写一个方法调用逻辑处理器 PasswordHandler 类,提供校验口令的功能,这个类需实现 InvocationHandler 接口,在 PasswordHandler 中维护一个被代理的目标对象,并在
invoke
方法中编写方法调用的逻辑处理;
public class PasswordHandler implements InvocationHandler {
private static final String PASSWORD = "芝麻开门";
private Object target;
private String password;
public PasswordHandler(Object target,String password) {
this.target = target;
this.password = password;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (isPasswordRight(password)) {
System.out.println("口令正确,计算结果为:");
return method.invoke(target,args);
} else {
throw new Exception("口令错误!");
}
}
private boolean isPasswordRight(String password) {
return password.equals(PASSWORD);
}
}
- 然后在客户端借助 Proxy 类的 newProxyInstance 方法获取动态生成的代理类的对象。
public static int computeByDynamicProxy(int num1, String operator, int num2 , String password) throws Exception {
// 设置环境变量保存动态代理类,默认名称以 $Proxy0 格式命名
System.getProperties().setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
// 1. 创建被代理的对象,OperationContext接口的实现类
OperationContextImpl operationContextImpl = new OperationContextImpl();
// 2. 获取对应的 ClassLoader
ClassLoader classLoader = operationContextImpl.getClass().getClassLoader();
// 3. 获取operationContextImpl实现的所有接口的Class对象,这里的OperationContextImpl只实现了一个接口OperationContext
Class[] interfaces = operationContextImpl.getClass().getInterfaces();
// 4. 创建一个将传给代理类的调用请求处理器,处理所有的代理对象上的方法调用
// 这里创建的是一个自定义的口令处理器,须传入被代理的对象 operationContextImpl
PasswordHandler passwordHandler = new PasswordHandler(operationContextImpl,password);
/*
5.根据上面提供的信息,创建代理对象
在这个过程中:
a.JDK会通过根据传入的参数信息动态地在内存中创建和.class 文件等同的字节码
b.然后根据相应的字节码转换成对应的class,
c.然后调用newInstance()创建代理实例
*/
OperationContext operationContextProxy = (OperationContext) Proxy.newProxyInstance(classLoader,interfaces,passwordHandler);
// 通过调用代理的方法来调用被代理类的方法
return operationContextProxy.execute(num1,operator,num2);
}
运行结果同静态代理结果。
InvocationHandler 和 Proxy 的主要方法介绍如下:
java.lang.reflect.InvocationHandler
Object invoke(Object proxy, Method method, Object[] args)
定义了代理对象调用方法时希望执行的动作,用于集中处理在动态代理类对象上的方法调用
java.lang.reflect.Proxy
static InvocationHandler getInvocationHandler(Object proxy)
用于获取指定代理对象所关联的调用处理器
static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)
返回指定接口的代理类
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
构造实现指定接口的代理类的一个新实例,所有方法会调用给定处理器对象的 invoke 方法
static boolean isProxyClass(Class<?> cl)
返回 cl 是否为一个代理类
九、代理类的调用过程
生成的代理类到底长什么样子呢?借助下面的代码,把代理类保存下来再探个究竟,我们在客户端生成代理类时如下设置下系统环境变量即可。
// 设置环境变量保存动态代理类,默认名称以 $Proxy0 格式命名
System.getProperties().setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
IDEA 再次运行之后就可以在 target 的类路径下找到 UserServiceProxy.class,双击后IDEA的反编译插件会将该二进制class文件变成方便查看的代码形式。
JDK 动态代理生成的代理类反编译后的代码:
public final class $Proxy0 extends Proxy implements OperationContext {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final int execute(int var1, String var2, int var3) throws Exception {
try {
return (Integer)super.h.invoke(this, m3, new Object[]{var1, var2, var3});
} catch (Exception | Error var5) {
throw var5;
} catch (Throwable var6) {
throw new UndeclaredThrowableException(var6);
}
}
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 int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m3 = Class.forName("com.pxy.designpattern.proxy.OperationContext").getMethod("execute", Integer.TYPE, Class.forName("java.lang.String"), Integer.TYPE);
m2 = Class.forName("java.lang.Object").getMethod("toString");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
从上面的代码中我们可以发现:
-
$Proxy0 继承了 Proxy 类,并且实现了被代理的所有接口(这里只有OperationContext接口)以及equals、hashCode、toString等方法
-
由于 $Proxy0 继承了 Proxy 类,所以会关联一个 InvocationHandler 方法调用处理器
-
类和所有方法都被
public final
修饰,所以代理类只可被使用,不可以再被继承 -
每个方法都有一个 Method 对象来描述,Method 对象在static静态代码块中创建,以
m + 数字
的格式命名 -
调用方法的时候通过
super.h.invoke(this, m1, (Object[])null);
调用,其中的super.h.invoke
实际上是在创建代理的时候传递给Proxy.newProxyInstance
的 PasswordHandler 对象,它继承自InvocationHandler 类,负责实际的调用处理逻辑
而 PasswordHandler 的 invoke 方法接收到 method、args 等参数后,进行一些处理,然后通过反射让被代理的对象 target 执行方法
十、总结
代理可以分为 “静态代理” 和 “动态代理”,动态代理又分为 “JDK动态代理” 和 “CGLIB动态代理” 实现。
静态代理
代理对象和实际对象都继承了同一个接口,在代理对象中指向的是实际对象的实例,这样对外暴露的是代理对象而真正调用的是 Real Object
- 优点:可以很好的保护实际对象的业务逻辑对外暴露,从而提高安全性。
- 缺点:不同的接口要有不同的代理类实现,会很冗余
JDK 动态代理:
为了解决静态代理中,生成大量的代理类造成的冗余;
- JDK 动态代理只需要实现 InvocationHandler 接口,重写 invoke 方法便可以完成代理的实现,
- jdk的代理是利用反射生成代理类 Proxyxx.class 代理类字节码,并生成对象
- jdk动态代理之所以只能代理接口是因为代理类本身已经extends了Proxy,而java是不允许多重继承的,但是允许实现多个接口
- 优点:解决了静态代理中冗余的代理实现类问题。
- 缺点:JDK 动态代理是基于接口设计实现的,如果没有接口,会抛异常。
CGLIB 代理:
由于 JDK 动态代理限制了只能基于接口设计,而对于没有接口的情况,JDK方式解决不了。
CGLib 采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑,来完成动态代理的实现。
-
实现方式为实现 MethodInterceptor 接口,重写 intercept 方法,通过 Enhancer 类的回调方法来实现。
-
CGLib在创建代理对象时所花费的时间比JDK多得多,所以对于单例的对象,因为无需频繁创建对象,用CGLib合适,反之,使用JDK方式要更为合适一些。
-
同时,由于CGLib由于是采用动态创建子类的方法,对于final方法,无法进行代理。
-
优点:没有接口也能实现动态代理,而且采用字节码增强技术,性能也不错。
-
缺点:技术实现相对难理解些。
十一、代理模式的应用
代理模式按照职责(使用场景)来分类,至少可以分为以下几类:1、远程代理。 2、虚拟代理。 8、智能引用(Smart Reference)代理等等。
1. 远程代理
RPC调用远程服务的方法。
2. 虚拟代理
通过代理来存放实例化需要很长时间的真实对象,让这些开销很大的对象可以按需创建。浏览器就是通过代理模式来优化很大的HTML网页的打开,先显示文字,再一张张地下载图片。
3. 安全代理
用来控制真实对象访问时的权限。一般用于对象有不同的访问权限的时候。
4. 智能指引
在访问一个实际对象前,加一些其他的处理,例如检查是否锁定、如果是第一次使用这个对象则将其缓存到内存中等。