天天看点

Mybatis源码分析(二)3、解析注解的sql4、整合Spring

3、解析注解的sql

前面我们描述的是XML的形式,这次我们来看注解的形式,跟前面一样先会去解析mybatis的配置文件,然后再解析mapper文件。我们这里采用的是注解形式也就不在看mapper文件的解析。前面解析也讲述过解析mybatis的配置文件这里不在重复,直接跳转到解析sql语句这里

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          //10.4自动扫描包下所有映射器
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
            //10.1使用类路径
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            //映射器比较复杂,调用XMLMapperBuilder
            //注意在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            //10.2使用绝对url路径
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            //映射器比较复杂,调用XMLMapperBuilder
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            //10.3使用java类名
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            //直接把这个映射加入配置
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }
           

我们具体看configuration.addMappers(mapperPackage),解析注解的具体就在这里,mybatis的配置文件需要配置扫描哪些包下,这里的mapperPackage就是我们配置的包路径

public <T> void addMapper(Class<T> type) {
    //mapper必须是接口!才会添加
    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 {
        //如果加载过程中出现异常需要再将这个mapper从mybatis中删除,这种方式比较丑陋吧,难道是不得已而为之?
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }
           

进入MapperAnnotationBuilder注解的解析类中,首先还是会去加载类名对应的xml文件

public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      //加载类名对应的.xml文件
      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();
  }
           

我们这里具体看注解解析,进入parseStatement(method),可以看到有SelectKey、ResultMap等注解解析

void parseStatement(Method method) {
    Class<?> parameterTypeClass = getParameterType(method);
    LanguageDriver languageDriver = getLanguageDriver(method);
    //获取方法上的注解
    SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
    if (sqlSource != null) {
      Options options = method.getAnnotation(Options.class);
      final String mappedStatementId = type.getName() + "." + method.getName();
      Integer fetchSize = null;
      Integer timeout = null;
      StatementType statementType = StatementType.PREPARED;
      ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
      SqlCommandType sqlCommandType = getSqlCommandType(method);
      boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
      boolean flushCache = !isSelect;
      boolean useCache = isSelect;

      KeyGenerator keyGenerator;
      String keyProperty = "id";
      String keyColumn = null;
      if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
        // first check for SelectKey annotation - that overrides everything else
        SelectKey selectKey = method.getAnnotation(SelectKey.class);
        if (selectKey != null) {
          keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
          keyProperty = selectKey.keyProperty();
        } else if (options == null) {
          keyGenerator = configuration.isUseGeneratedKeys() ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
        } else {
          keyGenerator = options.useGeneratedKeys() ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
          keyProperty = options.keyProperty();
          keyColumn = options.keyColumn();
        }
      } else {
        keyGenerator = new NoKeyGenerator();
      }

      if (options != null) {
        flushCache = options.flushCache();
        useCache = options.useCache();
        fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
        timeout = options.timeout() > -1 ? options.timeout() : null;
        statementType = options.statementType();
        resultSetType = options.resultSetType();
      }

      String resultMapId = null;
      ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
      if (resultMapAnnotation != null) {
        String[] resultMaps = resultMapAnnotation.value();
        StringBuilder sb = new StringBuilder();
        for (String resultMap : resultMaps) {
          if (sb.length() > 0) {
            sb.append(",");
          }
          sb.append(resultMap);
        }
        resultMapId = sb.toString();
      } else if (isSelect) {
        resultMapId = parseResultMap(method);
      }

      assistant.addMappedStatement(
          mappedStatementId,
          sqlSource,
          statementType,
          sqlCommandType,
          fetchSize,
          timeout,
          // ParameterMapID
          null,
          parameterTypeClass,
          resultMapId,
          getReturnType(method),
          resultSetType,
          flushCache,
          useCache,
          // TODO issue #577
          false,
          keyGenerator,
          keyProperty,
          keyColumn,
          // DatabaseID
          null,
          languageDriver,
          // ResultSets
          null);
    }
  }
           

我们具体去看获取方法上的注解的sql语句,进入getSqlSourceFromAnnotations,主要就是获取注解上的sql字符串,并且把#替换成?

