Mybatis-Spring源码分析

2,012 阅读15分钟
我们知道,我们在启动项目的时候,spring就会帮我们实例化好bean,那我们使用mybatis-spring的时候,编
写的maper是怎么交给spring容器的呢?这就是今天要探讨的问题。
一、扫描阶段

我们在接入mybatis-spring的时候会在相应的配置类增加这样的注解

@MapperScan(basePackages = "com.test.**.mapper")

关于开始提到的问题,就从这里开始。 点开@MapperScan这个注解,发现有如下这个注解

@Import({MapperScannerRegistrar.class})

这个注解正是关键所在,spring在bean创建之前首先会执行AbstractApplicationContext#invokeBeanFactoryPostProcessors(beanFactory)方法,执行bean工厂的处理器,在执行这个方法的时候就会扫描出所有的BeanDefinition,其中要扫描的范围就包括了@Import这个注解。那就需要看看MapperScannerRegistrar这个类做了什么事。

public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
    private ResourceLoader resourceLoader;

    public MapperScannerRegistrar() {
    }

    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
        ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
        if (this.resourceLoader != null) {
            scanner.setResourceLoader(this.resourceLoader);
        }
       
        // 省略部分代码
        scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef"));
        scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef"));
        List<String> basePackages = new ArrayList();
        String[] var10 = annoAttrs.getStringArray("value");
        int var11 = var10.length;

        int var12;
        String pkg;
        for(var12 = 0; var12 < var11; ++var12) {
            pkg = var10[var12];
            if (StringUtils.hasText(pkg)) {
                basePackages.add(pkg);
            }
        }

        var10 = annoAttrs.getStringArray("basePackages");
        var11 = var10.length;

        for(var12 = 0; var12 < var11; ++var12) {
            pkg = var10[var12];
            if (StringUtils.hasText(pkg)) {
                basePackages.add(pkg);
            }
        }

        scanner.registerFilters();
        scanner.doScan(StringUtils.toStringArray(basePackages));
    }

注:考虑到篇幅,上述源码省略了部分代码

上述代码主要做了以下几件事:

  • 获取注解@MapperScan的value属性和basePackages得到要扫描的包路径;
  • scanner.registerFilters();注册filters,其实就是扫描的规则,也就是说,basePackages包下面的哪些文件是需要被扫描的,哪些是不需要的。
  • scanner.doScan(StringUtils.toStringArray(basePackages));开始扫描指定包下面的文件。
  • scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef")); scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef")); 这两行代码是判断注解有没有对应的属性,有的话就添加到ClassPathMapperScanne这个扫描类。

ClassPathMapperScanner#doScan

    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
        if (beanDefinitions.isEmpty()) {
            this.logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
        } else {
            this.processBeanDefinitions(beanDefinitions);
        }

        return beanDefinitions;
    }

先调用父类(org.springframework.context.annotation.ClassPathBeanDefinitionScanner)扫描出符合条件的所有BeanDefinitionHolder并注册到spring容器中(beanDefinitionMap)。然后对每个BeanDefinition执行后置处理。

ClassPathMapperScanner#processBeanDefinitions

    private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
        Iterator var3 = beanDefinitions.iterator();

        while(var3.hasNext()) {
            BeanDefinitionHolder holder = (BeanDefinitionHolder)var3.next();
            GenericBeanDefinition definition = (GenericBeanDefinition)holder.getBeanDefinition();
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + definition.getBeanClassName() + "' mapperInterface");
            }

            definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());
            definition.setBeanClass(this.mapperFactoryBean.getClass());
            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;
            }

            if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
                if (explicitFactoryUsed) {
                    this.logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
                }

                definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
                explicitFactoryUsed = true;
            } else if (this.sqlSessionTemplate != null) {
                if (explicitFactoryUsed) {
                    this.logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
                }

                definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
                explicitFactoryUsed = true;
            }

            if (!explicitFactoryUsed) {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
                }

                definition.setAutowireMode(2);
            }
        }

    }
  • 1、这里对每个BeanDefinition进行后置处理。添加构造参数的值、设置beanClass为MapperFactoryBean类型。这样一来,我们编写的mapper就接口再spring容器中都对应着MapperFactoryBean。比如我们有OrderMapper这个接口,那在spring容器中存在的BeanDefinition的beanClass为MapperFactoryBean,当通过bename为orderMapper从springring容器中创建bean的是候其实先创建的是MapperFactoryBean,再根据MapperFactoryBean的getObject方法得到MapperProxy,最终自动注入OrderMapper的时候也是注入MapperProxy。 MapperFactoryBean#getObject的调用链如下:
    public T getObject() throws Exception {
        return this.getSqlSession().getMapper(this.mapperInterface);
    }

