mybatis開啓MapperScannerConfigurer導致properties不生效

背景

spring和mybatis集成過程中,我們可以通過MapperFactoryBean的方式配置Mapper接口。但是這樣需要在配置文件中,爲每個mapper配置相同的代碼塊,浪費時間。關鍵對於代碼潔癖的人來說,一點不能忍。

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

使用MapperScannerConfigurer進行自動轉換Mapper的方式,讓代碼更加簡潔優雅。

但是,當我在項目中,按照文文檔配置bean後,仍舊出現properties文件中jdbc的參數無法正常解析。

  • 相關配置文件信息
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:jaxws="http://cxf.apache.org/jaxws" xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
      http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
      http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
      http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
      http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
      http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"
      default-autowire="byName">

    <context:property-placeholder location="conf/*.properties"/>
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" lazy-init="true">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.user}"/>
        <property name="password" value="${jdbc.pwd}"/>
    </bean>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="mapperLocations" value="mapper/*.xml"/>
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.yanmushi.**.mapper"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    </bean>
	
</beans>

PS:請原諒在spring-boot,註解方式盛行的年代,還糾結xml中解決方法。

依賴版本

參考依賴

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>4.1.1-RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.2.8</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.2.2</version>
        </dependency>

分析過程

在搜索引擎中,找到了一種解決方法。

原理:使用sqlSessionFactoryBeanName注入,不會立即初始化sqlSessionFactory, 所以不會引發提前初始化問題。
同時還應注意在配置org.mybatis.spring.SqlSessionFactoryBean這個Bean時,id不能爲sqlSessionFactory,如果爲這樣的話會導致MapperScannerConigurer在bean定義加載時,加載PropertyPlaceholderConfigurer還沒來得及替換定義中的變量

以上方法,能夠正常解決問題,但是爲什麼呢?

爲什麼sqlsessionfactorybeanname注入呢?

打開MapperScannerConfigurer,會發現它實現了spring的四個接口:

  1. BeanDefinitionRegistryPostProcessor
  2. InitializingBean
  3. ApplicationContextAware
  4. BeanNameAware

問題的關鍵在於第一個接口,它是幹什麼的呢?主要是允許代碼像xml一樣,註冊自定義Bean到BeanFactory中。

通過這個接口,MapperScannerConfigurer在spring初始化過程中,將掃描basePackage中的自定義mapper接口轉換爲MapperFactoryBean,並註冊到BeanFactory中。

/**
 * Extension to the standard {@link BeanFactoryPostProcessor} SPI, allowing for
 * the registration of further bean definitions <i>before</i> regular
 * BeanFactoryPostProcessor detection kicks in. In particular,
 * BeanDefinitionRegistryPostProcessor may register further bean definitions
 * which in turn define BeanFactoryPostProcessor instances.
 */

spring容器本身已經很好到解決了Bean管理問題。爲什麼會導致提前初始化呢?

在AbstractApplicationContext#refresh()方法中,詳細註明類初始化的流程。

	@Override
	public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
			// Prepare this context for refreshing.
			prepareRefresh();

			// Tell the subclass to refresh the internal bean factory.
			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

			// Prepare the bean factory for use in this context.
			prepareBeanFactory(beanFactory);

			try {
				// Allows post-processing of the bean factory in context subclasses.
				postProcessBeanFactory(beanFactory);

				// Invoke factory processors registered as beans in the context.
				invokeBeanFactoryPostProcessors(beanFactory);

				// Register bean processors that intercept bean creation.
				registerBeanPostProcessors(beanFactory);

				// Initialize message source for this context.
				initMessageSource();

				// Initialize event multicaster for this context.
				initApplicationEventMulticaster();

				// Initialize other special beans in specific context subclasses.
				onRefresh();

				// Check for listener beans and register them.
				registerListeners();

				// Instantiate all remaining (non-lazy-init) singletons.
				finishBeanFactoryInitialization(beanFactory);

				// Last step: publish corresponding event.
				finishRefresh();
			}

			catch (BeansException ex) {
				logger.warn("Exception encountered during context initialization - cancelling refresh attempt", ex);

				// Destroy already created singletons to avoid dangling resources.
				destroyBeans();

				// Reset 'active' flag.
				cancelRefresh(ex);

				// Propagate exception to caller.
				throw ex;
			}
		}
	}

