【MyBatis】MyBatis与Spring和Spring Boot整合原理

目录

一、前言

二、Spring整合MyBatis

[2.1 在Spring中使用MyBatis](#2.1 在Spring中使用MyBatis)

[2.1.1 引入依赖](#2.1.1 引入依赖)

[2.1.2 完成初始化](#2.1.2 完成初始化)

[2.1.3 测试代码](#2.1.3 测试代码)

[2.1.4 小结](#2.1.4 小结)

[2.2 源码分析](#2.2 源码分析)

[2.2.1 SqlSessionFactoryBean](#2.2.1 SqlSessionFactoryBean)

[2.2.1.1 事务集成](#2.2.1.1 事务集成)

[2.2.2 Mapper接口注册](#2.2.2 Mapper接口注册)

[2.2.2.1 Spring是怎么管理Mapper接口的动态代理的?](#2.2.2.1 Spring是怎么管理Mapper接口的动态代理的?)

[2.2.2.1.1 Spring中Bean的产生过程](#2.2.2.1.1 Spring中Bean的产生过程)

[2.2.2.1.2 如何将Mapper的代理对象加入到Spring容器中](#2.2.2.1.2 如何将Mapper的代理对象加入到Spring容器中)

[2.2.2.1.3 最终的解决方案](#2.2.2.1.3 最终的解决方案)

[2.2.2.1.3.1 FactoryBean](#2.2.2.1.3.1 FactoryBean)

[2.2.2.1.3.2 Import](#2.2.2.1.3.2 Import)

[2.2.2.1.4 小结](#2.2.2.1.4 小结)

[2.2.2.2 创建Mapper接口的代理对象,并将其注册到Spring容器的源码分析](#2.2.2.2 创建Mapper接口的代理对象,并将其注册到Spring容器的源码分析)

[2.3 总结流程图](#2.3 总结流程图)

[三、Spring Boot整合MyBatis](#三、Spring Boot整合MyBatis)


一、前言

在真实的项目我们几乎不会将MyBatis 单独运用到项目中,而是将其整合到Spring框架或者Spring Boot中,本文将通过讲解MyBatis 与Spring和Spring Boot的整合的方法和原理。

二、 Spring 整合 MyBatis

Spring整合MyBatis的原理也是一道非常高频的面试题, Spring中集成Mybatis主要有两个难点,一是事务的集成,二是Mapper接口注册到Spring****容器中,Spring中集成MyBatis需要引入mybatismybatis-spring依赖包。

在Spring中我们通过mybatis-spring 中间框架将MyBatis和Spring 两个完全不相关的框架整合起来

该框架一方面负责加载和解析MyBatis相关配置,另一方面,该框架还会通过Spring提供的扩展点,把各种Dao接口对应的对象放入IOC容器中。

2.1 Spring 中使用 MyBatis

2.1.1 引入依赖

XML 复制代码
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.3</version>
</dependency>

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>2.0.3</version>
</dependency>

2.1.2 完成初始化

JavaConfig + 注解完成初始化:

我们可以通过JavaConfig + 注解将SqlSessionFactoryBean注册到Spring容器中,以及通过**@MapperScan@MapperScans**确定将哪些Mapper接口注册到Spring容器中来完成初始化。

java 复制代码
@Configuration
// 将这个包下面的所有Mapper接口注册到Spring容器中
@MapperScan(basePackages = {"com.eleven.icode.ispring.mapper"})
public class MyBatisConfig {
  
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) throws IOException {
        // 创建SqlSessionFactoryBean
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        // 解析全局配置文件和SQL映射文件
        // 配置数据源
        factoryBean.setDataSource(dataSource);
        // 配置全局配置文件
        factoryBean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
        // 配置SQL映射文件
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
        // 返回SqlSessionFactoryBean,将其注册到Spring容器中
        return factoryBean;
    }
}

XML 配置文件完成初始化:

我们也可以通过在Spring的配置文件SpringContext.xml中配置这些Bean,这些Bean主要用于配置数据源,配置sqlSessionFactory用于生成SqlSession,配置MapperScannerConfigurer用于扫描接口,这种方式也可以将配置的组件注册到Spring容器中,来完成初始化。配置文件如下:

XML 复制代码
<context:property-placeholder  location="jdbc.properties"/>
<context:component-scan base-package="com.jay"/>
<!--配置数据源-->
<bean id="dataSource" class="org.apache.ibatis.datasource.pooled.PooledDataSource">
    <!-- 配置与数据库交互的4个必要属性 -->
    <property name="driver" value="${jdbc.driver}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>
<!--配置sqlSessionFactory-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <!-- 给 SqlSessionFactory 配置数据源,这里引用上面的数据源配置 -->
    <property name="dataSource" ref="dataSource"/>
    <!--配置MyBatis-cfg.xml的路径-->
    <property name="configLocation" value="MyBatis-cfg.xml"/>
    <!--配置映射文件-->
    <property name="mapperLocations" value="mybatis/*.xml"/>
</bean>
<!--配置MapperScannerConfigurer-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <!--配合Dao接口所在的包-->
    <property name="basePackage" value="com.jay.mapper"/>
</bean>

不管用上面的哪种方法,我们都需要配置一下MyBatis的配置文件MyBatis-cfg.xml(具体内容在之前的笔记中已经讲过了:MyBatis的配置文件详解),配置如下:

XML 复制代码
<configuration>
     <settings>
         <setting name="cacheEnabled" value="true"/>
     </settings>
    <typeAliases>
        <typeAlias type="com.jay.entity.ClassRoom" alias="ClassRoom"/>
        <typeAlias type="com.jay.entity.Student" alias="Student"/>
    </typeAliases>
</configuration>

需要注意的是,MyBatis-cfg.xml 中的配置除了settings元素是必须配置在MyBatis的配置文件中,其余元素都可以配置到Spring的配置文件中。

2.1.3 测试代码

配置文件处理完成之后,接着我们来新建一个映射文件以及Dao 接口测试下。此处我新建了一个名为StudentMapper.xml的映射文件:

XML 复制代码
<mapper namespace="com.jay.mapper.StudentMapper">
    <resultMap id="studentResult" type="Student">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="age" column="age"/>
        <result property="sexEnum" column="sex"
                typeHandler="com.jay.Handler.GeneralEnumHandler"/>
        <association property="classRoom" javaType="ClassRoom">
            <id property="id" column="id"/>
            <result property="name" column="name"/>
        </association>
    </resultMap>
    
    <resultMap id="classRoomResult" type="ClassRoom">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
    </resultMap>
    <select id="selectStudentById" resultMap="studentResult">
        SELECT s.id,s.`name`,s.age,s.sex,c.id,c.`name` FROM student s, class c
        WHERE s.class_id=c.id AND s.id=#{id}
    </select>
</mapper>

对应的Dao接口如下:

java 复制代码
@Repository
public interface StudentMapper {
    /**
     * 根据id查询学生
     * @param id
     * @return
     */
    Student selectStudentById(Integer id);
}

最后我们新建一个测试类,测试下结果:

java 复制代码
@RunWith(SpringJUnit4ClassRunner.class)
// 配置Spring配置
@ContextConfiguration("classpath:SpringContext.xml")
public class StudentMapperTest {
    @Autowired
    private StudentMapper studentMapper;
    @Test
    public void testSelectStudentById() {
        Student student = studentMapper.selectStudentById(1);
        System.out.println(student.toString());
    }
}

测试结果如下:

2.1.4 小结

至此我们MyBatis与Spring的整合就完成了,当然这是一个很小的示范demo,但是其包括了Spring与MyBatis整合的要点。其流程无非就是:

  1. 先引入相关的依赖,Spring的依赖和MyBatis的依赖等。
  2. 接着就是对数据源,SqlSessionFactory等信息进行配置。
  3. 最后就是编写映射文件和Dao接口。

下面我们就深入源码,讲解Spring整合MyBatis的底层原理。

我们就以Spring初始化MyBatis为讲解入口,也就是上面讲过的代码2.1.2 完成初始化中:1、构造SqlSessionFactoryBean将其注册到Spring容器;2、将Mapper接口注册到Spring容器 这两个步骤来进行源码分析。

2.2 源码分析

2.2.1 SqlSessionFactoryBean

SqlSessionFactoryBean是一个FactoryBean,且重写了getObjectType方法将代理的Bean的类型设置为了原始的SqlSessionFactory类型,SqlSessionFactoryBean实现了InitializingBean接口,主要逻辑都在其实例化完成时调用的afterPropertiesSet()(实现了InitializingBean接口的类,可以实现这个接口的afterPropertiesSet()方法,这个方法会在Bean初始化的时候被调用),其作用就是创建Mybatis的SqlSessionFactory,其可以代替全局配置文件,若设置了配置文件可对配置文件内容做一个补充

java 复制代码
// SqlSessionFactoryBean是Spring整合MyBatis时使用的类,如果只是单独MyBatis框架的话,是不用这个类的。所以前面单独讲MyBatis源码的时候并没有出现这个类,毕竟这个类都实现了Spring提供的InitializingBean接口,这也进一步说明了这个类是和Spring有关的。
package org.mybatis.spring;
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
	    // 这个是MyBatis提供的构造类,在前面也已经讲过了:SqlSessionFactoryBuilder类,Spring也要通过它来初始化框架
	  private SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
	
	  // SqlSessionFactoryBean实现InitializingBean接口,需要实现其afterPropertiesSet()。SqlSessionFactoryBean 初始化的时候Spring会自动调用这个方法
	  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");
	      // 在方法buildSqlSessionFactory()中通过SqlSessionFactoryBean的成员属性sqlSessionFactoryBuilder对象来构建我们的sqlSessionFactory
	      this.sqlSessionFactory = buildSqlSessionFactory();
	  }
	
	  protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
	      // 声明一个Configuration对象用于保存mybatis的所有的配置信息
	      final Configuration targetConfiguration; 
	           // 声明一个XMLConfigBuilder对象,用于解析XML配置文件,这个类前面也讲过了:XMLConfigBuilder
	      XMLConfigBuilder xmlConfigBuilder = null;
	
	      // 初始化configuration对象,和设置其 `configuration.variables` 属性
	      if (this.configuration != null) {
	          // 把配置的SqlSessionFactoryBean配置的configuration赋值给targetConfiguration
	          targetConfiguration = this.configuration;
	          if (targetConfiguration.getVariables() == null) {
	              targetConfiguration.setVariables(this.configurationProperties);
	          } else if (this.configurationProperties != null) {
	              targetConfiguration.getVariables().putAll(this.configurationProperties);
	          }
	      }
	
	      // 创建xml配置构建器对象(xmlConfigBuilder),对全局配置文件mybatis-config.xml配置文件进行解析
	      else if (this.configLocation != null) { 
	          xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
	          targetConfiguration = xmlConfigBuilder.getConfiguration();
	      } else {
	          targetConfiguration = new Configuration();
	          // 判断configurationProperties不为空,则调用targetConfiguration.set方法把configurationProperties注入到Configuration对象中
	          Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);
	      }
	
	      Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory);
	      Optional.ofNullable(this.objectWrapperFactory).ifPresent(targetConfiguration::setObjectWrapperFactory);
	      Optional.ofNullable(this.vfs).ifPresent(targetConfiguration::setVfsImpl);
	
	      // typeAliasesPackage配置情况分为二种:在mybaits-config.xml中配置,在配置SqlSessionFactoryBean时配置
	      if (hasLength(this.typeAliasesPackage)) {
	          // 扫描typeAliasesPackage包路径下所有实体类class类型进行过滤注册到Configuration的别名映射器中
	          scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType).stream()
	              .filter(clazz -> !clazz.isAnonymousClass()).filter(clazz -> !clazz.isInterface())
	              .filter(clazz -> !clazz.isMemberClass()).forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias);
	      }
	      // 判断SqlSessionFactory是否配置了typeAliases,一般typeAliasesPackage配置了就没有必要配置typeAliases,注册到Configuration的别名映射器中
	      if (!isEmpty(this.typeAliases)) {
	          Stream.of(this.typeAliases).forEach(typeAlias -> { targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias);});
	      }
	
	      // 把自定义的插件注册到的mybatis的配置类上
	      if (!isEmpty(this.plugins)) {
	          Stream.of(this.plugins).forEach(plugin -> { targetConfiguration.addInterceptor(plugin);});
	      }
	
	      // 扫描自定义的类型处理器(用来处理的java类型和数据库类型的转化) 并且注册到的 targetConfiguration(批量注册)
	      if (hasLength(this.typeHandlersPackage)) {
	          scanClasses(this.typeHandlersPackage, TypeHandler.class).stream().filter(clazz -> !clazz.isAnonymousClass())
	              .filter(clazz -> !clazz.isInterface()).filter(clazz -> !Modifier.isAbstract(clazz.getModifiers()))
	              .forEach(targetConfiguration.getTypeHandlerRegistry()::register);
	      }
	
	      // 通过配置<TypeHandlers></TypeHandlers>的形式来注册的类型处理器对象
	      if (!isEmpty(this.typeHandlers)) { 
	          Stream.of(this.typeHandlers).forEach(typeHandler -> { targetConfiguration.getTypeHandlerRegistry().register(typeHandler);});
	      }
	
	      // Mybatis从3.2开始支持可插拔的脚本语言,因此可插入一种语言的驱动来写基于这种语言的动态SQL查询
	      if (!isEmpty(this.scriptingLanguageDrivers)) {
	          Stream.of(this.scriptingLanguageDrivers).forEach(languageDriver -> { targetConfiguration.getLanguageRegistry().register(languageDriver);});
	      }
	      Optional.ofNullable(this.defaultScriptingLanguageDriver).ifPresent(targetConfiguration::setDefaultScriptingLanguage);
	
	      // 设置数据库厂商
	      if (this.databaseIdProvider != null) { 
	          try {
	              targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
	          } catch (SQLException e) {
	              throw new NestedIOException("Failed getting a databaseId", e);
	          }
	      }
	
	      // 若二级缓存配置不为空,注册二级缓存
	      Optional.ofNullable(this.cache).ifPresent(targetConfiguration::addCache); 
	      if (xmlConfigBuilder != null) {
	          try {
	              // 真正的解析的配置(mybatis-config.xml)的document对象
	              xmlConfigBuilder.parse();
	          } catch (Exception ex) {
	              throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
	          } finally {
	              ErrorContext.instance().reset();
	          }
	      }
	
	      // 为的configuration设置一个环境变量
	      targetConfiguration.setEnvironment(new Environment(this.environment, this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory, this.dataSource));
	
	      // 循环遍历解析mapper.xml文件
	      if (this.mapperLocations != null) {
	          if (this.mapperLocations.length == 0) {
	          } else {
	              for (Resource mapperLocation : this.mapperLocations) {
	                  if (mapperLocation == null) {
	                      continue;
	                  }
	                  try {
	                      XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
	                      // 真正的解析mapper.xml文件。这个过程我们在前面也已经讲过了:解析SQL映射文件(解析XML映射文件)
	                      xmlMapperBuilder.parse(); 
	                  } catch (Exception e) {
	                      throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
	                  } finally {
	                      ErrorContext.instance().reset();
	                  }
	              }
	          }
	      }
	
	      // 通过建造者模式构建的SqlSessionFactory对象 默认是DefaultSqlSessionFactory。整个流程前面都已经讲过:SqlSessionFactoryBuilder类
	      return this.sqlSessionFactoryBuilder.build(targetConfiguration); 
	  }
	
	
	  private Set<Class<?>> scanClasses(String packagePatterns, Class<?> assignableType) throws IOException {
	      Set<Class<?>> classes = new HashSet<>();
	      // 把的多个配置报路径转换成字符串数组
	      String[] packagePatternArray = tokenizeToStringArray(packagePatterns, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
	      // 循环的包路径
	      for (String packagePattern : packagePatternArray) { 
	          // 把包路径下的class解析成的Resouce数组
	          Resource[] resources = RESOURCE_PATTERN_RESOLVER.getResources(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(packagePattern) + "/**/*.class");
	          for (Resource resource : resources) {
	              try {
	                  // 挨个解析成的class类型
	                  ClassMetadata classMetadata = METADATA_READER_FACTORY.getMetadataReader(resource).getClassMetadata();
	                  Class<?> clazz = Resources.classForName(classMetadata.getClassName());
	                  // 判断当前的class类型是不是被支持
	                  if (assignableType == null || assignableType.isAssignableFrom(clazz)) {
	                      // 加入到结合中
	                      classes.add(clazz); 
	                  }
	              } catch (Throwable e) {
	                  LOGGER.warn(() -> "Cannot load the '" + resource + "'. Cause by " + e.toString());
	              }
	          }
	      }
	      return classes;
	  }
	  
	
	    // 重写了getObjectType方法将代理的Bean的类型设置为了原始的SqlSessionFactory类型
	  public Class<? extends SqlSessionFactory> getObjectType() {
	      return this.sqlSessionFactory == null ? SqlSessionFactory.class : this.sqlSessionFactory.getClass();
	  }
	

		/**
		 * 实现FactoryBean接口的getObject方法
		 * 将SqlSessionFactory对象注入spring容器
		 * {@inheritDoc}
		 */
		@Override
		public SqlSessionFactory getObject() throws Exception {
		    if (this.sqlSessionFactory == null) {
		        afterPropertiesSet();
		    }
		    return this.sqlSessionFactory;
		}

}

可知SqlSessionFactoryBean主要通过对applicationContext.xml解析来完成对Configuration的实例化以及对映射文件*mapper.xml的解析。

关键点:

  1. *XMLConfigBuilder:在MyBatis中主要负责解释mybatis-config.xml
  2. 解析完后,如果我们自己设置了则使用我们的设置的进行覆盖,不做一一介绍了
  3. XMLMapperBuilder:负责解析映射配置文件
  4. targetConfiguration.setEnvironment 这里注意一下,事务工厂会使用一个新的new SpringManagedTransactionFactory(),而不是MyBatis之前的ManagedTransactionFactory。这个SpringManagedTransactionFactory会使用Spring事务中的dataSource,从而达到跟事务集成。
2.2.1.1 事务集成

事务集成是在SqlSessionFactoryBean中完成的,在为configuration设置环境变量时创建传入SpringManagedTransactionFactory,在MyBatis中是使用的ManagedTransactionFactory创建一个ManagedTransaction,其是直接从DataSource中获取的Connection

java 复制代码
public class DefaultSqlSessionFactory implements SqlSessionFactory {
    private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) {
        if (environment == null || environment.getTransactionFactory() == null) {
            return new ManagedTransactionFactory();
        }
        return environment.getTransactionFactory();
    }
}

public class ManagedTransactionFactory implements TransactionFactory {
    public Transaction newTransaction(Connection conn) {
        return new ManagedTransaction(conn, closeConnection);
    }
    public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
        return new ManagedTransaction(ds, level, closeConnection);
    }
}

public class ManagedTransaction implements Transaction {
    public Connection getConnection() throws SQLException {
        if (this.connection == null) {
            openConnection();
        }
        return this.connection;
    }
    protected void openConnection() throws SQLException {
        this.connection = this.dataSource.getConnection();
        if (this.level != null) {
            this.connection.setTransactionIsolation(this.level.getLevel());
        }
    }
}

SpringManagedTransactionFactory中创建的是SpringManagedTransaction,其获取的数据库连接是通过DataSourceUtils工具类从事务同步管理器****TransactionSynchronizationManager中获取的连接,从而达到与Spring事务集成的目的。

java 复制代码
public class SpringManagedTransactionFactory implements TransactionFactory {
    public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
        return new SpringManagedTransaction(dataSource);
    }
}

public class SpringManagedTransaction implements Transaction {
    public Connection getConnection() throws SQLException {
        if (this.connection == null) {
            openConnection();
        }
        return this.connection;
    }
    private void openConnection() throws SQLException {
        this.connection = DataSourceUtils.getConnection(this.dataSource);
        this.autoCommit = this.connection.getAutoCommit();
        this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
    }
}

2.2.2 Mapper 接口注册

2.2.2.1 Spring 是怎么管理 Mapper 接口的动态代理的 ?

面试的时候会经常问到Spring整合MyBatis的问题,而在这个问题上,我们重点要关注的就是这个代理对象。因为Spring整合MyBatis的目的就是:把某个Mapper的代理对象作为一个bean放入Spring容器中,使得能够像使用一个普通bean一样去使用这个代理对象,比如能被@Autowire自动注入。

比如当Spring和Mybatis整合之后,我们就可以使用如下的代码来使用MyBatis中的代理对象了:

java 复制代码
@Component
public class UserService {
    // 自动注入
    @Autowired
    private UserMapper userMapper;
    public User getUserById(Integer id) {
        return userMapper.selectById(id);
    }
}

UserService中的userMapper属性就会被自动注入MyBatis中的代理对象。如果你基于一个已经完成整合的项目去调试即可发现,userMapper的类型为:org.apache.ibatis.binding.MapperProxy@41a0aa7d。证明确实是MyBatis中的代理对象。(之前我们已经讲过了MyBatis给Mapper接口创建代理类的原理:3.1.1 为 Mapper 接口创建代理对象)

好,那么现在我们要解决的问题的就是:如何能够把MyBatis的代理对象作为一个bean放入Spring容器中?

要解决这个,我们需要对Spring的bean生成过程有一个了解。

2.2.2.1.1 Spring Bean 的产生过程

这一块的内容我们讲Spring源码的时候已经学过了(Spring IoC 容器源码分析------ClassPathXmlApplicationContext和Spring IoC 容器源码分析------AnnotationConfigApplicationContext),这里就简单复习一下。

Spring启动过程中,大致会经过如下步骤去生成bean:

  1. 扫描指定的包路径下的class文件
  2. 根据class信息生成对应的BeanDefinition
  3. 在此处,程序员可以利用某些机制去修改BeanDefinition
  4. 根据BeanDefinition生成bean实例
  5. 把生成的bean实例放入Spring容器中

假设有一个A类,假设有如下代码:

一个A类:

java 复制代码
@Component
public class A {
}

一个B类,不存在@Component注解

java 复制代码
public class B {
}

执行如下代码:

java 复制代码
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println(context.getBean("a"));

输出结果为:com.tulingxueyuan.beans.A@6acdbdf5

A类对应的bean对象类型仍然为A类。但是我们可以认为的改变这个结果,比如我们可以利用BeanFactory后置处理器来修改BeanDefinition,我们添加一个BeanFactory后置处理器:

java 复制代码
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        BeanDefinition beanDefinition = beanFactory.getBeanDefinition("a");
        beanDefinition.setBeanClassName(B.class.getName());
    }
}

这样就会导致,原本的A类对应的BeanDefiniton被修改了,被修改成了B类,那么后续正常生成的bean对象的类型就是B类。此时,调用如下代码会报错:

context.getBean(A.class);

但是调用如下代码不会报错,尽管B类上没有@Component注解:

context.getBean(B.class);

并且,下面代码返回的结果是:com.tulingxueyuan.beans.B@4b1c1ea0

java 复制代码
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println(context.getBean("a"));

之所以讲这个问题,是想说明一个问题:在Spring中,bean对象跟class没有直接关系,跟BeanDefinition才有直接关系

在Spring中,如果你想生成一个bean,那么得先生成一个BeanDefinition,就像你想new一个对象实例,得先有一个class。

那么回到我们要解决的问题:如何能够把MyBatis的代理对象作为一个bean放入Spring容器中?

2.2.2.1.2 如何将 Mapper 的代理对象加入到 Spring 容器中

继续回到我们的问题,我们现在想自己生成一个bean,那么得先生成一个BeanDefinition,只要有了BeanDefinition,通过在BeanDefinition中设置bean对象的类型,然后把BeanDefinition添加给Spring,Spring就会根据BeanDefinition自动帮我们生成一个类型对应的bean对象。

所以,现在我们要解决两个问题:

  1. MyBatis的代理对象的类型是什么?因为我们要设置给BeanDefinition
  2. 我们怎么把BeanDefinition添加给Spring容器?

注意:上文中我们使用的BeanFactory后置处理器,它只能修改BeanDefinition,并不能新增一个BeanDefinition。我们应该使用Import技术来添加一个BeanDefinition。后文再详细介绍如果使用Import技术来添加一个BeanDefinition,可以先看一下伪代码实现思路。

假设:我们有一个UserMapper接口,它的代理对象的类型为UserMapperProxy。

那么我们的思路就是这样的,伪代码如下:

java 复制代码
// 创建BeanDefinitoin
BeanDefinitoin bd = new BeanDefinitoin();
// 将代理类设置给BeanDefinitoin
bd.setBeanClassName(UserMapperProxy.class.getName());
// 将设置好的BeanDefinitoin注入到Spring容器中
SpringContainer.addBd(bd);

但是,这里有一个严重的问题,就是上文中的UserMapperProxy是我们假设的,它表示一个代理类的类型,然而MyBatis中的代理对象是利用的JDK的动态代理技术实现的,也就是代理对象的代理类是动态生成的,我们根本无法确定代理对象的代理类到底是什么。

所以回到我们的第一个问题:MyBatis的代理对象的类型是什么?

这个问题可能有两种答案:

  1. 代理对象对应的代理类
  2. 代理对象对应的接口

由上面的分析可知,答案1肯定是不可能了,因为代理类是动态生成的,那么我们来看答案2:代理对象对应的接口

如果我们采用答案2,那么我们的思路就是:

java 复制代码
BeanDefinition bd = new BeanDefinitoin();
// 注意这里,设置的是UserMapper
bd.setBeanClassName(UserMapper.class.getName());
SpringContainer.addBd(bd);

但是,实际上给BeanDefinition对应的类型设置为一个接口是行不通的,因为Spring没有办法根据这个BeanDefinition去new出对应类型的实例,接口是没法直接new出实例的。

那么现在问题来了,我要解决的问题:MyBatis的代理对象的类型是什么?

两个答案都被我们否定了,所以这个问题是无解的,所以我们不能再沿着这个思路去思考了,只能回到最开始的问题:如何能够把MyBatis的代理对象作为一个bean放入Spring容器中?

总结上面的推理:我们想通过设置BeanDefinition的class类型,然后由Spring自动的帮助我们去生成对应的bean,但是这条路是行不通的

2.2.2.1.3 最终的解决方案

那么我们还有没有其他办法,可以去生成bean呢?并且生成bean的逻辑不能由Spring来帮我们做了,得由我们自己来做。

2.2.2.1.3.1 FactoryBean

还是有一个解决方案的,那就是Spring中的FactoryBean接口。我们可以利用FactoryBean接口实现一个工厂类的Bean,去自定义我们要生成的bean对象,比如:

java 复制代码
@Component
public class MyFactoryBean implements FactoryBean {

    @Override
    public Object getObject() throws Exception {
        // 通过JDK动态代理创建代理对象
        Object proxyInstance = Proxy.newProxyInstance(MyFactoryBean.class.getClassLoader(), new Class[]{UserMapper.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                if (Object.class.equals(method.getDeclaringClass())) {
                    return method.invoke(this, args);
                } else {
                    // 执行代理逻辑
                    return null;
                }
            }
        });
        return proxyInstance;
    }

    @Override
    public Class<?> getObjectType() {
        return UserMapper.class;
    }
}

我们定义了一个MyFactoryBean,它实现了FactoryBean接口,getObject方法就是用来自定义生成bean对象逻辑的。

执行如下代码:

java 复制代码
public class Test {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        System.out.println("myFactoryBean: " + context.getBean("myFactoryBean"));
        System.out.println("&myFactoryBean: " + context.getBean("&myFactoryBean"));
        System.out.println("myFactoryBean-class: " + context.getBean("myFactoryBean").getClass());
    }
}

将打印:

myFactoryBean: com.tulingxueyuan.beans.myFactoryBean$1@4d41cee

&myFactoryBean: com.tulingxueyuan.beans.myFactoryBean@3712b94

myFactoryBean-class: class com.sun.proxy.$Proxy20

从结果我们可以看到,从Spring容器中拿名字为"myFactoryBean"的bean对象,就是我们所自定义的jdk动态代理所生成的代理对象。

所以,我们可以通过FactoryBean来向Spring容器中添加一个自定义的bean对象。上文中所定义的MyFactoryBean对应的就是UserMapper,表示我们定义了一个MyFactoryBean,相当于把UserMapper对应的代理对象作为一个bean放入到了容器中。

但是作为程序员,我们不可能每定义了一个Mapper,还得去定义一个MyFactoryBean,这是很麻烦的事情,我们改造一下MyFactoryBean,让他变得更通用,比如:

java 复制代码
@Component
public class MyFactoryBean implements FactoryBean {
    // 注意这里
    private Class mapperInterface;
    
    public MyFactoryBean(Class mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @Override
    public Object getObject() throws Exception {
        Object proxyInstance = Proxy.newProxyInstance(MyFactoryBean.class.getClassLoader(), new Class[]{mapperInterface}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                if (Object.class.equals(method.getDeclaringClass())) {
                    return method.invoke(this, args);
                } else {
                    // 执行代理逻辑
                    return null;
                }
            }
        });
        return proxyInstance;
    }

    @Override
    public Class<?> getObjectType() {
        return mapperInterface;
    }
}

改造MyFactoryBean之后,MyFactoryBean变得灵活了,可以在构造MyFactoryBean时,通过构造传入不同的Mapper接口。

实际上MyFactoryBean也是一个Bean,我们也可以通过生成一个BeanDefinition来生成一个MyFactoryBean,并给构造方法的参数设置不同的值,比如伪代码如下:

java 复制代码
BeanDefinition bd = new BeanDefinitoin();
// 注意一:设置的是MyFactoryBean
bd.setBeanClassName(MyFactoryBean.class.getName());
// 注意二:表示当前BeanDefinition在生成bean对象时,会通过调用MyFactoryBean的构造方法来生成,并传入UserMapper
bd.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class.getName())
SpringContainer.addBd(bd);

特别说一下注意二,表示表示当前BeanDefinition在生成bean对象时,会通过调用MyFactoryBean的构造方法来生成,并传入UserMapper的Class对象。那么在生成MyFactoryBean时就会生成一个UserMapper接口对应的代理对象作为bean了。

到此为止,其实就完成了我们要解决的问题:把MyBatis中的代理对象作为一个bean放入Spring容器中。只是我们这里是用简单的JDK代理对象模拟的MyBatis中的代理对象,这是为了让我们快速了解Spring将MyBatis的Mapper代理对象加入Spring容器的原理,在后面的章节我们会针对实际的源码进行分析。

2.2.2.1.3.2 Import

到这里,我们还有一个事情没有做,就是怎么真正的定义一个BeanDefinition,并把它添加到Spring中,上文说到我们要利用Import技术,比如可以这么实现:

定义如下类:

java 复制代码
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // 创建BeanDefinition构造器对象
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
        // 通过构造器创建一个BeanDefinition
        AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
        beanDefinition.setBeanClass(MyFactoryBean.class);
        beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class);
        // 添加beanDefinition
        registry.registerBeanDefinition("my"+UserMapper.class.getSimpleName(), beanDefinition);
    }
}

