概述
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容器中的实现思路,现在总结下主要有:
-
基于
BeanDefinitionRegistryPostProcessor接口实现扩展,然后动态向IoC容器中注入Bean; -
在注入时,会使用到
ClassPathMapperScanner类扫描器将所有的Mapper接口类解析成BeanDefinition集合注入; -
为了解决接口不能创建对象问题,再注入后又将
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.xmlmybatis.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集成提供了思路。
长按识别关注, 持续输出原创