【Mybatis进阶】动态代理

如果你想更深刻的理解Mybatis动态代理的原理,那么你应该先知道

  • 什么是代理模式?
  • 在没有动态代理的时候Mybatis是如何实现dao层的?

什么是代理模式

具体可以阅读笔者的博客—— 代理模式

在没有动态代理的时候Mybatis是如何实现dao层的

本篇博客基于mybatis的环境已经搭建完成,如果不知道如何搭建,具体可以阅读笔者的博客——【从零开始学Mybatis笔记(三)】Dao开发方法

  1. 接口
public interface UserMapper {
    
    List<User> findAll();
}
  1. xml
    <select id="findAll" resultType="User">
         select *
         from user
    </select>
  1. 实现类
public class UserMapperImpl implements UserMapper {

    private SqlSessionFactory factory;

    public UserMapperImpl(SqlSessionFactory factory) {
        this.factory = factory;
    }

    @Override
    public List<User> findAll() {
        //1.获取sqlSession对象
        SqlSession sqlSession = factory.openSession();
        //2.调用selectList方法
        List<User> list = sqlSession.selectList("com.tyust.dao.UserMapper.findAll");
        //3.关闭流
        sqlSession.close();
        return list;
    }
}

到这里我们知道,Mybatis实现的关键就在于List<User> list = sqlSession.selectList("com.example.dao.UserDao.findAll")中通过sqlSession来实现对相应方法的操作。其实动态代理的最终结果也是使用sqlSession的相应方法。

Mybatis动态代理的实现原理

    @Override
    public List<User> findAll() {
        //1.获取sqlSession对象
        SqlSession sqlSession = factory.openSession();
        //2.调用selectList方法
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        List<User> list = mapper.findAll();
        //3.关闭流
        sqlSession.close();
        return list;
    }

和Dao操作一样,只不过使用了getMapper()来获得一个代理类,进行所有的操作。所以这就是我们关注的关键对象。

在单步调试之前,我们需要知道动态代理中最重要的类:SqlSession、MapperProxy、MapperMethod。

好了我们开始吧

  1. 进入UserMapper mapper = sqlSession.getMapper(UserMapper.class);

因为SqlSesseion为接口,所以我们通过Debug方式发现这里使用的实现类为DefaultSqlSession。

@Override
  public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
  }

找到DeaultSqlSession中的getMapper方法,发现这里没有做其他的动作,只是将工作继续抛到了Configuration类中,Configuration为类不是接口,可以直接进入该类的getMapper方法中。

  1. 进入configuration.<T>getMapper(type, this)
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }

Configuration是MyBatis初始化后全局唯一的配置对象,它内部保存着配置文件解析过程中所有的配置信息。找到Configuration类的getMapper方法,这里也是将工作继续交到MapperRegistry的getMapper的方法中,所以我们继续向下进行。

  1. 进入mapperRegistry.getMapper(type, sqlSession)
  @SuppressWarnings("unchecked")
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

MapperRegistry是Mapper接口动态代理工厂类的注册中心。找到MapperRegistry的getMapper的方法,看到这里发现和以前不一样了。

首先Type就是我们的接口UserMapper;

mapperProxyFactory一个HashMap,private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();; 通过 (MapperProxyFactory<T>) knownMappers.get(type);我们就获得了MapperProxyFactory。
通过MapperProxyFactory的命名方式我们知道这里将通过这个工厂生成我们所关注的MapperProxy的代理类,然后我们通过mapperProxyFactory.newInstance(sqlSession);进入MapperProxyFactory的newInstance方法中。

  1. 进入mapperProxyFactory.newInstance(sqlSession);
  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

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

找到MapperProxyFactory的newIntance方法,通过参数类型SqlSession可以得知,上面的调用先进入第二个newInstance方法中并创建我们所需要重点关注的MapperProxy对象,第二个方法中再调用第一个newInstance方法并将MapperProxy对象传入进去,根据该对象创建代理类并返回。看到Proxy.newProxyInstance,我们应该就知道这是JDK的动态代理api,这里已经得到需要的代理类了,但是我们的代理类所做的工作还得继续向下看MapperProxy类。

  1. MapperProxy类

找到MapperProxy类,发现其确实实现了JDK动态代理必须实现的接口InvocationHandler,所以我们重点关注invoke()方法。