并且在JavaConfig类AppConfig上添加@Import注解:

java 复制代码
@Import(MyImportBeanDefinitionRegistrar.class)
public class AppConfig {
}

这样在启动Spring时就会新增一个BeanDefinition,该BeanDefinition会生成一个MyFactoryBean对象,并且在生成MyFactoryBean对象时会传入UserMapper.class对象,通过MyFactoryBean内部的逻辑,相当于会自动生产一个UserMapper接口的代理对象作为一个bean。

2.2.2.1.4 小结

总结一下,通过我们的分析,我们要整合Spring和Mybatis,需要我们做的事情如下:

  1. 定义一个MyFactoryBean
  2. 定义一个MyImportBeanDefinitionRegistrar
  3. 在AppConfig上添加一个注解@Import(MyImportBeanDefinitionRegistrar.class)

这样就可以基本完成整合的需求了,当然还有两个点是可以优化的。

第一,单独再定义一个@MyScan的注解,如下:

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Import(MyImportBeanDefinitionRegistrar.class)
public @interface MyScan {
}

这样在AppConfig上直接使用@MyScan即可

第二,在MyImportBeanDefinitionRegistrar中,我们可以去扫描Mapper,在MyImportBeanDefinitionRegistrar我们可以通过AnnotationMetadata获取到对应的@MyScan注解,所以我们可以在@MyScan上设置一个value,用来指定待扫描的包路径。然后在MyImportBeanDefinitionRegistrar中获取所设置的包路径,然后扫描该路径下的所有Mapper,生成BeanDefinition,放入Spring容器中。

