análisis de código fuente de mybatis 06: integración con spring

Nota: El análisis del código fuente de esta serie se basa en mybatis-spring 2.0.6, la versión correspondiente de mybatis es 3.5.6 y la dirección del almacén del código fuente es: funcy/mybatis-spring .

En cuanto a mybatis, la mayoría de las veces lo springusamos en combinación con el contenedor, este artículo analizará el proceso de integración de este mybatisy el contenedor.spring

1. Obtener el código fuente y preparardemo

1.1 mybatisy springproyecto de integración y código fuente

mybatisEl springproyecto integrado se llama mybatis-springy el código fuente está alojado en github. Por supuesto, también lo saqué a mi propio gitee para crearlo. La dirección es: funcy/mybatis . Sobre el código fuente checkouty pullla operación, no es el enfoque . de este artículo, así que no diré más.

mybatisLa versión analizada anteriormente es 3.5.6, la versión que analizamos aquí es y la mybatis-springversión 2.0.6de mybatis a la que se refiere es exactamente 3.5.6.

1.2 preparación de demostración

Después de obtener el código fuente, puede crear una demostración de ejemplo. src/testLa crearemos a continuación. El directorio completo es el siguiente:

Directorio de códigos:

archivo asignador.xml:

  1. preparar model/ mapper/service
/**
 * User.java
 */
public class User implements Serializable {

  /**
   * 用户id
   */
  private Long id;

  /**
   * 登录名
   */
  private String loginName;

  /**
   * 用户昵称
   */
  private String nick;

  ... 省略set/get方法
}

/**
 * UserMapper.java
 */
 public interface UserMapper {

  /**
   * 查询
   * 
   * @return
   */
  List<User> selectList(@Param("id") Long id, @Param("limit") Integer limit);

  /**
   * 更新
   * 
   * @param id
   * @param nick
   * @return
   */
  int updateUser(@Param("id") Long id, @Param("nick") String nick);
}

/**
 * UserService.java
 */
 @Service
public class UserService {

  @Autowired
  private UserMapper userMapper;

  /**
   * 获取列表
   * 
   * @param id
   * @param limit
   * @return
   */
  public List<User> getUser(Long id, Integer limit) {
    return userMapper.selectList(id, limit);
  }
}
复制代码

UserService.javaManejado por resorte e inyectado por resorte userMapper.

  1. UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.spring.demo.mapper.UserMapper">
  <select id="selectList" resultType="org.mybatis.spring.demo.model.User">
    select id, login_name as loginName, nick from user
    <trim prefix="where" prefixOverrides="AND |OR">
      <if test="id != null">
        and id = #{id}
      </if>
    </trim>
    <if test="limit != null">
      limit #{limit}
    </if>
  </select>

  <update id="updateUser">
    update user set nick = #{nick} where id = #{id}
  </update>
</mapper>
复制代码
  1. Preparar la clase de configuración
@Configuration
public class Demo01Config {

  @Bean
  public SqlSessionFactory sqlSessionFactory() throws Exception {
    SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
    // 设置数据源
    factoryBean.setDataSource(dataSource());
    // mapper.xml 文件路径
    Resource[] resources = new PathMatchingResourcePatternResolver()
      .getResources("classpath*:/mapper/**/*.xml");
    factoryBean.setMapperLocations(resources);
    return factoryBean.getObject();
  }

  @Bean
  public DataSource dataSource() throws Exception {
    Driver driver = new com.mysql.cj.jdbc.Driver();
    String url = "jdbc:mysql://localhost:3306/test";
    String username = "root";
    String password = "123";
    return new SimpleDriverDataSource(driver, url, username, password);
  }
}
复制代码
  1. preparar la clase principal

Finalmente la clase principal:

@ComponentScan
@MapperScan("org.mybatis.spring.demo.mapper")
public class Demo01Main {

  public static void main(String[] args) {
    ApplicationContext application 
        = new AnnotationConfigApplicationContext(Demo01Main.class);
    // 获取 userService 对象
    UserService userService = (UserService) application.getBean("userService");
    List<User> list = userService.getUser(3L, 10);
    System.out.println(list);
  }

}
复制代码

Ejecutar, los resultados son los siguientes:

[User{id=3, loginName='test', nick='test'}]
复制代码

Se puede ver que la integración de mybatis y spring es exitosa, y luego analizaremos el mybatis-springproyecto de acuerdo con esta demostración.

2. SqlSessionFactoryBeanClase

我们先来看看Demo01Config引入的Bean

  @Bean
  public SqlSessionFactory sqlSessionFactory() throws Exception {
    SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
    // 设置数据源
    factoryBean.setDataSource(dataSource());
    // mapper.xml 文件路径
    Resource[] resources = new PathMatchingResourcePatternResolver()
      .getResources("classpath*:/mapper/**/*.xml");
    factoryBean.setMapperLocations(resources);
    return factoryBean.getObject();
  }
复制代码

这个方法中,先是创建了SqlSessionFactoryBean,然后配置了数据源以及mapper.xml文件路径,然后调用SqlSessionFactory#getObject()方法获取SqlSessionFactory,看来SqlSessionFactoryBean还是挺关键的,我们看看它的定义:

/**
 * 实现了 FactoryBean
 */
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, 
        InitializingBean, ApplicationListener<ApplicationEvent> {
    ...
}
复制代码

可以看到,SqlSessionFactoryBean实现了FactoryBean,其中类为SqlSessionFactory,这样调用SqlSessionFactory#getObject()方法获取SqlSessionFactory了。

FactoryBean是spring中一类重要的bean,它可以将普通的类包装成FactoryBean注册到spring容器中,要获取该类的bean时,只需调用SqlSessionFactory#getObject()方法即可。

SqlSessionFactorymybatis中的重要类,用来生成SqlSession,而SqlSession正是用来获取XxxMapper实例、执行数据库操作的类。

SqlSessionFactoryBean可以定义很多很多的配置,它的setXxx方法如下:

实际上,在mybatis-config.xml配置文件中配置的内容都可以在这里找到对应的setXxx方法,示例demo中,只是设置了数据源与mapper.xml的文件路径。

了解了SqlSessionFactoryBean的相关功能后,我们来看看SqlSessionFactoryBean#getObject方法:

  public SqlSessionFactory getObject() throws Exception {
    if (this.sqlSessionFactory == null) {
      // 主动调用 afterPropertiesSet()
      afterPropertiesSet();
    }

    return this.sqlSessionFactory;
  }

  /**
   * 重写了 InitializingBean 的方法
   */
  public void afterPropertiesSet() throws Exception {
    ...
    // 这里构建了 sqlSessionFactory
    this.sqlSessionFactory = buildSqlSessionFactory();
  }

复制代码

我们继续进入buildSqlSessionFactory()方法:

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {

    final Configuration targetConfiguration;

    // 这里省略了好多好多的代码,都是处理 targetConfiguration 的配置
    ...

    // 处理数据源
    targetConfiguration.setEnvironment(new Environment(this.environment,
        this.transactionFactory == null ? new SpringManagedTransactionFactory() 
            : this.transactionFactory, this.dataSource));

    if (this.mapperLocations != null) {
      if (this.mapperLocations.length == 0) {
        LOGGER.warn(() -> "...");
      } else {
        for (Resource mapperLocation : this.mapperLocations) {
          if (mapperLocation == null) {
            continue;
          }
          try {
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(
                mapperLocation.getInputStream(), targetConfiguration, 
                mapperLocation.toString(), targetConfiguration.getSqlFragments());
            // 在这里解析 mapper.xml 文件
            xmlMapperBuilder.parse();
          } catch (Exception e) {
            throw new NestedIOException(...);
          } finally {
            ErrorContext.instance().reset();
          }
          LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
        }
      }
    } else {
      LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
    }

    return this.sqlSessionFactoryBuilder.build(targetConfiguration);
  }
复制代码

首先,这个方法非常长,以上代码是精简后的代码,省略的是大段大段的设置targetConfiguration属性的代码,剩下的是两个功能:

  1. 设置数据源
  2. 解析xml

这两个属性也是我们在sqlSessionFactory()中设置的。

处理完sqlSessionFactory()方法后,sqlSessionFactory 就注册到spring容器中了。

3. @MapperScan扫描流程

sqlSessionFactory注册到spring容器并不能实现MapperInterface的自动注入,像这样:

@Autowired
private UserMapper userMapper;
复制代码

这个功能的实现,就是@MapperScan的功劳了。

Demo01Main.java中,我们是这样配置@MapperScan注解的:

@ComponentScan
@MapperScan("org.mybatis.spring.demo.mapper")
public class Demo01Main {
    ...
}
复制代码

这里的org.mybatis.spring.demo.mapper就是MapperInterface的包路径了,也是mybatis要扫描的包路径.

我们进入@MapperScan:

...
// @Import 里的类是关键
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
    ...
}
复制代码

MapperScan中,有一个@Import标签,它引入了一个类:MapperScannerRegistrar,我们来看看它做了啥。

3.1 MapperScannerRegistrar

MapperScannerRegistrar的代码如下:

/**
 * 实现了 ImportBeanDefinitionRegistrar类
 */
public class MapperScannerRegistrar 
    implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {

  ...

  /**
   * 来自于 ImportBeanDefinitionRegistrar 的方法,可以往spring容器中注册beanDefinition
   */
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, 
        BeanDefinitionRegistry registry) {
    // 获取 MapperScan 的属性值
    AnnotationAttributes mapperScanAttrs = AnnotationAttributes
        .fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
    if (mapperScanAttrs != null) {
      // 注册mapper接口类
      registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
          generateBaseBeanName(importingClassMetadata, 0));
    }
  }

  ...
}
复制代码

MapperScannerRegistrar 实现了ImportBeanDefinitionRegistrar,在其registerBeanDefinitions中向spring容器注入了某些beanDefinition,我们继续进入registerBeanDefinitions(...)方法,看看注入了啥:

void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
      BeanDefinitionRegistry registry, String beanName) {

    // 创建 BeanDefinitionBuilder,构建 MapperScannerConfigurer
    BeanDefinitionBuilder builder = BeanDefinitionBuilder
        .genericBeanDefinition(MapperScannerConfigurer.class);
    builder.addPropertyValue("processPropertyPlaceHolders", true);

    // 以下为解析 @MapperScan 的各种属性,并它他们设置到 builder 中
    ...
    // 解析扫描的包路径
    List<String> basePackages = new ArrayList<>();
    basePackages.addAll(Arrays.stream(annoAttrs.getStringArray("value"))
        .filter(StringUtils::hasText).collect(Collectors.toList()));

    ...
    // 添加属性值
    builder.addPropertyValue("basePackage", 
            StringUtils.collectionToCommaDelimitedString(basePackages));

    // 注册 MapperScannerConfigurer 为 beanDefinition
    registry.registerBeanDefinition(beanName, builder.getBeanDefinition());

  }
复制代码

registerBeanDefinitions(...)方法先是为MapperScannerConfigurer创建了一个BeanDefinitionBuilder,然后就是解析@MapperScan的各种属性,并添加到BeanDefinitionBuilder中,然后调用registry.registerBeanDefinition容器中。上述方法是精简后的代码,由于@MapperScan支持的配置比较多,原本的代码非常长,精简后的代码中仅留下了我们设置的value值,它就是这样设置到BeanDefinitionBuilder中的。

因此,整个MapperScannerRegistrar就只是向spring容器中注册了一个类:MapperScannerConfigurer

3.2 MapperScannerConfigurer

接下来我们就把目光指向了MapperScannerConfigurer

/** 
 * 它实现了 BeanDefinitionRegistryPostProcessor
 */ 
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, 
    InitializingBean, ApplicationContextAware, BeanNameAware {

  /**
   * 来自于 BeanDefinitionRegistryPostProcessor 的方法
   * 确切地说,是来自于 BeanFactoryPostProcessor 的方法,因为 BeanFactoryPostProcessor 是
   * BeanDefinitionRegistryPostProcessor 的父类
   */
  @Override
  public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
    // 方法实现为空,没有任何内容,所以spring为何不在接口方法中加个default关键字呢
  }

  /**
   * 来自于 BeanDefinitionRegistryPostProcessor 的方法
   */
  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    if (this.processPropertyPlaceHolders) {
      processPropertyPlaceHolders();
    }

    // 定义扫描类
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    // 设置各种属性,一些是由 @MapperScan 中解析得到的
    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));
    }
    if (StringUtils.hasText(defaultScope)) {
      scanner.setDefaultScope(defaultScope);
    }
    // 注册过滤配置
    scanner.registerFilters();
    // 在这里处理扫描操作
    scanner.scan(
        // 这里就是前面解析的 basePackage
        StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }

}
复制代码

MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor,这是spring提供的BeanFactoryPostProcessor之一,可以在spring执行的早期向容器中注册beanDefinition,它提供了两个方法:postProcessBeanFactory(...)(定制化beanFactory的某些行为)与postProcessBeanDefinitionRegistry(...)(处理beanDefinition注册),由于postProcessBeanFactory(...)方法为空实现,因此我们重点关注postProcessBeanDefinitionRegistry(...)方法。

postProcessBeanDefinitionRegistry(...)方法中,先是创建了一个ClassPathMapperScanner的实例,该实例主要功能就是用来处理MapperInterface的扫描功能的,之后再对ClassPathMapperScanner实例进行一些属性设置,这些属性大部分来自于@@MapperScan的设置,最后调用ClassPathMapperScanner#scan(...)方法也进行扫描操作,在这个方法里,满足条件的MapperInterface会被转化为 BeanDefinition从而注册到spring容器中的。

接下来我们就重点关注ClassPathMapperScanner

3.3 ClassPathMapperScanner

public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
    ...
}
复制代码

可以看到,ClassPathMapperScanner继承了ClassPathBeanDefinitionScanner,关于ClassPathBeanDefinitionScanner,熟悉spring的小伙伴并不陌生,它就是spring中用来处理@ComponentScan注解扫描的类,也就是说,处理@MapperScan的扫描底层使用的也是与@ComponentScan一样的方式。

1. ClassPathMapperScanner#registerFilters(...)

我们知道,spring的@ComponentScan只扫描包下标记了@Component注解的类,而MapperInterface只是个接口,它为何也能扫描呢?这就是ClassPathMapperScanner#registerFilters(...)的功能了,在MapperScannerConfigurer#postProcessBeanDefinitionRegistry中我们也看到了它的调用:

  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    // 省略一些配置  
    ...

    // 注册过滤配置
    scanner.registerFilters();
    // 在这里处理扫描操作
    scanner.scan(
        // 这里就是前面解析的 basePackage
        StringUtils.tokenizeToStringArray(this.basePackage, 
            ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }
复制代码

我们来看看ClassPathMapperScanner的注册过滤配置的方法registerFilters

  public void registerFilters() {
    boolean acceptAllInterfaces = true;

    // 省略一些配置
    ...

    // 表示包下的接口也扫描
    if (acceptAllInterfaces) {
      addIncludeFilter((metadataReader, metadataReaderFactory) -> true);
    }

    // 过滤 package-info.java 文件
    addExcludeFilter((metadataReader, metadataReaderFactory) -> {
      String className = metadataReader.getClassMetadata().getClassName();
      return className.endsWith("package-info");
    });
  }

复制代码

这个方法里注册了一些过滤配置,这里我们只关注acceptAllInterfaces的处理,相关代码为:

if (acceptAllInterfaces) {
  addIncludeFilter((metadataReader, metadataReaderFactory) -> true);
}
复制代码

这表明包下的一切类、接口等都会被扫描为BeanDefinition

2.ClassPathMapperScanner#doScan

准备工作做完后,我们来看看具体的扫描流程,也就是scanner.scan(...)方法,ClassPathMapperScanner并没有重写该方法,调用的 是spring 提供的 ClassPathBeanDefinitionScanner#scan方法中:

public int scan(String... basePackages) {
    int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
    // 这是真正干活的方法
    doScan(basePackages);

    // Register annotation config processors, if necessary.
    if (this.includeAnnotationConfig) {
        AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
    }

    return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}
复制代码

doScan(...)方法ClassPathMapperScanner是有重写的,我们进入ClassPathMapperScanner#doScan方法:

  @Override
  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 {
      // 处理扫描得到的 beanDefinitions
      processBeanDefinitions(beanDefinitions);
    }

    return beanDefinitions;
  }
复制代码

doScan(...)方法继续调用父类的doScan(...)方法进行扫描,然后再调用processBeanDefinitions(...)处理扫描结果,调用super.doScan(basePackages)得到的结果如下:

org.mybatis.spring.demo.mapper包下的UserMapper已经被扫描为beanDefinition了,它的beanClassUserMapper,这是个接口,它该如何进行实例化呢?这就是processBeanDefinitions(...)的功能,我们继续看下去。

3. ClassPathMapperScanner#processBeanDefinitions

ClassPathMapperScanner#processBeanDefinitions方法的代码如下:

  private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    AbstractBeanDefinition definition;
    BeanDefinitionRegistry registry = getRegistry();
    // 遍历得到的 beanDefinitions
    for (BeanDefinitionHolder holder : beanDefinitions) {
      definition = (AbstractBeanDefinition) holder.getBeanDefinition();

      ...
      // 设置构造方法参数
      definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
      // 获取 beanClassName
      String beanClassName = definition.getBeanClassName();

      definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
      // 修改 beanClass 为 MapperFactoryBean
      definition.setBeanClass(this.mapperFactoryBeanClass);

      // 设置 MapperFactoryBean 的 factoryBeanObjectType 为 beanClassName
      definition.setAttribute(FACTORY_BEAN_OBJECT_TYPE, beanClassName);

      boolean explicitFactoryUsed = false;
      // 设置 sqlSessionFactory,这里的 sqlSessionFactoryBeanName 与 sqlSessionFactory 都是
      // 由 @MapperScan 中配置的,如果未配置,这两个属性的值就为 null
      if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
        // 设置 sqlSessionFactory 属性,设置时会调用 setSqlSessionFactory(...) 方法
        definition.getPropertyValues().add("sqlSessionFactory",
            // RuntimeBeanReference 会把传入的字符串解析为 bean
            new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
        explicitFactoryUsed = true;
      } else if (this.sqlSessionFactory != null) {
        // 如果传入的是 sqlSessionFactory,则直接设置值
        definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
        explicitFactoryUsed = true;
      }

      // 设置 sqlSessionTemplate,操作同设置sqlSessionFactory类似
      // 同样地,这里的 sqlSessionTemplateBeanName 与 sqlSessionTemplate 都是
      // 由 @MapperScan 中配置的,如果未配置,这两个属性的值就为 null
      if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
        // 设置 sqlSessionTemplate,这是个beanName,RuntimeBeanReference会beanName解析成对应的bean
        definition.getPropertyValues().add("sqlSessionTemplate",
            new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
        explicitFactoryUsed = true;
      } else if (this.sqlSessionTemplate != null) {
        // 设置 sqlSessionTemplate,直接设置 sqlSessionTemplate 的值
        definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
        explicitFactoryUsed = true;
      }

      if (!explicitFactoryUsed) {
        // 如果 sqlSessionFactory与sqlSessionTemplate都未设置,就使用byType的自动注入方式
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
      }
      
      // 省略了其他代码
      ...

    }
  }
复制代码

这个方法还是有点长的,经过精简后,代码还是比较清晰的,这里先来看看它是如何替换beanDefinition的beanClass的:

  1. 调用definition.getBeanClassName()获取原本的beanClassName,这里得到的beanClassNameorg.mybatis.spring.demo.mapper.UserMapper
  2. 设置构造方法参数为beanClassName,也就是org.mybatis.spring.demo.mapper.UserMapper
  3. definitionbeanClass设置为org.mybatis.spring.mapper.MapperFactoryBean

经过这三步之后,definitionbeanClass就是MapperFactoryBean了,就可以放心大胆地创建MapperFactoryBean实例了:

我们要的是org.mybatis.spring.demo.mapper.UserMapper,但spring给我们创建的却是org.mybatis.spring.mapper.MapperFactoryBean,这两者之间有何关系呢?我们进入MapperFactoryBean一探究竟:

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

  private Class<T> mapperInterface;

  public MapperFactoryBean(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

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

  ...

}
复制代码

从代码来看,它继承了FactoryBean,类型为mapperInterface,也就是我们处理构造方法传入的类型:

definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
复制代码

这样一来,相当于MapperFactoryBeanmapperInterface值为org.mybatis.spring.demo.mapper.UserMapper.

