Spring 源码(八):扩展点之 mybatis 集成

概述

mybatis
将与 spring
集成的代码拆分到了 mybatis-spring
模块,避免 mybatis
spring
之间的耦合,如果你只需要纯粹的使用 mybatis api
,就避免了必须将 spring
依赖也耦合进来的问题。 mybatis
使用中一般是将 Sql
语句写在 xml
文件中,为方便操作,我们会创建一个 Mapper
接口文件进行映射, mybatis
提供了采用动态代理方式对 Mapper
接口类进行包装,这样我们就可以像使用普通对象一样执行各种方法调用。

mybatis
spring
集成的一个核心任务就是将这些动态代理包装的 Mapper
对象注入到 IoC
容器中,这样其它 Bean
就可以方便的使用如 @Autowired
等方式进行依赖注入。

MapperScannerConfigurer

需要将 mybatis
生成的动态代理对象注入到 IoC
容器中,自然我们想到之前的 BeanFactoryPostProcessor
的子类 BeanDefinitionRegistryPostProcessor
这个扩展类。 MapperScannerConfigurer
就是实现了 BeanDefinitionRegistryPostProcessor
接口,然后在该接口中通过类扫描器 scanner
进行扫描注册。

public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}

ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAddToConfig(this.addToConfig);
scanner.setAnnotationClass(this.annotationClass);
scanner.setMarkerInterface(this.markerInterface);
scanner.setSqlSessionFactory(this.sqlSessionFactory);
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);//指定引用的SqlSessionFactory
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
scanner.setResourceLoader(this.applicationContext);
scanner.setBeanNameGenerator(this.nameGenerator);
scanner.registerFilters();
//basePackage指定扫描Mapper接口包路径
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}

ClassPathMapperScanner
这个就是继承之前介绍过的 Spring
ClassPathBeanDefinitionScanner
类扫描器进行了扩展,它可以实现将包路径下至少含有一个方法的接口类注册到 IoC
中。

这里有个问题:注册进入的 BeanDefinition
beanClass
指向的都是接口,到后续创建对象时会存在问题,接口是没法创建实例的。所以, ClassPathMapperScanner
扫描器在注册完成后,又会对 BeanDefinition
进行处理。处理逻辑位于 ClassPathMapperScanner#processBeanDefinitions()
方法中,其核心逻辑见下:

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
String beanClassName = definition.getBeanClassName();

definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59
definition.setBeanClass(this.mapperFactoryBeanClass);
...
}
}

其中最重要的一条语句: definition.setBeanClass(this.mapperFactoryBeanClass)
,偷偷的将 BeanDefinition
beanClass
替换成了 MapperFactoryBean
,而不再指向 Mapper
接口类。同时将 Mapper
接口类作为参数传入到了 MapperFactoryBean
中,即调用下面构造方法

public MapperFactoryBean(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}

MapperFactoryBean
实现了 FactoryBean
接口,这样实际上它是通过 getObject()
方法获取到对象然后注入到 IoC
容器中。而在getObject()方法中,我们就可以使用 mybatis api
获取到 Mapper
接口类的动态代理对象: SqlSession#getMapper()

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

上面我们分析了如何将 Mapper
接口类注入到 IoC
容器中的实现思路,现在总结下主要有:

  1. BeanDefinitionRegistryPostProcessor
    IoC
    Bean
    
  2. ClassPathMapperScanner
    Mapper
    BeanDefinition
    
  3. BeanDefinition
    beanClass
    FactoryBean
    MapperFactoryBean
    mybatis api
    SqlSession#getMapper()
    Mapper
    

扩展点引入

通过 MapperScannerConfigurer
,解决了如何将 Mapper
接口类注入到 IoC
容器的问题,现在还有另外一个问题,这个扩展点只有注册到 Spring
中才会起作用,那又如何将其注册到 Spring
中呢?

方式一:最直接方式就是直接创建 MapperScannerConfigurer
类型的 Bean
实例,比如:

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="org.simon.demo01.mapper" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>

这种方式是最简单直接的,但是使用角度来说不方便,所以, mybatis-spring-1.2
新增了两种方式: <mybatis-scan>
标签方式和 @MapperScan
注解方式。

首先来看下 <mybatis:scan>
标签方式,添加 mybatis
schema
,然后就可以使用 <mybatis:scan base-package="org.simon.demo01.mapper"/>
:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://mybatis.org/schema/mybatis-spring
http://mybatis.org/schema/mybatis-spring.xsd"
>


<!-- 自动扫描 -->
<context:component-scan base-package="org.simon.demo01" />

<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<property name="jdbcUrl" value="jdbc:mysql://127.0.0.1:3306/mybatis?serverTimezone=Asia/Shanghai" />
<property name="username" value="root" />
<property name="password" value="123456" />
</bean>

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

<mybatis:scan base-package="org.simon.demo01.mapper"/>

</beans>

后台处理类 NamespaceHandler
<scan>
标签注册解析MapperScannerBeanDefinitionParser

