SpringBoot-Mybatis框架使用与源码解析

前言:

    在上两篇文章 Spring-Mybatis框架使用与源码解析    原生Mybatis框架源码解析 中,我们说了使用mybatis有三种方式, 这两篇文章已经分析了原生Mybatis、Spring-Mybatis的使用及源码分析。

    现在微服务架构基本已经成为一种架构正确了,而SpringBoot技术也已经被应用在各个项目中。

    SpringBoot不仅仅那些传统的好处,更多是可以与其他组件进行结合,使用户可以更方便的使用。比如SpringBoot-Kafka、SpringBoot-Mybatis、SpringBoot-RabbitMQ等等。

    本文,笔者便分析一下SpringBoot-Mybatis的使用及其源码,让读者可以更深入的了解下SpringBoot是如何帮我们简化配置,节约开发时间的。

 

1.SpringBoot-Mybatis使用

    1)添加maven依赖

	
		org.springframework.boot
		spring-boot-starter-parent
		1.5.3.RELEASE
	
    
       
	        org.mybatis.spring.boot
	        mybatis-spring-boot-starter
	        1.1.1
	    
	    
	   	
			mysql
			mysql-connector-java
			5.1.32
		

    2)在application.yml中添加DataSource配置

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
    
mybatis:
  mapper-locations: classpath:mapper/*.xml  #注意:一定要对应mapper映射xml文件的所在路径
  type-aliases-package: com.example.demo.mybatis  # 注意:对应实体类的路径

    3)在mapper/User.xml中添加SQL





    

    4)添加IUser接口

public interface IUser {
    public User getUser(int id);
}

    5)添加User实体类

@Data
public class User {
	private int id;
	private String name;
	private String dept;
	private String phone;
	private String website;
}

    6)在Application中添加最重要的一环(注意:@MapperScan对应IUser所在路径)

@SpringBootApplication
@MapperScan("com.example.demo.mybatis")
public class SpringbootstudyApplication {
	public static void main(String[] args) {
		SpringApplication springApplication = new SpringApplication(SpringbootstudyApplication.class);
		springApplication.run(args);
	}
}

    7)添加测试

@RunWith(SpringRunner.class)
@SpringBootTest(classes=SpringbootstudyApplication.class)
public class SpringbootstudyApplicationTests {

	@Autowired
	private IUser userMapper;
	
	@Test
	public void contextLoads() {
		User user = userMapper.getUser(3);
		System.out.println(user);//User(id=3, name=jack, dept=devp, phone=xxx, website=www.baidu.com)
	}
}

    测试成功

 

2.写在源码分析之前

    SpringBoot一般以配置居多,各种类之间的关系错综复杂。我们在分析SpringBoot的分析中,一定要抓住核心点。

    比如当前Mybatis的分析,结合Spring-Mybatis来看,我们应该主动创建DataSource、SqlSessionFactoryBean、MapperFactoryBean,并且把这些bean添加到Spring容器中。但是从当前的使用配置来看,这些bean我们都没有显示声明,没有像Spring-Mybatis使用的时候那样显示声明在beans.xml中,所以一定是SpringBoot帮我们做了这些工作,我们的重点要放在SpringBoot如何帮我们声明这些bean。

 

    仔细回想一下,SpringBoot需要我们做的的工作主要就是:

    * 添加mybatis-spring-boot-starter依赖

    * 在Application上添加@MapperScan注解,并把IUser所在的包路径添加到注解中

 

    所以,我们就主要从这两面入手,来剖析下SpringBoot-Mybatis框架

 

3.分析mybatis-spring-boot-starter依赖

    我们在项目中引入该依赖,下面我们看下这个依赖的主要内容


  4.0.0
  
    org.mybatis.spring.boot
    mybatis-spring-boot
    1.1.1
  
  mybatis-spring-boot-starter
  mybatis-spring-boot-starter
  
    
      org.mybatis.spring.boot
      mybatis-spring-boot-autoconfigure
      1.1.1
    
  

    可以看到,主要就是引入mybatis-spring-boot-autoconfigure,我们来看下这个jar包的结构SpringBoot-Mybatis框架使用与源码解析_第1张图片

    结构很简单,总共只有三个类,感觉MybatisAutoConfiguration应该是很重要的类。

    在META-INF中,我们又看到熟悉的配置spring.factories,我们在这里总能发现惊喜,打开这个文件,可以看到

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

    又是熟悉的味道,MybatisAutoConfiguration被对应到EnableAutoConfiguration,那么MybatisAutoConfiguration类会在SpringBoot项目启动的时候被注入到Spring容器中。原因在这里不再赘述,读者可以看下笔者之前的博客,有详细介绍。

    那么下面我们就来看下MybatisAutoConfiguration这个类

 

4.分析MybatisAutoConfiguration(SqlSessionFactory的创建)

@Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnBean(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisAutoConfiguration {
    
  @Bean
  @ConditionalOnMissingBean
  public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    // 同样还是通过SqlSessionFactoryBean来创建SqlSessionFactory
    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()));
    }
    factory.setConfiguration(properties.getConfiguration());
    if (!ObjectUtils.isEmpty(this.interceptors)) {
      factory.setPlugins(this.interceptors);
    }
    if (this.databaseIdProvider != null) {
      factory.setDatabaseIdProvider(this.databaseIdProvider);
    }
    if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
      factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
    }
    if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
      factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
    }
    if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
      // 就是我们在application.yml中配置的mybatis.mapper-locations: classpath:mapper/*.xml
      factory.setMapperLocations(this.properties.resolveMapperLocations());
    }

    return factory.getObject();
  }
  ...

    可知:当前类被添加了@Configuration注解,里面的所有被@Bean注解的方法返回值都会被注入到Spring容器中

    在这里,我们可到熟悉的SqlSessionFactory,正是Mybatis需要的组件之一,同样还是通过SqlSessionFactoryBean来创建,具体创建过程笔者不再赘述,读者可参考上一篇 Spring-Mybatis文章

 

    总结:在这里我们看到了SqlSessionFactory被注入到容器中。

    疑问:可以看到,创建SqlSessionFactory的方法,入参为DataSource,那么在方法调用之前,DataSource应该已经被注入到容器中了,那么DataSource是在什么时候在哪里被注入的呢?

 

5.DataSource在哪里被创建?

    我们没有主动创建DataSource的bean,那么应该也是SpringBoot帮我们主动创建了,那么究竟是在哪里创建的呢?

    我们可以开启日志DEBUG模式,在bean创建的过程中,都会打印相应的日志,在日志中我们搜索DataSource,可以看到以下内容

09:06:36.075 [main] DEBUG o.s.c.a.ConfigurationClassBeanDefinitionReader - Registered bean definition for imported class 'org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration$Tomcat'
09:06:36.075 [main] DEBUG o.s.c.a.ConfigurationClassBeanDefinitionReader - Registering bean definition for @Bean method org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration$Tomcat.dataSource()

    看到在org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration中,有一个被注入了@Bean的方法dataSource(),感觉这个应该就是创建DataSource的方法了,我们进入该类看下:

abstract class DataSourceConfiguration {
    ...
	@ConditionalOnClass(org.apache.tomcat.jdbc.pool.DataSource.class)
	@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "org.apache.tomcat.jdbc.pool.DataSource", matchIfMissing = true)
	static class Tomcat extends DataSourceConfiguration {

		@Bean
		@ConfigurationProperties(prefix = "spring.datasource.tomcat")
		public org.apache.tomcat.jdbc.pool.DataSource dataSource(
				DataSourceProperties properties) {
			org.apache.tomcat.jdbc.pool.DataSource dataSource = createDataSource(
					properties, org.apache.tomcat.jdbc.pool.DataSource.class);
			DatabaseDriver databaseDriver = DatabaseDriver
					.fromJdbcUrl(properties.determineUrl());
			String validationQuery = databaseDriver.getValidationQuery();
			if (validationQuery != null) {
				dataSource.setTestOnBorrow(true);
				dataSource.setValidationQuery(validationQuery);
			}
			return dataSource;
		}

	}

    在这里确实有关于 org.apache.tomcat.jdbc.pool.DataSource的创建,而且Tomcat类也符合上面的Condition条件

    那么问题又来了,当前类DataSourceConfiguration只是一个抽象类,它是什么时候被引入的呢?

    经过一番查找,我们看到在org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration类中有关于其创建,代码如下

	@Configuration
	@Conditional(PooledDataSourceCondition.class)
	@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
	@Import({ DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Hikari.class,
			DataSourceConfiguration.Dbcp.class, DataSourceConfiguration.Dbcp2.class,
			DataSourceConfiguration.Generic.class })
	@SuppressWarnings("deprecation")
	protected static class PooledDataSourceConfiguration {

	}

    直接使用@Import来引入DataSourceConfiguration.Tomcat

 

    总结:到此为止,我们可以回答我们上文提出的问题了,DataSource是在DataSourceConfiguration类中被注入到Spring容器的;而DataSourceConfiguration是在DataSourceAutoConfiguration中被注入到Spring容器的。

 

6.@MapperScan()注解的分析(MapperFactoryBean的注入)

    到目前为止,DataSource被注入到Spring中,SqlSessionFactory被注入到Spring中,还缺少MapperFactoryBean的注入

    我们知道在@MapperScan()注解中,我们在注解value中添加了IUser所在的包路径,直觉上,MapperFactoryBean的注入应该跟该注解有关

    下面我们来看下该注解的内容:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)//重点在这里,注入了MapperScannerRegistrar类
public @interface MapperScan {}

    1)MapperScannerRegistrar分析

// 注意当前类实现了ImportBeanDefinitionRegistrar接口,有关于ImportBeanDefinitionRegistrar的作用不再赘述,
// 读者可自行查看。
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
    
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

    AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);

    ...
    scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef"));
    scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef"));

    List basePackages = new ArrayList();
    for (String pkg : annoAttrs.getStringArray("value")) {
      if (StringUtils.hasText(pkg)) {
        basePackages.add(pkg);
      }
    }
    for (String pkg : annoAttrs.getStringArray("basePackages")) {
      if (StringUtils.hasText(pkg)) {
        basePackages.add(pkg);
      }
    }
    for (Class clazz : annoAttrs.getClassArray("basePackageClasses")) {
      basePackages.add(ClassUtils.getPackageName(clazz));
    }
    scanner.registerFilters();
    // 重点看这里,实现在ClassPathMapperScanner.doScan()
    scanner.doScan(StringUtils.toStringArray(basePackages));
  }
}

//ClassPathMapperScanner.doScan() 
  public Set doScan(String... basePackages) {
    // 这个方法,会扫描包路径basePackages下的所有类,并生成BeanDefinitionHolder,注入到Spring中
    // 在当前场景下,会扫描到IUser接口,并注入Spring中
    Set beanDefinitions = super.doScan(basePackages);

    if (beanDefinitions.isEmpty()) {
      logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
    } else {
      // 重要方法,下面继续观察
      processBeanDefinitions(beanDefinitions);
    }

    return beanDefinitions;
  }

//processBeanDefinitions()
  private void processBeanDefinitions(Set beanDefinitions) {
    GenericBeanDefinition definition;
    for (BeanDefinitionHolder holder : beanDefinitions) {
      // 1.获得IUser对应的BeanDefinitionHolder
      definition = (GenericBeanDefinition) holder.getBeanDefinition();

      ...
      // 2.修改definition对应的Class
      // 看过Spring源码的都知道,getBean()返回的就是BeanDefinitionHolder中beanClass属性对应的实例
      // 所以我们后面ac.getBean(IUser.class)的返回值也就是mapperFactoryBean的实例
      definition.setBeanClass(this.mapperFactoryBean.getClass());

      definition.getPropertyValues().add("addToConfig", this.addToConfig);

      ...

      if (!explicitFactoryUsed) {
        if (logger.isDebugEnabled()) {
          logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
        }
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
      }
    }
  }

    总结:通过上述分析,我们知道,ClassPathMapperScanner.doScan() 方法执行后,便会将IUser对应的BeanDefinitionHolder注入到Spring中;

    通过后续的processBeanDefinitions()方法,重新修改BeanDefinitionHolder的BeanClass属性值为mapperFactoryBean.class

 

    又是熟悉的味道,MapperFactoryBean在这里被引入,然后在真正使用到的时候被创建对应的实例,而通过对Spring-Mybatis的分析可知,MapperFactoryBean真正返回的是IUser的代理类MapperProxy

 

 

总结:

    1)DataSource在哪里被注入?

        答:DataSourceAutoConfiguration类中被注入,默认值为org.apache.tomcat.jdbc.pool.DataSource

 

    2)SqlSessionFactory在哪里被注入?

        答:MybatisAutoConfiguration类中被注入

 

    3)IUser等Mapper接口在哪里被注入?

        答:@MapperScan注解引入的MapperScannerRegistrar中,会扫描用户给定的包路径,并将IUser对应的MapperFactoryBean注入到Spring中,真正使用IUser时,获取的MapperFactoryBean.getObject()返回值,也就是MapperProxy

 

 

 

 

 

你可能感兴趣的:(Mybatis,Mybatis源码解析)