所以,到此为止,Spring整合Mybatis的核心原理就结束了,再次总结一下:

  1. 定义一个MyFactoryBean,用来将Mybatis的代理对象生成一个bean对象
  2. 定义一个MyImportBeanDefinitionRegistrar,用来生成不同Mapper对象的MyFactoryBean
  3. 定义一个@MynScan,用来在启动Spring时执行MyImportBeanDefinitionRegistrar的逻辑,并指定包路径

以上这个三个要素分别对象org.mybatis.spring中的:

  1. MapperFactoryBean
  2. MapperScannerRegistrar
  3. @MapperScan

经过上面的分析,相信大家已经明白了将Mapper代理对象加入到Spring容器的原理,下面就带大家分析真实的源码。

2.2.2.2 创建 Mapper 接口的代理对象,并将其注册到 Spring 容器的源码分析

Mapper****接口扫描是通过**@MapperScan@MapperScans注解中导入了MapperScannerRegistrar来完成的,Mapper****接口注册到Spring容器中是借助FactoryBean来完成的,注解中设置了factoryBean注册Mapper接口的默认值为MapperFactoryBean**。

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
    String[] value() default {};
    String[] basePackages() default {};
    Class<?>[] basePackageClasses() default {};
    Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
    Class<? extends Annotation> annotationClass() default Annotation.class;
    Class<?> markerInterface() default Class.class;
    String sqlSessionTemplateRef() default "";
    String sqlSessionFactoryRef() default "";
    Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class;
    String lazyInitialization() default "";
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.RepeatingRegistrar.class)
public @interface MapperScans {
    MapperScan[] value();
}