SqlSessionTemplate#getMapper

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

Configuration#getMapper

  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }

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

注意,这里会从knownMappers这个map中拿出MapperProxyFactory然后通过mapperProxyFactory.newInstance(sqlSession)得到MapperProxy。 MapperProxyFactory#newInstance(org.apache.ibatis.session.SqlSession)

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

MapperProxyFactory#newInstance(org.apache.ibatis.binding.MapperProxy)

  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }
  • 2、为什么需要添加构造参数? 因为MapperFactoryBean中有个带参的构造方法
    public MapperFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

实例化MapperFactoryBean的时候会通过构造方法反射出来,所以需要为这个构造方法添加参数,但是这里直接添加一个字符串进去了,比如"com.test.orderMapper",在实例化的时候spring可以将这个字符串转换成OrderMapper.class。

  • 3、definition.setAutowireMode(2);最后一处代码,如果条件满足,就把MapperFactoryBean的自动装配模型改为2(RootBeanDefinition.AUTOWIRE_BY_TYPE);
    • (1)、 从源码可以不难看出,如果没有sqlSessionFactory和sqlSessionTemplate的时候就会条件满足。那为什么这里需要这么做呢?因为spring中,默认的自动装配模型是0(RootBeanDefinition.AUTOWIRE_NO),在自动装配模型是0的情况下,使用了@Autowired注解是可以完成属性的自动装配的。我们看到,在Mybatis中,没有使用到这样的注解,所以需要设置装配类型为根据类型自动装配,这样一来,就可以完成MapperFactoryBean中所需要的属性注入。
    • (2)、那什么情况下条件不满足呢?回顾开始的代码
scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef"));
scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef"));

就是在这里完成的,满足条件的话,就在这里直接把需要的属性值添加进去,后期就不需要通过自动装配来完成了,自然也不需要更改默认的自动装配类型。

小结:
  • 1、扫描出所有的mapper所对应的BeanDefinition;
  • 2、把mapper变成FactoryBean;
  • 3、为BeanDefinition添加构造方法的值,用于实例化。
二、初始化操作

下面的源码比较多,重要的部分用红色字体已经标出,可快速浏览。

MapperFactoryBean的类继承关系下,可以看到它不但是一个FactoryBean,还最终实现了InitializingBean接口,Mybstis的初始化动作正是这样来完成的。

MapperFactoryBean.png
MapperFactoryBean本身并没有实现InitializingBean接口的方法,这个是在DaoSupport中实现的,所以先看DaoSupport中的实现方法。

1、DaoSupport#afterPropertiesSet

	@Override
	public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
		// Let abstract subclasses check their configuration.
		checkDaoConfig();

		// Let concrete implementations initialize themselves.
		try {
			initDao();
		}
		catch (Exception ex) {
			throw new BeanInitializationException("Initialization of DAO failed", ex);
		}
	}

这里直接调用子类的checkDaoConfig方法,所以看MapperFactoryBean中的即可(initDao没有任何实现方法,就是个空方法,不用管)

2、MapperFactoryBean#checkDaoConfig

    protected void checkDaoConfig() {
        super.checkDaoConfig();
        Assert.notNull(this.mapperInterface, "Property 'mapperInterface' is required");
        Configuration configuration = this.getSqlSession().getConfiguration();
        if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
            try {
                configuration.addMapper(this.mapperInterface);
            } catch (Exception var6) {
                this.logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", var6);
                throw new IllegalArgumentException(var6);
            } finally {
                ErrorContext.instance().reset();
            }
        }

    }

