背景
是不是還在疑惑爲什麼我們在工程中定義了接口mybatis就可以直接操作我們的數據庫?
是不是想了解spring和mybaits整合的原理?
瞭解原理後我們能複用在工程上的東西是什麼?換句話說怎麼提高代碼的逼格?
目的
基於上述背景,筆者準備深入源碼帶大家一探究竟,讀完這篇文章大家可以的到的收穫
- 瞭解Mybatis和Spring整合的底層原理
- 知道爲什麼只定義了接口就可以直接操作數據庫
- 瞭解Spring中的拓展點和FactoryBean的使用
- 可以自己定義插件提高代碼逼格
- Spring中自動裝配的類型到底是什麼
分析問題
準備
代碼環境:
- JDK :1.8
- Spring Boot :2.3
- 基於註解
- 忽略一些不重要的細節 項目代碼如下
//對象
public class AD {
//id
private int id;
//名稱
private String name;
}
//mapper類
public interface AdMapper {
@Select("select id , name from " + " ad " + "where id=#{id} ")
AD findADById(@Param("id") int id);
}
//接口
public interface AdService {
/**
* 通過id獲取廣告對象
*
* @param id
* @return
*/
AD findADbyId(int id);
}
//接口實現類
@Service
public class AdServiceImpl implements AdService {
@Resource
private AdMapper adMapper;
@Override
public AD findADbyId(int id) {
return adMapper.findADById(id);
}
}
//啓動類
@MapperScan("com.learn.code.mybatis.mapper")
@SpringBootApplication
public class LearnCodeApplication {
public static void main(String[] args) {
SpringApplication.run(LearnCodeApplication.class, args);
}
}
問題1 Mapper
對象的BeanDefinition
是怎麼加入到工廠中的
問題由來:一個對象只有被Spring
創建並且放入到工廠中才能被其他對象注入,比如AdServiceImpl
就是加了@Service
註解並且結合包掃描。這樣環境中就會有這個對象,但是AdMapper
沒有加任何的註解,而我們的 AdServiceImpl
卻可以直接通過 @Resource
注入進來。說明這個對象是被Spring創建的。
回想Spring創建Bean的過程,幾乎所有的對象都是先變成BeanDefinition
然後再通過工廠創建,所以我們只要找到這個Mapper類是什麼時候變成BeanDefinition
的。
我們通過看代碼發現和Mybatis
相關的只有開始在配置類中加入的註解@MapperScan
,難道是這個註解起的作用嗎?
沒錯,這個註解是個入口,就像是把鑰匙,像只有我們帶了鑰匙才能開門一樣,只有加了這個註解(注意本文基於註解配置)才能實現上述的功能
那現在我們重點看一下這個註解
@MapperScan
屏蔽掉一些非關鍵信息 註解結構如下:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
String[] value() default {};
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
}
在這個註解類中發現其實也沒有做太多事,但是我們會發現類上邊有@Import(MapperScannerRegistrar.class)
這行.對Spring啓動源碼有了解的同學可能知道,在準備工廠階段,會把 @Import
引入的類當作配置類,後期通過Spring創建這個bean。所以我們應該能感覺MapperScannerRegistrar
這個類是有些作用的,照例點進去看一下源碼。
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware
發現這個類繼承了ImportBeanDefinitionRegistrar
實現了這個接口,這個接口是Spring當中的一個擴展點,基於接口中的registerBeanDefinitions方法我們可以做到向bean工廠中注入BeanDefinition,現在看來我們的方向是對的。
下面解析一下 registerBeanDefinitions 方法
/**
* importingClassMetadata 註解元素
* registry 用來向 BeanDefinitionRegistry 加入 BeanDefinition
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//獲取MapperScan註解中的屬性信息 @MapperScan("com.learn.code.mybatis.mapper")
// 類上可能會有很多註解 這裏指定名稱獲取
AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
//初始化一個 scanner 用作掃描指定包下的類
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
// Spring 3.1 版本需要有這個判斷 特殊邏輯
if (resourceLoader != null) {
// 設置資源加載器,作用:掃描指定包下的class文件。
scanner.setResourceLoader(resourceLoader);
}
//===============下邊是獲取MapperScan中的屬性值,賦值給scanner,以後會在doScan中使用============
Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass");
if (!Annotation.class.equals(annotationClass)) {
scanner.setAnnotationClass(annotationClass);
}
Class<?> markerInterface = annoAttrs.getClass("markerInterface");
if (!Class.class.equals(markerInterface)) {
scanner.setMarkerInterface(markerInterface);
}
Class<? extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator");
if (!BeanNameGenerator.class.equals(generatorClass)) {
scanner.setBeanNameGenerator(BeanUtils.instantiateClass(generatorClass));
}
Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
scanner.setMapperFactoryBean(BeanUtils.instantiateClass(mapperFactoryBeanClass));
}
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();
//執行掃描,並且會把 BD放入到工廠中,並更改BD中的一些屬性值
scanner.doScan(StringUtils.toStringArray(basePackages));
}
scanner.doScan(StringUtils.toStringArray(basePackages))方法
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
// 1
// 調用父類的掃描方法 向容器中加入 BD 並返回 beanDefinitions
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 {
// 2
// 操作 BD 引用 修改其中的屬性
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
doScan方法首先做的是調用父類的掃描方法,根據指定的包的路徑和一些其他的限制條件(比如只要接口不需要類
),來決定是否讀取這個BD加入到工廠中。由於這個方法比較簡單就不跟進去看了。
因爲掃描是基於spring的掃描器。掃描過程中應該會有類加入進來,但是斷點的時候會發現結果是隻有接口。其中有個條件是子類可以重寫的比如:
//只掃描接口
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
}
這樣掃描出來的東西也不會和spring本身掃描的類重複
問題2 Mapper
對象是個接口怎麼進行實例化
通過上邊的分析,我們已經解決了第一個問題,怎麼把這些類的BD放入到工廠中的,但是這樣就完了嗎?我們現在BD裏面的class類型是一個接口,這是沒有辦法被實例化的。所以我們需要解決對象實例化的問題。
其實解決對象實例化不外乎就是對接口進行動態代理,但是怎麼進行?代理完成後是個代理對象又怎麼通過@Resource注入進來。不妨來看一下Mybatis是怎麼實現的。
入口位置即是上邊代碼塊中我標註了2
的位置。下邊來看一下 2出的代碼實現
processBeanDefinitions(beanDefinitions)
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
GenericBeanDefinition definition;
//對傳進來的BD循環處理
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
if (logger.isDebugEnabled()) {
logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName()
+ "' and '" + definition.getBeanClassName() + "' mapperInterface");
}
// the mapper interface is the original class of the bean
// but, the actual class of the bean is MapperFactoryBean
// 1 設置 mapperInterface 屬性 也就是這個接口的原始類型 對那個接口進行代理
definition.getPropertyValues().add("mapperInterface", definition.getBeanClassName());
// 2 設置 BeanClass 實例化時候用到的類型 能被實例化就是改了這個 原來是實際的接口類型,現在是 mapperFactoryBean 類型 因爲 mapperFactoryBean 不是抽象類也不是接口所以可以被初始化
// 通過spring的getBean方法創建對象就是創建的beanClass類型的對象
definition.setBeanClass(this.mapperFactoryBean.getClass());
//添加屬性
definition.getPropertyValues().add("addToConfig", this.addToConfig);
boolean explicitFactoryUsed = false;
//設置 sqlSessionFactory
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionFactory != null) {
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}
//sqlSessionTemplate and sqlSessionFactor 都存在 會使用 sqlSessionTemplate
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
if (explicitFactoryUsed) {
logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
if (explicitFactoryUsed) {
logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}
if (!explicitFactoryUsed) {
if (logger.isDebugEnabled()) {
logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
}
// 3 設置自動裝配模型爲 BY_TYPE 通過類型進行裝配
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
}
}
這整個方法的作用實際是構造 mapperFactoryBean 類型 需要傳入的參數
其中 需要重點關注的是我標記了1、2、3
的部分
標註1的部分 設置代理的接口類型
在別的版本中是通過配置構造函數來做的如下,但是原理是一樣的。
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
需要注意的是通過這種形式加進去的是字符串類型,但是內部會幫我們轉成class類型。所以我們去看這個類的的構造方法的時候會發現他是有一個class的構造方法而沒有字符串的。
這部分的主要邏輯是 向 mapperFactoryBean 中傳入我們要進行動態代理的接口,至於爲什麼稍後我會把 mapperFactoryBean 的源碼貼出來 大家就明白了。
標註2的部分 設置實例化類的類型
開始我們再說的問題就是怎麼創建對象的問題,因爲BeanClass爲接口類型,讀過spring創建bean的源碼的同學都知道,在getBean時就是創建的BeanClass類型的對象,所以我們需要修改這個類型 是個具體的類 。這個具體的類就是 MapperFactoryBean ,有些人可能會問,那創建的這個類型不是我們需要的類型啊?我們怎麼使用?別急這些在我們說到 MapperFactoryBean 這個類的時候會做解釋
標註3的部分 修改自動裝配模型
自動裝配你真的瞭解嗎?
如果我們大家Spring中Bean的裝配模型是什麼?相信大部分人第一反應就是By_Type或者By_Name .爲什麼?因爲我們通過@Resource等註解可以進行注入。這裏我要給大家糾正一下概念。Spring中的裝配模型爲By_No,他只是依賴於自動裝配的技術完成了自動裝配。
看源碼
org.springframework.beans.factory.support.AbstractBeanDefinition#setAutowireMode
/**
* Set the autowire mode. This determines whether any automagical detection
* and setting of bean references will happen. Default is AUTOWIRE_NO,
* which means there's no autowire.
* @param autowireMode the autowire mode to set.
* Must be one of the constants defined in this class.
* @see #AUTOWIRE_NO
* @see #AUTOWIRE_BY_NAME
* @see #AUTOWIRE_BY_TYPE
* @see #AUTOWIRE_CONSTRUCTOR
* @see #AUTOWIRE_AUTODETECT
*/
public void setAutowireMode(int autowireMode) {
this.autowireMode = autowireMode;
}
看上邊的註釋我們會發現自動裝配的模型有5中但是默認的是Default is AUTOWIRE_NO
也就是不進行自動裝配
是不是顛覆了你的認知呢?
爲什麼修改裝配類型爲AUTOWIRE_BY_TYPE
言歸正傳,這個地方修改爲AUTOWIRE_BY_TYPE
的目的是什麼?
你們可以做個實驗試一下,如果把某個類的裝配類型改成 BY_TYPE ,那麼這個類的所有的set方法都會進行自動裝配
@Service
public class ByTypeTuan {
//=============第一種========
private AdService adService;
public void setAdService(AdService adService) {
this.adService = adService;
}
//=============第二種========
@Autowired
private AdService service;
}
如上述代碼 我們要獲得 adService 原來是通過 @Autowired
註解,底層是後置處理器做的賦值,現在可以通過第一種 直接set注入。
想一下這樣有什麼好處?
我們要操作數據庫還需要拿到數據庫的SqlSession,SqlSession是怎麼注入的呢?就是通過set方法注入到代理類中然後我們纔可以進行操作。
所以說這個地方也是一個重點,但是別的博文中很少有人提到,看到這是不是該給作者一個贊呢?
問題3產生的對象爲什麼可以使用,並且能操作數據庫
上邊說到,其實我們向Spring中加入的是 MapperFactoryBean 類型的Bean.現在我們來看一下這個對象的實現:
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
private Class<T> mapperInterface;
private boolean addToConfig = true;
//構造方法。傳入 mapperInterface
public MapperFactoryBean(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public MapperFactoryBean() {}
/**
* {@inheritDoc}
*/
@Override
protected void checkDaoConfig() {
super.checkDaoConfig();
notNull(this.mapperInterface, "Property 'mapperInterface' is required");
Configuration configuration = getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
configuration.addMapper(this.mapperInterface);
} catch (Exception e) {
logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
throw new IllegalArgumentException(e);
} finally {
ErrorContext.instance().reset();
}
}
}
/**
* 創建代理對象
*/
@Override
public T getObject() throws Exception {return getSqlSession().getMapper(this.mapperInterface);}
/**
* 創建的對象類型是 mapperInterface 即mapper接口類型
*/
@Override
public Class<T> getObjectType() { return this.mapperInterface;}
/**
* 創建的對象是否是單例
*/
@Override
public boolean isSingleton() { return true; }
//------------- mutators --------------
/**
* 設置 mapper interface
* @param mapperInterface class of the interface
*/
public void setMapperInterface(Class<T> mapperInterface) { this.mapperInterface = mapperInterface;}
public Class<T> getMapperInterface() {return mapperInterface;}
public void setAddToConfig(boolean addToConfig) {this.addToConfig = addToConfig;}
public boolean isAddToConfig() {return addToConfig;}
}
重要的代碼位置已經加上了註釋,通過這個類結構我們能更明確知道上一部在修改BD信息時的作用是什麼
通過觀察代碼的繼承結構,發現extends SqlSessionDaoSupport implements FactoryBean<T>
對 FactoryBean
比較瞭解的同學都知道 這個對象在Spring創建Bean時會創建兩個對象,一個是它本身還有一個是通過getObject方法返回我們需要的對象。
/**
* 創建代理對象
*/
@Override
public T getObject() throws Exception {return getSqlSession().getMapper(this.mapperInterface);}
看 getObject 內部的實現 和我們自己整合mybatis一樣 通過sqlSession.getMapper()來獲得Mapper對象,其中參數中傳的是 接口類。
還記得上邊拋了一個問題,爲什麼要修改這個對象的裝配屬性爲By_Type 因爲 繼承了 SqlSessionDaoSupport 而 SqlSessionDaoSupport 中有兩個set方法 是用來注入SqlSessionFactory的 我們拿到了SqlSessionFactory 就可以獲得SqlSession 從而獲得Mapper對象 然後操作數據庫。
總結一下:
- 定義一個類實現FactoryBean繼承SqlSessionDaoSupport
- 定義一個實現ImportBeanDefinitionRegistrar,用來生成不同Mapper對象的FactoryBean
- @Import 用來創建上邊實現的ImportBeanDefinitionRegistrar接口的對象
- 修改AUTOWIRE 可以通過set方法直接注入
我這篇文章 spring常用擴展點術語介紹大致說了一些拓展點的作用,以後會詳細具體怎麼使用,和Spring中的應用。大家可以關注一下