设计模式——代理模式(以及动态代理在mybatis中的应用)

一、代理模式

代理模式(Proxy)是java中的一种常见的设计模式,他提供了一种通过代理对象来访问目标对象的方法,可以在不修改目标对象源码的情况下对目标对象的方法进行修改和加强,符合设计模式中的开闭原则,即对拓展开放、对修改关闭。

以卖手机为例,厂商将手机生产出来就是为了将手机销售出去,传统的买手机的模式是厂商将手机下发给一级代理商,然后一级代理商再将手机一级一级下发,最后由商场里的某个基层手机代理商将手机销售给最终的用户。在这个手机从厂商到消费者手中的过程就类似于设计模式中的代理模式,消费者通过手机代理商间接地购买手机厂商的手机,而业务方通过代理对象来间接地调用目标对象的方法。在代理的过程中,手机代理商也会为消费者提供一些额外的服务,比如说贴个手机膜,送个充电宝以及维修手机等等,这也是我们需要代理模式的重要原因之一,就是我们可以通过代理对象来增强目标对象的功能,比如说可以通过代理对象来给目标对象的方法前后增加关键的日志,可以在调用目标方法之前对入参进行参数校验等等。

二、java中的三种代理模式

代理模式主要分为静态代理和动态代理,什么是静态代理呢,其实这里的静态、动态可以理解为代理对象的状态。所谓静态代理是指代理对象由开发人员实现定义好,在程序运行中可以直接获取代理对象的源码编译执行。动态代理没有事先编写目标对象的代理类,而是在程序执行的过程中,根据用户的定义的增强规则来"动态"地生成目标对象的代理对象。而动态代理又分为面向接口的jdk动态代理和面向类的Cglib动态代理。

2.1静态代理

静态代理的一般模式是,代理对象和目标对象实现同一个接口,然后代理对象持有目标对象的引用,并在方法中对目标方法进行相应的增强操作。下面我们就通过玩具代码来看一下静态代理的实现,首先我们定义一个手机销售的接口PhoneSeller,这个接口有一个方法sell,其中有两个参数分别是手机款式和价格:

/**
 * @author meichen
 * @since 2019/4/12 15:23
 */
public interface PhoneSeller {

    boolean sell(String phoneType,int price);
}

然后我们定义一个华为官方商店HuaWeiSeller,这个对象简单检查一下消费者想要购买的手机款式有没有,然后核对一下价格就把手机卖给消费者:

/**
 * @author meichen
 * @since 2019/4/12 15:29
 */
public class HuaWeiSeller implements PhoneSeller{

    @Override
    public boolean sell(String phoneType, int price) {
        if(!phoneType.startsWith("HuaWei") && !phoneType.startsWith("Honor")){
            System.out.println("对不起,本店没有您想要买的手机型号,请去隔壁小米或者OV专卖店购买");
            return false;
        }
        if(price <= 1000){
            System.out.println("对不起,本店不卖廉价手机");
            return false;
        }
        System.out.println("感谢您购买" + phoneType + "型号手机,一共收您" + price + "元!");
        return true;
    }
}

由于华为手机非常的畅销,利润也比较高,这个时候有人在杭州滨江区开了一家华为手机代理店HZBJHuaWeiProxy,为了提供竞争力,区别于华为官方的手机店,代理店决定用更好的服务来吸引顾客,凡是来店里看手机的,不管买不买先给顾客上一杯

茶,如果顾客购买了华为手机,不论型号均免费贴膜,送小米充电宝:

/**
 * @author meichen
 * @since 2019/4/12 15:41
 */
public class HZBJHuaWeiProxy implements PhoneSeller{
    
    private HuaWeiSeller huaWeiSeller;

    public HZBJHuaWeiProxy(HuaWeiSeller huaWeiSeller) {
        this.huaWeiSeller = huaWeiSeller;
    }

    @Override
    public boolean sell(String phoneType, int price) {
        System.out.println("尊敬的消费者,请边喝茶边看手机,不买也没关系。");
        if(!huaWeiSeller.sell(phoneType,price)){
            return false;
        }
        System.out.println("尊敬的消费者,手机已经帮您贴好膜了,再送您一个小米充电宝,欢迎下次再来光临!");
        return true;
    }
}

最后我们来看下测试类:

/**
 * @author meichen
 * @since 2019/4/12 15:56
 */
public class SellerMain {

    public static void main(String[] args){
        HZBJHuaWeiProxy hzbjHuaWeiProxy = new HZBJHuaWeiProxy(new HuaWeiSeller());
        hzbjHuaWeiProxy.sell("HuaWei Meta20 pro",5999);
    }
}

