Mybatis-Spring实现原理

MyBatis作为一个易于上手的开源持久化框架,想必大家已经非常熟悉。由于 Spring 3在 Mybatis 3 正式发布前就已经开发结束了,Spring 团队不希望基于 Mybatis 的未发布版来发布相关代码,因此 Spring 未对 Mybatis 提供官方的支持。为了弥补这一缺憾,Mybatis 社区将  MyBatis 与 Spring 的集成作为一个子项目进行开发,这便是 MyBatis-Spring。本文主要解析 MyBatis-Spring 的工作原理,事实上,其集成方式与 Spring 官方提供的对其它开源框架的集成(如 Hibernate、MyBatis 的前身 iBatis 等)是十分相似的,从中也可以体会到 Spring 的一些设计思想,对于大家在平时开发过程中抽取通用组件、简化开发过程会有一定的帮助。

MyBatis基本工作方式

这里我们从 MyBatis 最基本的工作方式讲起,一步步解析 MyBatis-Spring 是如何对代码进行抽象、封装,最后实现简化开发的目的。以下是 MyBatis 3 官方文档中的一个例子:

String resource = "org/mybatis/example/Configuration.xml";
Reader reader = Resources.getResourceAsReader(resource);
SqlSessionFacotry sqlMapper = new SqlSessionFactoryBuilder().build(reader);

每个 MyBatis 应用的核心都是一个 SqlSessionFactory,SqlSessionFactory 通过 SqlSessionFactoryBuilder 构建。需要强调的是,对于一个应用程序来说,SqlSessionFactory一旦被构建,就应该在程序的整个生命周期中一直存活,开发人员不应该重复的构建SqlSessionFactory。当然,开发人员可以自己采用单例模式来来达到这一目的,但MyBatis官方更推荐使用已有的依赖注入框架,如Spring、Google的Guice等。

SqlSessionFactoryBuilder 可以通过两种方式构建 SqlSesisonFactory:读取 XML 配置文件,或是以用户自定义的 Configuration 对象为参数。上面的代码采用了第一种方式。 MyBatis 官方文档中的XML配置文件  Configuration.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">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>

上面的 Configuration.xml 是MyBatis基础配置文件,包括数据源、事务管理等基本信息的定义,其 mapper 标签内引用的文件便是Sql语句的配置文件了,其路径为”org/mybatis/example/BlogMapper.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.example.BlogMapper">
<select id="selectBlog" parameterType="int" resultType="Blog">
select * from Blog where id = #{id}
</select>
</mapper>

有了 SqlSessionFactory, 便可以通过它得到 SqlSession 实例用以在数据库上执行Sql了。 SqlSession并 不是线程安全的,因此每个线程必须拥有自己独立的 SqlSession,SqlSession最好在方法中作为局部变量定义,使用结束后一定要在 finally 语句中将 其 关闭。如下面的代码,执行BlogMapper.xml文件中id为selectBlog的语句:

SqlSession session = sqlMapper.openSession();
try {
	Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
} finally {
	session.close();
}

上面的方法与 iBatis 的使用类似,但是 MyBatis 中提供了另一种更为干净、安全的形式。首需要构建一个 org.mybatis.example.BlogMapper 接口,然后便可以使用如下代码:

SqlSession session = sqlSessionFactory.openSession();
try {
	BlogMapper mapper = session.getMapper(BlogMapper.class);
	Blog blog = mapper.selectBlog(101);
} finally {
	session.close();
}

可以看到这种方式清晰自然,无需担心将文件路径写错,也不用担心类型转换的错误。MyBatis是如何实现的呢?SqlSession的实现类持有一个 Configuration 对象(之前提到可以用于构建 SqlSessionFactory实例的对象),这个Configuration对象有一个 MapperRegistry 类型的域mapperRegistry,当 SqlSession 第一次调用参数为特定接口的 getMapper 函数时,这个接口会被注册到 mapperRegistry 对象中,当从mapperRegistry中以这个接口为key取出相应value时(入口为getMapper方法),MyBatis的返回结果是一个实现了此接口的代理,此代理内的方法被调用时,通过对接口方法的参数、返回值、sql mapper配置文件等进行解析,构造相应的MapperMethod对象,最后调用 SqlSession 内的操作函数( selectXXX ()、update()、delete()、insert(),通过解析配置文件及函数返回值确定需要调用的具体函数)。这里的逻辑属于Mybatis的范围,就不展开说了,有兴趣的朋友可以阅读上述几个类的源代码。

