MyBatis之Mapper运行机制及其与Spring整合的底层原理
编辑时间:2019-07-11 赞:2 踩:0
导言:
本文主要从配置文件和底层源码的角度去解释MyBatis的Mapper的运行机制,同时也会对MyBatis与Spring整合的原理进行说明。在写作本文的时候,我是假定读者对Spring和MyBatis有一定的了解。(如果有空,我会写一些关于Spring和MyBatis基础知识的文章)正文:
在使用Spring整合MyBatis的时候我们只需要进行简单的配置被可以非常便利的地使用MyBatis。本文将从配置文件的角度去切入讲解Spring和MyBatis的交互流程。在使用SSM(Spring-SpringMVC-MyBatis)框架的时候我们常常会看见类似以下例子的代码片段:
ApplicationContext-MyBatis.xml
<!-- SqlSessionFactory mysql连接管理--> <!-- 让spring管理sqlsessionfactory 使用mybatis和spring整合包中的 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!-- 数据库连接池 --> <property name="dataSource" ref="dataSource" /> <!-- 加载mybatis的全局配置文件 --> <property name="configLocation" value="classpath:mybatis/SqlMapConfig.xml" /> </bean> <!-- Mapper映射文件的包扫描器 --> <!--basePackage 属性是让你为映射器接口文件设置基本的包路径。 你可以使用分号或逗号 作为分隔符设置多于一个的包路径。每个映射器将会在指定的包路径中递归地被搜索到。--> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.kshop.mapper,com.kshop.search.mapper" /> </bean>
TestMyBatisService.java
@Controller public class TestMyBatisService{ @Autowired private ItemMapper itemMapper; @RequestMapping("/test") @ResponseBody public String hello() { TbItem item = itemMapper.selectByPrimaryKey((long) 562379); return item.toString(); } }
作为初学者,在编写上诉代码的时候总是会有一丝疑惑。为什么进行这么简单的配置,就可顺利的使用上MyBatis的Mapper?Spring IOC容器中的Mapper是从哪里来的?Mapper又是如何使用MyBatis的SqlSsession对数据库进行操作的?
从配置文件中可以看出,为了使用Mapper,我往Spring的IOC容器注入了两个Bean。一个是SqlSessionFactoryBean一个是MapperScannerConfigurer。MapperScannerConfigurer虽然比较陌生,但是SqlSessionFactoryBean对于使用MyBatis进行开发的开发者来说应该是很熟悉。使用Mybatis开发DAO时,通常有两个方法,即原始Dao开发方法和Mapper代理接口开发方法。在使用原始DAO开发方法时,开发者会通过MyBatis配置文件提供的配置信息构造SqlSessionFactory即会话工厂由会话工厂创建sqlSession即会话,操作数据库需要通过sqlSession进行。由此可知,配置了SqlSessionFactory后,Spring便可以通过该Bean获取sqlSession,从而使用MyBatis对数据库进行操作。
上文已经解析了SqlSessionFactoryBean的用处,那么MapperScannerConfigurer又有什么用处呢?MapperScannerConfigurer是一个Mapper的包扫描器, 它将会查找basePackage属性所存储的类路径下的映射器并自动将它们创建成MapperFactoryBean。具体流程如下:MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口,该接口是Spring官方提供的接口,专门用于动态注册Bean,方法为postProcessBeanDefinitionRegistry(),代码如下:
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { //创建ClassPath扫描器,设置属性,然后调用扫描方法 ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); //设置scanner属性 ··· //如果配置了annotationClass,就将其添加到includeFilters scanner.registerFilters(); scanner.scan(this.basePackage); }MapperScannerConfigurer在该方法里面调用了ClassPathMapperScanner的scan方法对包进行扫描。ClassPathMapperScanner继承自Spring中的类ClassPathBeanDefinitionScanner,所以scan方法会调用到父类的scan方法,而在父类的scan方法中又调用到子类的doScan方法。
//ClassPathBeanDefinitionScanner的scan,子类ClassPathMapperScanner并没有覆盖该方法 public int scan(String... basePackages) { int beanCountAtScanStart = this.registry.getBeanDefinitionCount(); //子类ClassPathMapperScanner覆盖该方法,因此,会执行子类的doScan方法 doScan(basePackages); // Register annotation config processors, if necessary. if (this.includeAnnotationConfig) { AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry); } return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart); } //ClassPathBeanDefinitionScanner的doScan protected Set<BeanDefinitionHolder> doScan(String... basePackages) { // 其他操作 ··· //返回了basePackages里面复合特定条件的接口或类的BeanDefinition return beanDefinitions; } //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 { processBeanDefinitions(beanDefinitions); } return beanDefinitions; }
经过上述操作,所有的Mapper接口,并将其注册为BeanDefinition对象。接下来调用processBeanDefinitions()要配置这些BeanDefinition对象(Bean的定义主要由BeanDefinition来描述的。作为Spring中用于包装Bean的数据结构,Spring解析Bean的配置文件后会生成将其配置信息存放在一个个BeanDefinition中),代码片段如下:
public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner { private MapperFactoryBean<?> mapperFactoryBean = new MapperFactoryBean<Object>(); private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) { GenericBeanDefinition definition; for (BeanDefinitionHolder holder : beanDefinitions) { definition = (GenericBeanDefinition) holder.getBeanDefinition(); String beanClassName = definition.getBeanClassName(); ······· // the mapper interface is the original class of the bean // but, the actual class of the bean is MapperFactoryBean definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); //设置BeanDefinition的class definition.setBeanClass(this.mapperFactoryBeanClass); //添加属性addToConfig definition.getPropertyValues().add("addToConfig", this.addToConfig); ···· //添加属性sqlSessionFactory,这个也会根据特定的判断条件改为添加sqlSessionTemplate definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory); ······ } } }处理的过程很简单,就是往BeanDefinition对象中设置了一些属性。我们重点关注两个。
- 设置beanClass
设置BeanDefinition对象的BeanClass为MapperFactoryBean<?>。这意味着什么呢?以UserMapper为例,意味着当前的mapper接口在Spring容器中,beanName是userMapper,beanClass是MapperFactoryBean.class。那么在IOC初始化的时候,实例化的对象就是MapperFactoryBean对象。
-
设置sqlSessionFactory属性
MapperFactoryBean实现了FactoryBean接口,俗称工厂Bean。那么,当我们通过@Autowired注入某一个Mapper(dao)接口的时候,返回的对象就是MapperFactoryBean这个工厂Bean中的getObject()方法返回的代理对象(调用链如下:MapperFactoryBean.getObject-->SqlSession.getMapper——>Configuration.getMapper——>MapperRegistry.getMapper——>MapperProxyFactory.newInstance)。该代理类实现了 xxxMapper 接口。 因为代理创建在运行时环境中(Runtime,译者注),那么指定的映射器必须是一个接口,而不是一个具体的实现类。简单来说,由于MapperFactoryBean实现了FactoryBean接口,开发使用@Autowired注入的Mapper是由MapperFactoryBean的getObject方法返回的一个代理bean。
我们调用到Dao接口(Mapper)的方法时,则会调用到MapperProxy对象的invoke方法。MapperProxy对象的invoke方法底层就是调用SqlSession这些原始的Mybatis操作接口进行数据库操作。有兴趣的朋友可以去阅读MapperProxy中的invoke方法的源码。
最后,为了加强理解,强烈建议阅读本文的读者自己搭建一个简单的Spring+MyBatis工程,然后写一些简单的代码进行单步调试。毕竟纸上得来终觉浅,绝知此事要躬行。
参考资料:
https://blog.csdn.net/jokemqc/article/details/79082571
https://www.cnblogs.com/yxfmp426756/p/8882852.html
https://juejin.im/post/5a62e99e6fb9a01c9950e667
https://zhuanlan.zhihu.com/p/30590254