mybatis+spring核心配置项逻辑分析

本文详细解析了Spring整合MyBatis时的两个关键配置:SqlSessionFactoryBean用于加载SQL映射文件和数据源,MapperScannerConfigurer用于扫描DAO接口并建立映射关系。重点讲解了mapperLocations的配置注意事项和MapperFactoryBean的作用。
摘要由CSDN通过智能技术生成

先看一个配置案例:

<!-- config.xml-->

<bean id="targetScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage"
        value="test.**.dao,test1.**.dao" />
    <property name="sqlSessionFactoryBeanName" value="targetSqlSessionFactory" />
</bean>

<bean id="targetSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="mydatasource" />
    <property name="mapperLocations" value="classpath*:mybatis/*.xml" />
    <property name="configuration">
        <bean class="org.apache.ibatis.session.Configuration">
            <property name="mapUnderscoreToCamelCase" value="true" />
            <property name="localCacheScope" value="STATEMENT" />
        </bean>
    </property>
</bean>

<bean id="mydatasource" factory-bean="myDataSourceFactory"
    factory-method="getDataSource" />

在spring框架整合的场景下,mybatis只需要这三个核心配置就可以正确加载,datasource不多说,核心是另外两个,下面对每个配置做下探索

1、SqlSessionFactoryBean - sql映射文件(xml)解析

先整体看下这个类是做什么的再去分析配置项

sqlSessionFactoryBean的本身能力
public class SqlSessionFactoryBean
    implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent>

这个类实现了FactoryBean、InitializingBean、ApplicationListner三个接口

  • FactoryBean:说明这个类是个bean工厂,在bean加载的过程中会调用它的getObject方法来获取工厂生产的bean

public SqlSessionFactory getObject() throws Exception {
  if (this.sqlSessionFactory == null) {
    afterPropertiesSet();
  }
  return this.sqlSessionFactory;
}

它的getObject方法返回的是SqlSessionFactory对象,当该对象不存在的时候,调用afterPropertiesSet方法。sqlSessionFactory对象是负责管理数据连接和最终执行的,感兴趣可以自行研究,与本篇内容关系不大,这里不多赘述

  • InitializingBean:说明这个类在doCreateBean(create、populate、initialize)的initialize过程中会调用afterPropertiesSet方法做bean的自定义化处理

public void afterPropertiesSet() throws Exception {
  notNull(dataSource, "Property 'dataSource' is required");
  notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
  state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
      "Property 'configuration' and 'configLocation' can not specified with together");
  this.sqlSessionFactory = buildSqlSessionFactory();
}

看到里面做的动作,核心就是sqlSessionFactory的加载。因此它会在bean初始化的时候去加载自己的工厂产物:sqlSessionFactory,同时为了防止构造未完成,在spring主动加载sqlSessionFactory的时候,调用SqlSessionFactoryBean#getObject方法同样也会再进行一次加载

  • ApplicationListner:说明这个bean具备spring的事件监听能力

public void onApplicationEvent(ApplicationEvent event) {
  if (failFast && event instanceof ContextRefreshedEvent) {
    // fail-fast -> check all statements are completed
    this.sqlSessionFactory.getConfiguration().getMappedStatementNames();
  }
}

这个类会监听所有事件,当事件是spring上下文加载的时候通过refresh方法发送的事件 -- ContextRefreshedEvent时,sqlSessionFactory同步会执行getMappedStatementNames()这个方法,这里暂时不继续看

buildSqlSessionFactory方法

该方法实际上就是在解析在XML中的targetSqlSessionFactory这个bean配置的各个属性。

在方法中可以看到所有xml配置项的加载步骤。

首先是configuration和configLocation,二者只解析一个