MapperScannerRegistrar实现了ImportBeanDefinitionRegistrar接口,在容器扫描BeanDefinition时会调用registerBeanDefinitions。其作用其实就是为容器注册一个或多个MapperScannerConfigurer以及给该类设置一些从**@MapperScan注解上解析出来的属性。@MapperScans注解上可以配置多个@MapperScan注解在导入时通过RepeatingRegistrar遍历注册多个MapperScannerConfigurer**。

java 复制代码
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // 从传入的配置类中解析@MapperScan注解信息,把MapperScan注解的属性转化为AnnotationAttributes类型
        AnnotationAttributes mapperScanAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));

               // 若上一步解析出来的mapperScanAttrs不为空,说明配置类上加了@MapperScan注解
        if (mapperScanAttrs != null) { 
            // 调用重写的方法registerBeanDefinitions#generateBaseBeanName(importingClassMetadata, 0)即将注册的bean定义的名称进行处理
            registerBeanDefinitions(mapperScanAttrs, registry, generateBaseBeanName(importingClassMetadata, 0));
        }
    }

    void registerBeanDefinitions(AnnotationAttributes annoAttrs, BeanDefinitionRegistry registry, String beanName) {
        // 创建bean定义构造器通过够构造器来构建出的bean定义<MapperScannerConfigurer>应用到的设计模式[建造者模式]
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
        // 手动为MapperScannerConfigurer开启processPropertyPlaceHolders属性为true,需要着重研究下MapperScannerConfigurer类的继承结构
        builder.addPropertyValue("processPropertyPlaceHolders", true);
        // 为的MapperScannerConfigurer解析@MapperScanner指定扫描的的注解类型
        Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass");
        if (!Annotation.class.equals(annotationClass)) {
            builder.addPropertyValue("annotationClass", annotationClass);
        }
        Class<?> markerInterface = annoAttrs.getClass("markerInterface");
               // 是否配置了标记接口
        if (!Class.class.equals(markerInterface)) { 
            builder.addPropertyValue("markerInterface", markerInterface);
        }
        Class<? extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator");
               // 是否设置了MapperScannerConfigurer的beanName生成器对象
        if (!BeanNameGenerator.class.equals(generatorClass)) { 
            builder.addPropertyValue("nameGenerator", BeanUtils.instantiateClass(generatorClass));
        }
        // 解析@MapperScan注解属性MapperFactoryBean设置到MapperScannerConfigurer声明一个自定义的MapperFactoryBean返回一个代理对象
        Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
        if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
            builder.addPropertyValue("mapperFactoryBeanClass", mapperFactoryBeanClass);
        }
        // 解析@MapperScan的sqlSessionTemplateRef到底使用是哪个sqlSessionTemplate设置到MapperScannerConfigurer多数据源的情况下需要指定
        String sqlSessionTemplateRef = annoAttrs.getString("sqlSessionTemplateRef");
        if (StringUtils.hasText(sqlSessionTemplateRef)) {
            builder.addPropertyValue("sqlSessionTemplateBeanName", annoAttrs.getString("sqlSessionTemplateRef"));
        }
        // 解析@MapperScan的sqlSessionFactoryRef属性 设置到 MapperScannerConfigurer 多数据情况下的话 ,需要指定使用哪个 sqlSessionFactory
        String sqlSessionFactoryRef = annoAttrs.getString("sqlSessionFactoryRef");
        if (StringUtils.hasText(sqlSessionFactoryRef)) {
            builder.addPropertyValue("sqlSessionFactoryBeanName", annoAttrs.getString("sqlSessionFactoryRef"));
        }
        // 解析@MapperScan扫描的的包或者是class对象
        List<String> basePackages = new ArrayList<>();
        basePackages.addAll(Arrays.stream(annoAttrs.getStringArray("value")).filter(StringUtils::hasText).collect(Collectors.toList()));
        basePackages.addAll(Arrays.stream(annoAttrs.getStringArray("basePackages")).filter(StringUtils::hasText).collect(Collectors.toList()));
        basePackages.addAll(Arrays.stream(annoAttrs.getClassArray("basePackageClasses")).map(ClassUtils::getPackageName).collect(Collectors.toList()));
        String lazyInitialization = annoAttrs.getString("lazyInitialization");
               // 指定MapperScannerConfigurer是否为懒加载
        if (StringUtils.hasText(lazyInitialization)) {
            builder.addPropertyValue("lazyInitialization", lazyInitialization);
        }
        builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(basePackages));
               // 为的容器中注册了MapperScannerConfigurer的接口
        registry.registerBeanDefinition(beanName, builder.getBeanDefinition()); 
    }

    private static String generateBaseBeanName(AnnotationMetadata importingClassMetadata, int index) {
        return importingClassMetadata.getClassName() + "#" + MapperScannerRegistrar.class.getSimpleName() + "#" + index;
    }

    static class RepeatingRegistrar extends MapperScannerRegistrar {
        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            AnnotationAttributes mapperScansAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScans.class.getName()));
            if (mapperScansAttrs != null) {
                AnnotationAttributes[] annotations = mapperScansAttrs.getAnnotationArray("value");
                for (int i = 0; i < annotations.length; i++) {
                    registerBeanDefinitions(annotations[i], registry, generateBaseBeanName(importingClassMetadata, i));
                }
            }
        }
    }
}

