MyBatis 是一款常用的持久层框架,使得程序能够以调用方法的方式执行某个指定的SQL,将执行SQL的底层逻辑进行封装。多数与Spring结合使用,本文讨论Spring如何整合Mybatis,首先看下Mybatis运行机制
1. Mybatis运行机制
1.1 Mybatis功能架构设计
Mybatis的功能架构分为三层:
(1)API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。
(2)数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
(3)基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。
1.2 Mybatis运行方式
Mybatis支持两种方式执行SQL:Mapper接口和SqlSession,使用如下
public class MapperMain {
public static void main(String[] args) throws Exception {
File file = new File("/data/learncode/hobbit/src/main/resources/conf/mybatis/mybatis_config.xml");
InputStream inputStream = new FileInputStream(file);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
int id = 13;
// 方式1:Mapper接口发送SQL
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
System.out.println("Mapper接口发送:" + mapper.getById(id));
// 方式2:SqlSession发送SQL
UserEntity userEntity = sqlSession.selectOne("com.hobbit.mapper.UserMapper.getById", id);
System.out.println("SqlSession发送:" + userEntity);
}
}
复制代码
两种方式执行效果一致,如图
一般的话,比较推荐Mapper接口方法,因为手工写namespace和statementId极大增加了犯错误的概率,而且也降低了开发的效率。Mapper由MapperProxyFactory动态代理生成,封装了SqlSession。
2. Spring整合Mybatis要解决的问题
2.1 Mapper代理对象
重点关注下的Mapper动态代理对象,因为Spring整合Mybatis的核心目标是:把某个Mapper的代理对象作为一个bean放入Spring容器中,使得能够像使用一个普通bean一样去使用这个代理对象,比如能被@Autowire自动注入。常用如下通过Ioc容器把UserMapper注入了UserService:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public UserEntity queryUser(int id){
UserEntity userEntity = userMapper.getById(id);
return userEntity;
}
public void printServiceName(){
System.out.println("I'm UserService");
}
}
复制代码
通过运行时Debugger,确实与直接使用Mapper一样的代理对象。userMapper的类型为:org.apache.ibatis.binding.MapperProxy@4acf72b6。
问题:如何能够把Mybatis的代理对象作为一个bean放入Spring容器中?
2.2 Bean的生产过程
Spring启动过程中,bean的生命周期如下
- 扫描指定的包路径下的class文件或解析xml文件
- 生成对应的BeanDefinition
- BeanFactoryPostProcessor注册或修改BeanDefinition定义
- 根据BeanDefinition反射实例化Bean
- BeanPostProcessor修改Bean定义
- Bean的业务调用
- Bean的销毁
对于两个Service:UserService/OrderInfoService定义如下
UserService
@Service
public class UserService {
public void printServiceName(){
System.out.println("I'm UserService");
}
}
复制代码
OrderService, 无@service注解
public class OrderInfoService {
public void printServiceName(){
System.out.println("I'm OrderInfoService");
}
}
复制代码
执行如下代码:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println(ctx.getBean("userService"));
复制代码
结果如下:
com.hobbit.service.UserService@4167d97b
复制代码
增加一个FactoryBean后置处理器,修改userService的BeanDefinition定义。
@Component
public class RenameBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition("userService");
beanDefinition.setBeanClassName(OrderInfoService.class.getName());
}
}
复制代码
重新运行结果如下,生成了OrderInfoService对象。
com.hobbit.service.OrderInfoService@57ad2aa7
复制代码
所以在Spring中bean对象跟class或xml定义的bean无直接关系,跟最终的BeanDefinition有直接关系。
要想生成一个bean,首先要有一个BeanDefinition。那Mapper对应的BeanDefinition是?
3. MapperFactoryBean
3.1 Mapper BeanDefinition定义
Spring通过BeanDefinition的beanClassName生成对应的bean,那mapper的对应的beanClassName是什么?本可以有两个答案:
1. 代理对象对应的代理类
2. 代理对象对应的接口
因为代理类是动态生成的,spring启动时无法得知,无法使用。那么代理对象对应的接口?
思路如下:
BeanDefinition bd = new BeanDefinitoin();
// 注意这里,设置的是UserMapper
bd.setBeanClassName(UserMapper.class.getName());
SpringContainer.addBd(bd);
复制代码
实际上给BeanDefinition对应的类型设置为一个接口是行不通的,因为Spring没有办法根据这个BeanDefinition去new出对应类型的实例,接口是没法直接new出实例的。
所以想通过设置BeanDefinition的class类型,然后由Spring自动地帮助我们去生成对应的bean,但是这条路是行不通的。可以通过其它方式MapperFactoryBean来实现。
MapperFactoryBean继承关系
通过继承SqlSessionDaoSupport拥有了sqlSession,通过FactoryBean具备定制Bean的能力。对应的源码如下:
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
...
/**
* {@inheritDoc}
*/
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
/**
* {@inheritDoc}
*/
@Override
public Class<T> getObjectType() {
return this.mapperInterface;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isSingleton() {
return true;
}
...
}
复制代码
getObjectType返回的是Mapper接口,通过 AbstractBeanDefinition.AUTOWIRE_BY_TYPE时,可自动注入使用。
getObject返回了动态代理对象,跟之前的使用一致。至此完成了Mapper对应BeanDefinition定义的问题,那这些BeanDefinition是如何注册到Ioc容器呢?
3.2 Mapper BeanDefinition加载
Spring可通过多种方式加载BeanDefinition,从XmlBeanDefinitionReader到ClassPathBeanDefinitionScanner在到ConfigurationClassBeanDefinitionReader分别对应xml、@component、@configuration类定义的加载。Mapper BeanDefinition可通过2种形式加载MapperScannerConfigurer和@MapperScan注解,内部都是通过ClassPathMapperScanner实现。ClassPathMapperScanner继承了ClassPathBeanDefinitionScanner,类图如下
ClassPathMapperScanner在完成类加载的基础上,重新定义BeanDefinition,源码如下
public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
...
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
if (logger.isDebugEnabled()) {
logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName()
+ "' and '" + definition.getBeanClassName() + "' mapperInterface");
}
// the mapper interface is the original class of the bean
// but, the actual class of the bean is MapperFactoryBean
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
definition.setBeanClass(this.mapperFactoryBean.getClass());
definition.getPropertyValues().add("addToConfig", this.addToConfig);
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;
}
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
if (explicitFactoryUsed) {
logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
if (explicitFactoryUsed) {
logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}
if (!explicitFactoryUsed) {
if (logger.isDebugEnabled()) {
logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
}
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
}
}
...
}
复制代码
通过definition.setBeanClass(this.mapperFactoryBean.getClass()) 完成了beanClass的转换,同时设置了definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE)保证了通过类型自动注入。
两种方式如下
使用MapperScannerConfigurer加载
<!-- 指定要扫描的包,在此包下自动搜索映射器(接口) -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.hobbit.mapper" />
<property name="sqlSessionFactoryBeanName" value="sessionFactory" />
</bean>
复制代码
使用@MapperScan加载
@Data
@Import(HdsClassesDataSourceConfig.class)
@Configuration
@MapperScan(basePackages = "com.peppa.classes.server.**.dao", sqlSessionFactoryRef = "hdsSqlSessionFactory")
@ConfigurationProperties(prefix = "hds")
public class HdsDataSourceConfig {
private String appName;
private String group;
private String token;
private String configCenter;
private long refreshInterval;
static final String MAPPER_LOCATION = "classpath*:mapper/**/**.xml";
@Primary
@Bean(name = "hdsDataSource", initMethod = "init", destroyMethod = "close")
public HdsDataSource dsDataSource() {
final HdsDataSource hdsDataSource = new HdsDataSource();
hdsDataSource.setAppName(appName);
hdsDataSource.setGroup(group);
hdsDataSource.setToken(token);
hdsDataSource.setConfigCenter(configCenter);
hdsDataSource.setRefreshInterval(refreshInterval);
return hdsDataSource;
}
@Primary
@Bean(name = "hdsTransactionManager")
public DataSourceTransactionManager transactionManager(final HdsDataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Primary
@Bean(name = "hdsSqlSessionFactory")
public SqlSessionFactory hdsSqlSessionFactory(final HdsDataSource dataSource) throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
sessionFactory.setMapperLocations(resourcePatternResolver.getResources(MAPPER_LOCATION));
return sessionFactory.getObject();
}
}
复制代码
到此为止,Spring整合Mybatis的核心原理就结束了,再次总结一下:
- 定义MapperFactoryBean,用来封装Mapper对应的BeanDefinition
- 通过ClassPathMapperScanner重新定义BeanClass及AutowireMode,实现BeanDefinition加载及MapperInterface与MapperFactory整合
- 通过MapperScannerConfigurer或@MapperScan,分别扩展BeanDefinitionRegistryPostProcessor及ImportBeanDefinitionRegistrar 用来在启动Spring时执行调用ClassPathMapperScanner完成Mapper BeanDefinition的注册。
原文链接:
https://juejin.cn/post/6936842731205427236
如果觉得本文对你有帮助,可以转发关注支持一下