1. 程式人生 > >Spring Boot + Mybatis資料來源配置的三種方式

Spring Boot + Mybatis資料來源配置的三種方式

通過之前兩篇文章Spring Boot + JdbcTemplateSpring 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。

所以,我們可以總結出如下:

  1. 手動配置SqlSessionFactoryBean時,如果需要對mybatis進行設定,可以通過兩種方式,一是通過mybatis-config.xml檔案,並在repository-bean.xml檔案中定義configLocation property。二是通過在repository-bean.xml檔案中定義Configuration property。
  2. 不手動配置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進行介紹。