MapperScannerConfigurer中显示创建了ClassPathMapperScanner****包扫描器对象,其继承自ClassPathBeanDefinitionScanner,其作用是为了扫描**@MapperScan****注解**中配置包路径下的目标类。

java 复制代码
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
        if (this.processPropertyPlaceHolders) {
            processPropertyPlaceHolders(); // 若MapperScannerConfigurer属性的processPropertyPlaceHolders为ture时则执行processPropertyPlaceHolders(),解析动态参数
        }

        // 显示的new一个ClassPathMapperScanner包扫描器对象该对象继承了spring的ClassPathBeanDefinitionScanner为了扫描器指定@MapperScan属性
        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.registerFilters(); // 注册扫描规则过滤器
        // 真正的去扫描@MapperScan指定的路径下的bean定义信息,先会去调用ClassPathMapperScanner.scan()方法
        scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
    }

    private void processPropertyPlaceHolders() {
        // 因为postProcessBeanDefinitionRegistry是为注册bean定义的,但注册bean定义时需要解析${basepackaage},但PropertyResourceConfigurer类型的bean定义还没有实例化成bean对象 ,故不能提供解析${basepackaage}的能力
        // 故显示的设置processPropertyPlaceHolders为ture,即想通过applicationContext.getBeansOfType(PropertyResourceConfigurer.class),提前把PropertyResourceConfigurer组件实例化出来从而解析${basepackaage}
        Map<String, PropertyResourceConfigurer> prcs = applicationContext.getBeansOfType(PropertyResourceConfigurer.class);
        // 判断PropertyResourceConfigurer的集合不为空,且applicationContext是ConfigurableApplicationContext
        if (!prcs.isEmpty() && applicationContext instanceof ConfigurableApplicationContext) {
            // 通过名称去容器中获取MapperScannerConfigurer组件
            BeanDefinition mapperScannerBean = ((ConfigurableApplicationContext) applicationContext).getBeanFactory().getBeanDefinition(beanName);
            // PropertyResourceConfigurer类没有暴露任何的方法来处理的property placeholder substitution即${basepackaage},有且只有提供一个BeanFactoryPostProcessor接口来处理的bean定义
            // 但调用BeanFactoryPostProcessor.postProcessBeanFactory()方法需要一个beanFactory,若从外面传入一个Ioc的容器进来会提前破坏ioc容器,故这里创建了一个临时的ioc容器,然后把mapperScannerBean注册到该临时ioc容器中
            DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
            factory.registerBeanDefinition(beanName, mapperScannerBean);
            for (PropertyResourceConfigurer prc : prcs.values()) {
                              //这时就可以大胆放心的处理临时的ioc容器factory中的bean定义,即当前的mapperScannerBean
                prc.postProcessBeanFactory(factory); 
            }
                      // 处理完之后重新获取通过PropertyResourceConfigurer解析后的mapperScannerBean的属性
            PropertyValues values = mapperScannerBean.getPropertyValues(); 
                      // 更新MapperScannerBean属性可能有${}包裹的字段
            this.basePackage = updatePropertyValue("basePackage", values); 
            this.sqlSessionFactoryBeanName = updatePropertyValue("sqlSessionFactoryBeanName", values);
            this.sqlSessionTemplateBeanName = updatePropertyValue("sqlSessionTemplateBeanName", values);
            this.lazyInitialization = updatePropertyValue("lazyInitialization", values);
        }

        this.basePackage = Optional.ofNullable(this.basePackage).map(getEnvironment()::resolvePlaceholders).orElse(null);
        this.sqlSessionFactoryBeanName = Optional.ofNullable(this.sqlSessionFactoryBeanName).map(getEnvironment()::resolvePlaceholders).orElse(null);
        this.sqlSessionTemplateBeanName = Optional.ofNullable(this.sqlSessionTemplateBeanName).map(getEnvironment()::resolvePlaceholders).orElse(null);
        this.lazyInitialization = Optional.ofNullable(this.lazyInitialization).map(getEnvironment()::resolvePlaceholders).orElse(null);
    }
}