private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
    try {
      Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
      Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
      if (sqlAnnotationType != null) {
        if (sqlProviderAnnotationType != null) {
          throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
        }
        Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
        //获取注解上到sql字符串,并且把#替换成?
        final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
        //拼接字符串为sql语句
        return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
      } else if (sqlProviderAnnotationType != null) {
        Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
        return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation);
      }
      return null;
    } catch (Exception e) {
      throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + e, e);
    }
  }
           

后面的执行过程也就跟前面一样了

4、整合Spring

现在并不是单独去使用mybatis,而是结合spring使用,所以这篇分析是怎么结合spring的。

首先得去配置mybatis的一些相关信息,数据源,sqlSessionFactory和mapper文件扫描类

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">    
<!-- 加载数据源 -->    
<property name="dataSource" ref="dataSource"/>    
<property name="mapperLocations" value="classpath*:mappers/*Mapper.xml"/>
</bean> 
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">    
<!-- 指定扫描的包,如果存在多个包使用(逗号,)分割 -->    
<property name="basePackage" value="com.test.bean"/>    
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
           

因为需要整合spring,所以所有的创建对象都要交给spring管理,所以我们需要用spring的工厂类SqlSessionFactoryBean,它实现InitializingBean,结合spring的知识它的afterPropertiesSet方法会在bean初始化的时候执行

public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {

public void afterPropertiesSet() throws Exception {
    notNull(dataSource, "Property 'dataSource' is required");
    notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
    state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
              "Property 'configuration' and 'configLocation' can not specified with together");

    this.sqlSessionFactory = buildSqlSessionFactory();
  }

public SqlSessionFactory getObject() throws Exception {
    if (this.sqlSessionFactory == null) {
      afterPropertiesSet();
    }

    return this.sqlSessionFactory;
  }

}
           

在该方法里创建了sqlSessionFactory,方法中主要去创建configuration并把相关信息设置,如果有配mybatis的配置文件,还会去解析配置信息。另外mybatis中是用TransactionFactory创建事务的,结合spring是默认会实例化SpringManagedTransaction Factory,Executor是需要TransactionFactory创建的,执行中需要去事务工厂拿到连接。因为两者都有事务管理,所以为了统一默认就用SpringManagedTransactionFactory解决共用数据库连接的问题。

protected SqlSessionFactory buildSqlSessionFactory() throws IOException {

Configuration configuration;

    XMLConfigBuilder xmlConfigBuilder = null;
    if (this.configuration != null) {
      configuration = this.configuration;
      if (configuration.getVariables() == null) {
        configuration.setVariables(this.configurationProperties);
      } else if (this.configurationProperties != null) {
        configuration.getVariables().putAll(this.configurationProperties);
      }
    } else if (this.configLocation != null) {
      xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
      configuration = xmlConfigBuilder.getConfiguration();
    } else {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
      }
      configuration = new Configuration();
      if (this.configurationProperties != null) {
        configuration.setVariables(this.configurationProperties);
      }
    }


.......

if (this.transactionFactory == null) {
      this.transactionFactory = new SpringManagedTransactionFactory();
  }

.......
if (xmlConfigBuilder != null) {
      try {
        xmlConfigBuilder.parse();

        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Parsed configuration file: '" + this.configLocation + "'");
        }
      } catch (Exception ex) {
        throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
      } finally {
        ErrorContext.instance().reset();
      }
    }


.....
return this.sqlSessionFactoryBuilder.build(configuration);

}
           

最后一句还是会去通过mybatis的sqlSessionFactoryBuilder.build(configuration)去创建mybatis的SqlSessionFactory。因为SqlSessionFactoryBean还是一个工厂类,最后通过getObject方法把DefaultSqlSessionFactory返回出去。

接下来看MapperScannerConfigurer怎么去扫描包创建代理对象,并把sql语句信息保存到configuration中

public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {

public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    if (this.processPropertyPlaceHolders) {
      processPropertyPlaceHolders();
    }

    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.registerFilters();
    scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }
}
           

