认识代理模式-终极篇

通过学习本篇博文,你将彻底明白代理模式在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静态代理

代理类和委托类都实现一个接口


认识代理模式-终极篇_第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, 这是利用一些技术手段实现“横向”模块之间相同的功能,或者增强功能。


认识代理模式-终极篇_第2张图片
面向切面,面向方面,也叫刀削面。

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 类文件。


认识代理模式-终极篇_第3张图片
类文件1

VS


认识代理模式-终极篇_第4张图片
类文件2

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 }

你可能感兴趣的:(认识代理模式-终极篇)