三萬字盤點Spring/Boot的那些常用擴充套件點

2022-07-26 15:00:59

大家好,我是三友。

Spring對於每個Java後端程式設計師來說肯定不陌生,日常開發和麵試必備的。本文就來盤點Spring/SpringBoot常見的擴充套件點,同時也來看看常見的開源框架是如何基於這些擴充套件點跟Spring/SpringBoot整合的。

話不多說,直接進入正題。

FactoryBean

提起FactoryBean,就有一道「著名」的面試題「說一說FactoryBean和BeanFactory的區別」。其實這兩者除了名字有點像,沒有半毛錢關係。。

BeanFactory是Bean的工廠,可以幫我們生成想要的Bean,而FactoryBean就是一種Bean的型別。當往容器中注入class型別為FactoryBean的型別的時候,最終生成的Bean是用過FactoryBean的getObject獲取的。

來個FactoryBean的Demo

定義一個UserFactoryBean,實現FactoryBean介面,getObject方法返回一個User物件

public class UserFactoryBean implements FactoryBean<User> {

    @Override
    public User getObject() throws Exception {
        User user = new User();
        System.out.println("呼叫 UserFactoryBean 的 getObject 方法生成 Bean:" + user);
        return user;
    }

    @Override
    public Class<?> getObjectType() {
        // 這個 FactoryBean 返回的Bean的型別
        return User.class;
    }

}

  

測試類:

public class Application {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        //將 UserFactoryBean 註冊到容器中
        applicationContext.register(UserFactoryBean.class);
        applicationContext.refresh();

        System.out.println("獲取到的Bean為" + applicationContext.getBean(User.class));
    }

}

  

結果:

呼叫 UserFactoryBean 的 getObject 方法生成 Bean:com.sanyou.spring.extension.User@396e2f39
獲取到的Bean為com.sanyou.spring.extension.User@396e2f39

從結果可以看出,明明註冊到Spring容器的是UserFactoryBean,但是卻能從容器中獲取到User型別的Bean,User這個Bean就是通過UserFactoryBean的getObject方法返回的。

FactoryBean在開源框架中的使用

1、 在Mybatis中的使用

Mybatis在整合Spring的時候,就是通過FactoryBean來實現的,這也就是為什麼在Spring的Bean中可以注入Mybatis的Mapper介面的動態代理物件的原因。

程式碼如下,省略了不重要的程式碼。

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
  
  // mapper的介面型別
  private Class<T> mapperInterface;
 
  @Override
  public T getObject() throws Exception {
    // 通過SqlSession獲取介面的動態搭理物件
    return getSqlSession().getMapper(this.mapperInterface);
  }
  
  @Override
  public Class<T> getObjectType() {
    return this.mapperInterface;
  }
  
}

getObject方法的實現就是返回通過SqlSession獲取到的Mapper介面的動態代理物件。

而@MapperScan註解的作用就是將每個介面對應的MapperFactoryBean註冊到Spring容器的。

2、在OpenFeign中的使用

FeignClient介面的動態代理也是通過FactoryBean注入到Spring中的。

class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
    
    // FeignClient介面型別
    private Class<?> type;
    
    @Override
   public Object getObject() throws Exception {
      return getTarget();
   }
    
    @Override
   public Class<?> getObjectType() {
      return type;
   }
}

getObject方法是呼叫getTarget方法來返回的動態代理。

@EnableFeignClients註解的作用就是將每個介面對應的FeignClientFactoryBean注入到Spring容器的。

一般來說,FactoryBean 比較適合那種複雜Bean的構建,在其他框架整合Spring的時候用的比較多。

@Import註解

@Import註解在專案中可能不常見,但是下面這兩個註解肯定常見。

@Import({SchedulingConfiguration.class})
public @interface EnableScheduling {
}

@Import({AsyncConfigurationSelector.class})
public @interface EnableAsync {
    //忽略
}

