2.2Mybatis——代理与SQL映射

合集总览:Mybatis框架梳理   


“Java中非静态方法的运行需要实例对象才能运行(即对象点方法),Mybatis中的Mapper都是接口,也没有实现类,那么接口中的方法怎么就被调用执行了呢?”

这是当时用Mybatis时最困扰我的一个问题,搜的资料博客中,大多来一句“动态代理”一笔带过。留我一人风中凌乱,难道"动态代理"四个字这么形象生动、易于理解吗…
也因为这个疑问,了解了代理模式,动态代理,以及Mybatis如何使用动态代理完成mapper接口方法的调用。

1.代理模式

这里简单说一下,例子就不举了,网上都是。说一下工作中的使用场景,一般是某个类无法修改或不敢改动,那就在目标类外面包一层代理类,即不用修改目标类,又可以对目标类做功能的增强。其原理就是:通过实现和目标类相同的接口来平替目标类,然后通过构造函数获取目标类的对象引用,增强的功能由代理类完成,核心的逻辑依旧通过目标类进行调用。(我记得好像写过对应的设计模式笔记,感兴趣的可以翻一下)

2.如何执行接口方法

前面提到的代理模式和文章开头的问题有关系吗?当然有关系,如果你理解了什么是代理,文章开头的问题就可以通过代理来实现。
即使接口没有实现类,也可以通过InvocationHandler为其创建代理类,通过代理类执行接口方法。这是java反射包下的一个接口,可以为接口生成代理对象。简单代码示例:

示例demo

// 定义接口
public interface Interface01 {
    String m1();
    int m2();
}
// 定义调用处理器
public class Interface01Handler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if(method.getName().equals("m1")) {
            return "hello world";
        }
        else  {
            return 1024;
        }
    }
}
// 创建代理对象测试
public static void main(String[] args) {
	// 生成代理对象
    Object proxy = Proxy.newProxyInstance(Interface01.class.getClassLoader(),
            new Class[]{Interface01.class}, new Interface01Handler());

    if(proxy instanceof Interface01) {
        // 调用接口方法
        Interface01 a = (Interface01) proxy;
        System.out.println(a.m1());
        System.out.println(a.m2());
    }
}

上述代码中,接口并没有实现类,但接口方法可以被调用。当然你也可以理解成:接口是以另一种形式被实现,接口方法以另一种形式被定义。这种在JVM运行期间动态的为接口创建代理对象的过程,我们称为JDK动态代理。

3.Mybatis是如何做的

通过反射可以为接口创建代理对象。知道了Java中的这个机制后,回到Mybatis的疑问,mapper也是接口,也没有实现类但最终其方法可以被调用,类比一下,你应该可以猜到Mybatis是如何来实现的。

3.1猜想

我们只定义了接口和方法,在未提供接口实现类的情况下却可以直接对接口方法进行调用。基于Mybatis的这个特性,再结合demo示例中Java反射提供的机制。于是我们猜想:

  1. Mybatis内部应该也是使用了JDK动态代理来执行mapper接口中的方法;
  2. 由于JDK动态代理中需要调用处理器去执行方法逻辑,我们虽然没有为mapper接口编写对应的调用处理器。但是我们为每个mapper接口定义了同名了mapper.xml,而且mapper.xml中的SQL片段其实就是这个方法的执行逻辑。所以Mybatis内部应该做了某些处理,调用处理器的invoke方法执行的应该就是对应的SQL片段:
// 调用处理器 InvocationHandler.invoke
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
	// <select id="">
	// <insert id="">
	// <update id="">
	// <delete id="">
}

上面是我们合理的猜想,如果得到印证,那么Mybatis内部使用动态代理的过程也就清晰了。接下来我们通过调试源码进行印证。

3.2源码探究

@SpringBootTest
public class SqlMappingTest {
    @Resource
    AddressMapper addressMapper;
    @Test
    public void test(){
        Address address = addressMapper.selectById(1L);
        System.out.println(address);
    }
}

以查询为例,我们都知道,spring容器为我们注入了一个代理对象addressMapper,代理对象类型为MapperProxy,刚好实现了InvocationHandler接口:

public class MapperProxy<T> implements InvocationHandler, Serializable {
	 @Override
	 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
	   try {
	     if (Object.class.equals(method.getDeclaringClass())) {
	       return method.invoke(this, args);
	     } else if (method.isDefault()) {
	       return invokeDefaultMethod(proxy, method, args);
	     }
	   } catch (Throwable t) {
	     throw ExceptionUtil.unwrapThrowable(t);
	   }
	   final MapperMethod mapperMethod = cachedMapperMethod(method);
	   return mapperMethod.execute(sqlSession, args);
	 }
}

再看一下invoke的执行逻辑是否是调用mapper.xml中的SQL片段,以及它是如何进行调用:

public class MapperMethod {

  private final SqlCommand command;
  private final MethodSignature method;

  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    ...
  }

  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        ...
        break;
      }
      case UPDATE: {
        ...
        break;
      }
      case DELETE: {
        ...
        break;
      }
      case SELECT:
        ...
        break;
      case FLUSH:
        ...
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    return result;
  }
}

上述代码中可以看到:
mapper.xml中的SQL片段最终被加载成MapperMethod对象,InvocationHandler.invoke的调用是执行MapperMehtod.execute方法。execute方法就是将SQL分成CRUD几个类型,然后根据当前MapperMehtod的类型进行SQL的执行,显然后面还有SQL参数、结果集的处理等逻辑,但我们的猜想到这里其实就差不多可以被印证了。

扩展:虽然关于Mybatis中未实现的Mapper接口如何被调用执行 的猜想已经被印证,其过程和我们的猜想符合。但在过程中可能会引出其他的思考,比如:

  1. 代理对象是如何创建的;
  2. mapper.xml中的方法如何被转换为MapperMethod对象;
  3. execute方法中,确定SQL类型后,SQL的处理、结果集的处理等等

这些如果有精力的话,后面会陆续展开描述。

梳理与总结

  1. JDK的动态代理是通过实现InvocationHandler接口的形式来实现的。即使接口没有定义实现类,也可以通过JDK动态代理的方式为接口创建代理对象。就接口实现而言,你确实可以把InvocationHandler的这种机制看成是另一种接口实现,所以,既然可以直接去实现接口,为什么还要多此一举去使用InvocationHandler呢?如果你有此疑问,Mybatis给出了很好的答案:接口一定需要显式定义实现类吗?接口和实现可以解耦,定义的接口不再局限于通过接口实现类去实现,可以根据需要以其他方式(mapper.xml)去实现接口;
  2. Mybatis中,MapperProxy通过实现InvocationHandler接口为mapper接口创建代理对象 ;
  3. 根据InvocationHandler的调用机制,对mapper接口中方法的调用,最终都将交给调用处理器的invoke方法,Mybatis在该方法中完成了对mapper.xml SQL方法的实际调用

  • 如有理解错误或不足之处,欢迎大家留言讨论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值