【Spring】Spring之整合MyBatis底层源码解析

目的

  • 介绍Spring整合MyBatis的思路及实现
  • 介绍Spring整合外部框架的实现方案及重要类
  • 介绍MyBatis运行的基本原理
  • MyBatis的详细介绍,参考MyBatis官方文档:https://mybatis.org/mybatis-3/zh/index.html

整合思路

把其他框架所产生的对象放到Spring容器中,让其成为Bean。

比如MyBatis,MyBatis框架可以单独使用,而单独使用MyBatis框架就需要用到MyBatis所提供的一些类构造出对应的对象,然后使用该对象,就能使用到MyBatis框架给我们提供的功能,MyBatis整合Spring就是为了将这些对象放入Spring容器中成为Bean,只要成为了Bean,在我们的Spring项目中就能很方便的使用这些对象了,也就能很方便的使用MyBatis框架所提供的功能了。

手写mybatis-spring

既然我们知道了Spring整合MyBatis的核心思想就是将MyBatis的相关对象注入到Spring容器成为Bean,那么我们就可以自己动手试着整合下MyBatis,实现一个简易版的mybatis-spring组件。实际开发中,我们就是通过MyBatis提供的mybatis-spring组件来实现与Spring整合的。

代码结构

【Spring】Spring之整合MyBatis底层源码解析_第1张图片

AppConfig.java

构建一个SqlSessionFactory,并且需要指定Mapper的路径,该路径下的所有接口Mapper都要被放到Spring容器中成为Bean。
mybatis.xml中配置的是数据库连接。

@ComponentScan("com.firechou") // spring扫描bean路径
@FireMapperScan("com.firechou.mapper") // mybatis扫描Mapper路径
public class AppConfig {

	/**
	 * 定义一个SqlSessionFactory对象
	 * @return
	 * @throws IOException
	 */
	@Bean
	public SqlSessionFactory sqlSessionFactory() throws IOException {
		InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
		SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
		return sqlSessionFactory;
	}
}

mybatis.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
  <environments default="development">
    <environment id="development">
      <!-- 使用jdbc事务管理 -->
      <transactionManager type="JDBC"/>
      <!-- 数据库连接池 -->
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url"
                  value="jdbc:mysql://rm-cn-zsk3ar7jm0010m0o.rwlb.rds.aliyuncs.com:3306/db_test?characterEncoding=utf-8&useSSL=false"/>
        <property name="username" value="firechou"/>
        <property name="password" value="xxx"/>
      </dataSource>
    </environment>
  </environments>

</configuration>

@FireMapperScan注解

@Retention(RetentionPolicy.RUNTIME) // 运行时生效
@Target(ElementType.TYPE) // 类注解
@Import(FireImportBeanDefinitionRegistrar.class)
public @interface FireMapperScan {
	String value();
}

通过@Import注解配置FireImportBeanDefinitionRegistrar,Spring在启动时会执行FireImportBeanDefinitionRegistrar#registerBeanDefinitions方法,该方法里面可以实现扫描路径下的Mapper成为Bean。

FireImportBeanDefinitionRegistrar.java

/**
 * 扫描指定路径下的mapper成为spring容器bean
 */
public class FireImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
		// 扫描路径
		Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(FireMapperScan.class.getName());
		String path = (String) annotationAttributes.get("value");

		// 自定义扫描器
		FireBeanDefinitionScanner scanner = new FireBeanDefinitionScanner(registry);

		// 扫描指定路径下所有文件
		scanner.addIncludeFilter(new TypeFilter() {
			@Override
			public boolean match(@NotNull MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
				return true;
			}
		});

		scanner.scan(path);
	}
}

自定义扫描器FireBeanDefinitionScanner来实现Mapper接口扫描逻辑。

FireBeanDefinitionScanner.java

/**
 * 自定义扫描器
 */
public class FireBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {
	public FireBeanDefinitionScanner(BeanDefinitionRegistry registry) {
		super(registry);
	}

	@Override
	protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
		Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);

		for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
			BeanDefinition beanDefinition = beanDefinitionHolder.getBeanDefinition();
			// 指定构造器
			beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
			// 指定生成代理类的FactoryBean
			beanDefinition.setBeanClassName(FireFactoryBean.class.getName());
		}

		return beanDefinitionHolders;
	}

	@Override
	protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
		return beanDefinition.getMetadata().isInterface();
	}
}

定义生成Mapper接口代理类的FactoryBean为FireFactoryBean。

FireFactoryBean.java

/**
 * 生成mapper代理对象
 */
public class FireFactoryBean implements FactoryBean {

	/**
	 * 表实体对应的mapper接口
	 */
	private Class mapperInterface;

	private SqlSession sqlSession;

	/**
	 * 构造方法注入mapper接口,扫描器指定该构造器注入接口
	 * @param mapperInterface
	 */
	public FireFactoryBean(Class mapperInterface) {
		this.mapperInterface = mapperInterface;
	}

	/**
	 * 注入AppConfig中定义的SqlSessionFactory对象
	 * @param sqlSessionFactory
	 */
	@Autowired
	public void setSqlSession(SqlSessionFactory sqlSessionFactory) {
		sqlSessionFactory.getConfiguration().addMapper(mapperInterface);
		this.sqlSession = sqlSessionFactory.openSession();
	}