@EnableScheduling和@EnableAsync兩個註解,一個是開啟定時任務,一個是開啟非同步執行。通過這兩個註解可以看出,他們都使用了@Import註解,所以真正起作用的是@Import註解。並且在很多情況下,@EnbaleXXX這種格式的註解,都是通過@Import註解起作用的,代表開啟了某個功能。

@Import註解匯入的設定類的分類

@Import註解匯入的設定類可以分為三種情況:

第一種:設定類實現了 ImportSelector 介面

public interface ImportSelector {

   String[] selectImports(AnnotationMetadata importingClassMetadata);

   @Nullable
   default Predicate<String> getExclusionFilter() {
      return null;
   }

}

  

當設定類實現了 ImportSelector 介面的時候,就會呼叫 selectImports 方法的實現,獲取一批類的全限定名,最終這些類就會被註冊到Spring容器中。

UserImportSelector實現了ImportSelector,selectImports方法返回User的全限定名,代表吧User這個類註冊容器中

public class UserImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        System.out.println("呼叫 UserImportSelector 的 selectImports 方法獲取一批類限定名");
        return new String[]{"com.sanyou.spring.extension.User"};
    }

}

  

測試:

// @Import 註解匯入 UserImportSelector
@Import(UserImportSelector.class)
public class Application {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        //將 Application 註冊到容器中
        applicationContext.register(Application.class);
        applicationContext.refresh();

        System.out.println("獲取到的Bean為" + applicationContext.getBean(User.class));
    }

}

結果:

呼叫 UserImportSelector 的 selectImports 方法獲取一批類限定名
獲取到的Bean為com.sanyou.spring.extension.User@282003e1

所以可以看出,的確成功往容器中注入了User這個Bean

第二種:設定類實現了 ImportBeanDefinitionRegistrar 介面

public interface ImportBeanDefinitionRegistrar {

   default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,BeanNameGenerator importBeanNameGenerator) {
       registerBeanDefinitions(importingClassMetadata, registry);
   }

   default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
   }

}

當設定類實現了 ImportBeanDefinitionRegistrar 介面,你就可以自定義往容器中註冊想注入的Bean。這個介面相比與 ImportSelector 介面的主要區別就是,ImportSelector介面是返回一個類,你不能對這個類進行任何操作,但是 ImportBeanDefinitionRegistrar 是可以自己注入 BeanDefinition,可以新增屬性之類的。

來個demo:

實現ImportBeanDefinitionRegistrar介面

public class UserImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
        //構建一個 BeanDefinition , Bean的型別為 User
        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class)
                // 設定 User 這個Bean的屬性username的值為三友的java日記
                .addPropertyValue("username", "三友的java日記")
                .getBeanDefinition();

        System.out.println("往Spring容器中注入User");
        //把 User 這個Bean的定義註冊到容器中
        registry.registerBeanDefinition("user", beanDefinition);
    }

}

測試:

// 匯入 UserImportBeanDefinitionRegistrar
@Import(UserImportBeanDefinitionRegistrar.class)
public class Application {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        //將 Application 註冊到容器中
        applicationContext.register(Application.class);
        applicationContext.refresh();

        User user = applicationContext.getBean(User.class);
        System.out.println("獲取到的Bean為" + user + ",屬性username值為:" + user.getUsername());
    }

}

結果:

往Spring容器中注入User
獲取到的Bean為com.sanyou.spring.extension.User@6385cb26,屬性username值為:三友的java日記

第三種:設定類什麼介面都沒實現

這種就不演示了,就是一個普普通通的類。

總結

@Import註解作用示意圖

其實不論是什麼樣的設定類,主要的作用就是往Spring容器中註冊Bean,只不過注入的方式不同罷了。

這種方式有什麼好處呢?