下面是购买的过程,看,是不是服务更加贴心了呢(笑:

尊敬的消费者,请边喝茶边看手机,不买也没关系。
感谢您购买HuaWei Meta20 pro型号手机,一共收您5999元!
尊敬的消费者,手机已经帮您贴好膜了,再送您一个小米充电宝,欢迎下次再来光临!

Process finished with exit code 0

静态代理优点:简单、清晰易理解

静态代理缺点:维护起来比较麻烦,如果目标对象中增加了新的方法,代理对象也要跟着一起维护,维护起来非常的麻烦。

2.2 jdk动态代理

动态代理的底层原理不是本文关注的重点,我们将忽略底层的实现把重点放在如何使用jdk动态代理的功能。jdk动态代理的核心是InvocationHandler接口:

public interface InvocationHandler {

    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

我们需要通过实现InvocationHandler接口来完成对代理对象方法的调用和增强。除了这个接口以外,还有一个重要的方法是:

public static Object newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h)

通过这个方法可以创建目标对象的代理对象。这个方法是Proxy类中的静态方法,一共有三个参数,分别是:

  • ClassLoader loader:目标对象实现接口的类加载器
  • Class[] interfaces:目标对象实现的接口类型
  • InvocationHandler h:方法的改造处理器,目标对象的方法都会传入该处理器中进行增强调用

下面我们以一个小demo来展示一下如何实现jdk的动态代理,依旧是以华为代理商为例,PhoneSeller和HuaWeiSeller同上不做改变,我们需要创建一个InvocationHandler的实现类:

/**
 * @author meichen
 * @since 2019/4/12 16:18
 */
public class PhoneSellerHandler implements InvocationHandler {

    private PhoneSeller phoneSeller;

    public PhoneSellerHandler(PhoneSeller phoneSeller) {
        this.phoneSeller = phoneSeller;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("尊敬的消费者,请边喝茶边看手机,不买也没关系。");
        if(!(Boolean) method.invoke(phoneSeller,args)){
            return false;
        }
        System.out.println("尊敬的消费者,手机已经帮您贴好膜了,再送您一个小米充电宝,欢迎下次再来光临!");
        return true;
    }
}

从上面的代码中我们可以看出,作为构造函数参数被传入的是目标对象,在invoke中通过反射来调用目标函数中的方法,并在前后增加额外的服务。然后我们来看一下jdk动态代理对象的创建:

/**
 * @author meichen
 * @since 2019/4/12 15:56
 */
public class SellerMain {

    public static void main(String[] args){
        PhoneSeller huaweiProxy = (PhoneSeller) Proxy.newProxyInstance(PhoneSeller.class.getClassLoader(),new Class[]{PhoneSeller.class},
                new PhoneSellerHandler(new HuaWeiSeller()));
        huaweiProxy.sell("HuaWei P30",3999);
    }
}

下面是动态对象的执行结果,我们可以看出动态代理是可以实现和静态代理一样的功能:

尊敬的消费者,请边喝茶边看手机,不买也没关系。
感谢您购买HuaWei P30型号手机,一共收您3999元!
尊敬的消费者,手机已经帮您贴好膜了,再送您一个小米充电宝,欢迎下次再来光临!

Process finished with exit code 0

jdk动态代理是对接口的代理,要求被代理的目标对象必须实现一个接口,而如果要对一个完全没有实现接口的单独的对象进行代理,jdk动态代理就无能为力了,不过我们还可以通过另一种方法来实现类的代理,这种方法就是下一节要讲的Cglib代理。

2.3 Cglib代理

Cglib是一个功能强大、性能高效的代码生成库,他被广泛应用于各种基于代理的框架中,例如AOP、Hibernate等。与jdk动态代理相比,Cglib更加灵活,因为它生成代理对象时不需要目标对象去实现一个公共的接口,可以直接实现对类的代理。Cglib底层是通过asm字节码生成框架来生成代理类的字节码,并最终在java虚拟机生成代理对象的,对cglib感兴趣的同学可以自行查阅相关的资料,cglib的实现不在本文的范围之内。

Cglib中两个重要的接口和类是MethodInterceptor接口和Enhancer类,我们通过实现MethodInterceptor接口来定义增强代理对象的功能,通过Enhancer类来生成代理类的二进制字节码,并通过Enhancer内部的Class.forName方法加载二进制字节码生成Class对象,最终通过反射机制来构造并初始化代理对象。

下面我们以类似的代理手机店为例,首先我们需要引入Cglib,在maven中添加如下的依赖


     cglib
     cglib
     3.1

然后定义一个没有实现接口的目标类小米官方商店MiSeller

/**
 * @author meichen
 * @since 2019/4/18 17:09
 */
public class MiSeller {

    public boolean sell(String phoneType, int price) {
        if(!phoneType.startsWith("Mi") && !phoneType.startsWith("RedMi")){
            System.out.println("对不起,本店没有您想要买的手机型号,请去隔壁华为或者OV专卖店购买");
            return false;
        }
        System.out.println("感谢您购买" + phoneType + "型号手机,一共收您" + price + "元!");
        return true;
    }
}

然后我们需要定义如何增强目标对象的方法,这需要我们去实现MethodInterceptor这个接口

/**
 * @author meichen
 * @since 2019/4/18 17:14
 */
public class MiProxyInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("店里响起了名曲:Are you ok?");
        boolean suc = (Boolean) methodProxy.invokeSuper(o,objects);
        if(suc){
            System.out.println("尊敬的消费者,手机已经帮您贴好膜了,欢迎多来店里体验体验其他产品");
            return true;
        }
        return false;
    }
}