public class NamespaceHandler extends NamespaceHandlerSupport {

@Override
public void init() {
registerBeanDefinitionParser("scan", new MapperScannerBeanDefinitionParser());
}

}

再看下 MapperScannerBeanDefinitionParser
解析器:

protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);

ClassLoader classLoader = ClassUtils.getDefaultClassLoader();

builder.addPropertyValue("processPropertyPlaceHolders", true);
try {
String annotationClassName = element.getAttribute(ATTRIBUTE_ANNOTATION);
if (StringUtils.hasText(annotationClassName)) {
@SuppressWarnings("unchecked")
Class<? extends Annotation> annotationClass = (Class<? extends Annotation>) classLoader
.loadClass(annotationClassName);
builder.addPropertyValue("annotationClass", annotationClass);
}
String markerInterfaceClassName = element.getAttribute(ATTRIBUTE_MARKER_INTERFACE);
if (StringUtils.hasText(markerInterfaceClassName)) {
Class<?> markerInterface = classLoader.loadClass(markerInterfaceClassName);
builder.addPropertyValue("markerInterface", markerInterface);
}
String nameGeneratorClassName = element.getAttribute(ATTRIBUTE_NAME_GENERATOR);
if (StringUtils.hasText(nameGeneratorClassName)) {
Class<?> nameGeneratorClass = classLoader.loadClass(nameGeneratorClassName);
BeanNameGenerator nameGenerator = BeanUtils.instantiateClass(nameGeneratorClass, BeanNameGenerator.class);
builder.addPropertyValue("nameGenerator", nameGenerator);
}
String mapperFactoryBeanClassName = element.getAttribute(ATTRIBUTE_MAPPER_FACTORY_BEAN_CLASS);
if (StringUtils.hasText(mapperFactoryBeanClassName)) {
@SuppressWarnings("unchecked")
Class<? extends MapperFactoryBean> mapperFactoryBeanClass = (Class<? extends MapperFactoryBean>) classLoader
.loadClass(mapperFactoryBeanClassName);
builder.addPropertyValue("mapperFactoryBeanClass", mapperFactoryBeanClass);
}
} catch (Exception ex) {
XmlReaderContext readerContext = parserContext.getReaderContext();
readerContext.error(ex.getMessage(), readerContext.extractSource(element), ex.getCause());
}

builder.addPropertyValue("sqlSessionTemplateBeanName", element.getAttribute(ATTRIBUTE_TEMPLATE_REF));
builder.addPropertyValue("sqlSessionFactoryBeanName", element.getAttribute(ATTRIBUTE_FACTORY_REF));
builder.addPropertyValue("lazyInitialization", element.getAttribute(ATTRIBUTE_LAZY_INITIALIZATION));
builder.addPropertyValue("basePackage", element.getAttribute(ATTRIBUTE_BASE_PACKAGE));

return builder.getBeanDefinition();
}

最关键的就是第一句 BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
,又是将 MapperScannerConfigurer
动态注入到 Spring
中,下面一堆都是解析标签属性进行依赖注入。

再来看下 @MapperScan
注解方式,如: @MapperScan(basePackages = "org.simon.demo01.mapper")

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
public @interface MapperScan {
}

@MapperScan
注解上面使用了使用了一种非常常见的扩展方式: @Import
扩展。通过 @Import
注解,引入了 MapperScannerRegistrar
,它是 ImportBeanDefinitionRegistrar
类型,通常和 @Import
注解组合使用,实现动态注入功能:

 @Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//获取注解上属性
AnnotationAttributes mapperScanAttrs = AnnotationAttributes
.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
if (mapperScanAttrs != null) {
registerBeanDefinitions(mapperScanAttrs, registry, generateBaseBeanName(importingClassMetadata, 0));
}
}

void registerBeanDefinitions(AnnotationAttributes annoAttrs, BeanDefinitionRegistry registry, String beanName) {
//创建一个MapperScannerConfigurer的BeanDefinition
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);

/**
* 下面就是解析注解属性值,通过PropertyValue方式进行依赖注入到Bean中
*/

builder.addPropertyValue("processPropertyPlaceHolders", true);

Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass");
if (!Annotation.class.equals(annotationClass)) {
builder.addPropertyValue("annotationClass", annotationClass);
}

Class<?> markerInterface = annoAttrs.getClass("markerInterface");
if (!Class.class.equals(markerInterface)) {
builder.addPropertyValue("markerInterface", markerInterface);
}

...//各种依赖注入


builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(basePackages));

//将生成的BeanDefinition注册到IoC中
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());

}

方法中同样有 BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class)
这句,动态的将 MapperScannerConfigurer
注入到 Spring
中,然后是一堆的解析注解属性进行依赖注入,这样通过 @Import
+ ImportBeanDefinitionRegistrar
动态注入,就实现了将 MapperScannerConfigurer
扩展点注册到 Spring
中。

SpringBoot自动装配

