代理模式
代理模式是属于结构型的设计模式,指客户端的请求到达真正的对象之前,做一些额外的操作。
举个例子,
- 你需要找房子,那么通过向中介支付金额就可以找到心宜的房子,而中介需要跟房东商量好差价,衔接租户与房东,此时的中介就是代理.
- 过年需要回家,你不会操作12306的app,但是美团和支付宝出台了"帮你抢票"的功能,你无需操作12306,只需要向美团和支付宝支付金额,让平台帮你去抢票即可,这其实也是一种代理的体现.
静态代理模式
下面我们通过代码来实现静态代理.
需求:
- 租客手里有1000块,需要租房.
- 中介可以帮租客租房,但是需要收取100块的中介费.
- 房东手里有房子,但是找不到真正的租客.
- RentSubject
建立一个租房的接口,用户通过操作该接口,即可进行支付获取房子的钥匙.
package com.tea.modules.design.proxy;
import java.math.BigDecimal;
/**
* @author jaymin<br>
* 租房子的主题.<br>
* 对于租客来说,只需要付钱即可.<br>
* 2021/2/14 18:40
*/
public interface RentSubject {
/**
* 支付租金寻找房子.
* @param rent 租金
* @return
*/
String findHouse(BigDecimal rent);
}
- LandlordProxied
房东对象,房东只管收钱和交接钥匙.
package com.tea.modules.design.proxy;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
/**
* @author jaymin.<br>
* 房东,只负责收钱交房子即可.不关心谁进行支付.<br>
* 2021/2/14 18:48
*/
@Slf4j
public class LandlordProxied implements RentSubject {
@Override
public String findHouse(BigDecimal rent) {
log.info("房东收到了:{}租金,交出钥匙.", rent);
return "Lock";
}
}
- RentAgencyProxy
中介,中介负责从租客手里收钱,收取中介费后,向房东支付租金和获取钥匙,然后交给租客.
package com.tea.modules.design.proxy.statics;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
/**
* @author jaymin.<br>
* 房租中介机构,负责帮租客找房子.<br>
* 同时,找到房子后,中介需要向房东支付租金.<br>
* 2021/2/14 18:45
*/
@Slf4j
public class RentAgencyProxy implements RentSubject {
private RentSubject rentSubject;
public RentAgencyProxy(RentSubject rentSubject) {
this.rentSubject = rentSubject;
}
@Override
public String findHouse(BigDecimal rent) {
BigDecimal actualRent = beforeRealSubject(rent);
return this.rentSubject.findHouse(actualRent);
}
private BigDecimal beforeRealSubject(BigDecimal rent) {
log.info("中介收取当前租客租金:{}", rent);
// 中介赚取中间差价后,支付给房东
BigDecimal actualRent = rent.subtract(BigDecimal.valueOf(100));
return actualRent;
}
}
- TenantClient
租客,租客支付支付租金,获取钥匙.
package com.tea.modules.design.proxy;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
/**
* @author jaymin.<br>
* 租客,目前租客想找房子,手里有1000块钱.<br>
* 中介找到了900块的房子,将订单接收了下来,收取100块的中介费.<br>
* 房东此时有空置的房子,900块。<br>
* 2021/2/14 18:53
*/
@Slf4j
public class TenantClient {
public static void main(String[] args) {
RentSubject rentSubject = new RentAgencyProxy(new LandlordProxied());
String house = rentSubject.findHouse(BigDecimal.valueOf(1000));
log.info("租客拿到了房门钥匙:" + house);
}
}
- Result
19:30:41.553 [main] INFO com.tea.modules.design.proxy.RentAgencyProxy - 中介收取当前租客租金:1000
19:30:41.558 [main] INFO com.tea.modules.design.proxy.LandlordProxied - 房东收到了:900租金,交出钥匙.
19:30:41.558 [main] INFO com.tea.modules.design.proxy.TenantClient - 租客拿到了房门钥匙:Lock
静态代理存在的缺陷
此时对于中介来说,它的目的已经很明确了,即赚取差价.中介其实并不关心真正需要做的是什么业务,无论是租房、买房、买家具...只需要从客户手里拿到钱,然后找到真正的服务商进行交付即可。
那么此时对于Proxy类来说,无论最终的RealSubject中的逻辑是什么,它只负责代理(即经过代理类的金额会自动扣除100).想象一下此时如果有一个新的业务市场,也是同样的赚取差价,那么通过静态代理的方式仍然需要重新封装一套逻辑。如果这样的类越来越多,而代理逻辑都是一致的,那么最终项目的类会膨胀地非常快,同时加剧了维护成本.
此时对于代理来说,代理逻辑是确定的,被代理的类(targetObject)可以是未知的,如何做到将代理逻辑与原始类逻辑分离?
这个时候,我们就需要动态代理.
动态代理
动态代理技术在Spring AOP中分为两种:
- 基于JDK原生的动态代理.
提供一种在运行时创建一个实现了一组接口的新类.由于Java是不支持实例化接口的,因此JDK会在运行期间生成一个代理类对给定的接口进行实现,在调用该代理类接口的时候,将实现逻辑转发到调用处理器中(Invocation handler).
使用JDK进行动态代理的类必须实现接口(所有的代理类都是java.lang.reflect.Proxy
的子类,类名以$Proxy
开始).
- 基于CGLIB的动态代理.
CGLIB(Code Generation Library)是基于ASM(对Java字节码进行操作的框架)的类库.在
Spring AOP
中,如果被代理类(targetObject)没有实现接口,即无法通过JDK的动态代理生成代理类,那么就会选择CGLIB来进行代理.
CGLIB动态代理的原理:创建一个targetObject的子类,覆盖掉需要父类的方法,在覆盖的方法中对功能进行增强。
注意,由于是采用继承覆盖的方式,所以由final
方法修饰的类无法使用CGLIB进行代理.
1. 使用JDK动态代理实现代理模式
- IntermediaryInvocationHandler
package com.tea.modules.design.proxy.dynamic.jdkproxy;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.math.BigDecimal;
/**
* @author jaymin.<br>
* JDK动态代理实现中介赚取差价的逻辑.<br>
* 此处封装切面逻辑,相对于AOP中的Aspect.<br>
* 2021/2/14 19:56
*/
@Slf4j
public class IntermediaryInvocationHandler implements InvocationHandler {
private Object targetObject;
public IntermediaryInvocationHandler(Object targetObject) {
this.targetObject = targetObject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
BigDecimal actualPrice = beforeRealSubject(((BigDecimal) args[0]));
args[0] = actualPrice;
Object result = method.invoke(targetObject, args);
return result;
}
private BigDecimal beforeRealSubject(BigDecimal money) {
log.info("中介收取费用:{}", money);
// 中介赚取中间差价后,支付给服务商
BigDecimal actualPrice = money.subtract(BigDecimal.valueOf(100));
return actualPrice;
}
}
使用JDK的动态代理需要实现
InvocationHandler
接口,然后使用java.lang.reflect.Proxy#newProxyInstance
来生成代理类.
- DynamicProxyDemo
package com.tea.modules.design.proxy.dynamic;
import com.tea.modules.design.proxy.dynamic.jdkproxy.IntermediaryInvocationHandler;
import com.tea.modules.design.proxy.statics.LandlordProxied;
import com.tea.modules.design.proxy.statics.RentSubject;
import net.sf.cglib.proxy.MethodInterceptor;
import java.lang.reflect.InvocationHandler;
import java.math.BigDecimal;
/**
* @author jaymin.<br>
* 动态代理: <br>
* 1. JDK的动态代理,需要被代理类实现接口.<br>
* 2. CGLIB动态代理. <br>
* 2021/2/14 20:11
*/
public class DynamicProxyDemo {
public static void main(String[] args) {
jdkDynamicProxy();
}
/**
* JDK的动态代理.<br>
* 在这里,我们只需要提供一个切面逻辑的IntermediaryInvocationHandler即可完成代理逻辑的复用.<br>
* 更难得的是,只要类实现了任意接口,并且方法参数中的第一个参数为金额,那么中介就可以无缝进行赚取差价了,而不是通过创建类的形式.<br>
* 形象的来说,中介的逻辑在运行时被"织入"了.<br>
* 通过debug可以看到,被代理的对象引用前缀为:$Proxy <br>
*/
private static void jdkDynamicProxy() {
RentSubject targetObject = new LandlordProxied();
InvocationHandler handler = new IntermediaryInvocationHandler(targetObject);
// 获取当前被代理类的类加载器
ClassLoader classLoader = targetObject.getClass().getClassLoader();
Class<?>[] interfaces = targetObject.getClass().getInterfaces();
RentSubject rentSubject = (RentSubject) Proxy.newProxyInstance(classLoader, interfaces, handler);
System.out.println("当前对象是否为代理类:" + Proxy.isProxyClass(rentSubject.getClass()));
rentSubject.findHouse(BigDecimal.valueOf(1000));
}
}
- Result
当前对象是否为代理类:true
16:42:23.870 [main] INFO com.tea.modules.design.proxy.dynamic.jdkproxy.IntermediaryInvocationHandler - 中介收取费用:1000
16:42:23.870 [main] INFO com.tea.modules.design.proxy.statics.LandlordProxied - 房东收到了:900租金,交出钥匙.
可以看到,将实现了接口的
LandlordProxied
作为targetObject
,通过Proxy.newProxyInstance
创建出代理对象,就会在其执行findHouse
时回调IntermediaryInvocationHandler
中的invoke
方法.
2. 使用CGLIB实现代理模式
CGLIB并非JDK原生的包,所以我们需要导入CGLIB的依赖.
- pom.xml
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.9</version>
</dependency>
- 创建一个没有实现接口的业务类
package com.tea.modules.design.proxy.dynamic.cglibproxy;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
/**
* @author jaymin.<br>
* 对CGLIB测试,是否能增强没有实现接口的类.<br>
* 此类为普通的房东,没有实现任何接口,纯收钱交房.<br>
* 2021/2/14 21:00
*/
@Slf4j
public class NormalLandlord {
public String findHouse(BigDecimal rent) {
log.info("房东收到了:{}租金,交出钥匙.", rent);
return "Lock";
}
}
- IntermediaryMethInterceptor
package com.tea.modules.design.proxy.dynamic.cglibproxy;
import lombok.extern.slf4j.Slf4j;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
import java.math.BigDecimal;
/**
* @author jaymin.<br>
* 基于CGLIB实现动态代理.<br>
* 2021/2/14 20:46
*/
@Slf4j
public class IntermediaryMethInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
BigDecimal actualPrice = beforeRealSubject(((BigDecimal) args[0]));
args[0] = actualPrice;
Object result = methodProxy.invokeSuper(object, args);
return result;
}
private BigDecimal beforeRealSubject(BigDecimal money) {
log.info("中介收取费用:{}", money);
// 中介赚取中间差价后,支付给服务商
BigDecimal actualPrice = money.subtract(BigDecimal.valueOf(100));
return actualPrice;
}
}
CGLIB中创建代理类需要先写好一个切面类,该类需要实现
MethodInterceptor
.在intercept
方法中对业务进行增强,调用目标类的方法为methodProxy.invokeSuper
.
- DynamicProxyDemo
package com.tea.modules.design.proxy.dynamic;
import com.tea.modules.design.proxy.dynamic.cglibproxy.IntermediaryMethInterceptor;
import com.tea.modules.design.proxy.dynamic.cglibproxy.NormalLandlord;
import com.tea.modules.design.proxy.statics.LandlordProxied;
import com.tea.modules.design.proxy.statics.RentSubject;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import java.lang.reflect.InvocationHandler;
import java.math.BigDecimal;
/**
* @author jaymin.<br>
* 动态代理: <br>
* 1. JDK的动态代理,需要被代理类实现接口.<br>
* 2. CGLIB动态代理. <br>
* 2021/2/14 20:11
*/
public class DynamicProxyDemo {
public static void main(String[] args) {
cglibDynamicProxy();
}
/**
* CGLIB.创建一个目标类的子类,重写其中的方法.最终逻辑委托到MethodInterceptor中
*/
private static void cglibDynamicProxy(){
NormalLandlord targetObject = new NormalLandlord();
MethodInterceptor methInterceptor = new IntermediaryMethInterceptor();
NormalLandlord proxy = (NormalLandlord) Enhancer.create(targetObject.getClass(), methInterceptor);
proxy.findHouse(BigDecimal.valueOf(1000));
}
}
关键的代码其实就一行:
Enhancer.create(targetObject.getClass(), methInterceptor)
,其中methInterceptor
则是我们的切面类.
- Result
16:54:46.102 [main] INFO com.tea.modules.design.proxy.dynamic.cglibproxy.IntermediaryMethInterceptor - 中介收取费用:1000
16:54:46.118 [main] INFO com.tea.modules.design.proxy.dynamic.cglibproxy.NormalLandlord - 房东收到了:900租金,交出钥匙.
小结
- JDK动态代理要求被代理类实现接口.切面类需要实现
InvocationHandler
. - CGLIB采用继承+方法覆盖的形式实现切面,在重写方法中将逻辑委托给
MethodInterceptor#intercept
. - CGLIB对代理类基本没有限制,但是需要注意被代理的类不可以被
final
修饰符修饰.因为Java无法重写final类.
深入浅出动态代理
1.JDK动态代理到底是怎么实现的?
很多朋友都会有疑惑,这些动态代理的类看不见摸不着,虽然可以看到效果,但是底层到底是怎么做的,为什么要求实现接口呢?
OK,下面我们从JDK的动态代理入手,来看看代理类到底长啥样.
- 从Proxy.newProxyInstance入手
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);
final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/*
* Look up or generate the designated proxy class.
* 查找或生成指定的代理类
*/
Class<?> cl = getProxyClass0(loader, intfs);
// 省略若干代码
}
第一步,尝试获取代理类,该代理类可能会被缓存,如果没有缓存,那么进行生成逻辑.
- java.lang.reflect.Proxy#getProxyClass0
private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
// 如果代理类已经通过类加载器对给定的接口进行实现了,那么从缓存中返回其副本
// 否则,它将通过ProxyClassFactory创建代理类
return proxyClassCache.get(loader, interfaces);
}
- java.lang.reflect.Proxy.ProxyClassFactory#apply
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
// 一些验证、缓存、同步的操作,不是我们研究的重点
/*
* Generate the specified proxy class.
* 生成特殊的代理类
*/
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
/*
* A ClassFormatError here means that (barring bugs in the
* proxy class generation code) there was some other
* invalid aspect of the arguments supplied to the proxy
* class creation (such as virtual machine limitations
* exceeded).
*/
throw new IllegalArgumentException(e.toString());
}
}
}
ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);
,这段代码即为生成动态代理类的关键,执行完后会返回该描述该代理类的字节码数组.随后程序读取该字节码数组,将其转化为运行时的数据结构-Class对象,作为一个常规类使用.
- sun.misc.ProxyGenerator#generateProxyClass(java.lang.String, java.lang.Class<?>[], int)
public static byte[] generateProxyClass(final String var0, Class<?>[] var1, int var2) {
ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2);
final byte[] var4 = var3.generateClassFile();
// 如果声明了需要持久化代理类,则进行磁盘写入.
if (saveGeneratedFiles) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
try {
int var1 = var0.lastIndexOf(46);
Path var2;
if (var1 > 0) {
Path var3 = Paths.get(var0.substring(0, var1).replace('.', File.separatorChar));
Files.createDirectories(var3);
var2 = var3.resolve(var0.substring(var1 + 1, var0.length()) + ".class");
} else {
var2 = Paths.get(var0 + ".class");
}
Files.write(var2, var4, new OpenOption[0]);
return null;
} catch (IOException var4x) {
throw new InternalError("I/O exception saving generated file: " + var4x);
}
}
});
}
return var4;
}
这里我们找到了一个关键的判断条件-
saveGeneratedFiles
,即是否需要将代理类进行持久化.
private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));
这里会判断
sun.misc.ProxyGenerator.saveGeneratedFiles
变量是否为true.默认为false.
- 在main方法启动时将saveGeneratedFiles设置为true.
public class DynamicProxyDemo {
public static void main(String[] args) {
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
jdkDynamicProxy();
}
}
为了定位生成的类,我们在
Files.write(var2, var4, new OpenOption[0]);
中断点一下查看路径.
path
-
生成的代理类
$proxy
package com.sun.proxy;
import com.tea.modules.design.proxy.statics.RentSubject;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import java.math.BigDecimal;
public final class $Proxy0 extends Proxy implements RentSubject {
// 省略若干代码
public final String findHouse(BigDecimal var1) throws {
try {
return (String)super.h.invoke(this, m3, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
// 省略若干代码
}
我们将目光聚焦在
findHouse
上,可以看到,调用代理的findHouse
会去执行super.h.invoke
,其中h
即为Proxy类中的protected InvocationHandler h;
,那么此时也印证了我们的想法是对的。
同时,你也应该注意到,代理类继承自Proxy
并且实现了给定的RentSubject
接口.
有理有据,此时你应该对JDK动态代理有了更深的理解了.
这里贴一下从知乎上看到的关于动态代理更形象的解释:
2. 为什么有时候会产生代理失效?
下面给出一个例子来演示失效的场景.
假设此时有一个日志记录的注解:@Log
,在另一个类注入了SimpleServiceImpl
,并且调用了其中的simpleServiceImpl.foo()
,那么此时的bar()
方法是不会执行切面逻辑的。
public class SimpleServiceImpl implements SimpleService {
public void foo() {
// 通过foo()调用了方法内的bar()
this.bar();
}
@Log
public void bar() {
// some logic...
}
}
原因这里简单说一下:Spring对
SimpleServiceImpl
进行了代理,但是@Log
注解仅标注在bar()
上,那么需要通过SimpleServiceImpl.bar()
这样的形式才可以进入代理类的逻辑中,因此此时持有的是代理类的引用.
换个角度思考一下,SimpleServiceImpl#foo
将逻辑委托到了target
类进行执行,此时在target
类中调用了this.bar()
,this指向的引用是target
类本身,而不是代理类的引用,因此是无法被代理类进行环绕的.
如果还不理解,可以访问以下文章加深理解:
3. 既然CGLIB更加自由(不用实现接口),为什么Spring AOP还要内置JDK动态代理?
JDK的动态代理是Java官方推出的动态代理模式,官方对此进行维护和优化,无需引入第三方依赖.
CGLIB属于第三方框架,随着JDK版本的升级,项目也许需要更换CGLIB来兼容最新的JDK.
性能上,随着JDK版本的更新,已经跟CGLIB差别不大.
4. 动态代理会对程序有性能影响么?
如果使用动态代理生成了大量的类,可能会引发方法区的内存溢出.
JDK8开始,JVM去除了永久代,取而代之的是元空间.在默认设置下,由框架生成的动态代理类难以使JVM产生方法区内存溢出的异常.
但是,JDK8之前的版本,会产生动态代理类大量填充方法区引起内存溢出的问题.
相关的例子可以查看周志明的《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》
其中,关于CGLIB,可以查看相关文章:
CGLib: The Missing Manual
总结
OK,看到这里,相信你对动态代理技术已经有了一定的理解了,其实我们平时编程用到动态代理的场景比较少,大部分都是充斥着业务代码。但是学习框架底层的原理,会让你更好地理解Spring AOP
,来规避掉一些平时常见的错误。
网友评论