首先会根据在**@MapperScan****注解上设置的注解类annotationClass**、以及markerInterface来添加过滤器,若未设置将会将包下的所有接口扫描出来。

在调用超类ClassPathBeanDefinitionScanner的scan方法扫描包时会调用子类ClassPathMapperScanner中的doScan,而该类又去调用了超类doScan方法,但有一点不同的是这复写了isCandidateComponent方法只扫描接口,而Spring中默认恰好相反。

当将所有符合条件的Mapper****接口BeanDefinition信息扫描出来后,进行遍历将beanClass设置成MapperFactoryBean的Class,从而达到偷天换日将Mapper接口通过MapperFactoryBean来创建生成Bean。因为接口是不能被实例化生成****Bean的。

java 复制代码
public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
    // 注册扫描规则过滤器
    public void registerFilters() { 
        boolean acceptAllInterfaces = true;
        // 若annotationClass不为空,表示用户设置了此属性,则根据此属性生成过滤器以保证达到用户想要的效果,而封装此属性的过滤器就是 AnnotationTypeFiter,其保证在扫描对应Java文件时只接受标记有注解为annotationClass的接口
        if (this.annotationClass != null) {
            addIncludeFilter(new AnnotationTypeFilter(this.annotationClass));
            acceptAllInterfaces = false;
        }
        // 如果markerlnterface不为空,表示要根据此属性生成过滤器以保证达到用户想要的效果,而封装此属性的过滤器就是实现AssignableTypeFiter接口的局部类,扫描过程中只有实现markerIntrface接口的接口才会被接受
        if (this.markerInterface != null) {
            addIncludeFilter(new AssignableTypeFilter(this.markerInterface) {
                @Override
                protected boolean matchClassName(String className) {
                    return false;
                }
            });
            acceptAllInterfaces = false;
        }
        // 若接受所有接口,则添加自定义INCLUDE过滤器TypeFilter,全部返回true
        if (acceptAllInterfaces) { 
            // default include filter that accepts all classes
            addIncludeFilter((metadataReader, metadataReaderFactory) -> true);
        }
        // 对于命名为package-info Java文件,默认不作为逻辑实现接口,将其排除掉,使用TypeFiltler接口的局部类实现match 法
        addExcludeFilter((metadataReader, metadataReaderFactory) -> {
            String className = metadataReader.getClassMetadata().getClassName();
            return className.endsWith("package-info");
        });
    }

    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        // 调用父类ClassPathBeanDefinitionScanner来进行扫描
        Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages); 
        // 若扫描后mapper包下有接口类,则扫描bean定义就不会为空
        if (beanDefinitions.isEmpty()) { 
            LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
        } else {
            // 这里是将Mapper接口注册到Spring容器中的关键
            processBeanDefinitions(beanDefinitions); 
        }
        return beanDefinitions;
    }

    private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
        GenericBeanDefinition definition;
        // 循环所有扫描出mapper的bean定义出来
        for (BeanDefinitionHolder holder : beanDefinitions) { 
            // 获取的bean定义
            definition = (GenericBeanDefinition) holder.getBeanDefinition(); 
            // 获取的bean定义的名称
            String beanClassName = definition.getBeanClassName();  
            // 设置ConstructorArgumentValues会通过构造器初始化对象
            definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); 
            // 进行真的偷天换日操作,将当前Bean的Class设置成MapperFactoryBean的Class
            definition.setBeanClass(this.mapperFactoryBeanClass);
            definition.getPropertyValues().add("addToConfig", this.addToConfig);
            // 为的Mapper对象绑定sqlSessionFactory引用,实际上是为MapperFactoryBean添加一个sqlSessionFactory的属性
            // 然后SpringIoc在实例化MapperFactoryBean时为通过populate()为UserMapper(MapperFactoryBean)的sqlSessionFactory属性赋值,调用set方法
            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;
            }
            // 同上sqlSessionFactory
            if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) { 
                definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
                explicitFactoryUsed = true;
            } else if (this.sqlSessionTemplate != null) {
                // 将sqlSessionTemplate通过AUTOWIRE_BY_TYPE自动装配
                definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate); 
                explicitFactoryUsed = true;
            }
            // 设置UserMapper<MapperFactoryBean>定义的注入模型是通过包扫描进来的,所有默认注入模型为AutowireCapableBeanFactory.AUTOWIRE_NO=0标识通过@Autowire注解注入
            // 因为字段上是没有@AutoWired注解,则MapperFactoryBean的字段属性永远自动注入不了值,故需要把UserMapper<MapperFactoryBean>bean定义的注入模型给改成的AUTOWIRE_BY_TYPE=1表示根据类型自动装配
            if (!explicitFactoryUsed) {
                definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
            }
            // 设置bean定义的加载模型(是否为懒加载)
            definition.setLazyInit(lazyInitialization); 
        }
    }
    
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
    }
}