MyBatis-Spring的作用

上面对 MyBatis 的基本使用方法进行了介绍,不知道各位会不会觉得整个使用过程十分繁琐:
1. 需要保证SqlSessionFactory的单例实现;每次调用,都需要构造 SqlSession、进行所需调用、处理异常、关闭 SqlSession,代码重复且侵入了业务逻辑;
2. 需自行处理事务处理。MyBatis中进行事务处理需实现 Transaction 接口,而 MyBatis 仅默认提供了 JdbcTransaction 与 ManagedTransaction 这两个极为简单的实现类,事务处理的整个流程、异常处理都需要自己来控制。

既然有上述的种种不便,我们的 MyBatis-Spring 自然就是为了解决他们而存在,归纳起来 MyBatis-Spring 所做的处理包括:
1. 简化 dao 层的配置、调用,保持业务逻辑代码的干净;
2. 将 MyBatis 的异常统一转换为 Spring的异常信息;
3. 将事务控制与 Spring 的事务控制进行整合,通过 Spring 已有的方式进行事务控制。当然,到底是通过XML配置或者注解的方式就随使用者的喜好了。

说到这里,各位是不是已经充满了好奇, MyBatis-Spring 就是是如何做到上面几点的,别担心,下面将以第一点为例进行详细的讲解。第二点应该就不用多讲了,第三点会在后续介绍 Spring 事务控制的文章中讲解。

构造SqlSessionFactory

现在让我们思考一下,如果我们自己是开发着,会如何进行改造?首先当然是从 SqlSessionFactory 开始了,因为它的生命周期是整个应用程序范围,在spring中将其配置成一个scope为singleton的bean是再好不过了。其它类想引用它时,只要通过Spring注入即可。在 Mybatis-Spring 中,定义了一个 SqlSessionFactoryBean 类,看到这个类是以 ”FactoryBean“ 结尾,大家就可以猜到这个类是 继承自 Spring 的 FactoryBean 类,引用它的对象实际上得到的是 getObject() 方法返回的结果。不过这个名字可能会让人产生误解,它返回的结果是 SqlSessionFactory 而不是 SqlSession,因此这个类的全名大家可以想象成是 SqlSessionFactoryFactoryBean 。
    
我们再来看看这个 SqlSessionFactoryFactoryBean 做了些什么吧。一个典型的配置如下所示:

<bean id= "sqlSessionFactory" class= "org.mybatis.spring.SqlSessionFactoryBean" >
	<property name = "dataSource" ref= "dataSource" />
	<!-- <property name="mapperLocations" value="classpath*: org/mybatis/ dao/**/*.xml " /> --> 
</bean >

<bean id= "dataSource" class = "org.apache.commons.dbcp.BasicDataSource" destroy-method= "close" >
	.....
</bean >

dataSource属性对于 SqlSessionFactoryBean 来说是必须的,这里的 dataSource 大家可以按照自己的需求来提供,无论是通过 dbcp 数据池得到,或是 c3p0 ,或者是为了提供多数据源的  AbstractRoutingDataSource 的实现类都可以。mapperLocations 属性用于配置sql映射文件的路径,默认sql映射文件与对应的接口文件(上面让大家新建的 org.mybatis.example.BlogMapper 接口)在classpath的相同路径之下,因此一般情况下是不需要配置此属性的。在之前提供的MyBatis核心配置文件Configuration.xml 中,必要的信息包括数据源、事务管理、sql映射文件这三种,由于事务使用spring进行管理,因此核心配置文件中的必要信息在上述sqlSessionFactory的配置中已经全部定义了。但是MyBatis的配置文件中可以包含的信息不止这几样,还可以配置如别名的的其他信息,如果是这样的话,你还可以通过SqlSessionFactoryBean的configLocation属性来指定自己的配置文件的路径。