首先调用父类的checkDaoConfig方法

    protected void checkDaoConfig() {
        Assert.notNull(this.sqlSession, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");
    }

sqlSessionFactory和sqlSessionTemplate在上面提过,要么通过spring自动装配进去,要么自己添加进去。 执行完父类方法后就开始执行configuration.addMapper(this.mapperInterface);方法。 MapperRegistry#addMapper

  public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }
  • 为每个mapper创建一个对应类型的MapperProxyFactory,然后put到map中去。前面说过,最终注入的是MapperProxy对象,而MapperProxy这个对象就是从MapperProxyFactory产生的,所以需要提前创建好。
  • 解析mapper。

3、MapperAnnotationBuilder#parse

  public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      parseCache();
      parseCacheRef();
      Method[] methods = type.getMethods();
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
  }
  • 加载xml文件
  • 设置当前的名称空间,就是Mapper的权限定名,如com.test.mapper.OrderMapper
  • 解析Statement 在xml文件中的,比如OrderMapper.xml,这里就对OrderMapper.xml进行解析

4、MapperAnnotationBuilder#loadXmlResource

  private void loadXmlResource() {
    // Spring may not know the real resource name so we check a flag
    // to prevent loading again a resource twice
    // this flag is set at XMLMapperBuilder#bindMapperForNamespace
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
      String xmlResource = type.getName().replace('.', '/') + ".xml";
      InputStream inputStream = null;
      try {
        inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
      } catch (IOException e) {
        // ignore, resource is not required
      }
      if (inputStream != null) {
        XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
        xmlParser.parse();
      }
    }
  }
  • 根据Mapper的全限定名,替换成对应的xml文件名,这也是为什么我们自己直接在同目录下写个同名的xml文件不用添加别的配置就能被解析到的原因。
  • 加载xml文件,然后解析xml文件。

5、XMLMapperBuilder#parse

  public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingChacheRefs();
    parsePendingStatements();
  }
  • parser.evalNode("/mapper"),顾名思义,接可以解析出xml的每个节点文件;
  • resource:就是com/test/mapper/OrderMapper.xml这样的字符串。

6、XMLMapperBuilder#configurationElement

  private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
    }
  }

此时context中就是xml文件中所有原生的标签形式的sql语句,诸如下面这样:

<mapper namespace="com.test.mapper.OrderMapper">
	<select id="select" resultType="String">
		select * from tableName where id='1'
	</select>
</mapper>
  • 这里的namespace就是com.test.mapper.OrderMapper。然后依次解析cache-ref标签、Cache标签、mapper标签中的parameterMap标签、mapper标签中的resultMap标签、mapper标签中的sql标签;select|insert|update|delete这几个标签。
  • 通过buildStatementFromContext这个方法的调用链执行到XMLStatementBuilder#parseStatementNode这个方法

7、XMLStatementBuilder#parseStatementNode

  public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultType = context.getStringAttribute("resultType");
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    Class<?> resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
    }

    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }
  • boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); boolean useCache = context.getBooleanAttribute("useCache", isSelect);

    注意这三行代码,首先判断是不是select类型的sql语句,如果是的话,再看有没有填充flushCache和useCache的值,如果有就用填充的,没有就设置默认值。

  • 这里整个方法来看就是拿到标签的属性值,解析出sql,最终通过addMappedStatement方法将这些结果包装成MappedStatement添加到mappedStatements这个map中。比如我们在OrderMapper.xml中一个id为select的方法,mappedStatements这个map的key就是com.test.mapper.select,value就是解析后包装好的MappedStatement。当使用的时候就是根据id在这个map中拿到MappedStatement,再拿出sql去数据库执行的

小结:

利用InitializingBean接口,完成mapper信息的初始化

三、执行sql

由于注入的是代理,所以当执行Mapper中的sql的时候会执行MapperProxy的invoke方法

1、MapperProxy#invoke

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

首先通过cachedMapperMethod这个方法得到MapperMethod,开始先从缓存中获取,如果从缓存中获取不到就构建出一个MapperMethod,并把构建的MapperMethod加入到缓存,最后返回MapperMethod。cachedMapperMethod方法代码如下:

2、MapperProxy#cachedMapperMethod

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

MapperMethod中只有SqlCommand和MethodSignature这两个属性,看名字也大概能知道是存放什么信息的。得到MapperMethod就执行它的execute方法。

3、MapperMethod#execute

        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);