除了构造方法外,ClassPathMapperScanner#processBeanDefinitions方法还为MapperFactoryBean设置了其他的一些值,这里我们主要关注sqlSessionFactorysqlSessionTemplate的设置。

先看sqlSessionFactory的设置,如果@MapperScan中配置了sqlSessionFactoryBeanName,就会将其设置到MapperFactoryBeansqlSessionFactory属性中。如果sqlSessionFactoryBeanName未配置,就看@MapperScan有没有配置sqlSessionFactory,如果配置了,就直接设置。

处理sqlSessionFactoryBeanName设置操作时,有两点需要注意:

  1. 传入的sqlSessionFactoryBeanName是字符串,如果转换为spring bean呢?这就是RuntimeBeanReference的功能了,它会字符串解析为具体的spring bean
  2. MapperFactoryBean并没有sqlSessionFactory的属性,不仅如此,就边它的父类也没有,它该如何设置呢?实际上,在spring中设置属性值,并不需要属性存在,只需要属性对应的setXxx(...)方法存在就可以了,而MapperFactoryBean的父类SqlSessionDaoSupport正好有setSqlSessionFactory(...)方法,因此设置sqlSessionFactory属性时,调用的是setSqlSessionFactory(...)方法。

关于sqlSessionTemplate的设置同sqlSessionFactory非常类似,这里就不再分析了。

在我们的demo中,@MapperScan并没有配置sqlSessionFactoryBeanName/sqlSessionFactory/sqlSessionTemplateName/sqlSessionTemplateMapperFactoryBeansqlSessionFactory是从何而来的呢?

注意看processBeanDefinitions方法,如果sqlSessionFactorysqlSessionTemplateexplicitFactoryUsed的值就为false,接下来会有一个关键操作:

definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
复制代码

这个设置表明,该beanDefinition的注入模式为byType,也就是按类型注入,spring在处理注入时,会搜索MapperFactoryBeansetXxx(),为其设置值。

在我们的示例demo中,MapperFactoryBeansetSqlSessionFactory(...)方法如下:

  public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
    if (this.sqlSessionTemplate == null || sqlSessionFactory 
        != this.sqlSessionTemplate.getSqlSessionFactory()) {
      this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
    }
  }
复制代码

spring 会先获取到setSqlSessionFactory(...)方法,然后获知其参数类型为SqlSessionFactory,接着就从spring容器中找到SqlSessionFactory类型的bean,这个bean我们在Demo01Config已经创建了:

@Configuration
public class Demo01Config {

  @Bean
  public SqlSessionFactory sqlSessionFactory() throws Exception {
    ...
  }

  ...
}
复制代码

找到SqlSessionFactory类型的bean后,就以此bean为参数,调用setSqlSessionFactory(...)方法。至于sqlSessionFactory如何设置到MapperFactoryBean的具体操作,那就是setSqlSessionFactory(...)的操作了。

一句话,spring 提供sqlSessionFactory参数,setSqlSessionFactory(...)负责具体的设置操作。

4. 获取注入的XxxMapperMapperFactoryBean#getObject

MapperInterface注入到spring容器的bean是MapperFactoryBean,在自动注入时,该如何拿到对应的MapperInterface bean 呢?实际上,spring 在处理FactoryBean的自动注入时,会调用FactoryBean#getObject获取FactoryBean实际表示的bean,我们进入MapperFactoryBeangetObject()方法:

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

这个方法分为两部分:getSession()getMapper(),我们先来看getSession()方法,它位于SqlSessionDaoSupport内:

  public SqlSession getSqlSession() {
    return this.sqlSessionTemplate;
  }
复制代码

这个方法只是简单地返回了sqlSessionTemplate。在前面我们并没有设置sqlSessionTemplate,这个sqlSessionTemplate是从何而来呢?通过调试,我们发现它是从setSqlSessionFactory(...)中来的:

public abstract class SqlSessionDaoSupport extends DaoSupport {

  private SqlSessionTemplate sqlSessionTemplate;

  /**
   * 设置 SqlSessionFactory
   */
  public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
    if (this.sqlSessionTemplate == null 
            || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
      this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
    }
  }

  /**
   * 创建 SqlSessionTemplate
   */
  protected SqlSessionTemplate createSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    return new SqlSessionTemplate(sqlSessionFactory);
  }

  ...
}
复制代码