ImportSelector和ImportBeanDefinitionRegistrar的方法是有入參的,也就是註解的一些屬性的封裝,所以就可以根據註解的屬性的設定,來決定應該返回樣的設定類或者是應該往容器中注入什麼樣的型別的Bean,可以看一下 @EnableAsync 的實現,看看是如何根據@EnableAsync註解的屬性來決定往容器中注入什麼樣的Bean。

@Import的核心作用就是匯入設定類,並且還可以根據配合(比如@EnableXXX)使用的註解的屬性來決定應該往Spring中注入什麼樣的Bean。

Bean的生命週期

第一節講的FactoryBean是一種特殊的Bean的型別,@Import註解是往Spring容器中註冊Bean。其實不論是@Import註解,還是@Component、@Bean等註解,又或是xml設定,甚至是demo中的register方法,其實主要都是做了一件事,那就是往Spring容器去註冊Bean。

Bean註冊示意圖

為什麼需要去註冊Bean?

當然是為了讓Spring知道要為我們生成Bean,並且需要按照我的要求來生成Bean,比如說,我要@Autowired一個物件,那麼你在建立Bean的過程中,就得給我@Autowired一個物件,這就是一個IOC的過程。所以這就涉及了Bean的建立,銷燬的過程,也就是面試常問的Bean的生命週期。我之前寫過兩篇文章,來剖析Bean的生命週期的原始碼,有需要的小夥伴可以看一下關注微信公眾號 三友的java日記 回覆 Bean 即可獲取。

本節來著重看一下,一個Bean在建立的過程中,有哪些常見的操作Spring在Bean的建立過程中給我們完成,並且操作的順序是什麼樣的。

話不多說,直接測試,基於結果來分析。

Bean生命週期的回撥

先來測試

建立LifeCycle類

建立了一個LifeCycle,實現了 InitializingBean、ApplicationContextAware、DisposableBean介面,加了@PostConstruct、@PreDestroy註解,注入了一個User物件。

public class LifeCycle implements InitializingBean, ApplicationContextAware, DisposableBean {

    @Autowired
    private User user;

    public LifeCycle() {
        System.out.println("LifeCycle物件被建立了");
    }

    /**
     * 實現的 Aware 回撥介面
     *
     * @param applicationContext
     * @throws BeansException
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println("Aware介面起作用,setApplicationContext被呼叫了,此時user=" + user);
    }

    @PostConstruct
    public void postConstruct() {
        System.out.println("@PostConstruct註解起作用,postConstruct方法被呼叫了");
    }

    /**
     * 實現 InitializingBean 介面
     *
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("InitializingBean介面起作用,afterPropertiesSet方法被呼叫了");
    }

    /**
     * 通過 {@link Bean#initMethod()}來指定
     *
     * @throws Exception
     */
    public void initMethod() throws Exception {
        System.out.println("@Bean#initMethod()起作用,initMethod方法被呼叫了");
    }

    @PreDestroy
    public void preDestroy() throws Exception {
        System.out.println("@PreDestroy註解起作用,preDestroy方法被呼叫了");
    }

    /**
     * 通過 {@link Bean#destroyMethod()}來指定
     *
     * @throws Exception
     */
    public void destroyMethod() throws Exception {
        System.out.println("@Bean#destroyMethod()起作用,destroyMethod方法被呼叫了");
    }

    /**
     * 實現 DisposableBean 註解
     *
     * @throws Exception
     */
    @Override
    public void destroy() throws Exception {
        System.out.println("DisposableBean介面起作用,destroy方法被呼叫了");
    }

}

宣告LifeCycle

通過@Bean宣告了LifeCycle,並且initMethod和destroyMethod屬性分別指定到了LifeCycle類的initMethod方法和destroyMethod方法

@Bean(initMethod = "initMethod", destroyMethod = "destroyMethod")
public LifeCycle lifeCycle() {
    return new LifeCycle();
}

  

測試

public class Application {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        //將 LifeCycle 註冊到容器中
        applicationContext.register(Application.class);
        applicationContext.refresh();

