大家可以關注一下公衆號“Java架構師祕籍” 純乾貨綠色天然無污染
前言:
現在微服務架構基本已經成爲一種架構正確了,而SpringBoot技術也已經被應用在各個項目中。
SpringBoot不僅僅那些傳統的好處,更多是可以與其他組件進行結合,使用戶可以更方便的使用。比如SpringBoot-Kafka、SpringBoot-Mybatis、SpringBoot-RabbitMQ等等。
本文,分析一下SpringBoot-Mybatis的使用及其源碼,讓讀者可以更深入的瞭解下SpringBoot是如何幫我們簡化配置,節約開發時間的。
歡迎大家加Q羣:230419550 學習交流討論架構師進階知識
1.SpringBoot-Mybatis使用
1)添加maven依賴
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.3.RELEASE</version>
</parent>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.32</version>
</dependency>
**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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mybatis.IUser">
<select id="getUser" parameterType="int"
resultType="com.example.demo.mybatis.User">
SELECT *
FROM USER
WHERE id = #{userId}
</select>
</mapper>
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所在路徑)
@RunWith(SpringRunner.class)br/>@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依賴
我們在項目中引入該依賴,下面我們看下這個依賴的主要內容
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot</artifactId>
<version>1.1.1</version>
</parent>
<artifactId>mybatis-spring-boot-starter</artifactId>
<name>mybatis-spring-boot-starter</name>
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-autoconfigure</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>
</project>
可以看到,主要就是引入mybatis-spring-boot-autoconfigure,我們來看下這個jar包的結構
https://img-blog.csdn.net/20180803113940659?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI2MzIzMzIz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70
結構很簡單,總共只有三個類,感覺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 })br/>@ConditionalOnBean(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)br/>@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisAutoConfiguration {
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 {br/>...
@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)br/>@Target(ElementType.TYPE)
@Documentedbr/>@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<String> basePackages = new ArrayList<String>();
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<BeanDefinitionHolder> doScan(String... basePackages) {
// 這個方法,會掃描包路徑basePackages下的所有類,並生成BeanDefinitionHolder,注入到Spring中
// 在當前場景下,會掃描到IUser接口,並注入Spring中
Set<BeanDefinitionHolder> 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<BeanDefinitionHolder> 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