Mybatis源码研读(二)—— 从接口到SQL

从接口如何调用到SQL

上文中,我们了解了我们的存放在XML里面的SQL是如何被解析到Mybatis的框架中了。
但是我们实际的Spring项目中使用是定义的一个接口,然后与通过这个接口中的方法来调用的实际的SQL。那在这其中Mybatis又为我们做了哪写事儿呢。

修改项目为一个Spring项目

主要的修改:

  1. 增加SpringBoot 的主类
  2. Mapper.java中增加标签@Repository
  3. 增加application.yaml

主入口类:

package com.example.mybatis.mybatiddemo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.example.mybatis.mybatiddemo.mapper")
public class MybatiddemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(MybatiddemoApplication.class, args);
    }

}

Mapper类:

package com.example.mybatis.mybatiddemo.mapper;

import com.example.mybatis.mybatiddemo.module.User;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface UserMapper {

    User getUser(@Param("id") Integer id);

    User getUserName(@Param("id") Integer id);
}

application.yaml:

server:
  port: 8080

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapping/*Mapper.xml
  type-aliases-package: com.example.mybatis.mybatiddemo.module

#showSql
logging:
  level:
    com:
      example:
        mapper: debug

Controller类

package com.example.mybatis.mybatiddemo.controller;

import com.example.mybatis.mybatiddemo.mapper.UserMapper;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@RequestMapping("/user")
public class UserController {
    @Resource
    private UserMapper userMapper;

    @RequestMapping("")
    public Object userInfo() {
        return userMapper.getUser(1);
    }
}

启动这个demo,访问localhost:8080/user 可以获取到结果数据。

代码分析

在这个Demo中,在Application类中有一个标签

@MapperScan("com.example.mybatis.mybatiddemo.mapper")

以及Mapper中心增加的标签:

@Repository

MapperScan标签的处理类为 org.mybatis.spring.annotation.MapperScannerRegistrar, 这个类中生成 org.mybatis.spring.mapper.MapperScannerConfigurer 的Configuration处理类。

接着来看 org.mybatis.spring.mapper.MapperScannerConfigurer 这个类

public class MapperScannerConfigurer
    implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
  private String basePackage;
  private boolean addToConfig = true;
  private String lazyInitialization;
  private SqlSessionFactory sqlSessionFactory;
  private SqlSessionTemplate sqlSessionTemplate;
  private String sqlSessionFactoryBeanName;
  private String sqlSessionTemplateBeanName;
  private Class<? extends Annotation> annotationClass;
  private Class<?> markerInterface;
  private Class<? extends MapperFactoryBean> mapperFactoryBeanClass;
  private ApplicationContext applicationContext;
  private String beanName;
  private boolean processPropertyPlaceHolders;
  private BeanNameGenerator nameGenerator;

这个Configuration的类实现了几个接口,最为重要的是BeanDefinitionRegistryPostProcessor。它可以 动态的注册Bean信息,方法为postProcessBeanDefinitionRegistry()。

  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    if (this.processPropertyPlaceHolders) {
      processPropertyPlaceHolders();
    }
    //构建ClassPathMapperScanner用于扫描Mapper包
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    scanner.setAddToConfig(this.addToConfig);
    scanner.setAnnotationClass(this.annotationClass);
    scanner.setMarkerInterface(this.markerInterface);
    scanner.setSqlSessionFactory(this.sqlSessionFactory);
    scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
    scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
    scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
    scanner.setResourceLoader(this.applicationContext);
    scanner.setBeanNameGenerator(this.nameGenerator);
    scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
    if (StringUtils.hasText(lazyInitialization)) {
      scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
    }
    //设置Scanner的Filter
    scanner.registerFilters();
    //扫描包
    scanner.scan(
        StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }

而ClassPathMapperScanner继承自Spring中的ClassPathBeanDefinitionScanner,所以在上面的scan方法,实际上是调用的父类的ClassPathBeanDefinitionScanner.scan方法。在scan方法中又会回调子类的doScan 方法,也就是 ClassPathMapperScanner.doScan .

public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
  public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

    if (beanDefinitions.isEmpty()) {
      LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
          + "' package. Please check your configuration.");
    } else {
      processBeanDefinitions(beanDefinitions);
    }
    return beanDefinitions;
  }
}

其中会调用父类的 super.doScan() 方法,获取BeanDefinitionHolder,也就是扫描到带有 @Repository 的类。
然后在 processBeanDefinitions(beanDefinitions) 中对Holder进行实例化,初始化。

Mybatis始化Mapper类

ClassPathMapperScanner.processBeanDefinitions(beanDefinitions)
截取出比较重要的:

  private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    GenericBeanDefinition definition;
    for (BeanDefinitionHolder holder : beanDefinitions) {
      definition = (GenericBeanDefinition) holder.getBeanDefinition();
      String beanClassName = definition.getBeanClassName();
      // the mapper interface is the original class of the bean
      // but, the actual class of the bean is MapperFactoryBean
      definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59
      definition.setBeanClass(this.mapperFactoryBeanClass);

      definition.getPropertyValues().add("addToConfig", this.addToConfig);

      boolean explicitFactoryUsed = false;
      if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
        definition.getPropertyValues().add("sqlSessionFactory",
            new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
        explicitFactoryUsed = true;
      } else if (this.sqlSessionFactory != null) {
        definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
        explicitFactoryUsed = true;
      }
    }
  }

其中最重要的点是:

// 设置mapper接口的名称到构造参数
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); 
// 设置BeanDefinition的class
definition.setBeanClass(this.mapperFactoryBeanClass);
//设置属性addToConfig
definition.getPropertyValues().add("addToConfig", this.addToConfig);
//设置属性sqlSessionFactory
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
  1. 设置beanClass
    设置BeanDefinition对象的BeanClass为MapperFactoryBean<?>。这意味着什么呢?以UserMapper为例,意味着当前的mapper接口在Spring容器中,beanName是userMapper,beanClass是MapperFactoryBean.class。那么在IOC初始化的时候,实例化的对象就是MapperFactoryBean对象( 这一点是关键点 ).这说明在Spring实例化该对象的时候是去创建这个对象。

  2. 设置参数 sqlSessionFactory , 这个参数会在初始化Mapper的时候使用到。

生成代理Bean

上面初始化BeanDefinition的时候说到BeanDefinition对象就是beanClass为MapperFactoryBean,所以我肯定要去看这个类。

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
  private Class<T> mapperInterface;
  private boolean addToConfig = true;
  public MapperFactoryBean() {
    // intentionally empty
  }
  //初始化的使用调用这个方法
  public MapperFactoryBean(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }
  @Override
  public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }

在实例化UserMapper的时候,就会调用到MapperFactoryBean的构造方法。

而MapperFactoryBean它继承自FactoryBean,自然也实现了接口方法getObject() , 于是spring会通过这个FactoryBean调用getObject 创建的Bean实例.

其中接口调用getSqlSession 。
看到这里又可以回到上一节中,这个SqlSession就是上节中的SqlSessionFactory。

这里的调用链就是。
MapperFactoryBean.getObject
-> SqlSessionTemplate.getMapper
-> MapperRegistry.getMapper
-> MapperProxyFactory.newInstance
-> Proxy.newProxyInstance

public class MapperProxyFactory<T> {
    protected T newInstance(MapperProxy<T> mapperProxy) {
        return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
    }
    public T newInstance(SqlSession sqlSession) {
        MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
        return this.newInstance(mapperProxy);
    }
}

所以最终获取到的Mapper类是由Proxy生成的代理类,代理的接口是 UserMapper,而代理类为MapperProxy。

MapperProxy代理处理

接着看这个MapperProxy代理类。

public class MapperProxy<T> implements InvocationHandler, Serializable {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) {
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
  }
}

会先获取MapperMethodInvoker ,然后调用invoke。
而MapperMethodInvoker这个类封装了MapperMethod。在构造MapperMethod的过程可以看如下:

  private MapperMethodInvoker cachedInvoker(Method method) {
      MapperMethodInvoker invoker = methodCache.get(method);
      if (invoker != null) {
        return invoker;
      }
      return methodCache.computeIfAbsent(method, m -> {
          return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
      });
  }

在构造MapperMethod的参数中传入了,sqlSession.getConfiguration

  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }

其中构造了 SqlCommand

    public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
      //获取方法名
      final String methodName = method.getName();      
      final Class<?> declaringClass = method.getDeclaringClass();
      //从Config中获取MappedStatement 。这个就是第二节中,我们解析xml后保存在config中的数据了。
      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
          configuration);
        name = ms.getId();
        type = ms.getSqlCommandType();
        if (type == SqlCommandType.UNKNOWN) {
          throw new BindingException("Unknown execution method for: " + name);
        }
    }

接下来就是调用invoke的时候会去执行MapperMethod的execute函数。

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

在execute方法中根据Command的类型,路由到不同的调用方法。比如下面是Select中的路由调用。

    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;

接下来以SelectOne为实例
然后开始调用SqlSessionTemplate.selectOne -> DefaultSqlSession.selectOne -> DefaultSqlSession.selectList

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  }

这里也很熟悉。也是第二节中保存在Config中的MappedStatement。然后通过statement获取到之后用于Executor来执行。

接下来才是找到重点

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

在上面的代码中,通过ms以及参数获取到了BoundSql,这个BoundSql也就是在第二节中,通过xml解析出来的sql 。

这时候,SQL也解析出来了。就可以最后调用

  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;    
    list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    return list;
  }

可以看到这个queryFromDatabase ,看着名字就知道终于找到调用数据库的了。

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