if (this.configuration != null) {
  targetConfiguration = this.configuration;
  if (targetConfiguration.getVariables() == null) {
    targetConfiguration.setVariables(this.configurationProperties);
  } else if (this.configurationProperties != null) {
    targetConfiguration.getVariables().putAll(this.configurationProperties);
  }
} else if (this.configLocation != null) {
  xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
  targetConfiguration = xmlConfigBuilder.getConfiguration();

使用configLocation说明需要读取专门的xml配置文件做mybatis的配置项,例如:

<bean id="sqlSessionFactoryBean_1" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/>
    ……
</bean>

mybatis自定义插件解析

if (!isEmpty(this.plugins)) {
     Stream.of(this.plugins).forEach(plugin -> {
      targetConfiguration.addInterceptor(plugin);
      LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
    });
 }

使用了插件的配置项例如:

<bean id="sqlSessionFactoryBean_1" class="org.mybatis.spring.SqlSessionFactoryBean">
    ……
    <property name="plugins">
        <list>
            <bean id="filterLargeObjectSqlPlugin" class="test.FilterLargeObjectSqlPlugin"/>
        </list>
    </property>
</bean>

 下面一个核心的配置项是mapperLocations,这个配置项决定了mybatis去哪里找sql映射文件

try {
  XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
      targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
  xmlMapperBuilder.parse();

在XmlMapperBuilder#parse方法可以找到mapper中的各个标签的解析,感兴趣可以自行了解,这里大致梳理下解析的最终产物:映射文件对应的class类

在parse方法中,解析完xml后,调用bindMapperForNamespace方法,通过反射构造映射文件对应的class类

private void bindMapperForNamespace() {
  String namespace = builderAssistant.getCurrentNamespace();
  if (namespace != null) {
    Class<?> boundType = null;
    try {
      boundType = Resources.classForName(namespace);
    } catch (ClassNotFoundException e) {
      // ignore, bound type is not required
    }
    if (boundType != null && !configuration.hasMapper(boundType)) {
      // Spring may not know the real resource name so we set a flag
      // to prevent loading again this resource from the mapper interface
      // look at MapperAnnotationBuilder#loadXmlResource
      configuration.addLoadedResource("namespace:" + namespace);
      configuration.addMapper(boundType);
    }
  }
}

这里可以看出来,在映射文件中配置的namespace起到的作用是通过namespace提供的Type建立xml与Dao之间的映射

跟踪MapperRegistry#addMapper方法,可以看到底层实际上是通过代理的方式,通过预编译sql创造一个映射文件的代理类,每当执行Dao层接口的时候,由代理类执行真正的预编译后的sql及建立数据连接的方法

knownMappers.put(type, new MapperProxyFactory<>(type));

MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();

预编译的流程在parse方法中,感兴趣可以继续深入了解

这里把type和对应的代理类工厂存到了一个内存map中,有存必然有取,取的地方在MapperRegistry#getMapper中,一路向上找,发现找到的是MapperFactoryBean这个类

关于这个类等会再看

mapperLocations的配置逻辑
private Resource[] mapperLocations;

看到SqlSessionFactoryBean中的mapperLocations变量,类型是Resource[],那么在xml中应该如何去配置它?我们看以下四种配置:

<!--  配置方案1  -->

<bean id="resourceTest" class="com.huawei.cbc.udrmetricservice.resourcetest.ResourceTest">
    <property name="resources" value="classpath*:mybatis/*.xml" />
</bean>

<!--  配置方案2  -->

<bean id="resourceTest2" class="com.huawei.cbc.udrmetricservice.resourcetest.ResourceTest">
    <property name="resources">
        <list>
            <value>classpath*:mybatis/BatchCompositeEventDao.xml</value>
            <value>classpath*:mybatis/BatchEventDao.xml</value>
        </list>
    </property>
</bean>

<!--  配置方案3  -->

<bean id="resourceTest3" class="com.huawei.cbc.udrmetricservice.resourcetest.ResourceTest">
    <property name="resources" value="classpath*:mybatis/BatchCompositeEventDao.xml,classpath*:mybatis/BatchEventDao.xml" />
</bean>

<!--  配置方案4  -->

<bean id="resourceTest4" class="com.huawei.cbc.udrmetricservice.resourcetest.ResourceTest">
    <property name="resources" value="classpath*:mybatis.BatchCompositeEventDao.xml" />
</bean>

 哪一种方案可以拿到正确的映射文件路径?

@Setter
@Getter
public class ResourceTest {
    private Resource[] resources;
}

public void test() {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    ResourceTest resourceTest = (ResourceTest) context.getBean("resourceTest");
    System.out.println(resourceTest.getResources().length);
    ResourceTest resourceTest2 = (ResourceTest) context.getBean("resourceTest2");
    System.out.println(resourceTest2.getResources().length);
    ResourceTest resourceTest3 = (ResourceTest) context.getBean("resourceTest3");
    System.out.println(resourceTest3.getResources().length);
    ResourceTest resourceTest4 = (ResourceTest) context.getBean("resourceTest3");
    System.out.println(resourceTest4.getResources().length);
}

28
2
0
0

可见第三种配置方案是拿不到对应的映射文件的,当代码执行到mybatis接口dao,就会产生找不到映射报错

为什么会这样?其实这里是mybatis借助了spring提供的资源路径模糊匹配的能力,具体可以了解doCreateBean中的populateBean方法,这里只是简单顺一下这个思路:

在populateBean方法注入属性需要做属性转换,会走到TypeConverterDelegate#convertIfNecessary方法中,这个方法先找自定义的属性转换器,如果不存在,找默认的属性转换器

PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);
……
if (editor == null) {
	editor = findDefaultEditor(requiredType);
}
convertedValue = doConvertValue(oldValue, convertedValue, requiredType, editor);

自定义转换器就是自己实现PropertyEditor接口的转换器,它也是在refresh流程中被加载到上下文中的

而spring提供了一个默认转换器ResourceArrayPropertyEditor,专门用来转换Resource类型的属性注入,可以通过配置String路径实现Resource属性注入。

public void setAsText(String text) {
	String pattern = resolvePath(text).trim();
	try {
		setValue(this.resourcePatternResolver.getResources(pattern));
	}
	catch (IOException ex) {
		throw new IllegalArgumentException(
				"Could not resolve resource location pattern [" + pattern + "]: " + ex.getMessage());
	}
}

借助这个转换器做转换时,实际上是通过PathMatchingResourcePatternResolver这个工具来做转换的,代码在getReousrces方法中,转换逻辑并不复杂,实际上就是在解析绝对路径和模糊路径,找到路径下所有的的文件

// 首先判断以classpath*:开头
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
    ……
    // 模糊查找
    if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
	    return findPathMatchingResources(locationPattern);
    }
    else {
        // 按绝对路径查找
	    return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
    }