	/**
	 * 生成代理对象
	 * @return
	 * @throws Exception
	 */
	@Override
	public Object getObject() throws Exception {
		// mybatis提供的生成代理对象的方法,底层通过jdk动态代理实现
		return sqlSession.getMapper(mapperInterface);
	}

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

注意:
不要将FactoryBean和BeanFactory混淆了。
FactoryBean:是一个Java Bean,但是它是一个能生产出当前对象的工厂Bean,它的实现和工厂模式及修饰器模式很像,比如上面通过实现FactoryBean来自定义生成具体对象的逻辑。
BeanFactory:是一个工厂接口,是整个Spring IOC容器的核心内容,生产并存储很多的bean,常用的就是getBean()方法。比如AnnotationConfigApplicationContext及时一个BeanFactory。

测试验证

定义mapper:

public interface UserMapper {

	@Select("select 'user'")
	String selectById();
}

定义service:

@Component
public class UserService {

	@Autowired
	private UserMapper userMapper;

	public void test(){
		System.out.println(userMapper.selectById());
	}
}

测试类:

public class FireTest {

	public static void main(String[] args) {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
		UserService userService = context.getBean(UserService.class);
		userService.test();
	}
}

运行结果:
【Spring】Spring之整合MyBatis底层源码解析_第2张图片
UserMapper中定义的sql被执行了,验证成功。

MyBatis源码实现

mybatis-spring 1.3.2版本实现

  1. 通过@MapperScan导入了MapperScannerRegistrar类
  2. MapperScannerRegistrar类实现了ImportBeanDefinitionRegistrar接口,所以Spring在启动时会调用MapperScannerRegistrar类中的registerBeanDefinitions方法
  3. 在registerBeanDefinitions方法中定义了一个ClassPathMapperScanner对象,用来扫描mapper
  4. 设置ClassPathMapperScanner对象可以扫描到接口,因为在Spring中是不会扫描接口的
  5. 同时因为ClassPathMapperScanner中重写了isCandidateComponent方法,导致isCandidateComponent只会认为接口是备选者Component
  6. 通过利用Spring的扫描后,会把接口扫描出来并且得到对应的BeanDefinition
  7. 接下来把扫描得到的BeanDefinition进行修改,把BeanClass修改为MapperFactoryBean,把AutowireMode修改为byType
  8. 扫描完成后,Spring就会基于BeanDefinition去创建Bean了,相当于每个Mapper对应一个FactoryBean
  9. 在MapperFactoryBean中的getObject方法中,调用了getSqlSession()去得到一个sqlSession对象,然后根据对应的Mapper接口生成一个Mapper接口代理对象,这个代理对象就成为Spring容器中的Bean
  10. sqlSession对象是Mybatis中的,一个sqlSession对象需要SqlSessionFactory来产生
  11. MapperFactoryBean的AutowireMode为byType,所以Spring会自动调用set方法,有两个set方法,一个setSqlSessionFactory,一个setSqlSessionTemplate,而这两个方法执行的前提是根据方法参数类型能找到对应的bean,所以Spring容器中要存在SqlSessionFactory类型的bean或者SqlSessionTemplate类型的bean。
  12. 如果你定义的是一个SqlSessionFactory类型的bean,那么最终也会被包装为一个SqlSessionTemplate对象,并且赋值给sqlSession属性
  13. 而在SqlSessionTemplate类中就存在一个getMapper方法,这个方法中就产生一个Mapper接口代理对象
  14. 到时候,当执行该代理对象的某个方法时,就会进入到Mybatis框架的底层执行流程

mybatis-spring 2.0.6版本实现

  1. 通过@MapperScan导入了MapperScannerRegistrar类
  2. MapperScannerRegistrar类实现了ImportBeanDefinitionRegistrar接口,所以Spring在启动时会调用MapperScannerRegistrar类中的registerBeanDefinitions方法
  3. 在registerBeanDefinitions方法中注册一个MapperScannerConfigurer类型的BeanDefinition
  4. 而MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口,所以Spring在启动过程中时会调用它的postProcessBeanDefinitionRegistry()方法
  5. 在postProcessBeanDefinitionRegistry方法中会生成一个ClassPathMapperScanner对象,然后进行扫描
  6. 后续的逻辑和1.3.2版本一样

带来的好处是,可以不使用@MapperScan注解,而可以直接定义一个Bean,比如:

@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
	MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
	mapperScannerConfigurer.setBasePackage("com.firechou.mapper");
	return mapperScannerConfigurer;
}

Spring整合MyBatis后一级缓存失效

Mybatis中的一级缓存是基于SqlSession来实现的,所以在执行同一个sql时,如果使用的是同一个SqlSession对象,那么就能利用到一级缓存,提高sql的执行效率。

但是在Spring整合Mybatis后,如果在执行某个方法时,该方法上没有加@Transactional注解,也就是没有开启Spring事务,那么后面在执行具体sql时,每执行一个sql时都会新生成一个SqlSession对象来执行该sql,这就是我们说的一级缓存失效(也就是没有使用同一个SqlSession对象),而如果开启了Spring事务,那么该Spring事务中的多个sql,在执行时会使用同一个SqlSession对象,从而一级缓存生效。

Spring整合MyBatis后一级缓存失效,并不是一个问题,反而是一个正常现象。因为没有加事务时,一个方法中的多个sql执行本来就应该属于不同的SqlSession,当加了事务时,才应该将多个sql放在同一个SqlSession来执行,从而与数据库底层事务对应起来。

总结

  • Spring整合其他框架实际就是将其他框架需要用到的对象交由Spring容器来管理
  • 调用MyBatis的Mapper接口实际调用的是Spring生成的代理类,在代理类中实现事务的管理和sql的解析执行
  • Spring整合MyBatis后一级缓存失效并不是问题,一级缓存是基于SqlSession的,没有开启事务时多个sql执行不就属于不同的SqlSession。生产中建议关闭MyBatis的一级缓存

你可能感兴趣的:(#,SSM框架,spring,mybatis,java,一级缓存)