        // 關閉上下文,觸發銷燬操作
        applicationContext.close();
    }

    @Bean(initMethod = "initMethod", destroyMethod = "destroyMethod")
    public LifeCycle lifeCycle() {
        return new LifeCycle();
    }

    @Bean
    public User user() {
        return new User();
    }

}

  

執行結果:

LifeCycle物件被建立了
Aware介面起作用,setApplicationContext被呼叫了,此時user=com.sanyou.spring.extension.User@57d5872c
@PostConstruct註解起作用,postConstruct方法被呼叫了
InitializingBean介面起作用,afterPropertiesSet方法被呼叫了
@Bean#initMethod()起作用,initMethod方法被呼叫了
@PreDestroy註解起作用,preDestroy方法被呼叫了
DisposableBean介面起作用,destroy方法被呼叫了
@Bean#destroyMethod()起作用,destroyMethod方法被呼叫了

分析結果

通過測試的結果可以看出,Bean在建立和銷燬的過程當我們實現了某些介面或者加了某些註解,Spring就會回撥我們實現的介面或者執行的方法。

同時,在執行setApplicationContext的時候,能列印出User物件,說明User已經被注入了,說明注入發生在setApplicationContext之前。

這裡畫張圖總結一下Bean建立和銷燬過程中呼叫的順序。

回撥順序

紅色部分發生在Bean的建立過程,灰色部分發生在Bean銷燬的過程中,在容器關閉的時候,就會銷燬Bean。

這裡說一下圖中的Aware介面指的是什麼。其餘的其實沒什麼好說的,就是按照這種方式設定,Spring會呼叫對應的方法而已。

Aware介面是指以Aware結尾的一些Spring提供的介面,當你的Bean實現了這些介面的話,在建立過程中會回撥對應的set方法,並傳入響應的物件。

這裡列舉幾個Aware介面以及它們的作用

介面作用
ApplicationContextAware 注入ApplicationContext
ApplicationEventPublisherAware 注入ApplicationEventPublisher事件釋出器
BeanFactoryAware 注入BeanFactory
BeanNameAware 注入Bean的名稱

有了這些回撥,比如說我的Bean想拿到ApplicationContext,不僅可以通過@Autowired注入,還可以通過實現ApplicationContextAware介面拿到。

通過上面的例子我們知道了比如說@PostConstruct註解、@Autowired註解、@PreDestroy註解的作用,但是它們是如何在不同的階段實現的呢?接著往下看。

BeanPostProcessor

BeanPostProcessor,中文名 Bean的後置處理器,在Bean建立的過程中起作用。

BeanPostProcessor是Bean在建立過程中一個非常重要的擴充套件點,因為每個Bean在建立的各個階段,都會回撥BeanPostProcessor及其子介面的方法,傳入正在建立的Bean物件,這樣如果想對Bean建立過程中某個階段進行自定義擴充套件,那麼就可以自定義BeanPostProcessor來完成。

說得簡單點,BeanPostProcessor就是在Bean建立過程中留的口子,通過這個口子可以對正在建立的Bean進行擴充套件。只不過Bean建立的階段比較多,然後BeanPostProcessor介面以及他的子介面InstantiationAwareBeanPostProcessor、DestructionAwareBeanPostProcessor就提供了很多方法,可以使得在不同的階段都可以拿到正在建立的Bean進行擴充套件。

來個Demo

現在需要實現一個這樣的需求,如果Bean的型別是User,那麼就設定這個物件的username屬性為 」三友的java日記「。

那麼就可以這麼寫:

public class UserBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof User) {
            //如果當前的Bean的型別是 User ,就把這個物件 username 的屬性賦值為 三友的java日記
            ((User) bean).setUsername("三友的java日記");
        }

        return bean;
    }

}

測試:

public class Application {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        //將 UserBeanPostProcessor 和  User 註冊到容器中
        applicationContext.register(UserBeanPostProcessor.class);
        applicationContext.register(User.class);
        applicationContext.refresh();

        User user = applicationContext.getBean(User.class);
        System.out.println("獲取到的Bean為" + user + ",屬性username值為:" + user.getUsername());
    }

}

測試結果:

獲取到的Bean為com.sanyou.spring.extension.User@21a947fe,屬性username值為:三友的java日記

從結果可以看出,每個生成的Bean在執行到某個階段的時候,都會回撥UserBeanPostProcessor,然後UserBeanPostProcessor就會判斷當前建立的Bean的型別,如果是User型別,那麼就會將username的屬性設定為 」三友的java日記「。

Spring內建的BeanPostProcessor

這裡我列舉了常見的一些BeanPostProcessor的實現以及它們的作用

BeanPostProcessor作用
AutowiredAnnotationBeanPostProcessor 處理@Autowired、@Value註解
CommonAnnotationBeanPostProcessor 處理@Resource、@PostConstruct、@PreDestroy註解
AnnotationAwareAspectJAutoProxyCreator 處理一些註解或者是AOP切面的動態代理
ApplicationContextAwareProcessor 處理Aware介面注入的
AsyncAnnotationBeanPostProcessor 處理@Async註解
ScheduledAnnotationBeanPostProcessor 處理@Scheduled註解

通過列舉的這些BeanPostProcessor的實現可以看出,Spring Bean的很多註解的處理都是依靠BeanPostProcessor及其子類的實現來完成的,這也回答了上一小節的疑問,處理@Autowired、@PostConstruct、@PreDestroy註解是如何起作用的,其實就是通過BeanPostProcessor,在Bean的不同階段來呼叫對應的方法起作用的。

BeanPostProcessor在Dubbo中的使用

在Dubbo中可以通過@DubboReference(@Reference)來參照生產者提供的介面,這個註解的處理也是依靠ReferenceAnnotationBeanPostProcessor,也就是 BeanPostProcessor 的擴充套件來實現的。

public class ReferenceAnnotationBeanPostProcessor 
       extends AbstractAnnotationBeanPostProcessor 
       implements ApplicationContextAware, BeanFactoryPostProcessor {
        // 忽略
}

當Bean在建立的某一階段,走到了ReferenceAnnotationBeanPostProcessor這個類,就會根據反射找出這個類有沒有@DubboReference(@Reference)註解,有的話就構建一個動態搭理注入就可以了。

BeanPostProcessor在Spring Bean的擴充套件中扮演著重要的角色,是Spring Bean生命週期中很重要的一部分。正是因為有了BeanPostProcessor,你就可以在Bean建立過程中的任意一個階段擴充套件自己想要的東西。

BeanFactoryPostProcessor

通過上面一節我們知道 BeanPostProcessor 是對Bean的處理,那麼BeanFactoryPostProcessor很容易就猜到是對BeanFactory,也就是Spring容器的處理。

舉個例子,如果我們想禁止迴圈依賴,那麼就可以這麼寫。

public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // 禁止迴圈依賴
        ((DefaultListableBeanFactory) beanFactory).setAllowCircularReferences(false);
    }

}

後面只需要將注入到Spring容器中就會生效。

BeanFactoryPostProcessor是可以對Spring容器做處理的,方法的入參就是Spring的容器,通過這個介面,就對容器進行為所欲為的操作。

Spring SPI機制

SPI全稱為 (Service Provider Interface),是一種動態替換髮現的機制,一種解耦非常優秀的思想,SPI可以很靈活的讓介面和實現分離, 讓api提供者只提供介面,第三方來實現,然後可以使用組態檔的方式來實現替換或者擴充套件,在框架中比較常見,提高框架的可延伸性。

JDK有內建的SPI機制的實現ServiceLoader,Dubbo也有自己的SPI機制的實現ExtensionLoader,但是這裡我們都不講。。