……

回看配置项1,实际上是一个模糊路径,配置项2实际上是两个绝对路径,配置项3其实并不是路径,尽管看起来像是两个绝对路径,但这个解析工具的执行流程并不是把一条String做分割符分割得到最终路径的

2、MapperScannerConfigurer - dao文件扫描及接口-sql映射关系建立

看MapperScannerConfigurer之前不得不先看另一个Mybatis提供的配置项:MapperFactoryBean。因为这个MapperFactoryBean的getObject方法可以直接获取到接口type与xml映射文件的代理类之间的映射关系

public T getObject() throws Exception {
  return getSqlSession().getMapper(this.mapperInterface);
}

先看MapperFactoryBean的配置案例:

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

</bean>

 可以看到这里配置了一个mapperInterface属性,和一个sqlSessionFactory属性

private Class<T> mapperInterface;

因为mapperInterface属性是一个纯Class类型的对象,说明MapperFactoryBean并不适合配置大批量的DAO层文件

而MapperScannerConfigurer是怎么跟MapperFactoryBean产生关联的呢?

MapperScannerConfigurer的本身能力
public class MapperScannerConfigurer
    implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware

MapperScannerConfigurer同样实现了InitializingBean和BeanDefinitionRegistryPostProcessor两个接口

  • InitializingBean:通过初始化操作验证属性注入

public void afterPropertiesSet() throws Exception {
  notNull(this.basePackage, "Property 'basePackage' is required");
}