有了目标类和增强目标类的拦截方法,我们就可以通过Enhancer来动态地生成代理对象了

/**
 * @author meichen
 * @since 2019/4/18 17:18
 */
public class MiSellerMain {

    public static void main(String[] args){
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(MiSeller.class);
        enhancer.setCallback(new MiProxyInterceptor());
        MiSeller hzbjMiProxy = (MiSeller)enhancer.create();
        hzbjMiProxy.sell("Mi 9",2999);
    }
}

最后我们可以看一下执行结果,看是否可以实现和jdk动态代理类似的功能

店里响起了名曲:Are you ok?
感谢您购买Mi 9型号手机,一共收您2999元!
尊敬的消费者,手机已经帮您贴好膜了,欢迎多来店里体验体验其他产品

Process finished with exit code 0

Cglib相比jdk动态代理可以实现直接对类的代理,不过它也有一些缺点,例如目标类的方法如果是final方法或者目标类是final类,则无法被代理(因为Cglib生成的代理对象的二进制字节码的反编译后的类,实际上是继承了目标对象,而final方法是无法被继承的)。

三、动态代理在Mybatis中的应用

在上面简单介绍了动态代理的使用之后,我们来看一个动态代理在实际项目中的应用,本节主要介绍jdk动态代理在mybatis中的应用。MyBatis 是一种应用非常广泛的持久层框架,它 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

如果使用过Mybatis,我们就会发现Mybatis的使用非常简单,首先定义一个dao接口,然后编写一个与dao接口的对应的配置文件,java对象与数据库字段的映射关系和dao接口对应的sql语句都是以配置的形式写在配置文件中,非常的简单清晰。但是笔者在使用的过程中就曾经有过这样的疑问,dao接口是怎么和mapper文件映射起来的呢?只有一个dao接口又是怎么以对象的形式来实现数据库的读写操作呢?相信有疑问的肯定不止我一个人,当然,在看了上面两节之后,应该很容易猜到可以通过代理模式来动态的创建dao接口的代理对象,并通过这个代理对象来实现数据库的操作。

我们首先来看一下MapperProxyFactory这个类

public class MapperProxyFactory {

  private final Class mapperInterface;
  private Map methodCache = new ConcurrentHashMap();

  public MapperProxyFactory(Class mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  public Class getMapperInterface() {
    return mapperInterface;
  }

  public Map getMethodCache() {
    return methodCache;
  }

  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy mapperProxy = new MapperProxy(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

}

这个类的代码不多,主要看它的构造函数和newInstance方法就可以了,构造方法传入了一个Class类,通过名字mapperInterface我们可以很容易地猜到这个类就是dao接口。而newInstance先创建了一个MapperProxy类,然后通过Proxy.newProxyInstance方法创建了一个对象并返回。诶!看到了熟悉的东西,在2.2节中我们曾通过这个方法来动态创建代理对象,这里显然也是通过同样的方法返回了mapperInterface接口的代理对象,而上面提到的MapperProxy类显然是InvocationHandler接口的实现。所以MapperProxyFactory类就是一个创建代理对象的工厂类,它通过构造函数传入我们自定义的dao接口,并通过newInstance方法返回dao接口的代理对象。

看到这里有了一种豁然开朗的感觉,但同时新的疑问又来了,我们定义的dao接口的方法并没有实现啊,那这个代理对象又是如何来实现增删改查的呢?带着这个疑问,我们来看一下MapperProxy类,看看它是怎么来改造增强我们的接口方法的

public class MapperProxy implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;
  private final Class mapperInterface;
  private final Map methodCache;

//...忽略构造函数
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
      try {
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

  private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }

}

这个类主要重写了invoke方法,来实现接口方法的增强,这个方法我们只要看最后两行就可以了,前面的Object.class.equals(method.getDeclaringClass())主要是为了让代理对象可以实现一些Object类的公共方法,所有我们自定义的接口方法都只会执行invoke方法的最后两行。

首先我们来看一下MapperProxy的三个成员变量:

第一个成员变量是SqlSession,这个变量通过名字可以猜出它是一个定义了执行sql的接口,我们简单看一下它的接口定义

public interface SqlSession extends Closeable {
 