对于MapperFactoryBean代码就比较简单了,上面在给BeanDefinition设置属性时,给构造函数参数列表中添加了一个参数即原始Mapper类名,故实例化MapperFactoryBean时会调用有参构造函数。

java 复制代码
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
    private Class<T> mapperInterface;
    public MapperFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }
    public T getObject() throws Exception {
        return getSqlSession().getMapper(this.mapperInterface);
    }
    public Class<T> getObjectType() {
        return this.mapperInterface;
    }
}

public class SqlSessionTemplate implements SqlSession, DisposableBean {
    public <T> T getMapper(Class<T> type) {
        // 最终去sqlSessionFactory.Configuration.mapperRegistry去获取我们的Mapper对象
        return getConfiguration().getMapper(type, this);
    }
}

2.3 总结流程图

三、 Spring Boot 整合 MyBatis

接下来我们学习下在SpringBoot 中整合MyBatis。其实基本原理和上面讲的Spring是一样的,就不重复讲了,这里我们就介绍一下Spring Boot整合MyBatis的使用方法。

首先我们需要新建一个Spring Boot项目,新建项目的细节再此处不详细说明。新建完项目之后之后我们首先需要添加依赖

XML 复制代码
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.0.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

然后在application.yml中添加相关的数据源,MyBatis的相关信息