在调用setSqlSessionFactory(...)方法时,会创建sqlSessionTemplate。我们再进入SqlSessionTemplate方法,看看它的处理逻辑:

public class SqlSessionTemplate implements SqlSession, DisposableBean {

  /** 传入的的 sqlSessionFactory */
  private final SqlSessionFactory sqlSessionFactory;

  /** 这就是 sqlSessionProxy */
  private final SqlSession sqlSessionProxy;

  /**
   * SqlSessionTemplate 的构造方法
   */
  public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {
    // 传入的 sqlSessionFactory
    this.sqlSessionFactory = sqlSessionFactory;
    // 又是一个动态代理对象,代理的是 SqlSession 对象
    this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class }, new SqlSessionInterceptor());
    ...
  }

  ...

}
复制代码

SqlSessionTemplate 中在两个重要的成员变量:sqlSessionFactorysqlSessionProxy,这个sqlSessionFactory就是在SqlSessionDaoSupport#setSqlSessionFactory(...)中传入的sqlSessionFactory,而sqlSessionProxySqlSession的代理对象,它的InvocationHandler实例为SqlSessionInterceptor,关于它的妙用我们后面再分析。

回到MapperFactoryBean#getObject()方法,看完了getSession()后,我们再来看看getMapper的逻辑:

public class SqlSessionTemplate implements SqlSession, DisposableBean {

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

  ...
}
复制代码

这个方法比较简单,就是先获取Configuration,然后从Configuration中获取UserMapper的实例,这里的typeUserMapper.classthisSqlSessionTemplate的实例。继续跟进去,到达MapperRegistry#getMapper方法:

    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
        if (mapperProxyFactory == null) {
            throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        } else {
            try {
                // 创建对就的 type 实例
                return mapperProxyFactory.newInstance(sqlSession);
            } catch (Exception var5) {
                throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
            }
        }
    }
复制代码

可以看到,XxxMapper实例是在调用MapperRegistry#getMapper方法时创建的,关于创建流程及mapperProxyFactory,这些是mybatis相关的内容,本文主要关注与spring的整合,这些就不分析了。

经过了以上步骤,MapperFactoryBean#getObject就能成功获取UserMapper实例了,UserService中的UserMapper也能注入成功了。

4. 执行数据库操作

接下来我们来看看数据库的执行操作,也就是userMapper.selectList(...)方法。

关于mybatisXxxMapper的执行流程,在mybatis源码分析02:执行sql语句一文已经详细分析过了,本文不再赘述,本节我们主要关注SqlSessionTemplate的相关操作。

在前面的分析文章中,我们提到mybatis提供的sqlSession默认实现DefaultSqlSession不是线程安全的,在前面得到的UserMapper中,使用的是SqlSessionTemplate,它是线程安全的吗?我们通过调试来进行分析。

userMapper.selectList(...)的调用过程中,经过一系列的调用栈后,最终会来到MapperMethod#executeForMany方法:

MapperMethod#executeForMany方法中,又会调用sqlSession.selectList(...)方法来获取数据,而这个sqlSession就是SqlSessionTemplate

再进入SqlSessionTemplate#selectList(...)方法:

  public <E> List<E> selectList(String statement, Object parameter) {
    return this.sqlSessionProxy.selectList(statement, parameter);
  }
复制代码

它是调用sqlSessionProxy对象的selectList方法进行查询的。对于sqlSessionProxy,上一节在分析SqlSessionTemplate的构造方法时我们提到,它是在SqlSessionTemplate中创建的:

// 又是一个动态代理对象,代理的是 SqlSession 对象
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
    new Class[] { SqlSession.class }, new SqlSessionInterceptor());
复制代码

没错,它是个动态代理对象,它的InvocationHandler实例为SqlSessionInterceptor,执行sqlSessionProxy.selectList(...)时,SqlSessionInterceptor#invoke(...)会先被执行,我们进入SqlSessionInterceptor#invoke(...)方法:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  // 获取 sqlSession,获取到的类型是 DefaultSqlSession
  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)) {
      sqlSession.commit(true);
    }
    return result;
  } catch (Throwable t) {
    ...
  } finally {
    ...
  }
}
复制代码