这个方法里面就是判断是INSERT、UPDATE、DELETE还是SELECT语句,然后转换参数。由于逻辑比较单一,代码又多。由于我这边使用的是直接根据主键查询一条结果进来的,所以选择了其中两行代码来说明。

  • 1、convertArgsToSqlCommandParam:顾名思义,这个方法就是进行参数转换
    public Object convertArgsToSqlCommandParam(Object[] args) {
      final int paramCount = params.size();
      if (args == null || paramCount == 0) {
        return null;
      } else if (!hasNamedParameters && paramCount == 1) {
        return args[params.keySet().iterator().next().intValue()];
      } else {
        final Map<String, Object> param = new ParamMap<Object>();
        int i = 0;
        for (Map.Entry<Integer, String> entry : params.entrySet()) {
          param.put(entry.getValue(), args[entry.getKey().intValue()]);
          // issue #71, add param names as param1, param2...but ensure backward compatibility
          final String genericParamName = "param" + String.valueOf(i + 1);
          if (!param.containsKey(genericParamName)) {
            param.put(genericParamName, args[entry.getKey()]);
          }
          i++;
        }
        return param;
      }
    }

要说明的是参数的名字和值会在param这个map中存在两份,一份是我们在写mapper中的sql时通过@Param注解执行的名字,一份是根据参数的顺序,生成param1、param2、param3……..这样的名字。param1这种名字的用法。就是我们在写mapper中可以不指定参数名字,然后再xml文件中写sql的参数引用时按照顺序写param1、param2、param3这样也能被正确的将对应的参数值赋进去。

  • 2、selectOne:执行查询方法,会执行到SqlSessionInterceptor的invoke方法(SqlSessionInterceptor是SqlSessionTemplate的内部类,实现了InvocationHandler接口。)注意SqlSessionInterceptor的invoke方法里面又是对Mybatis(注意是Mybatis,不是Mybatis-spring)里面的查询方法进行的代理,所以这个invoke方法里面会使用method.invoke先来执行原生Mybatis下对应的方法(本案例对应的为DefaultSqlSession#selectOne(java.lang.String, java.lang.Object))。

    关于SqlSessionInterceptor的invoke方法如下,后文还会提到这个方法 SqlSessionInterceptor#invoke

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);

            Object unwrapped;
            try {
                Object result = method.invoke(sqlSession, args);
                if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
                    sqlSession.commit(true);
                }

                unwrapped = result;
            } catch (Throwable var11) {
                unwrapped = ExceptionUtil.unwrapThrowable(var11);
                if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
                    SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
                    sqlSession = null;
                    Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped);
                    if (translated != null) {
                        unwrapped = translated;
                    }
                }

                throw (Throwable)unwrapped;
            } finally {
                if (sqlSession != null) {
                    SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
                }

            }

            return unwrapped;
        }

跟进DefaultSqlSession#selectOne的调用链,会执行到如下方法:

4、DefaultSqlSession#selectList

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
  • 首先根据statement从mappedStatements这个map中得到MappedStatement。statement就是方法的唯一id,比如:com.test.mapper.OrderMapper.select。这个MappedStatement就是在7、XMLStatementBuilder#parseStatementNode加进去的;
  • 执行查询方法。

上述查询方法会执行到下面的方法

5、BaseExecutor#query

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

从上面代码很容易可以看出做了下面这几件事情:

  • 1、这里的ms.isFlushCacheRequired()实际上就是我们在写xml中的sql时设定的flushCache属性的值(或者默认值)。下面的configuration.getLocalCacheScope()顾名思义就是本地缓存的作用于,可以看到,这两处成立的话都会使用clearLocalCache方法清除本地缓存,每次都会从数据库查询数据。不同的是ms.isFlushCacheRequired()是针对具体的MappedStatement的,而configuration.getLocalCacheScope()是针对整个mybatis配置的,也就是说这个是对所有的MappedStatement生效的。clearLocalCache方法如下:
  @Override
  public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }

关于clearLocalCache方法下文还会提到

  • 2、首先从本地缓存localCache中取结果,如果缓存中没有再执行queryFromDatabase方法从数据库查结果,并把查询结果put到本地缓存localCache中。最后,将查询的结果一步步返回。
四、使用缓存还是从数据库查

其实上面已经提到了一部分关于缓存的问题,这节相当于上面的有效补充。

	public void getById(String id){
		orderMapper.select(id);
		orderMapper.select(id);
	}

