通过学习本篇博文,你将彻底明白代理模式在JAVA世界中的应用。
1.代理模式
英语:Proxy Pattern,是计算机程序设计中的一种设计模式。所谓代理者是指一个可以完成委托者任务的接口,代理真实的对象做事情。
日常现实生活中的代理有很多:火车票/汽车票/飞机票代售点、品牌的分级代理销售商、电子商务网站。
整个Java体系中运用代理模式的框架很多,例如数据库持久层Mybatis,Hibernate , 例如Spring整个Bean初始化过程,以及核心AOP等等。
整个架构体系中的代理也有很多:我们翻越GFW所使用的代理,NGINX反向代理。
2.方法论
在开始之前,说一下我对学习方法论的一些认识。大家都知道5W2H分析法,但是在学习新知识,2W1H更适用,即WHAT,WHY,HOW。
创造一门知识的人,是为了解决特定的问题,而后来者,学习这门知识,则要明白它是什么,为什么要用它而不用其他的,具体又是怎么实现的。所以在学习代理模式时,抱着2W1H来看,会事半功倍。
有的程序员写了一辈子Mybatis的Mapper,可能也不知道为什么不用写实现类,就可以达到操作sql的功能。其实内部就是利用JAVA动态代理的原理生成Mapper的代理类,再利用SqlSession操作方法。
2.1 Mybatis getMapper调用栈
- SqlSession.getMapper()
public T getMapper(Class type) {
return configuration.getMapper(type, this);
}
- Configuration.getMapper()
public T getMapper(Class type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
- MapperRegistry.getMapper()
public T getMapper(Class type, SqlSession sqlSession) {
final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) knownMappers.get(type);
return mapperProxyFactory.newInstance(sqlSession);
}
- MapperProxyFactory.newInstance
public T newInstance(SqlSession sqlSession) {
//生成Mapper代理类
final MapperProxy mapperProxy = new MapperProxy(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy mapperProxy) {
//根据代理类,生成代理对象,实现Mapper接口
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
- MapperProxy
public MapperProxy(SqlSession sqlSession, Class mapperInterface, Map methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(this, args);
}
Mybatis利用Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);生成代理对象。
关于Mybatis的动态代理部分先介绍到这里,后续有时间再写点其他东西。
4.Java的代理模式
共有三种:静态代理
,动态代理
,CGLIB
4.1静态代理
代理类和委托类都实现一个接口
4.1.1静态代理举例
火车票代售例子
- 4.1.1.1 票务中心
铁道部出票核心接口,所有售票窗口都需要拥有该功能(再一次证明:接口表示拥有什么,继承表示是什么
)
public interface TicketCenter{
public boolean sell();
}
- 4.1.1.2铁道部火者站售票窗口
public class RailwayStationSeller implements TicketCenter{
public boolean sell(){
//真实售票流程
}
}
- 4.1.1.3 火车票代售点
public class TicketProxySeller implements TicketCenter{
private TicketCenter center;
public TicketProxySaller(TicketCenter center){
this.center = center;
}
public boolean sale(){
//售票前做点什么
doSomeThingBefore();
//真实售票流程
center.sell();
//售票后做点什么
doSomeThingAfter();
}
}
- 4.1.1.4 用户买票
模拟用户去代售点买票。
public class TestBuyTicketClient{
public static void main(String[] args){
TicketCenter railwayStation = new RailwayStationSeller();
TicketCenter proxy = new TicketProxySeller(railwayStation);
//代理售票
proxy.sell();
}
}
4.1.2 静态代理的缺点
如果现在需要增加网络代售,那么需要重新编写一个代理类。而且当核心接口增加一个方法时,所有的代理类和实现类都需要修改。这个时候就需要引进动态代理了。
4.1.3 Decorator Pattern
Decorator Pattern:装饰模式,一个和静态代理极为相似的设计模式。
装饰模式动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。
他们的唯一区别是,代理模式中代理类,对委托类具有完全的控制权,执行与否。而装饰模式中,对委托类没有的控制权,必须执行委托类的原生方法。
所以,他们两者的区别只是概念的区别。
例子:
OutputStream out = new DataOutputStream( new FileOutputStream( "HelloWorld.java") )
public class DataOutputStream extends FilterOutputStream implements DataOutput {
public DataOutputStream(OutputStream out) {
super(out);
}
}
public class FilterOutputStream extends OutputStream {
protected OutputStream out;
}
DataOutputStream封装了一个FileOutputStream, 方便进行输出流处理。
4.2 动态代理
一个静态代理只能代理一种类型,而且是在编译器就已经确定被代理的对象。而动态代理是在程序运行期间由JVM根据反射等机制动态的生成,所以不存在代理类的字节码文件。代理类和委托类的关系是在程序运行时确定。
在Java中要想实现动态代理机制,需要java.lang.reflect.InvocationHandler接口和 java.lang.reflect.Proxy 类的支持。
所以:java.lang.OutOfMemoryError: PermGen space 就有可能发生在动态代理调用的地方,因为Class在被 Load的时候被放入PermGen space区域,这部分区域存放Class和Meta的信息。出现这种异常,需要调整JVM的PermSize和MaxPermSize参数。
4.2.1 火车票代售功能
- 4.2.1.1 调用处理器接口
public class ProxyInvocationHandler implements InvocationHandler{
// 这个就是我们要代理的真实对象
private Object subject;
// 构造方法,给我们要代理的真实对象赋初值
public ProxyInvocationHandler(Object subject){
this.subject = subject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 在代理真实对象前我们可以添加一些自己的操作
System.out.println("before sell ticket");
System.out.println("Method:" + method);
// 当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用
method.invoke(subject, args);
// 在代理真实对象后我们也可以添加一些自己的操作
System.out.println("after sell ticket");
return null;
}
}
- 4.2.1.2客户买票
public class TestBuyTicketClient{
public static void main(String[] args){
// 我们要代理的真实对象
TicketCenter railwayStation = new RailwayStationSeller();
// 我们要代理哪个真实对象,就将该对象传进去,最后是通过该真实对象来调用其方法
InvocationHandler handler = new ProxyInvocationHandler(railwayStation);
/*
* 通过Proxy的newProxyInstance方法来创建我们的代理对象,我们来看看其三个参数
* 第一个参数 handler.getClass().getClassLoader() ,我们这里使用handler这个类的ClassLoader对象来加载我们的代理对象
* 第二个参数realSubject.getClass().getInterfaces(),我们这里为代理对象提供的接口是真实对象所实行的接口,表示我要代理的是该真实对象,这样我就能调用这组接口中的方法了
* 第三个参数handler, 我们这里将这个代理对象关联到了上方的 InvocationHandler 这个对象上
*/
TicketCenter proxy = (TicketCenter)Proxy.newProxyInstance(railwayStation.getClass().getClassLoader(), railwayStation
.getClass().getInterfaces(), handler);
//打印代理类名
System.out.println(railwayStation.getClass().getName());
//代理售票
proxy.sell();
}
}
4.2.2 初探InvocationHandler
直译:调用处理器
意思是,通过代理对象调用委托类(原生类)的方法时,作为监控和管理类。具体实现则是在InvocationHandler自定的子类中。
4.2.3 Proxy
上面 System.out.println(railwayStation.getClass().getName());打印结果:
4.2.4 代理类$Proxy0
$Proxy0 继承了Proxy ,并实现了railwayStation.getClass().getInterfaces()所有的接口,并重写了hashCode()和equals()方法。
它长这样:
public final class $Proxy0 extends Proxy implements TicketCenter{
private static Method m1;
private static Method m3;
private static Method m0;
private static Method m2;
public $Proxy0(InvocationHandler paramInvocationHandler) {
super(paramInvocationHandler);
}
public final boolean equals(Object paramObject){
return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue();
}
public final String sell(){
return (String)this.h.invoke(this, m3, null);
}
public final int hashCode() {
return ((Integer)this.h.invoke(this, m0, null)).intValue();
}
public final String toString(){
return (String)this.h.invoke(this, m2, null);
}
static{
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
m3 = Class.forName("$TicketCenter").getMethod("sell", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
}
}
顺便解释下这段话
TicketCenter proxy = (TicketCenter)Proxy.newProxyInstance(railwayStation.getClass().getClassLoader(), railwayStation
.getClass().getInterfaces(), handler);
因为Proxy.newProxyInstance产生的代理类 $Proxy0,实现了railwayStation的所有接口,那么肯定可以强转为任意一个接口类型,TicketCenter就是railwayStation的一个接口,所以强转成功。
4.2.5 再探InvocationHandler
看$Proxy0 实现的接口方法
public final String sell(){
return (String)this.h.invoke(this, m3, null);
}
this指代理类对象自己,m3则是原生对象的方法,恍然大悟,动态代理其实内部逻辑还是静态代理。代理类的方法浓缩到了handler的invoke方法中:增加原生方法执行前后的操纵+执行原生方法。静态代理初始化的原生对象改为有handler持有。
所以动态的好处为,自动动态生成代理类,而不需要我们提前编写,在编译器确认,而是在运行时生成。这样做的好处,减少了程序员的开发量,但实际永久带中的class信息并没有减少。
4.2.2 动态代理的局限性
动态代理的局限性也是静态代理的局限性,那就是必须依赖接口,即只能针对接口的方法进行扩展和额外的动作。
4.3 CGLIB
4.3.2 AOP
4.3.2.1 AOP来历
在面向对象编程语言,程序都是采用继承,接口,层层实现而来。这是一种“纵向”的关系,而一些“横向”的操作在面向对象世界中则提现得很少。
AOP:Aspect Oriented Programming, 这是利用一些技术手段实现“横向”模块之间相同的功能,或者增强功能。
4.3.2.1 AOP的梗
和每个程序语言都有一个HelloWorld梗一样,AOP的梗便是著名的 log 例子,当然还有其他很多通用的梗:权限。
4.3.3 ASM
4.3.4 严格模式
4.3.4.1 Javascript的严格模式
在ECMAScript6增加严格模式,"use strict"; 将启用严格检查,一些错误的,模棱两可的旧的语法,将在严格模式下报错。它是Javascript更合理、更安全、更严谨的发展方向,也让前端程序员变得更优秀。
4.3.4.2 class文件的严格模式
Java 源文件经过 javac 编译器编译之后,将会生成对应的二进制文件。每个合法的 Java 类文件都具备精确的定义,而正是这种精确的定义,才使得 Java 虚拟机得以正确读取和解释所有的 Java 类文件。
VS
4.3.4 CGLIB
4.3.5 Spring王国
4.4 动态代理填坑
4.4.1 永久代内存溢出
调整JVM的PermSize和MaxPermSize参数
4.4.2 Spring动态代理填坑
ClassCastException: $Proxy0 cannot be cast to...
Spring AOP部分使用JDK动态代理或者CGLIB来为目标对象创建代理对象,如果被代理的目标对象实现了至少一个接口,则会使用JDK动态代理,所有该目标类型实现的接口都将被代理。若该目标对象没有实现任何接口,则创建一个CGLIB代理。
使用BeanNameAutoProxyCreator来进行事务代理的话,它的proxyTargetClass这个属性设置为false(默认是false),会使用JDK动态代理,如果你的service类没有实现接口的话,就会报类型转换错误。
解决办法有:
1、给service类添加一个接口,让service类实现它,则创建代理类时使用JDK动态代理就不会出现问题
2、设置beanNameAutoProxyCreator的proxyTargetClass属性为true,意思是强制使用CGLIB代理
4.4.3 动态代理增强功能填坑
在创建动态代理时,
public static Object newProxyInstance(ClassLoader loader,Class>[] interfaces,InvocationHandler h)
如果interfaces传参数railwayStation.getClass().getInterfaces(),可能为抛类型转换异常,最安全的做法是:
直接使用需要转换代理类的接口类型,比如,我要讲代理类强转为TicketCenter,interfaces传参数可以修改为:
new Class[] { TicketCenter.class }