mybatis 源码探究:两次JDK动态代理

mybatis中所有Dao接口的实现类都是MapperProxy


问题

问题描述:在使用mybatis时,我们只定义了接口,然后在XxxMapper.xml中写sql,若使用Mapper接口,甚至连XxxMapper.xml都可以省略,这一切究竟是如何实现的呢?


大多数项目都会使用spring+mybatis来搭建,本文参照mybatis-spring(1.2.0)分析上述问题。

将mybatis集成到spring中,添加配置spring-dao.xml
<!-- 数据库连接池信息 BoneCP configuration -->
    <bean id="dataSource"  class="com.jolbox.bonecp.BoneCPDataSource" destroy-method="close">
        <property name="driverClass" value="${jdbc.driverClass}" />
        <property name="jdbcUrl" value="${jdbc.jdbcUrl}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />
    </bean>
    <!-- 集成Mybatis -->
    <bean id="sessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
    </bean>
    <!--MyBatis的DAO 无需实现类配置 自动扫描实现mapper.xml文件方法-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.foo.dao" />
        <property name="sqlSessionFactoryBeanName" value="sessionFactory" />
    </bean>

上述配置文件为mybatis的入口,阅读源码首先要找到入口,不能漫无目的地在源码里闲逛。这里需要Spring的一点基础知识(例如FactoryBean的作用)。与Spring的集成,本质是是把自己扮成Spring的bean,然后Spring能感知到你的存在,从而对你进行管理。咱们可以看到,mybatis在这里所有的配置信息,都是在<bean>标签里的。
这三个bean中,咱们挨个分析。

  1. id为dataSource的bean,最简单。只是封装了一些数据库的连接(包括配置)信息在里面。
  2. id为sessionFactory的bean,定义了SqlSessionFactoryBean,将dataSource bean作当做了一个参数。暂且不管,继续。
  3. 最后一个bean,可以看到,没有定义id,说明没有人显示地引用它。其实它就是spring-mybatis的入口。参数basePackage 是我们的Dao接口包,用于后面接口扫描使用。后一个参数sqlSessionFactoryBeanName,只是简单地给他赋了值。

咱们现在已经知道spring-mybatis的入口了,开干吧。


image1.png

以下的分析内容,可以认为是在对image1的解释。
进入MapperScannerConfigurer,

BeanDefinitionRegistryPostProcessor, 
InitializingBean, 
ApplicationContextAware, 
BeanNameAware 

发现其实现了这四个接口。又是Spring的知识了,这些都是spring的扩展点,不多做介绍。重点看,BeanDefinitionRegistryPostProcessor。这个接口里只有一个方法postProcessBeanDefinitionRegistry,会在BeanDefinition加载完之后执行。进入该方法:

public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
   //ignore unrelated code
    scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }

忽略非重点代码。省略的代码,是在设置属性。重点看保留下来的最后一句代码。这句话的意思是扫描包。不知各位还记得spring-dao.xml中这么一句话:

<property name="basePackage" value="com.foo.dao" />

那么这里的scan(),扫描了com.foo.dao包下面的所有类,即咱们定义的DAO接口。根据scan()方法层层推进,来到了,org.mybatis.spring.mapper.ClassPathMapperScanner#doScan()。这个方法里有三句话比较重要:

① definition.getPropertyValues().add("mapperInterface", definition.getBeanClassName());
② definition.setBeanClass(MapperFactoryBean.class);
③ definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));

①:将我们自定义的DAO接口作为参数放入BeanDefinition中,属性名为mapperInterface;
②:设置BeanDefinition的BeanClass,MapperFactoryBean。这里对于文章开头“mybatis中所有Dao接口的实现类都是MapperProxy”的这句话,已初见端倪;
③:新增属性名为sqlSessionFactory的属性。

从这里可以看出两点:

  1. 咱们自定义的DAO接口,被新增了两个属性(mapperInterface与sqlSessionFactory),那么在创建bean的时候,会给这两个属性赋值,即调用这两个属性的set方法;
  2. DAO接口BeanDefinition的BeanClass是MapperFactoryBean。并且细心的小伙伴发现,这是一个工程bean(FactoryBean),在创建bean时,是从其getObject()方法获取真正的bean。

BeanDefinition加载完后,开始创建bean。


image2.png
开始image2的分析:

由于我们自定义DAO接口的BeanClasss是MapperFactoryBean,所以创建的是MapperFactoryBean,遂进入MapperFactoryBean源码。
上文咱们提到,会调用sqlSessionFactory的set方法(setSqlSessionFactory)。发现该方法在MapperFactoryBean的父类SqlSessionDaoSupport中,进入:

 public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
    if (!this.externalSqlSession) {
      this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
    }
  }

层层跟进,直至最底层构造方法:

public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
    notNull(executorType, "Property 'executorType' is required");

    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());
  }

此处咱们看到了jdk动态代理(SqlSessionInterceptor),这也是咱们image2中的标注的第一次动态代理。跟进:

private class SqlSessionInterceptor implements InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      final SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);
      try {
        Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
      }
    }

与数据库的操作都是通过SqlSession来完成的。这里咱们看到生成了sqlSession,然后根据具体情况,来判断是否自动提交。
setSqlSessionFactory()方法到这算基本讲完了,新建了一个SqlSessionTemplate,回到MapperFactoryBean。
由于MapperFactoryBean是FactoryBean的实现类,所以会调用其getObject()方法来获取真正的bean。进入该方法:

public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }

无实际内容,重点内容都在getMapper()里面。by the way,这里的mapperInterface,咱们在image1里有提到,若遗忘,往上翻。由于咱们刚才通过setSqlSessionFactory()方法,新建了一个SqlSessionTemplate,所以这里通过SqlSessionTemplate#getMapper()层层跟进,直至出现明显有特殊意义的代码,到了MapperRegistry#getMapper():

 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);
   }
 }

跟进mapperProxyFactory.newInstance(sqlSession):

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<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

这两个方法,其实又是一次动态代理(image2的第二次动态代理)。
回到getMapper(),最后返回了MapperProxy 对象。


image3.png

通过image2的分析,咱们知道,我们自定义的DAO接口,最后被封装成了MapperProxy。当我们在调用DAO接口里面的方法时,实际是调用了MapperProxy#invoke()。

进入MapperProxy的invoke()方法:

 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

这里通过动态代理获取到了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;
  }

这一大段代码基本都是比较熟悉的代码。根据SqlCommandType判断是增删改查的哪一个,然后调用SqlSession对应的方法去执行。请记住,这里的SqlSession已经被动态代理过了,实际上是调用了SqlSessionInterceptor#invoke()方法。


As always and let me know what you think, leave the comments below. Bye :)

项目完整源码github地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,593评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,080评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,025评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,317评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,329评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,036评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,639评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,557评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,089评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,197评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,330评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,006评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,689评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,189评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,313评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,676评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,339评论 2 358

推荐阅读更多精彩内容

  • 心理学家表示,微笑是人类最好的表情、最美的无声语言。微笑代表着良好的心态,是全社会最好的通行证。 请记住,如果您对...
    圣雅妃妮阅读 267评论 0 1
  • 1. 模仿湖南儿歌《月亮粑粑》写一段荒诞不经然而押韵的文字(不用一韵到底,可以几句一变化): 风儿飘飘,扇子摇摇,...
    尘世一眼阅读 124评论 1 0
  • 我看着妈妈, 想象着我老了的样子。 花白的头发,浑浊的眼睛, 有很多故事的抬头纹, 还有那被时间刻上烙印的老茧。 ...
    橘子小姐酱阅读 178评论 3 0