Spring Boot + Mybatis資料來源配置的三種方式
通過之前兩篇文章Spring Boot + JdbcTemplate和Spring Boot + Mybatis CRUD可以看出,無論是使用什麼框架,資料來源及框架的的一些配置總是不可避免的。在之前的兩篇文章中分別使用了application.properties和Java Config的方式進行了配置。其實Mybatis也可以使用這兩中方式進行配置,除此之外,Mybatis還可以通過使用xml配置的方式進行配置。本片文章將講述一下三種配置的配置方法。
1. 專案結構
| pom.xml | springboot-06-mybatis-config.iml | +---src | +---main | | +---java | | | \---com | | | \---zhuoli | | | \---service | | | \---springboot | | | \---mybatis | | | \---config | | | | SpringBootMybatisConfigApplicationContext.java | | | | | | | +---controller | | | | UserController.java | | | | | | | +---repository | | | | +---conf | | | | | DataSourceConfig.java | | | | | | | | | +---mapper | | | | | UserMapper.java | | | | | | | | | +---model | | | | | User.java | | | | | | | | | \---service | | | | | UserRepository.java | | | | | | | | | \---impl | | | | UserRepositoryImpl.java | | | | | | | \---service | | | | UserControllerService.java | | | | | | | \---impl | | | UserControllerServiceImpl.java | | | | | \---resources | | | application.properties | | | mybatis-config.xml | | | repository-bean.xml | | | | | \---base | | \---com | | \---zhuoli | | \---service | | \---springboot | | \---mybatis | | \---config | | \---repository | | \---mapper | | UserMapper.xml | | | \---test | \---java
2. application.properties配置
application.properties檔案時Spring Boot預設載入的檔案,並會通過檔案內容進行一些預設配置。比如在使用jdbcTemplate時,如果application.properties檔案中存在”spring.datasource.url”、”spring.datasource.password”、”spring.datasource.username”時,Spring Boot會預設自動配置DataSource,它將優先採用HikariCP連線池,如果沒有該依賴的情況則選取tomcat-jdbc,如果前兩者都不可用最後選取Commons DBCP2。在使用Mybatis時,通常在application.properties檔案中做如下配置:
#資料來源Datasource配置 spring.datasource.url=jdbc:mysql://115.47.149.48:3306/zhuoli_test?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false spring.datasource.password=zhuoli spring.datasource.username=zhuoli #mybatis配置 #Mybatis mapper.xml檔案位置 mybatis.mapper-locations=classpath:base/com/zhuoli/service/springboot/mybatis/curd/repository/mapper/*.xml #設定這個以後再Mapper.xml檔案中在parameterType的值就不用寫成全路徑名了,可以寫成parameterType = "User" mybatis.type-aliases-package=com.zhuoli.service.springboot.mybatis.curd.repository.model # 駝峰命名規範 如:資料庫欄位是order_id,那麼實體欄位就要寫成orderId mybatis.configuration.map-underscore-to-camel-case=true
除此之外不用做任何配置,Spring Boot會預設載入application.properties檔案,並進行預設配置
3. Java Config配置
在文章Spring Boot + Mybatis CRUD中,已經演示瞭如何使用Java Config Mybatis配置,這裡總結一些配置步驟,如下:
3.1 application.properties檔案定義資料來源資訊
其實這一步也可以不配置,如果不配置,在後續DataSourceConfig.java檔案中,就要將資料來源資訊寫死,不夠靈活。
##資料來源配置
test.datasource.url=jdbc:mysql://115.47.149.48:3306/zhuoli_test?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false
test.datasource.username=zhuoli
test.datasource.password=zhuoli
test.datasource.driverClassName=com.mysql.jdbc.Driver
3.2 新增DataSourceConfig.java配置類
@Configuration
public class DataSourceConfig {
@Value("${test.datasource.url}")
private String url;
@Value("${test.datasource.username}")
private String user;
@Value("${test.datasource.password}")
private String password;
@Value("${test.datasource.driverClassName}")
private String driverClass;
@Bean(name = "dataSource")
public DataSource dataSource() {
PooledDataSource dataSource = new PooledDataSource();
dataSource.setDriver(driverClass);
dataSource.setUrl(url);
dataSource.setUsername(user);
dataSource.setPassword(password);
return dataSource;
}
@Bean(name = "transactionManager")
public DataSourceTransactionManager dataSourceTransactionManager() {
return new DataSourceTransactionManager(dataSource());
}
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
/*設定mapper檔案位置*/
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:base/com/zhuoli/service/springboot/mybatis/config/repository/mapper/*.xml"));
/*設定實體類對映規則: 下劃線 -> 駝峰*/
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
sessionFactory.setConfiguration(configuration);
return sessionFactory.getObject();
}
}
注意,要使用@Configuration註解標註,表明這個類是個配置類,相當於一個Spring配置的xml檔案。@Bean註解在方法上,聲明當前方法的返回值是一個Bean。
3.3 Spring Boot啟動類通過@Import載入配置類
@SpringBootApplication
@Import(DataSourceConfig.class)
public class SpringBootMybatisConfigApplicationContext {
public static void main(String[] args) {
SpringApplication.run(SpringBootMybatisConfigApplicationContext.class, args);
}
}
4. xml配置
xml配置的方式,是通過自定義datasource Bean的方式實現資料來源配置,一般都會結合效能較好的資料庫連線池(Druid、Zebra……)定義資料來源,並定義sqlSessionFactory、transactionManager Bean。如下展示,使用PooledDatasource進行配置:
4.1 application.properties檔案定義資料來源資訊
其實這一步也可以不配置,如果不配置,在後續repository-bean.xml檔案中,就要將資料來源資訊寫死,不夠靈活。
##資料來源配置
test.datasource.url=jdbc:mysql://115.47.149.48:3306/zhuoli_test?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false
test.datasource.username=zhuoli
test.datasource.password=zhuoli
test.datasource.driverClassName=com.mysql.jdbc.Driver
4.2 repository-bean.xml定義資料來源DataSource
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--datasource-->
<bean id="commDataSource" class="org.apache.ibatis.datasource.pooled.PooledDataSource">
<property name="url" value="${test.datasource.url}"/>
<property name="username" value="${test.datasource.username}"/>
<property name="password" value="${test.datasource.password}"/>
<property name="driver" value="${test.datasource.driverClassName}"/>
</bean>
<bean id="commSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"
name="commSqlSessionFactory">
<property name="dataSource" ref="commDataSource" />
<property name="mapperLocations" value="classpath:base/com/zhuoli/service/springboot/mybatis/config/repository/mapper/*.xml" />
<!--<property name="configuration">
<bean class="org.apache.ibatis.session.Configuration">
<property name="mapUnderscoreToCamelCase" value="true"/>
</bean>
</property>-->
<!--通過configLocation使用其他配置檔案配置,但是configLocation與configuration不能共存-->
<property name="configLocation" value="classpath:mybatis-config.xml" />
</bean>
<bean id="commTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="commDataSource"/>
</bean>
</beans>
上述配置通過SqlSessionFactory Bean的configLocation property屬性,指明瞭mybatis配置檔案位置,通過mybatis-config.xml檔案的配置構造SqlSessionFactory。其實也可以通過上述配置中註釋的部分進行配置,效果是一樣的。
4.3 mybatis-config.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>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
</configuration>
上述mybatis-config.xml配置了一個重要屬性 mapUnderscoreToCamelCase,當該屬性為true時,從資料庫中查詢到的資料對映到實體類,會把下劃線對映成駝峰形式。如果在不設定resultMap的情況下,實體類又是駝峰定義的,這個屬性是必設的,否則實體類所有的駝峰成員都將拿不到值。
4.4 Spring Boot啟動類通過@ImportResource載入配置檔案
@SpringBootApplication
@ImportResource(locations = {"classpath:repository-bean.xml"})
public class SpringBootMybatisConfigApplicationContext {
public static void main(String[] args) {
SpringApplication.run(SpringBootMybatisConfigApplicationContext.class, args);
}
}
5. 關於自動配置和手動配置的一個問題
在測試這個xml配置的時候,我最開始的配置檔案長這個樣子:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--datasource-->
<bean id="commDataSource" class="org.apache.ibatis.datasource.pooled.PooledDataSource">
<property name="url" value="${test.datasource.url}"/>
<property name="username" value="${test.datasource.username}"/>
<property name="password" value="${test.datasource.password}"/>
<property name="driver" value="${test.datasource.driverClassName}"/>
</bean>
<bean id="commSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"
name="commSqlSessionFactory">
<property name="dataSource" ref="commDataSource" />
<property name="mapperLocations" value="classpath:base/com/zhuoli/service/springboot/mybatis/config/repository/mapper/*.xml" />
</bean>
<bean id="commTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="commDataSource"/>
</bean>
</beans>
跟我最終的配置的區別在於,SqlSessionFactoryBean沒有配置configuration和configLocation property。我當時的想法是,Spring Boot應該能預設載入mybatis-config.xml檔案的配置(駝峰設定),這樣我最終查詢到的資料應該是沒問題的。但是在測試時,卻發現一個現象,實體類的駝峰成員變數值都為null,結果返回長這個樣子:
{
"id": 5,
"userName": null,
"description": "Michael is a student",
"isDeleted": null
}
很明顯,Spring Boot並沒有載入到mybatis-config.xml檔案中的配置。後來我依次做了如下嘗試:
- 是不是Spring Boot不知道mybatis-config.xml配置檔案的位置,所以我在application.properties檔案中又加了一行:
mybatis.config-location=classpath:mybatis-config.xml
結果,實體類的駝峰成員變數值依然都為null
- 在application.properties檔案中新增配置,如下:
mybatis.configuration.map-underscore-to-camel-case=true
結果,實體類的駝峰成員變數值依然都為null
- 後來我想到,Spring Boot預設自動配置Mybatis的時候,肯定也初始化了一個預設的SqlSessionFactoryBean,假如不手動配置了,改用Spring Boot預設配置,情況會怎樣
所以我把repository-bean.xml配置檔案的SqlSessionFactoryBean整個註釋掉了,然後在application.properties檔案中加了一行:
mybatis.config-location=classpath:mybatis-config.xml
奇蹟出現了,實體類的駝峰成員變數值正常拿到了,後來我做了各種組合情況測試,發現一個規律:當手動配置SqlSessionFactoryBean的時候,application.properties中mybatis的配置是不起作用的,也無法通過指定mybatis配置檔案位置的方式,獲取mybatis-config.xml配置檔案中的配置
一度很糾結,在一通debug之後,發現了根源所在:
在使用Spring Boot預設配置時,會在MybatisAutoConfiguration類中定義一個SqlSessionFactory的Bean,該方法中呼叫了SqlSessionFactory的getObject方法,SqlSessionFactory例項在SqlSessionFactoryBean類中完成初始化,在初始化過程中會載入配置。將核心程式碼粘出,如下:
/*1. MybatisAutoConfiguration定義SqlSessionFactory Bean*/
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
/*載入application.properties檔案mybatis.config-location屬性*/
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
/*載入application.properties檔案mybatis.configuration.*屬性*/
org.apache.ibatis.session.Configuration configuration = this.properties.getConfiguration();
if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) {
configuration = new org.apache.ibatis.session.Configuration();
}
//省略……
factory.setConfiguration(configuration);
/*載入application.properties檔案mybatis.configuration-properties.*屬性*/
if (this.properties.getConfigurationProperties() != null) {
factory.setConfigurationProperties(this.properties.getConfigurationProperties());
}
//省略……
return factory.getObject();
}
/*2. SqlSessionFactoryBean類getObject方法*/
public SqlSessionFactory getObject() throws Exception {
/*3. sqlSessionFactory為null*/
if (this.sqlSessionFactory == null) {
this.afterPropertiesSet();
}
return this.sqlSessionFactory;
}
/*4. 呼叫buildSqlSessionFactory獲取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();
}
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
XMLConfigBuilder xmlConfigBuilder = null;
Configuration configuration;
/*從上述1的SqlSessionFactoryBean的configuration獲取配置*/
if (this.configuration != null) {
configuration = this.configuration;
if (configuration.getVariables() == null) {
configuration.setVariables(this.configurationProperties);
} else if (this.configurationProperties != null) {
configuration.getVariables().putAll(this.configurationProperties);
}
} else if (this.configLocation != null) {
/*從上述1的SqlSessionFactoryBean的configLocation獲取配置*/
xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), (String)null, this.configurationProperties);
configuration = xmlConfigBuilder.getConfiguration();
} else {
/*configuration和configLocation都沒配置,載入預設配置*/
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
}
configuration = new Configuration();
if (this.configurationProperties != null) {
configuration.setVariables(this.configurationProperties);
}
}
//省略……
if (xmlConfigBuilder != null) {
try {
/*parse mybatis-config.xml配置檔案*/
xmlConfigBuilder.parse();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Parsed configuration file: '" + this.configLocation + "'");
}
} catch (Exception var22) {
throw new NestedIOException("Failed to parse config resource: " + this.configLocation, var22);
} finally {
ErrorContext.instance().reset();
}
}
//省略……
/*獲取SqlSessionFactory例項*/
return this.sqlSessionFactoryBuilder.build(configuration);
}
而在使用手動sqlSessionFactory配置時(在repository-bean.xml檔案中配置SqlSessionFactory Bean),Spring Boot是通過如下方式載入的:
/*1. AbstractAutowireCapableBeanFactory */
protected BeanWrapper instantiateBean(String beanName, RootBeanDefinition mbd) {
//省略……
/*構造configuration*/
beanInstance = this.getInstantiationStrategy().instantiate(mbd, beanName, this);
//省略……
}
/*2. SimpleInstantiationStrategy*/
public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {
//省略……
return BeanUtils.instantiateClass(constructorToUse, new Object[0]);
//省略……
}
/*3. BeanUtils*/
public static <T> T instantiateClass(Constructor<T> ctor, Object... args) throws BeanInstantiationException {
//省略……
//執行ctor.newInstance(args)構造configuration
return KotlinDetector.isKotlinType(ctor.getDeclaringClass()) ? BeanUtils.KotlinDelegate.instantiateClass(ctor, args) : ctor.newInstance(args);
}
/*4. DelegatingConstructorAccessorImpl*/
public Object newInstance(Object[] var1) throws InstantiationException, IllegalArgumentException, InvocationTargetException {
return this.delegate.newInstance(var1);
}
/*5. NativeConstructorAccessorImpl*/
public Object newInstance(Object[] var1) throws InstantiationException, IllegalArgumentException, InvocationTargetException {
if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.c.getDeclaringClass())) {
ConstructorAccessorImpl var2 = (ConstructorAccessorImpl)(new MethodAccessorGenerator()).generateConstructor(this.c.getDeclaringClass(), this.c.getParameterTypes(), this.c.getExceptionTypes(), this.c.getModifiers());
this.parent.setDelegate(var2);
}
/*反射Configuration類的建構函式構造Configuration物件*/
return newInstance0(this.c, var1);
}
/*6. Configuration 預設建構函式構造一個預設Configuration物件*/
public Configuration() {
this.safeResultHandlerEnabled = true;
this.multipleResultSetsEnabled = true;
//省略……, 預設建構函式
}
/*7. AbstractAutowireCapableBeanFactory populateBean載入repository-bean.xml property屬性,set第6步生成的configuration, mapUnderscoreToCamelCase屬性就會在這一步set進去*/
public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid) throws BeansException {
List<PropertyAccessException> propertyAccessExceptions = null;
List<PropertyValue> propertyValues = pvs instanceof MutablePropertyValues ? ((MutablePropertyValues)pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues());
Iterator var6 = propertyValues.iterator();
while(var6.hasNext()) {
PropertyValue pv = (PropertyValue)var6.next();
//省略……
this.setPropertyValue(pv);
//省略……
}
//省略……
}
/*8. AbstractAutowireCapableBeanFactory構造SqlSessionFactoryBean*/
protected void invokeInitMethods(String beanName, Object bean, @Nullable RootBeanDefinition mbd) throws Throwable {
//省略……
((InitializingBean)bean).afterPropertiesSet();
}
/*9. SqlSessionFactoryBean類*/
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();
}
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
XMLConfigBuilder xmlConfigBuilder = null;
Configuration configuration;
/*this.configuration為第6步構造的預設Configuration*/
if (this.configuration != null) {
configuration = this.configuration;
if (configuration.getVariables() == null) {
configuration.setVariables(this.configurationProperties);
} else if (this.configurationProperties != null) {
configuration.getVariables().putAll(this.configurationProperties);
}
} else if (this.configLocation != null) {
/*repository-bean.xml檔案的 configLocation property*/
xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), (String)null, this.configurationProperties);
configuration = xmlConfigBuilder.getConfiguration();
} else {
/*configuration和configLocation都沒配置,載入預設配置*/
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
}
configuration = new Configuration();
if (this.configurationProperties != null) {
configuration.setVariables(this.configurationProperties);
}
}
/*10. 構造出repository-bean.xml檔案的commSqlSessionFactory*/
從上述兩段原始碼可以看出,當手動配置SqlSessionFactoryBean時,其實是不會載入application.properties檔案中的配置的,只會載入repositroy-bean.xml檔案中的property屬性set Configuration的成員,並最終影響生成的SqlSessionFactoryBean。
所以,我們可以總結出如下:
- 手動配置SqlSessionFactoryBean時,如果需要對mybatis進行設定,可以通過兩種方式,一是通過mybatis-config.xml檔案,並在repository-bean.xml檔案中定義configLocation property。二是通過在repository-bean.xml檔案中定義Configuration property。
- 不手動配置SqlSessionFactoryBean時,application.properties檔案中的mybatis配置是可以生效的。如果想使用額外的mybatis-config.xml配置檔案,只需在application.properties檔案中加入”mybatis.config-location=classpath:mybatis-config.xml“設定即可。
其實,在本篇文章的示例程式碼中,之所以這麼看重mapUnderscoreToCamelCase屬性設定,是因為我在UserMapper中沒有設定resultMap(資料表列和Model成員的對映關係),實際開發中其實會設定resultMap的,所以並不需要對mapUnderscoreToCamelCase進行特殊設定。特別在使用Mybatis Generator這種逆向工程外掛,Model、Examp、Mapper、Mapper.xml都是外掛自動生成的,可以滿足絕大多數情況的使用,且不用手寫sql,使用Mybatis預設配置就能很好的工作,我會在下一篇文章對Mybatis Generator進行介紹。