该类实现了BeanDefinitionRegistryPostProcessor后置处理器,所以会在bean初始化的时候调用postProcessBeanDefinition Registry方法,在这里面就是会去扫描mapper接口和mapper文件,核心逻辑在doScan方法中

public Set<BeanDefinitionHolder> doScan(String... basePackages) {
  // 调用父类的doScan()方法,遍历basePackages中指定的所有包,扫描每个包下的Java文件并进行解析。
  // 使用之前注册的过滤器进行过滤,得到符合条件的BeanDefinitionHolder对象
  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 {
    // 处理扫描得到的BeanDefinitionHolder集合
    processBeanDefinitions(beanDefinitions);
  }

  return beanDefinitions;
}
           

在processBeanDefinitions方法中会对doScan()方法中扫描到的BeanDefinition集合进行修改,主要是将其中记录的接口类型改造为MapperFactoryBean类型

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
  GenericBeanDefinition definition;
  for (BeanDefinitionHolder holder : beanDefinitions) {
    definition = (GenericBeanDefinition) holder.getBeanDefinition();

    if (logger.isDebugEnabled()) {
      logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() 
        + "' and '" + definition.getBeanClassName() + "' mapperInterface");
    }

    // 将扫描到的接口类型作为构造方法的参数
    definition.getPropertyValues().add("mapperInterface", definition.getBeanClassName());
    // 将BeanDefinition中记录的Bean类型修改为MapperFactoryBean
    definition.setBeanClass(this.mapperFactoryBean.getClass());
    // 构造MapperFactoryBean的属性,将sqlSessionFactory、sqlSessionTemplate
    // 等信息填充到BeanDefinition中
    // 修改自动注入方式
  }
}
           

MapperFactoryBean类的动态代理功能是通过实现了Spring提供的FactoryBean接口实现的,该接口是一个用于创建Bean对象的工厂接口,通过geObject()方法获取真实的对象。完成了可以直接将Mapper接口注入到 Service 层的Bean中,这样就不需要编写任何DAO实现的代码。

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {

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

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

所以每个Mapper接口对应的实现类还是MyBatis提供的MapperProxy代理对象,MapperFactoryBean类只不过是包装了一下,让真正的对象能够注入到Spring容器中。所以Mapper接口对应的实现类是作为单例一直存在Spring容器中的,它的sql执行方式我前面讲述过。

但是在这里spring多封装了一层,从构造方法中可以看到这里的sqlSession不是mybatis的,而是SqlSessionTemplate,SqlSessionTemplate实现了SqlSession接口,在 MyBatis与Spring集成开发时,用来代替MyBatis中的DefaultSqlSession的功能。所以该类主要是用于获取 MapperProxy代理对象和执行SQL操作。

在mybatis中sqlSession,sql操作的相关方法是会话级别的不能共享,一次操作完成后就需要去关闭。而SqlSessionTemplate是线程安全的,可以在DAO之间共享使用,我们主要看SqlSessionTemplate是怎么代理的

public class SqlSessionTemplate implements SqlSession, DisposableBean {

// SqlSession 的代理类
private final SqlSession sqlSessionProxy;

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;
  // 创建SqlSession的代理类,给sqlSessionProxy属性赋值
  this.sqlSessionProxy = (SqlSession) newProxyInstance(
      SqlSessionFactory.class.getClassLoader(),
      new Class[] { SqlSession.class },
      new SqlSessionInterceptor());
 }
}
           

在创建SqlSessionTemplate的时候内部会创建sqlSessionProxy代理对象,代理对象具体逻辑就在SqlSessionInterceptor中

private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      // 通过SqlSessionUtils.getSqlSession()获取SqlSession对象,同一个事务共享SqlSession
      SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);
      // 调用SqlSession对象的相应方法
      Object result = method.invoke(sqlSession, args);
      // 检测事务是否由Spring进行管理,并据此决定是否提交事务
      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;
    }
  }
           

可以看到在这里面才是真正的去用mybatis的SqlSession去执行sql操作,并且会去提交事务,我们就不用每次执行完后还要手动提交一下了。从这里也可以看出原先mybatis是会把结果集放到缓存中的,而结合spring之后是把mybatis缓存去掉了

继续阅读