转载

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

概述

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

mybatisspring 集成的一个核心任务就是将这些动态代理包装的 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 这个就是继承之前介绍过的 SpringClassPathBeanDefinitionScanner 类扫描器进行了扩展,它可以实现将包路径下至少含有一个方法的接口类注册到 IoC 中。

这里有个问题:注册进入的 BeanDefinitionbeanClass 指向的都是接口,到后续创建对象时会存在问题,接口是没法创建实例的。所以, 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) ,偷偷的将 BeanDefinitionbeanClass 替换成了 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> 标签方式,添加 mybatisschema ,然后就可以使用 <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 注解将第三方集成进来,你还需要初始化一些依赖对象,比如这里的 DataSourceSqlSessionFactory 等。当一个项目集成了很多第三方模块时,每个模块都这样搞一下,配置的工作量就大了,比如最常使用的 ssm 集成配,传统 Spring 集成要搞一大堆配置。

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

SpringBoot 中使用 mybatis ,直接依赖 mybatis-spring-boot-starter ,它会把 mybatismybatis-springmybatis-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) 引入,然后在 AutoConfiguredMapperScannerRegistrarBeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class) 这句又是动态注入 MapperScannerConfigurer 。不过,主动装配配置类中,还会把相关的依赖也一起创建、初始化,比如: SqlSessionFactorySqlSessionTemplate

@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

总结

从上面来看, mybatisspring 集成的关键的是将 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
正文到此结束
Loading...