最后,SqlSessionFactoryBean返回的SqlSessionFactory对象与MyBatis使用的SqlSessionFactory对象相同,均为DefaultSqlSessionFactory对象,只不过如之前所述,配置信息的来源不同而已。

抽取dao层代码

通过SqlSessionFactoryBean得到SqlSessionFactory后,该如何使用?首先,我们来把程序代码模块化,将于数据持久层代码抽取出来。一般来说对数据库的操作代码在 dao 层,service 层为业务逻辑层,对 dao 层代码进行调用即可。我们之前新建的 BlogMapper,实际上就是 dao 层的接口,service 层使用的即是它的一个实现类。假设实现类代码与配置如下:

public class BlogMapperImpl implements BlogMapper {

  private SqlSession sqlSession;

  public void setSqlSession(SqlSession sqlSession) {
    this.sqlSession = sqlSession;
  }

  public User selectBlog(String id) {
    return (User) sqlSession.selectOne("org.mybatis.example.BlogMapper.selectBlog", id);
  }
}

<bean id="BlogMapper" class=""org.mybatis.example.BlogMapperImpl">
  <property name="sqlSession" ref="sqlSession" />
</bean>

通过 spring 为 service 层对象注入此实现类的 bean 后,在 service 层内就可以调用此 bean 的方法来进行数据库操作,这样,dao层的代码即被抽取出来。但是这里有一个问题,在之前提到过,SqlSession 并不是线程安全的,每个方法都需要新建一个 SqlSession,使用结束后还需要关闭此 SqlSession。这样的话,如何来初始化 BlogMapperImpl 中的 SqlSession 呢?MyBatis-spring 是通过 SqlSessionTemplate 来解决的。SqlSessionTemplate 是线程安全的,多个 dao 类可以使用同一个 SqlSessionTemplate,而且 SqlSessionTemplate 实现了 SqlSession 接口,这样,我们只需要将一个 SqlSessionTemplate 实例注入到 BlogMapperImple 的 sqlSession 域就可以了。

SqlSessionTemplate

SqlSessionTemplate实现了SqlSession接口,并持有了一个SqlSession的代理实现 sqlSessionProxy,它将 SqlSession 中的各数据操作函数(selectXXX、update、delete、insert)均委托给此 sqlSessionProxy 类来实现。前文讲到,每个 SqlSession 的作用域最好是方法范围,并且在执行完之后一定要close掉。既然 SqlSessionTemplate 将所有的数据库操作都委托给 sqlSessionProxy 域的对象,那么 sqlSessionProxy 肯定需要在每次执行时都构造一个相应的SqlSession,并在使用结束后将其关闭掉,这样才能保证线程安全。sqlSessionProxy 作为一个 Proxy,其对应的 InvocationHandler 的 invoke 方法内进行了上述处理;用于出事化它的 InvocationHandler 为 SqlSessionTemplate 的内部类 SqlSessionInterceptor:

private final SqlSessionFactory sqlSessionFactory;

private final ExecutorType executorType;

private final PersistenceExceptionTranslator exceptionTranslator;


..................................

private class SqlSessionInterceptor implements InvocationHandler {
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		final SqlSession sqlSession = SqlSessionUtils.getSqlSession(
				SqlSessionTemplate.this.sqlSessionFactory,
				SqlSessionTemplate.this.executorType,
				SqlSessionTemplate.this.exceptionTranslator);
		try {
			Object result = method.invoke(sqlSession, args);
			if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
				sqlSession.commit();
			}
			return result;
		} catch (Throwable t) {
			Throwable unwrapped = ExceptionUtil.unwrapThrowable(t);
			if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
				Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
				if (translated != null) {
					unwrapped = translated;
				}
			}
			throw unwrapped;
		} finally {
			SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
		}
	}
}