这个方法里,主要是处理sqlSession的获取操作,得到sqlSession之后会再调用原本方法。这个获取到的sqlSession是不是线程安全的呢?从运行结果来看,它的实际类型依旧是DefaultSqlSession,并不是线程安全的。那这个操作依然不是线程安全的吗?不一定,衡量DefaultSqlSession是否线程安全,并不一定它本身是否线程安全,如果每个线程都有自己的DefaultSqlSessionDefaultSqlSession不会被跨线程共享,那么即使DefaultSqlSession非线程安全,但整个操作还是线程安全的。

每个线程都有自己的DefaultSqlSession的办法或许你已经想到了,就是ThreadLocalmbyatis提供的SqlSessionManager就是这样实现的,而SqlSessionTemplate由于是与spring结合,这块功能它托管给了spring来做,我们且看sqlSession的获取:

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

    // 获取 SqlSessionHolder,托管给了 spring
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager
        .getResource(sessionFactory);

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

    // 新建一个 sqlSession
    session = sessionFactory.openSession(executorType);
    // 注册SessionHolder
    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }
复制代码

从代码来看,SqlSession是由TransactionSynchronizationManager.getResource(...)获取的,获取不到时才会新建。获取到的SqlSession是所有线程共享的,还是一个线程独占的呢?关于TransactionSynchronizationManager.getResource(...),本文并不打算分析,我们直接看它的方法说明:

从这个方法的说明来看,它就是用来返回与当前线程的绑定的资源的,这也就是说,通过TransactionSynchronizationManager.getResource(...)得到的SqlSession就是线程独占的,因此不存在线程安全问题。

另外,据本人验证,只有在开启事务时,SqlSession才会被保存,如果未开启事务,TransactionSynchronizationManager.getResource(...)得到的SqlSession一直为null,每次调用getSqlSession(...)时都会创建一个新的SqlSession.

到了这里,我们就明白了,在执行MapperInterface的方法时,获取到的SqlSession由spring容器托管,并且线程独占。

5. 总结

Este artículo analiza principalmente el proceso de integración de mybatis y spring:

  1. SqlSessionFactoryLa configuración de propiedades de está SqlSessionFactoryBeana cargo de esta clase, que proporciona muchos setXxx(...)métodos para personalizar mybatisuna serie de configuraciones;
  2. @MapperScanLa ruta de escaneo que se puede especificar MapperInterface, estas rutas de paquetes se MapperScannerConfigurerescanearán en la clase y se registrarán en el contenedor de primavera;
  3. La inicial obtenida al escanear la ruta del paquete es beanDefinitionla beanClassinterfaz, que mybatisse convertirá ClassPathMapperScanner#processBeanDefinitionsen , es decir , el tipo real es ;beanDefinitionbeanClassMapperFactoryBeanFactoryBeanMapperInterface
  4. Cuando se obtiene el bean a inyectar , se obtiene el bean correspondiente MapperInterfacea través del método , y el bean obtenido es un objeto proxy dinámico, entre los cuales se encuentra ;MapperFactoryBean#getObject(...)MapperInterfacesqlSessionSqlSessionTemplate
  5. SqlSessionTemplateExiste una variable miembro: sqlSessionProxy, cuyo tipo es SqlSession, cuando se ejecuta el método MapperInterface, eventualmente se usará el método de operación de la base de datos sqlSessionProxyllamado ;SqlSession
  6. Dado que sqlSessionProxyes un objeto de proxy dinámico, cuando se ejecuta un método, pasará a través del Interceptor#invoke(...)método y el método obtendrá el enlace al subproceso actual DefaultSqlSession, que también se utiliza en la ejecución real DefaultSqlSession, evitando así problemas de seguridad del subproceso.

El enlace al texto original de este artículo: my.oschina.net/funcy/blog/… , limitado al nivel personal del autor, inevitablemente hay errores en el texto, ¡bienvenidos a corregirme! La originalidad no es fácil. Para reimpresiones comerciales, comuníquese con el autor para obtener autorización. Para reimpresiones no comerciales, indique la fuente.

Supongo que te gusta

Origin juejin.im/post/7102812278247915533
Recomendado
Clasificación