在afterPropertiesSet方法中就只做了一个判断,即basePackage属性不能为空

  • BeanDefinitionRegistryPostProcessor:在上下文refresh的流程中,beanDefinition加载完后,bean开始创建之前,修改beanDfinition的一个入口

public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    ……
    scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));

可以看出来,在创建bean之前调用这个方法,实际上是为了让spring能扫到DAO文件,并且把DAO文件注册给spring托管

String CONFIG_LOCATION_DELIMITERS = ",; \t\n";

通过这个属性能看出来,basePackage属性支持的分割符包括“,”、“;”、空格符、换行符

public int scan(String... basePackages) {
	int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
	doScan(basePackages);
	// Register annotation config processors, if necessary.
	if (this.includeAnnotationConfig) {
		AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
	}
	return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}

processBeanDefinitionsClassPathMapperScanner#doScan方法中对于包路径的解析和扫描,以及注册到spring的逻辑感兴趣可以自行了解,这里只看其中的一个核心步骤:

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    ……
    definition.setBeanClass(this.mapperFactoryBeanClass);

将扫到的bd设置了一下BeanClass属性,设置为MapperFactoryBean类型,这里有啥用处呢?可以看出来MapperFactoryBean是个工厂类,那么它的getObject方法就必定会在bean初始化的时候被调用。

其实是在spring加载bean的环节中,doGetBean流程会判断bean已经加载完成了之后,调用getObjectForBeanInstance方法获取真正的实例

beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, null);

如果创建出来的bean是一个工厂bean,就会调用工厂bean的getObject方法

if (mbd != null) {
	mbd.isFactoryBean = true;
}
……
// 这里向下分析,就可以找到getObject方法了
object = getObjectFromFactoryBean(factory, beanName, !synthetic);

而isFactoryBean属性的来源,就是刚刚看到的beanClass属性

protected boolean isFactoryBean(String beanName, RootBeanDefinition mbd) {
	Boolean result = mbd.isFactoryBean;
	if (result == null) {
                // 这里的beanType取的就是beanClass属性
		Class<?> beanType = predictBeanType(beanName, mbd, FactoryBean.class);
               // 判断beanType属于FactoryBean的实现类
		result = (beanType != null && FactoryBean.class.isAssignableFrom(beanType));
		mbd.isFactoryBean = result;
	}
	return result;
}

到这里逻辑其实就闭环了,通过MapperScannerConfigurer提供的扫包能力,将扫到的Dao层接口打上MapperFacotryBean的标记,这样就可以在spring加载bean时使接口与mapper的代理类建立绑定关系。

3、总结

  • SqlSessionFactoryBean配置项实现了mybatis获取数据源连接和mapper映射文件扫描

    • mapperLocations属性配置了mapper映射文件的位置,当出现找不到mapper文件的时候,首先考虑这里的配置是否正确

    • mapperLocations属性不支持用逗号分割的形式配置,只支持一条配置一个路径,但是路径可以是模糊匹配的

    • 当集成类似tablepartition这种增加了新的mapper映射文件的组件时,需要注意,组件的mapper映射文件路径是否和项目中配置一样,若不一样,需要新增对应配置

    • 扫描完成的mapper映射文件会生成代理类存储在内存中

Could not find resource com/zhou/dao/UserMapper.xml

UserMapper is not known to the MapperRegistry.
  • MapperScannerConfigurer配置项实现了mybatis对dao接口文件的扫描,同时建立dao文件和mapper映射文件代理类之间的映射关系

    • basePackage属性配置了dao文件路径,支持用逗号、分号、空格、空行的形式做拆分

    • 当出现了无法加载DAO文件对应的bean这类问题,首先考虑是不是这个配置项出了问题

    • 当继承像tablepartition这样增加新的dao文件的组件时,需要注意,组件的dao文件目录需要在basePackage中进行配置才能在spring中进行托管

    • 当出现了类似dao层的接口与mapper文件没有映射关系的时候,首先考虑dao层接口的全路径与mapper中的namespace是否一致,以及接口方法与id是否一致

Invalid bound statement (not found):
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值