   T selectOne(String statement);

   T selectOne(String statement, Object parameter);

  //下面省略.....
}

这个接口方法的入参是statement和参数,返回值是数据对象,这里的statement有些人可能会误解为是sql语句(笔者最初也是这么认为的),但其实这里的statement是指dao接口方法的名称,我们自定义的sql语句都缓存在Configuration对象中,在sqlSession中可以通过dao接口的方法名称找到对应的sql语句。因此我们可以想到代理对象本质上就是将要执行的方法名称和参数传入SqlSession的对应方法中,根据方法名找到对应的sql语句并替换参数,最后得到返回的结果。

第二个成员变量是mapperInterface,它的作用要结合第三个成员变量来说明。

第三个成员变量是methodCache,它是一个map型的结构,key是Method,value是MapperMethod。

接下来我们回到invoke方法的最后两行,它首先通过cachedMapperMethod方法找到与要执行的dao接口方法对应的MapperMethod,然后调用MapperMethod的execute方法来实现数据库的操作,这里显然是将sqlSession传入到MapperMethod内部,并在MapperMethod的内部将要执行的方法名和参数再传入sqlSession对应的方法中去执行。

最后我们来看一下MapperMethod类的内部,看看它具体是怎么完成sql的执行的

public class MapperMethod {

  private final SqlCommand command;
  private final MethodSignature method;

}

这个类有两个成员变量,分别是SqlCommand和MethodSignature,虽然这两个类代码看起来很多,但实际上这两个内部类非常简单,SqlCommand主要解析了接口的方法名称和方法类型,方法名称类似于com.nju.dao.smsResultDao.insert这种形式,而方法类型是一个枚举类,主要定义了诸如INSERT、SELECT、DELETE等数据库操作的类型。MethodSignature则是解析了接口方法的签名,即接口方法的参数名称和参数值的映射关系,即通过MethodSignature类可以将入参的值转换成参数名称和参数值的映射,这里就不具体分析SqlCommand和MethodSignature的具体实现了,感兴趣的同学可以自行阅读。

最后我们来看一下MapperMethod类中最重要的execute方法

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    if (SqlCommandType.INSERT == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
    } else if (SqlCommandType.UPDATE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
    } else if (SqlCommandType.DELETE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
    } else if (SqlCommandType.SELECT == command.getType()) {
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        result = executeForMap(sqlSession, args);
      } else {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
      }
    } else {
      throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName() 
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

通过上面对SqlCommand和MethodSignature的简单分析,我们很容易理解这段代码,首先它根据SqlCommand中解析出来的方法类型来选择对应的SqlSession中的方法,即如果是INSERT类型的,就选择SqlSession.insert方法来执行数据库操作。其次,它通过MethodSignature将参数值转换为Map的映射,Key是方法的参数名称,Value是参数的值,最后将方法名和方法参数传入对应的SqlSession的方法中执行。至于我们在配置文件中定义的sql语句,则是缓存在了SqlSession的成员变量Configuration中

设计模式——代理模式(以及动态代理在mybatis中的应用)_第1张图片

在Configuration中有着非常多的参数,其中有一个参数是mappedStatements,这里面保存了我们在配置文件中定义的所有方法,我们可以点开其中的一个方法,查看mappedStatement的内部结构

设计模式——代理模式(以及动态代理在mybatis中的应用)_第2张图片

里面保存了我们在配置文件中定义的各种参数,包括sql语句。到这里,我们应该对mybatis中如何通过将配置与dao接口映射起来,如何通过代理模式生成代理对象来执行数据库读写操作有了较为宏观的认识,至于sqlSession中如果将参数与sql语句结合,组装成完整的sql语句,以及如何将数据库字段与java对象映射,这些内容不在本文的范围之内,感兴趣的同学可以自行阅读相关的源码。

四、小结

本文主要简单介绍了设计模式中的代理模式,在第二节中详细给出了三种代理模式的样例,并在第三节中较详细地介绍了jdk动态代理方法在mybatis中的应用。除此之外,动态代理在spring aop、hibernate等应用中发挥着非常重要的作用。

你可能感兴趣的:(mybatis,设计模式)