SqlSessionTemplate的sqlSessionFactory、executorType、exceptionTranslator域均是final,因此可用于 SqlSessionInterceptor 通过 SqlSessionUtils 来构造这个实际执行 sql 的 SqlSession。此 SqlSession 也是通过 SqlSessionFactory 的 openSession 函数得到,只不过涉及到spring的事务,所以做了一些相应的处理。这样,通过代理的方式,SqlSessionTemplate 帮我们完成了构造局部 SqlSession、调用数据操作方法、处理异常、关闭 SqlSession 的任务,我们只需要在 dao 的实现类中注入它的实例即可,而且代码层次也已经清晰多了。

注入MapperFactoryBean

现在我们给每个 dao 接口的实现类都注入了 SqlSessionTemplate,service 层调用这些 dao 实现类的接口即可完成持久化操作了。但接下来问题又来了,一个接口若有10个方法,那岂不是要在它的实现类中将每个接口都实现一遍?而且它们的实现方式都是类似的,仅仅只是调用 SqlSessionTemplate 的相关方法,能不能进行统一的处理呢?对了,还记不记得之前我们通过 SqlSession 的 getMapper() 方法直接得到接口的代理类?这里我们也可以用相同的方式来统一实现各 dao 接口,是时候让 MapperFactoryBean 登场了。

话不多说,我们先来一个直观的例子:

<bean id="BlogMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
  <property name="mapperInterface" value="org.mybatis.example.BlogMapper" />
  <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

很明显,这是一个 dao 实现类的定义,其作用与之前的 BlogMapper bean 一样,用于在 service 层中使用。但它的 class 属性为 MapperFactoryBean,它是一个factoryBean,因此我们得到的是它 getObject 方法返回的对象。需要返回什么对象?应该还记得上面的这段代码吧:

BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);

不错,返回的就是BlogMapper的代理实现类。需要代理的接口通过属性 mapperInterface 来定义,而 sqlSessionFactory 属性自然是用来指定之前介绍过的 SqlSessionFactory 对象了。MapperFactoryBean 内相关如下所示:

protected void checkDaoConfig() {
	super.checkDaoConfig();
   
	Assert.notNull(this.mapperInterface, "Property 'mapperInterface' is required");

	Configuration configuration = getSqlSession().getConfiguration();
	if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
		try {
			//重要,将此mapperInterface注册到configuration的MapperRegistry 中
			configuration.addMapper(this.mapperInterface);
		} catch (Throwable t) {
			logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", t);
			throw new IllegalArgumentException(t);
		} finally {
			ErrorContext.instance().reset();
		}
	}
}

.......

//FactoryBean类实际返回对象的方法,如前面介绍,返回一个代理对象,其每个方法被解析为调用实际的数据库操作方法
public T getObject() throws Exception {
	return getSqlSession().getMapper(this.mapperInterface);
}

MapperFactoryBean 间接继承了 InitializingBean 接口,其 checkDaoConfig() 方法会在 afterPropertiesSet() 方法中执行,保证在调用 getObject() 方法之前会通过 configuration 的 addMapper() 方法将此接口注册到 MapperRegistry 中。

自动扫描

好了,有了 MapperFactoryBean 的话,对于每个 dao 接口我们只需要进行上面那几行配置就可以了。但是如果有20个 dao 接口,岂不是还要配置20个 bean?别急,mybatis-spring 已经提供了解决的方法 --MapperScannerConfigurer 类,它实现了 spring 的 BeanDefinitionRegistryPostProcessor 接口,可以自动对指定目录进行扫描,并构造各 dao 接口相应的 MapperFactoryBean:

	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<property name="basePackage" value="org.mybatis.example" />
	</bean>

basePackage 属性指定了需要进行扫描的根包名,这样,连配置MapperFactoryBean的工作的省去了,是不是十分方便?

结尾

以上就是 MyBatis-Spring 的整个实现思路,而对于事务管理这一块,由于与 spring 的事务处理关联较大,在后续的文章中会一起进行详细的讲解。
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值