spring和mybatis整合爲什麼只定義了接口?爲什麼設置自動裝配模型爲BY_TYPE

背景

是不是還在疑惑爲什麼我們在工程中定義了接口mybatis就可以直接操作我們的數據庫?
是不是想了解spring和mybaits整合的原理?
瞭解原理後我們能複用在工程上的東西是什麼?換句話說怎麼提高代碼的逼格?

目的

基於上述背景,筆者準備深入源碼帶大家一探究竟,讀完這篇文章大家可以的到的收穫

  1. 瞭解Mybatis和Spring整合的底層原理
  2. 知道爲什麼只定義了接口就可以直接操作數據庫
  3. 瞭解Spring中的拓展點和FactoryBean的使用
  4. 可以自己定義插件提高代碼逼格
  5. 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對象 然後操作數據庫。

總結一下:

  1. 定義一個類實現FactoryBean繼承SqlSessionDaoSupport
  2. 定義一個實現ImportBeanDefinitionRegistrar,用來生成不同Mapper對象的FactoryBean
  3. @Import 用來創建上邊實現的ImportBeanDefinitionRegistrar接口的對象
  4. 修改AUTOWIRE 可以通過set方法直接注入

我這篇文章 spring常用擴展點術語介紹大致說了一些拓展點的作用,以後會詳細具體怎麼使用,和Spring中的應用。大家可以關注一下

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章