@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 {
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

method就是我们的findAll()方法,而proxy就是之前通过newInstance生成的代理类。

getDeclaringClass()方法返回表示声明由此Method对象表示的方法的类的Class对象,说白了就是UserMapper。这里的判断就是方法是不是代理本身的方法,如果是,直接调用方法。如果不是,通过缓存方法生成一个对象再调用。具体是什么对象呢,我们看cachedInvoker方法。

  1. MapperMethodInvoker

在看cachedInvoker方法之前,我们先了解一下,什么是MapperMethodInvoker。

  interface MapperMethodInvoker {
    Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable;
  }

  private static class PlainMethodInvoker implements MapperMethodInvoker {
    private final MapperMethod mapperMethod;

    public PlainMethodInvoker(MapperMethod mapperMethod) {
      super();
      this.mapperMethod = mapperMethod;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
      return mapperMethod.execute(sqlSession, args);
    }
  }

  private static class DefaultMethodInvoker implements MapperMethodInvoker {
    private final MethodHandle methodHandle;

    public DefaultMethodInvoker(MethodHandle methodHandle) {
      super();
      this.methodHandle = methodHandle;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
      return methodHandle.bindTo(proxy).invokeWithArguments(args);
    }
  }
}

我们可以看到MapperMethodInvoker 是MapperProxy的一个内部接口,有两个实现类DefaultMethodInvoker和PlainMethodInvoker,二者都有一个构造函数和一个invoke方法,区别就在于构造函数封装的实例和invoke方法的逻辑。

PlainMethodInvoker类:是Mapper接口普通方法的调用类,它实现了MethodInvoker接口。其内部封装了MapperMethod实例。
DefaultMethodInvoker类:MethodInvoker接口的默认方法实现类。其内部封装了MethodHandle 实例。

MapperMethod:封装了Mapper接口中对应方法的信息,以及对应的SQL语句的信息;它是mapper接口与映射配置文件中SQL语句的桥梁。

Mapper接口中的每一个方法都对应一个MapperMethodInvoker对象,而MapperMethodInvoker对象里面的MapperMethod保存着对应的SQL信息和返回类型以完成SQL调用。

  1. 进入cachedInvoker方法
  private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
      // A workaround for https://bugs.openjdk.java.net/browse/JDK-8161372
      // It should be removed once the fix is backported to Java 8 or
      // MyBatis drops Java 8 support. See gh-1929
      MapperMethodInvoker invoker = methodCache.get(method);
      if (invoker != null) {
        return invoker;
      }

      return methodCache.computeIfAbsent(method, m -> {
        if (m.isDefault()) {
          try {
            if (privateLookupInMethod == null) {
              return new DefaultMethodInvoker(getMethodHandleJava8(method));
            } else {
              return new DefaultMethodInvoker(getMethodHandleJava9(method));
            }
          } catch (IllegalAccessException | InstantiationException | InvocationTargetException
              | NoSuchMethodException e) {
            throw new RuntimeException(e);
          }
        } else {
          return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
        }
      });
    } catch (RuntimeException re) {
      Throwable cause = re.getCause();
      throw cause == null ? re : cause;
    }
  }

根据被调用接口方法的Method对象,从缓存中获取MapperMethodInvoker对象,如果没有则创建一个并放入缓存。然后会判断用户当前调用的是否是接口的default方法,如果不是就会创建一个PlainMethodInvoker对象并返回。

搞懂了cacheInvoker,我们再看 return cachedInvoker(method).invoke(proxy, method, args, sqlSession);,可以发现当cacheInvoker返回了PalinMethodInvoker实例之后,紧接着调用了这个实例的PlainMethodInvoker::invoke方法。进入PlainMethodInvoker::invoke方法我们发现它底层调用的是MapperMethod::execute方法

  1. 进入mapperMethod.execute(sqlSession, args)
  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        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 if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        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;
  }

MapperMethod类是整个代理机制的核心类,对SqlSession中的操作进行了封装使用。 该类里有两个内部类SqlCommand和MethodSignature。 SqlCommand用来封装CRUD操作,也就是我们在xml中配置的操作的节点。每个节点都会生成一个MappedStatement类。MethodSignature用来封装方法的参数以及返回类型,在execute的方法中我们发现在这里又回到了SqlSession中的接口调用,和我们自己实现UerDao接口的方式中直接用SqlSession对象调用DefaultSqlSession的实现类的方法是一样的,经过一大圈的代理又回到了原地,这就是整个动态代理的实现过程了。

参考文献

Mybatis mapper动态代理的原理详解
https://blog.csdn.net/tianjindong0804/article/details/105862940
手把手带你阅读Mybatis源码(一)构造篇
手把手带你阅读Mybatis源码(二)执行篇

posted @ 2021-01-07 10:44  朱李洛克  阅读(208)  评论(0编辑  收藏  举报
// 侧边栏目录 // https://blog-static.cnblogs.com/files/douzujun/marvin.nav.my1502.css