目录
Spring提供了与Hibernate、JPA的集成类,没有提供与MyBatis的集成类,但MyBatis提供了与Spring的无缝集成库MyBatis-Spring,官方网站为http://mybatis.org/spring/zh/index.html。(进入官网需要注意MyBatis-Spring、MyBatis、Spring、Java这几个的版本关系)
MyBatis与Spring整合后,可以不再需要MyBatis的全局配置文件。会话工厂及会话交由Spring容器管理,事务交由Spring事务管理器管理,MyBatis异常转换为Spring的DataAccessException异常进行统一处理。MyBatis虽然自带连接池,但功能较弱,两者的集成也可以引入DBCP2等连接池库。
一、MyBatis的开发方式
先创建全局配置文件和Mapper接口以及Mapper配置文件,如下:
MyBatis使用SqlSessionFactory创建会话对象,使用会话或者通过映射器调用映射方法。会话工厂SqlSessionFactory使用配置文件的输入流初始化。如下是MyBatis一个基本的查询操作:
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = userMapper.getUserList();
}
二、整合的主要组件类
MyBatis提供了SqlSessionFactoryBean类,该类实例使用数据源参数构建。SqlSessionFactoryBean实现了FactoryBean接口,是SqlSessionFactory的工厂Bean。工厂Bean使用getObject()返回SqlSessionFactory实例。
除了MyBatis本身的会话工厂和会话外,MyBatis-Spring在设计上沿袭和与Spring JDBC相同的框架结构,两者比较如下表:
Spring JDBC | MyBatis-Spring |
DataSource | DataSource |
无 | SqlSessionFactoryBean |
JdbcTemplate | SqlSessionTemplate |
DAO(自定义) | MapperFactoryBean(SqlSessionDaoSupport) |
SqlSessionTemplate是MyBatis-Spring的一个核心类,类似JdbcTemplate,是MyBatis数据操作的模板处理类。提供的功能包括:
- 管理MyBatis的SqlSession;
- 调用数据操作方法;
- 异常转换,将MyBatis异常统一转换为Spring的DataAccessException
SqlSessionTemplate实现了SqlSession接口。所以具备获取映射器(getMapper())和增删改查等数据操作方法,可以用来替代MyBatis的DefaultSqlSession。SqlSeesionTemplate是线程安全的,可以多个DAO共用。虽然SqlSessionTemplate可以单独使用,但基本是在框架内部使用。框架对外提供更高层级的数据操作方式MapperFactoryBean。MapperFactoryBean继承自SqlSessionDaoSupport。
SqlSessionDaoSupport是基于MyBatis的DAO层抽象父类,继承自Spring的DaoSupport抽象类。该类维护了SqlSeesionTemplate类型的成员变量,也提供了获取SqlSessionFactory的方法。
MapperFactoryBean封装了SqlSessionTemplate映射器获取方法,支持使用接口参数通过动态代理的方式获取映射器。如果映射器接口类路径有一个同名的XML映射文件时,会被MapperFactoryBean自动解析,如果不同路径或者文件名不同,则可以使用SqlSessionFactoryBean的configLocation属性进行配置。
三、整合方式
MyBatis的全局配置文件主要配置和数据源相关的基本连接。池连接属性、事务处理和映射文件引用,也会有一些高级的配置,比如类型别名、类型处理器和插件等。与Spring整合后,MyBatis的全局配置文件就可以不需要了。先创建Spring核心配置文件applicationContext.xml。
1.数据源
数据源使用Spring容器管理的数据源,数据源基本属性和池连接属性在数据源Bean中配置。以DBCP2连接池数据源为例,Bean配置如下:
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/mystudy"></property>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
<property name="maxTatal" value="20"></property>
</bean>
2.映射文件引用
映射文件和MyBatis参数设置通过SqlSessionFactoryBean方式进行配置,配置格式如下:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
<property name="mapperLocations" value="classpath*:com/mec/mybatis/mapper/**/*.xml"></property>
</bean>
在以上配置中:
- dataSource:配置数据源,必要的配置。
- mapperLocations:用于匹配XML配置文件,支持Ant样式来匹配目录中的文件。设置格式类似classpath*:com/mec/mybatis/mapper/**/*.xml(该配置会加载com,mec.mybatis.mapper包和它的子包中的 MyBatis 映射器 XML 配置文件)。如果接口类和映射XML文件位于同目录下可以省略。
除了上面两个常用配置之外,SqlSessionFactoryBean可选的配置属性还有:
- transactionFactory:事务工厂,与Spring整合后,使用的基本是Spring事务管理器,所以该属性一般不会配置。
- plugins:Mybatis的插件配置;
- typeHandlers:类型转换器配置;
- typeHandlersPackage:自定义类型转换器类所在的包路径;
- typeAliasesSuperType:类型别名配置;
- databaseIdProvider:数据库提供商;
- cache:缓存配置;
- objectFactory:对象工厂;
- configurationProperties:使用属性标签配置MyBatis的属性。除了这种方式之外,SqlSessionFactoryBean还支持使用configuration属性配置Configuration类型的Bean或者使用configLocation配置MyBatis的XML配置文件,configuration的配置示例如下:
<property name="configuration">
<bean class="org.apache.ibatis.session.Configuration">
<property name="mapUnderscoreToCamelCase" value="true"></property>
</bean>
</property>
需要注意的是:如果接口类和映射XML文件不再同一路径下,需要在SqlSessionFactoryBean配置中使用mapperLocations属性指定XML映射文件的地址。如果XML映射文件目录有其他非映射的XML配置文件,则需要使用*Mapper.xml等方式进行匹配,否则MyBatis会将其他XML文件也作为映射文件来解析。
3.映射器配置
SqlSessionFactoryBean可以完全取代MyBatis全局配置文件,使用SqlSessionFactory实例作为参数配置SqlSessionTemplate或MapperFactoryBean的Bean。MapperFactoryBean是对单个接口代理生成器,也可以使用MapperScannerConfigurer扫描包下所有的接口进行代理。
3.1 单个接口的映射器配置
以UserMapper接口为例,映射器配置如下:
<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
<property name="mapperInterface" value="com.mec.mybatis.mapper.UserMapper"></property>
<property name="sqlSessionFactory" ref="sqlSessionFactory"></property>
</bean>
mapperInterface指定接口(必须是接口)的全限定名;sqlSessionFactory属性引用SqlSessionFactoryBean类型Bean,也可以使用SqlSessionTemplate属性及对应的Bean替代,如果两者都设置,则以sqlSessionTemplate优先。
映射器在Spring容器中配置后,使用容器的getBean()方法获取,也可以使用@Autowired注解自动装载。如下:
public static void main(String[] args) throws IOException {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
context.registerShutdownHook();
UserMapper userMapper = (UserMapper) context.getBean("userMapper");
List<User> userList = userMapper.getUserList();
}
注意此时已经不需要mybatis-config.xml配置文件了。
3.2 自动扫描接口代理生成映射器(重要)
每个映射器都在Spring配置文件中配置,繁琐且不易维护。MyBatis-Spring提供了配置MapperScannerConfigurer的Bean,可以自动扫描下面的所有接口并自动代理生成映射器。除此之外,MyBatis-Spring还提供了标签方式来简化配置。
MapperScannerConfigurer配置
MapperScannerConfigurer类型(实现了BeanDefinitionRegistryPostProcessor接口)的Bean需要注入basePackage和sqlSessionFactoryBeanName的属性值,其会根据basePackage配置的类路径查找接口并自动将他们创建成MapperFactoryBean类型的对象。配置示例如下:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.mec.mybatis.mapper"></property>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
</bean>
- basePackage属性设置扫描接口的路径,多个路径使用分号或者逗号分隔。
- sqlSessionFactoryBeanName配置会话工厂的Bean名称。如果Spring容器中只有一个SqlSessionFactory实例,则该属性可以省略。此外,虽然也可以使用sqlSessionFactory属性和ref来引用Bean,但已经过时,不建议使用。
如果应用程序基于Spring注解进行配置,则使用@MapperScan注解,如下:
@Configuration //配置类注解
@MapperScan("org.mybatis.spring.sample.mapper") //组件扫描路径
public class AppConfig {
// ...
}
配置
MyBatis-Spring使用配置标签来实现接口的自动扫描。首先需要导入MyBatis的命名空间和文档定义,它发现映射器的方法与 Spring 内建的发现 bean 的方法非常类似。如下在中加入mybatis命名空间:
xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
在xsi:schemaLocation中加入文档结构定义的映射:
http://mybatis.org/schema/mybatis-spring
http://mybatis.org/schema/mybatis-spring.xsd
使用时利用base-package指定接口的扫描路径:
<mybatis:scan base-package="com.mec.mybatis.mapper" />
base-package属性允许你设置映射器接口文件的基础包。通过使用逗号或分号分隔,你可以设置多个包。并且会在你所指定的包中递归搜索映射器。
注意,不需要为指定SqlSessionFactory或SqlSessionTemplate,这是因为它将使用能够被自动注入的MapperFactoryBean。但如果你正在使用多个数据源,自动注入可能不适合你。 在这种情况下,你可以使用 factory-ref或template-ref 属性指定你想使用的 bean 名称。
使用@ MapperScan注解
当我们基于Java代码进行配置时,可以使用该注解配合@Configuration注解来自动扫描接口,该注解中的value属性用于指定需要被扫描的包路径。它和的工作方式是一样的。
4.整合事务管理
MyBatis整合Spring后,MyBatis自动参与到Spring事务管理中,无需额外配置。需要注意的是:MyBatis的SqlSessionFactoryBean引用的数据源与Spring事务管理器DataSourceTransactionManager引用的数据源要保持一致。
4.1本地事务管理
本地事务配置Spring的DataSourceTransactionManager的Bean,并开启事务注解驱动,MyBatis-Spring的SqlSessionFactoryBean不需要做任何改动。配置如下:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<tx:annotation-driven/><!-- 事务注解驱动 -->
4.2 JTA事务
JTA的实现有两种方式:
- 使用JNDI从容器中获取数据源,通过Spring调用应用服务器的全局事务管理器进行管理;
- 导入独立的JTA实现库,比如Atomikos或JOTM。
应用服务器的全局事务管理方式只需要修改事务管理器类为JtaTransactionManager,配置如下:
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
<!--这里的数据源是通过JNDI方式查找的-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<tx:annotation-driven/>
如果使用JTA的独立实现库方式,首先需要导入相关库,然后配置JtaTransactionManager的transactionManager和userTransaction的值。以Atomikos为例,如下:
<bean id="atomikosTransactionManager" class="com.atomikos.icatch.jta.UserTransactionManager" depends-on="userTransactionService"></bean>
<bean id="atomikosUserTransaction" class="com.atomikos.icatch.jta.UserTransactionImp"></bean>
<bean id="jtaTransactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManager" ref="atomikosTransactionManager"></property>
<property name="userTransaction" ref="atomikosUserTransaction"></property>
</bean>
除了使用Spring管理事务之外,MyBatis-Spring也允许配置自身的事务管理器,配置方式是设置SqlSessionFactoryBean的transactionFactory属性,示例如下:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
<property name="transactionFactory">
<bean class="org.apache.ibatis.transaction.managed.ManagedTransactionFactory"></bean>
</property>
</bean>
4.3 事务整合注意事项
SqlSession提供了commit()和rollback()等事务操作方法,但在MyBatis-Spring中是不允许直接调用这些方法处理事务的。原因是在SqlSessionTemplate中对这些方法进行了改写,这些方法被调用时,会抛出不支持操作的异常。如果应用程序要用到编程式事务,就需要使用Spring的事务编码方式。推荐方式是使用开启事务注解后,使用@Transactional进行事务控制。
四、有关整合的一些源码
1.FactoryBean介绍
FactoryBean是一个接口(区别于BeanFactory,它是用于创建和管理Spring容器中的Bean的),它是一个工厂Bean,可以生成某一个类型的Bean实例。它的最大作用就是可以让我们自定义Bean的创建过程。该接口提供三个方法:
public class UserFactoryBean implements FactoryBean<User> {
//根据getObjectType中设置的Bean类型返回Bean对象实例
@Override
public User getObject() throws Exception {
User u = new User();
u.setName("一个测试的User实例");
return u;
}
//设置Bean的类型
@Override
public Class<?> getObjectType() {
return User.class;
}
//设置是否为单例
@Override
public boolean isSingleton() {
return true;
}
}
将UserFactoryBean注入到容器中,然后进行测试:
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
User u = (User) context.getBean(User.class);
System.out.println(u.getName()); //输出:一个测试的User实例
}
注意从容器中获取到的是UserFactoryBean的getObject()方法返回的Bean实例。
2. SqlSessionFactoryBean
MyBatis-Spring进行整合时必须配置SqlSessionFactoryBean,比如上面的数据源和映射文件的配置也是注入到该对象对应的属性中去的,除此之外,还可以通过该对象进行一些其他配置,比如可以指定MyBatis全局配置文件等等。如下是该类的所有属性,在该对象实例中注入对应的属性就是进行相应的配置:
ublic class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
private static final Log LOGGER = LogFactory.getLog(SqlSessionFactoryBean.class);
private Resource configLocation; //全局配置方式一:全局配置文件路径,MyBatis会解析配置文件
private Configuration configuration; //全局配置方式二:通过注入全局配置类
private Resource[] mapperLocations; //映射文件路径
private DataSource dataSource; //数据源
private TransactionFactory transactionFactory; //事务管理工厂
private Properties configurationProperties;
private SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
private SqlSessionFactory sqlSessionFactory;
private String environment = SqlSessionFactoryBean.class.getSimpleName();
private boolean failFast;
private Interceptor[] plugins;
private TypeHandler<?>[] typeHandlers;
private String typeHandlersPackage;
private Class<?>[] typeAliases;
private String typeAliasesPackage;
private Class<?> typeAliasesSuperType;
private DatabaseIdProvider databaseIdProvider;
private Class<? extends VFS> vfs;
private Cache cache;
private ObjectFactory objectFactory;
private ObjectWrapperFactory objectWrapperFactory;
}
可以看到该类也实现了FactoryBean可以看出来,该工厂Bean用于创建SqlSessionFactory的实例,那么直接查看其getObject()方法,如下:
public Class<? extends SqlSessionFactory> getObjectType() {
return this.sqlSessionFactory == null ? SqlSessionFactory.class : this.sqlSessionFactory.getClass();
}
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
this.afterPropertiesSet();
}
return this.sqlSessionFactory;
}
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.dataSource, "Property 'dataSource' is required");
Assert.notNull(this.sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
Assert.state(this.configuration == null && this.configLocation == null || this.configuration == null || this.configLocation == null, "Property 'configuration' and 'configLocation' can not specified with together");
this.sqlSessionFactory = this.buildSqlSessionFactory();
}
上述就是两个方法的调用,从afterPropertiesSet()方法中可以看出:
- dataSource是必要的配置,因为如果没有注入dataSource,这里断言如果为真,则会抛出IllegalArgumentException异常。
- 全局配置类和全局配置文件不能同时出现。
其实这里创建SqlSessionFactory这个Bean实例的核心逻辑就在buildSqlSessionFactory()方法中。
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
XMLConfigBuilder xmlConfigBuilder = null;
Configuration configuration;
//中间部分都是构建configuration对象的过程,从XML文件中读取,从我们通过属性注入的configuration中读取。
//如果都没有则创建一个默认的configuration实例,还进行了其他一些额外的设置。
//这里的源码就省略了。
return this.sqlSessionFactoryBuilder.build(configuration);
}
//SqlSessionFactoryBuilder类中
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
综上,可以看到SqlSessionFactoryBean中解析了全局配置类并实例化了DefaultSqlSessionFactory对象,将其注册到容器中。
3. MapperFactoryBean
在通过Mapper方式开发时,还需要配置MapperFactoryBean。在上面示例中注入了两个属性:mapperInterface和sqlSessionFactory(从SqlSessionFactoryBean的源码中可以看出这里注入的其实是DefaultSqlSessionFactory实例)。mapperInterface就是用于指定Mapper接口的路径的(必须设置)。
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
private Class<T> mapperInterface;
private boolean addToConfig = true;
//省略其他方法
public T getObject() throws Exception {
//通过sqlSession获取Mapper接口的代理对象
return this.getSqlSession().getMapper(this.mapperInterface);
}
public Class<T> getObjectType() {
return this.mapperInterface;
}
public boolean isSingleton() {
return true;
}
@Override
protected void checkDaoConfig() {
//检查this.sqlSessionTemplate是否为null
super.checkDaoConfig();
//检查mapperInterface是否未注入
notNull(this.mapperInterface, "Property 'mapperInterface' is required");
Configuration configuration = getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
//向configuration中添加mapperInterface
configuration.addMapper(this.mapperInterface);
} catch (Exception e) {
logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
throw new IllegalArgumentException(e);
} finally {
ErrorContext.instance().reset();
}
}
}
}
从上面getObject()方法源码中可以看出,这个MapperFactoryBean就是用于创建自定义Mapper接口类型的Bean的,而该Bean实例则是sqlSession通过getMapper()方法获取的接口代理。MyBatis的Mapper开发方式,我们之所以不需要自定义Mapper、接口的实现类,就是由于getMapper()方法底层帮我们动态创建了该接口的代理,调用Mapper接口的方法实则是调用代理类的方法。
getSqlSession()方法并不是定义在MapperFactoryBean中,而是继承自其父类SqlSessionDaoSupport。该类如下:
public abstract class SqlSessionDaoSupport extends DaoSupport {
private SqlSession sqlSession;
private boolean externalSqlSession;
public SqlSessionDaoSupport() {
}
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
if (!this.externalSqlSession) {
this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
}
}
public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
this.sqlSession = sqlSessionTemplate;
this.externalSqlSession = true;
}
public SqlSession getSqlSession() {
return this.sqlSession;
}
protected void checkDaoConfig() {
Assert.notNull(this.sqlSession, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");
}
}
从源码中可以发现,这里的sqlSession其实是SqlSessionTemplate,这个包含了DefaultSqlSession中的所有方法,而且它是线程安全的。
其中两个set方法也可以应证(三 3.1)中的话:sqlSessionFactory属性引用SqlSessionFactoryBean类型Bean,也可以使用SqlSessionTemplate属性及对应的Bean替代,如果两者都设置,则以sqlSessionTemplate优先。
现在思路回到MapperFactoryBean中的getObject()方法中,可以知道调用的是SqlSessionTemplate对象的getMapper()方法,调用链如下:
//1.SqlSessionTemplate类中
public <T> T getMapper(Class<T> type) {
return this.getConfiguration().getMapper(type, this);
}
//2.Configuration类中
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return this.mapperRegistry.getMapper(type, sqlSession);
}
//3.MapperRegistry类中
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
} else {
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception var5) {
throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
}
}
}
这里需要注意的是getMapper()是从Configuration配置类中通过Mapper接口的类型获取的(存储在knownMappers这个Map中),所以我们要保证Configuration配置类中存在这个类型。否则就会报出Mapper未注册的异常。从上面可以看出,在MapperFactoryBean的checkDaoConfig()方法中会帮我们注册进去,checkdaoConfig()方法的定义是在DaoSupport中,只不过MapperFactoryBean间接继承了该类并实现了该方法。
更多内容可以关注:https://mybatis.org/spring/zh/