MapperScannerConfigurer和PropertySourcesPlaceholderConfigurer兩個類,都是BeanFactoryPostProcessor的實現類。但是MapperScannerConfigurer實現了BeanFactoryPostProcessor的子接口,BeanDefinitionRegistryPostProcessor。

在執行invokeBeanFactorPostProcessors(beanFacotry)方法時,是調用到PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory, List<BeanFactoryPostProcessor>)方法,在這個方法中,BeanDefinitionRegistryPostProcessor被優先執行。 所以,在執行MapperScannerConfigurer中不能關聯需要讀取properties文件的Bean,因爲Properties沒有被初始化

	public static void invokeBeanFactoryPostProcessors(
			ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {

		// Invoke BeanDefinitionRegistryPostProcessors first, if any.
		Set<String> processedBeans = new HashSet<String>();

		if (beanFactory instanceof BeanDefinitionRegistry) {
			BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
			List<BeanFactoryPostProcessor> regularPostProcessors = new LinkedList<BeanFactoryPostProcessor>();
			List<BeanDefinitionRegistryPostProcessor> registryPostProcessors =
					new LinkedList<BeanDefinitionRegistryPostProcessor>();

			for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
				if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) {
					BeanDefinitionRegistryPostProcessor registryPostProcessor =
							(BeanDefinitionRegistryPostProcessor) postProcessor;
					registryPostProcessor.postProcessBeanDefinitionRegistry(registry);
					registryPostProcessors.add(registryPostProcessor);
				}
				else {
					regularPostProcessors.add(postProcessor);
				}
			}

			// Do not initialize FactoryBeans here: We need to leave all regular beans
			// uninitialized to let the bean factory post-processors apply to them!
			// Separate between BeanDefinitionRegistryPostProcessors that implement
			// PriorityOrdered, Ordered, and the rest.
			String[] postProcessorNames =
					beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);

			// First, invoke the BeanDefinitionRegistryPostProcessors that implement PriorityOrdered.
			List<BeanDefinitionRegistryPostProcessor> priorityOrderedPostProcessors = new ArrayList<BeanDefinitionRegistryPostProcessor>();
			for (String ppName : postProcessorNames) {
				if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
					priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
					processedBeans.add(ppName);
				}
			}
			OrderComparator.sort(priorityOrderedPostProcessors);
			registryPostProcessors.addAll(priorityOrderedPostProcessors);
			// 此時去執行:postProcessor.postProcessBeanFactory(beanFactory)方法
			invokeBeanDefinitionRegistryPostProcessors(priorityOrderedPostProcessors, registry);

			// ……
	}

爲什麼我們使用sqlSessionFacotryBeanName,但是依舊會出現properties沒有被引用的問題呢?

這是因爲在spring.xml文件中,beans節點,我們配置了**default-autowire="byName"**屬性。 開啓後,使得BeanFacotry在實例化bean的時候,會自動根據bean名字和屬性名字進行自動匹配。 從而在實例化MapperScannerConfigurer的時候,自動匹配到sqlSessionFactory屬性,觸發sqlSessionFactory的實例化,引發dataSource進行實例化,而此時properties屬性並沒有進行解析。

總結

  1. 關閉default-autowire屬性;否則,需要更改SqlSessionFactory這個bean的id,避免自動關聯。
  2. 如果只有一個數據源,MapperScannerConfigurer會自動關聯的;如果有多個,可以通過sqlSessionFactoryBeanName來配置bean的名字。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:jaxws="http://cxf.apache.org/jaxws" xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
      http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
      http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
      http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
      http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
      http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <context:property-placeholder location="conf/*.properties"/>
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" lazy-init="true">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.user}"/>
        <property name="password" value="${jdbc.pwd}"/>
    </bean>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="mapperLocations" value="mapper/*.xml"/>
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.yanmushi.**.mapper"/>
        <!-- <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>-->
    </bean>
</beans>

參考文章

  1. http://www.mybatis.org/spring/zh/
  2. https://www.cnblogs.com/daxin/p/3545040.html
  3. http://blog.sina.com.cn/s/blog_8e5354210101i03l.html
  4. https://www.cnblogs.com/yxh1008/p/6012230.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章