假设有这样的方法,同一条sql执行两次(不要纠结这个方法是void类型的,这里只是说明执行同样的sql时是否缓存的问题)。 执行流程和上面讲的一样。现在再回过头看一下上文提到的 SqlSessionInterceptor#invoke 方法:

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);

            Object unwrapped;
            try {
                Object result = method.invoke(sqlSession, args);
                if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
                    sqlSession.commit(true);
                }

                unwrapped = result;
            } catch (Throwable var11) {
                unwrapped = ExceptionUtil.unwrapThrowable(var11);
                if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
                    SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
                    sqlSession = null;
                    Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped);
                    if (translated != null) {
                        unwrapped = translated;
                    }
                }

                throw (Throwable)unwrapped;
            } finally {
                if (sqlSession != null) {
                    SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
                }

            }

            return unwrapped;
        }

通过 Object result = method.invoke(sqlSession, args);这行代码执行完后拿到结果,再根据条件判断是否执行commit方法,以及finally块的closeSqlSession方法,这两个方法都和清除缓存有关。 跟进commit方法的调用链,会经过上文提到的clearLocalCache方法清楚本地缓存,closeSqlSession也会通过DefaultSqlSession#close执行经过clearLocalCache。

SqlSessionUtils#closeSqlSession

    public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {
        Assert.notNull(session, "No SqlSession specified");
        Assert.notNull(sessionFactory, "No SqlSessionFactory specified");
        SqlSessionHolder holder = (SqlSessionHolder)TransactionSynchronizationManager.getResource(sessionFactory);
        if (holder != null && holder.getSqlSession() == session) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Releasing transactional SqlSession [" + session + "]");
            }

            holder.released();
        } else {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Closing non transactional SqlSession [" + session + "]");
            }

            session.close();
        }

    }

那如何能不通过这两处代码执行clearLocalCache方法使得第二次查询的时候使用缓存呢?那就得使用@Transactional注解

	@Transactional
	public void getById(String id){
		orderMapper.select(id);
		orderMapper.select(id);
	}

那么,@Transactional注解是如何使得SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)为true呢?

@Transactional是如何使得Mybatis一级缓存生效的

回顾上文提到的***SqlSessionInterceptor#invoke***方法,这个方法首先会执行

     SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);

SqlSessionUtils#getSqlSession

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Creating a new SqlSession");
    }

    session = sessionFactory.openSession(executorType);

    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }

首先从TransactionSynchronizationManager.getResource中获取SqlSessionHolder。(跟进getResource方法其实就是从一个本地线程的map中取值,这里就不贴源码了。)第一次肯定获取不到,所以就会执行下面的逻辑registerSessionHolder来注册SqlSessionHolder(这个方法里面通过TransactionSynchronizationManager.bindResource(sessionFactory, holder);最终将SqlSessionHolder注册到map)

现在我们再次回到 SqlSessionInterceptor#invoke 方法中SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)这行代码

  public static boolean isSqlSessionTransactional(SqlSession session, SqlSessionFactory sessionFactory) {
    notNull(session, NO_SQL_SESSION_SPECIFIED);
    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);

    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

    return (holder != null) && (holder.getSqlSession() == session);
  }

由于已经在map中注册了SqlSessionHolder,所以这里是可以拿到的,这个SqlSessionHolder中的session就是DefaultSqlSession,这里会返回true。 所以外围的代码自然不会执行。

 if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
                    sqlSession.commit(true);
 }

这里commit方法不会执行。本地缓存就不清理。 SqlSessionUtils#closeSqlSession 中同理也不会关闭连接。

以上不会清除本地缓存的前提是一级缓存用的默认的session级别,如果将一级缓存级别改为statement的话虽然不会通过commit方法和closeSqlSession方法清理本地缓存,但是会通过上文提到的5、BaseExecutor#query中的方法清理本地缓存

小结:
  • 1、如果一级缓存的级别是statement,不管加不加事务注解都会清理本地缓存;
  • 2、如果一级缓存的级别是session,在增加了@Transactional事务注解后,会给一个本地线程map中添加(DefaultSqlSessionFactory,SqlSessionHolder),这个SqlSessionHolder中的session的属性值就是DefaultSqlSession。