不论是通过 <mybatis:scan>
标签方式,还是 @MapperScan
注解方式,这些是常规的第三方模块与 Spring
进行集成方式。这种集成方式比较繁琐的是:你不光要通过 <mybatis:scan>
@MapperScan
注解将第三方集成进来,你还需要初始化一些依赖对象,比如这里的 DataSource
SqlSessionFactory
等。当一个项目集成了很多第三方模块时,每个模块都这样搞一下,配置的工作量就大了,比如最常使用的 ssm
集成配,传统 Spring
集成要搞一大堆配置。

所以, SpringBoot
提出了一个比较优秀的思想:自动装配。需要什么模块直接把依赖添加进来,自动完成装配,对于个性化可以在属性文件中进行配置,从使用角度来说,即插即用,不需要有太多的编码。第三方程序和 spring
就像完全融入一体一样,简化项目构建时集成成本,也降低项目配置的复杂性,所以 SpringBoot
会被越来越多的项目所采用,进而也推动微服务的兴起。

SpringBoot
中使用 mybatis
,直接依赖 mybatis-spring-boot-starter
,它会把 mybatis
mybatis-spring
mybatis-spring-boot-autoconfigure
三个依赖包都添加进来。前面两个依赖包好理解,这里关键是第三个依赖包,就是通过它实现了 mybatis
自动装配功能。下面我们来看下 SpringBoot
是如何实现 mybatis
的主动装配。

1、首先,定义一个 mybatis
主动装配配置类,如下:

@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {

public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider,
ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider,
ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider,
ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider)
{
...
}

@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
applyConfiguration(factory);
...//省略一堆配置
if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
}

return factory.getObject();
}


@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
if (executorType != null) {
return new SqlSessionTemplate(sqlSessionFactory, executorType);
} else {
return new SqlSessionTemplate(sqlSessionFactory);
}
}


public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {

private BeanFactory beanFactory;

@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

if (!AutoConfigurationPackages.has(this.beanFactory)) {
logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
return;
}

logger.debug("Searching for mappers annotated with @Mapper");

List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
if (logger.isDebugEnabled()) {
packages.forEach(pkg -> logger.debug("Using auto-configuration base package '{}'", pkg));
}

BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
builder.addPropertyValue("processPropertyPlaceHolders", true);
builder.addPropertyValue("annotationClass", Mapper.class);
builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));
BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
Stream.of(beanWrapper.getPropertyDescriptors())
.filter(x -> x.getName().equals("lazyInitialization")).findAny()
.ifPresent(x -> builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}"));
registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
}

@Override
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}

}


@org.springframework.context.annotation.Configuration
@Import(AutoConfiguredMapperScannerRegistrar.class)
@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
}
}

这里主要利用 MapperScannerRegistrarNotFoundConfiguration
类上的 @Import(AutoConfiguredMapperScannerRegistrar.class)
引入,然后在 AutoConfiguredMapperScannerRegistrar
BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class)
这句又是动态注入 MapperScannerConfigurer
。不过,主动装配配置类中,还会把相关的依赖也一起创建、初始化,比如: SqlSessionFactory
SqlSessionTemplate

@EnableConfigurationProperties(MybatisProperties.class)
mybatis
相关配置引入进来,这样在创建、初始化过程中的定制需求就可以通过配置修改。

2、有了这个主动装配配置类还不行,下一步就是看如何让主动装配配置类生效。SpringBoot提供了 SpringFactoriesLoader
工厂加载机制,类似于 JDK
中的 SPI
机制,实现将模块 META-INF/spring.factories
文件中配置注入到 Spring
容器中。 mybatis-spring-boot-autoconfigure
模块下 META-INF/spring.factories
文件中就有 MybatisAutoConfiguration

org.springframework.boot.autoconfigure.EnableAutoConfiguration=/
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,/
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

3、使用时依赖添加进来,配置下属性,就可以直接使用,基本不再需要编码:

mybatis.mapper-locations: classpath:mapper/*Mapper.xml
mybatis.type-aliases-package: com.example.demo.entity

总结

从上面来看, mybatis
spring
集成的关键的是将 mybatis-spring
模块下 MapperScannerConfigurer
集成进来,因为,它是一个 BeanDefinitionRegistryPostProcessor
类型的扩展,内部通过自定义 scanner
扫描 Mapper
接口自动注册到 IoC
容器中,这一点在各种集成方式中是统一一样的。不同点在于: MapperScannerConfigurer
扩展类是如何被引入的。传统的 Spring
方式通过 @Mapper
注解或 <mybatis:scan>
自定义标签实现,但是对于一些依赖对象还是需要手工创建,比较繁琐;而 SpringBoot
利用自动装配,让第三方模块集成变成了一个插件,即插即用,无需太多编码。

分析了 mybatis
集成方式,从中也学习了如何利用 Spring
的各种扩展点进行定制,更重要的是也为我们开发自己模块和 Spring
集成提供了思路。


 长按识别关注,

持续输出原创
  

Spring 源码(八):扩展点之 mybatis 集成

原文 

https://mp.weixin.qq.com/s/bXKlFFUOO7DR7St75bfotg

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » Spring 源码(八):扩展点之 mybatis 集成

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址