bash 复制代码
#DataSource config 配置数据源信息
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mybatisdemo?useUnicode=true&characterEncoding=utf-8
    username: root
    password: admin
#配置MyBatis的配置文件地址和MyBatis的映射文件地址  
mybatis:
  config-location: MyBatis-cfg.xml
  mapper-locations: mybatis/*.xml

接着我们还需要在启动类MybatisSpringbootDemoApplication中添加dao接口的扫描信息

java 复制代码
@SpringBootApplication
@MapperScan(value = "com.jay.mapper")
public class MybatisSpringbootDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(MybatisSpringbootDemoApplication.class, args);
    }
}

最后我们看看测试类

java 复制代码
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class StudentMapperTest {
    // 自动注入Mapper对象
    @Autowired
    private StudentMapper studentMapper;
    @Test
    public void selectStudentById() throws Exception {
        Student student = studentMapper.selectStudentById(1);
        System.out.println("---->查询结果为={}"+student);
    }
}

测试结果如下:

通过上面的这个demo,我们能看出来Spring Boot的配置相对Spring较少,这与SpringBoot的理念相关,约定大于配置,注解多于配置。


相关文章: 【MyBatis】MyBatis缓存原理详解-CSDN博客

【MyBatis】MyBatis的一级缓存和二级缓存简介-CSDN博客

相关推荐
netyeaxi3 小时前
Java:使用spring-boot + mybatis如何打印SQL日志?
java·spring·mybatis
猴哥源码3 小时前
基于Java+SpringBoot的动物领养平台
java·spring boot
华子w9089258594 小时前
基于 SpringBoot+VueJS 的农产品研究报告管理系统设计与实现
vue.js·spring boot·后端
猴哥源码4 小时前
基于Java+SpringBoot的在线小说阅读平台
java·spring boot
程序猿小D6 小时前
[附源码+数据库+毕业论文]基于Spring+MyBatis+MySQL+Maven+jsp实现的个人财务管理系统,推荐!
java·数据库·mysql·spring·毕业论文·ssm框架·个人财务管理系统
上上迁7 小时前
分布式生成 ID 策略的演进和最佳实践,含springBoot 实现(Java版本)
java·spring boot·分布式
永日456707 小时前
学习日记-spring-day42-7.7
java·学习·spring
秋千码途7 小时前
小架构step系列07:查找日志配置文件
spring boot·后端·架构
二十雨辰8 小时前
[尚庭公寓]07-Knife快速入门
java·开发语言·spring