SpringBoot模組化開發:@Import註解的應用

👉 這是一個或許對你有用的社群
🐱 一對一交流/面試小冊/簡歷最佳化/求職解惑,歡迎加入芋道快速開發平臺知識星球。下面是星球提供的部分資料:
👉這是一個或許對你有用的開源專案
國產 Star 破 10w+ 的開源專案,前端包括管理後臺 + 微信小程式,後端支援單體和微服務架構。
功能涵蓋 RBAC 許可權、SaaS 多租戶、資料許可權、商城、支付、工作流、大屏報表、微信公眾號、ERPCRMAI 大模型等等功能:
  • Boot 多模組架構:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 微服務架構:https://gitee.com/zhijiantianya/yudao-cloud
  • 影片教程:https://doc.iocoder.cn
【國內首批】支援 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 雙版本 

在使用Spring Boot開發後端應用程式時,很多時候我們使用四層架構來完成對單體應用程式的開發。雖然四層架構在SSM單體應用程式中能夠很清晰明瞭地劃分每一層,從資料到功能,最後到API。
但是隨著我們單體專案功能的增加,專案仍然會變得更加臃腫,比如說當我們再開啟很久之前的專案,或者接手其它專案時,看到側邊欄一長條的xxxDAO或者xxxService時,很多時候一時半會也是緩不過神的:
當然,這僅僅是一個還未開發完成的小型單體專案,如果功能再複雜一點,那麼其臃腫程度我們將無法想象,也使得我們繼續開發、維護變得困難。
這時我們可以對專案分模組,即按照功能拆分為多個Maven模組,然後可以透過依賴或者配置的方式將多個功能整合在主要模組上,甚至還可以控制是否啟用某個功能。
需要注意的是,這裡的應用拆分並不是把應用拆分成Spring Cloud分散式微服務多模組,而是僅對一個單體專案而言,它仍然是單體專案,但是每一個功能放在每個模組中,而不再是所有功能放在一個Spring Boot工程中。
要想實現Spring Boot模組化開發,我們可以藉助@Import註解,實現在一個模組中,匯入另一個模組中的類並將其也初始化為Bean註冊到IoC容器。
下面,我們就透過一個簡單的例子來學習一下。

1,再看@ComponentScan

在學習今天的內容之前,我們可以先回顧一下關於IoC容器掃描元件的基本知識。

1) IoC容器的掃描起點

相信大家對@SpringBootApplication這個註解並不陌生,我們建立的每個Spring Boot工程主類都長這樣差不多:
package

 com.gitee.swsk33.mainmodule;

import

 org.springframework.boot.SpringApplication;

import

 org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
publicclassMainModuleApplication

{

publicstaticvoidmain(String[] args)

{

  SpringApplication.run(MainModuleApplication

.classargs)

;

 }
}

從初學Spring Boot開始,我們就知道要想讓一個類被掃描並例項化成Bean交給IoC容器託管,除了給那些類標註相關的註解(比如@Component)之外,還需要將其放在主類(也就是標註了@SpringBootApplication的類)所在的軟體包或者其子包層級下,這樣在IoC容器初始化時,我們的類才會被掃描到。
可見@SpringBootApplication事實上標註了IoC容器建立Bean時掃描的起點,不過@SpringBootApplication是一個複雜的複合註解,它是下列註解的組合:
而事實上,真正起到標註掃描起點作用的註解是@ComponentScan,當該註解標註在一個類上時,這個類就會被標記為IoC容器的掃描起點,相信大家初學Spring時都寫過這樣類似的入門示例:
package

 com.gitee.swsk33.springdemo;

import

 com.gitee.swsk33.springdemo.service.MessageService;

import

 org.springframework.context.annotation.AnnotationConfigApplicationContext;

import

 org.springframework.context.annotation.ComponentScan;

import

 org.springframework.context.ApplicationContext;

@ComponentScan
publicclassMain

{

publicstaticvoidmain(String[] args)

{

// 建立基於註解的上下文容器例項,並傳入配置類Main以例項化其它標註了Bean註解的類

        ApplicationContext context = 

new

 AnnotationConfigApplicationContext(Main

.class)

;

// 利用Spring框架,取出Bean:MessageService物件

        MessageService messageService = context.getBean(MessageService

.class)

;

// 這時,就可以使用了!

        messageService.printMessage();

    }
}

可見上述是我的主類,其標註了@ComponentScan註解,主類位於軟體包com.gitee.swsk33.springdemo,那麼IoC容器初始化時,就會遞迴掃描位於軟體包com.gitee.swsk33.springdemo中及其下所有子包中標註了相關注解(例如@Component@Service)的類,並將它們例項化為Bean放入IoC容器託管,上述程式碼中,MessageService位於com.gitee.swsk33.springdemo中的子包service下且標註了相關注解,因此能夠被例項化為Bean並放入IoC容器,後續我們可以取出。
事實上,無論是@ComponentScan還是@SpringBootApplication註解,都是可以指定掃描位置的,比如說:
@SpringBootApplication

(scanBasePackages = 

"com.gitee.swsk33.mainmodule"

)

publicclassMainModuleApplication

{

// ...

}

這表示啟動程式時指定掃描軟體包com.gitee.swsk33.mainmodule中及其所有子包下對應的類,只不過平時大多數時候我們都預設這個引數,這樣預設情況下,@ComponentScan或者@SpringBootApplication就是以自身為起點向下掃描當前包以及所有的子包中的類了。

2) 匯入其它模組作為依賴?

首先假設現在有一個Maven多模組工程,其中有三個Spring Boot工程如下:
上述是一個按照功能拆分的Spring Boot多模組的專案示例,main-module工程是主功能,而另外兩個是兩個子功能模組,主功能模組需要以Maven依賴的形式匯入子功能模組,它們才能組成一個完整的系統。
如果說現在在上述主功能中,將功能1以Maven依賴形式引入,啟動主功能,功能1模組中的FunctionOneService類也會被掃描到並例項化為Bean嗎?
很顯然並不會。因為主功能中主類位於軟體包com.gitee.swsk33.mainmodule中,那麼啟動時就會掃描該軟體包及其子包下的類,不可能說掃描到功能1中的軟體包com.gitee.swsk33.functionone了。
當然,這個問題很好解決,我們可以在@SpringBootApplication註解中指定scanBasePackages欄位將兩個子模組的包路徑加進去就行了,這樣確實沒有問題,但是好像總覺得不是很優雅:如果我需要按需停用或者啟用功能,那就需要修改這個主類的註解中傳入的引數。
有沒有別的辦法呢?當然,@Import註解也可以實現這個功能。
基於 Spring Boot + MyBatis Plus + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
  • 專案地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 影片教程:https://doc.iocoder.cn/video/

2,@Import註解的基本使用

@Import註解通常標註在配置類上,它可以在IoC容器初始化當前配置類的同時,將其它的指定類也引入進來並初始化為Bean,例如:
@Configuration
@Import

(DemoFunction

.

class

)

publicclassFunctionImportConfig

{

}

可見上述FunctionImportConfig是一個配置類,該類會在IoC容器初始化時被掃描並初始化為Bean,那麼在IoC容器掃描這個FunctionImportConfig的同時,也會讀取到它上面的@Import註解,而@Import註解中指定了類DemoFunction,這就可以使得DemoFunction類也被加入掃描的候選類,最終也被例項化為Bean並交給IoC容器。
事實上,無論被標註@Import的類放在哪裡,主要這個類能被掃描到,且標註了@Configuration等註解、能被例項化為Bean,那麼其上的@Import註解中指定的類也會被連帶著加入掃描以及初始化為Bean的候選。
當然,上述這個被匯入的DemoFunction類也是有要求的,它必須是一個配置類,分下面兩種情況討論:
  • 被匯入的DemoFunction@Configuration標註的類: Spring會將這個DemoFuntion配置類初始化為Bean並載入到IoC容器中,這意味著只有該配置類本身、以及其中顯示宣告的Bean才會被載入到容器中,其他未宣告的bean則不會被載入
  • 被匯入的DemoFunction@ComponentScan標註的類: Spring則會在匯入該配置類同時,還會根據@ComponentScan指定的掃描包路徑,掃描其指定的全部包下對應的類(標註了@Component等等註解的)並初始化為Bean,預設則是將該類及其所在包的所有子包下的相關類初始化為Bean
回到上面的多模組專案場景中,可見我們只需要使用@Import註解不就可以在主模組中,把功能1模組中的類全部匯入並初始化為Bean嗎?
下面,我們就來嘗試一下。

1) 匯入其它模組的@ComponentScan類

大家可以根據上述工程結構建立一個多模組Maven專案,先是建立一個父模組的pom.xml,然後主模組、功能模組1和功能模組2都繼承這同一個父專案,這樣它們之間可以相互引用。
首先我們來看功能模組1,該模組作為一個功能,不需要作為一個完整的Spring Boot應用程式啟動,因此該模組中不需要主類,只編寫起點配置類和功能程式碼(比如Service層的類)即可,刪除功能模組1的全部依賴,然後只加一個spring-boot-starter作為一些註解的基本支援即可:
然後刪除功能模組1的主方法main,並將@SpringBootApplication改成@ComponentScan,僅作為掃描起點類即可,該類位於功能模組1最頂層軟體包中,其中內容如下:
package

 com.gitee.swsk33.functionone;

import

 org.springframework.context.annotation.ComponentScan;

@ComponentScan
publicclassFunctionOneApplication

{

}

然後再給功能模組1開發一個Service類,內容如下:
package

 com.gitee.swsk33.functionone.service;

import

 jakarta.annotation.PostConstruct;

import

 lombok.extern.slf4j.Slf4j;

import

 org.springframework.stereotype.Service;

@Slf

4j

@Service
publicclassFunctionOneService

{

@PostConstruct
privatevoidinit()

{

  log.info(

"功能1,啟動!"

);

 }
}

可見使其被初始化為Bean時列印一句話,讓我們知道該類被掃描並且被初始化即可。
現在回到主模組,在其中將功能模組1以依賴形式引入:
然後在主模組中建立一個配置類,使用@Import匯入功能模組1中的掃描起點(標註了@ComponentScan的類):
package

 com.gitee.swsk33.mainmodule.config;

import

 com.gitee.swsk33.functionone.FunctionOneApplication;

import

 com.gitee.swsk33.functiontwo.FunctionTwoApplication;

import

 org.springframework.context.annotation.Configuration;

import

 org.springframework.context.annotation.Import;

/**

 * 用於匯入其它模組的配置,使得其它模組中的Bean也能夠交給IoC託管

 */


@Configuration
@Import

(FunctionOneApplication

.

class

)

publicclassFunctionImportConfig

{

}

事實上,@Import可以匯入多個類,傳入陣列形式即可,這裡我們只匯入模組1的起點類。
現在,啟動主模組,可見模組1中的服務類也被成功掃描到並初始化為Bean了:
可見當我們的主模組啟動時:
  • 首先初始化主模組中的配置類FunctionImportConfig,同時讀取到該配置類上的@Import註解中指定的模組1中的類FunctionOneApplication
  • 模組1中的類FunctionOneApplication@ComponentScan標註,因此新增掃描起點,將FunctionOneApplication所在的包及其所有子包也加入掃描路徑
  • 這樣不僅僅主模組自身,還有模組1下所有標註了對應註解的類都被掃描並初始化為了Bean,並加入了IoC容器中
  • 這樣,我們就可以在主模組中,自動裝配模組1中的類了
可見@Import註解可以很方便地將一個其它模組,甚至其它外部庫中的對應配置類匯入,並加入掃描初始化為Bean,加入到我們當前的IoC容器中去,並且在我們使用@Import匯入@ComponentScan標註的類時,可以實現新增一個掃描起點的效果,而不僅僅是隻掃描我們當前專案中的包路徑,這樣就將其它模組中的包路徑也加入掃描。

2) 封裝@Import註解

事實上,@EnableAsync以及@EnableDiscoveryClient這些註解,都是基於@Import實現的,當我們給自己專案的主類或者某個配置類打上該註解時,就能夠啟用某些功能,反之對應功能不會載入。
我們也可以來封裝一個@EnableFunctionOne註解,在主模組中編寫該註解程式碼如下:
package

 com.gitee.swsk33.mainmodule.annotation;

import

 com.gitee.swsk33.functionone.FunctionOneApplication;

import

 org.springframework.context.annotation.Import;

import

 java.lang.annotation.ElementType;

import

 java.lang.annotation.Retention;

import

 java.lang.annotation.RetentionPolicy;

import

 java.lang.annotation.Target;

/**

 * 結合

@Import

註解,實現註解控制功能模組1是否啟用

 */


@Target

(ElementType.TYPE)

@Retention

(RetentionPolicy.RUNTIME)

@Import

(FunctionOneApplication

.

class

)

public

 @

interfaceEnableFunctionOne

{

}

可見我們定義一個註解類,然後在這個註解類上標註@Import註解,並在其中指定需要匯入的類,比如功能1的掃描起點。
現在我們可以刪除之前主模組中的FunctionImportConfig,而是在主模組啟動類上標註我們這個自定義註解:
啟動專案,可以達到相同的效果:
可見使用這種方式似乎更加地“優雅”了,我們也可以透過是否標註註解,來控制某個功能的開啟或者關閉。
這也說明Spring在掃描註解時是會遞迴解析註解的,當其掃描到讀取到主類的@EnableFunctionOne時,也會讀取到@EnableFunctionOne中的@Import註解,並獲取要匯入的類的資訊,完成匯入。
事實上,大家還可以嘗試將這個@EnableFunctionOne放在別的地方,比如某個配置類上,也可以起到一樣的效果。
基於 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
  • 專案地址:https://github.com/YunaiV/yudao-cloud
  • 影片教程:https://doc.iocoder.cn/video/

3,動態匯入

上面只有在@Import中宣告的類就會被匯入,那麼能不能更加靈活一點控制類的匯入呢?事實上也是可以的。
事實上,@Import中指定的類,可以有三種:
  • @Configuration或者@ComponentScan標註的配置類
  • 實現了ImportSelector介面的類
  • 實現了ImportBeanDefinitionRegistrar介面的類
上面我們只是涉及到了第1種用法,而另外的用法可以透過自定義程式碼的方式,實現自定義的匯入邏輯。
下面,我們就來一一探索一下其它的用法。
現在大家可以新建一個模組2,和模組1一樣只有一個起點類,和一個服務類,並在服務類中透過@PostConstruct在啟動時列印一個訊息。

1) 指定實現了ImportSelector介面的類

ImportSelector介面是Spring中的一個擴充套件介面,用於動態地控制哪些配置類應該被匯入。透過實現ImportSelector介面,我們就可以根據特定的條件或邏輯在執行時決定要匯入的配置類。
這個介面定義了一個方法selectImports,該方法返回一個字串陣列,陣列中就包含了需要匯入的配置類的全限定類名。Spring在載入配置類時會呼叫selectImports方法,並根據方法返回的類名動態地匯入對應的類並初始化為Bean。
我們在主模組中新建一個實現了ImportSelector介面的類如下:
package

 com.gitee.swsk33.mainmodule.selector;

import

 org.springframework.context.annotation.ImportSelector;
的全限定名放在字串資料返回,則會匯入返回的陣列中指定的類

  */

@Override
public

 String[] selectImports(AnnotationMetadata importingClassMetadata) {

// 列印被標註@Import的類的元資料

  System.out.println(

"被標註@Import的類名:"

 + importingClassMetadata.getClassName());

// 直接引入第一個和第二個功能的主類
returnnew

 String[]{

"com.gitee.swsk33.functionone.FunctionOneApplication"

,

"com.gitee.swsk33.functiontwo.FunctionTwoApplication"

  };

 }
}

上述程式碼中,大家可以透過註釋瞭解一下該介面方法及其引數、返回值的意義,這裡我直接返回了字串陣列,其中指定了需要匯入的其它模組的起點類的全限定名。
然後再建立一個配置類使用@Import匯入上述實現了ImportSelector介面的類即可:
package

 com.gitee.swsk33.mainmodule.config;

import

 com.gitee.swsk33.mainmodule.selector.DemoImportSelector;

import

 org.springframework.context.annotation.Configuration;

import

 org.springframework.context.annotation.Import;

/**

 * 指定匯入實現了ImportSelector的類,然後就會根據其中selectImports方法返回值,實現自定義匯入指定類

 */


@Configuration
@Import

(DemoImportSelector

.

class

)

publicclassFunctionImportConfig

{

}

啟動:
可見這一次我們的應用程式初始化時,在載入FunctionImportConfig配置類時,讀取@Import註解,而其中指定的類是一個實現了ImportSelector的類,那麼這時Spring框架就會執行實現了ImportSelector的類中的介面方法selectImports,並獲取其返回值,根據返回值指定的全限定類名引入相關的類,並初始化為Bean。
需要注意的是:
  • 實現ImportSelector介面的類無需標註@Component等註解
  • 介面方法selectImports返回的需要匯入的類,也無需一定要是配置類,而可以是任何標註了@Component等等相關Bean註解的類
大家也可以將上述這個@Import註解進行封裝,實現一個自己的@EnableXXX註解。

2) 指定實現了ImportBeanDefinitionRegistrar介面的類

ImportBeanDefinitionRegistrar介面是Spring中的另一個擴充套件介面,它允許我們在執行時動態地註冊BeanDefinition,從而實現更高階的配置管理。與ImportSelector不同的是,ImportBeanDefinitionRegistrar不僅可以匯入配置類,還可以動態地註冊Bean定義。
ImportBeanDefinitionRegistrar介面定義了一個方法registerBeanDefinitions,該方法接受兩個引數:
  • AnnotationMetadata 用於獲取當前類的註解資訊
  • BeanDefinitionRegistry 用於註冊Bean定義
透過實現ImportBeanDefinitionRegistrar介面,我們就可以根據特定的條件或邏輯在執行時註冊Bean定義,從而實現更加靈活和動態的配置管理。
現在,我們在主模組建立一個實現了ImportBeanDefinitionRegistrar介面的類如下:
package

 com.gitee.swsk33.mainmodule.selector;

import

 com.gitee.swsk33.functionone.FunctionOneApplication;

import

 com.gitee.swsk33.functiontwo.FunctionTwoApplication;

import

 org.springframework.beans.factory.support.BeanDefinitionRegistry;

import

 org.springframework.beans.factory.support.GenericBeanDefinition;

import

 org.springframework.context.annotation.ImportBeanDefinitionRegistrar;

import

 org.springframework.core.type.AnnotationMetadata;

/**

 * 實現ImportBeanDefinitionRegistrar介面後,可在其中使用自定義的邏輯,實現動態地將對應類註冊為Bean

 */


publicclassDemoImportSelectorimplementsImportBeanDefinitionRegistrar

{

/**

  * 定義一個自定義邏輯,在其中可以動態地將對應的類註冊為Bean

  *

  * 

@param

 importingClassMetadata 標註了

@Import

註解的類的元資料

  * 

@param

 registry               用於將指定類註冊到IoC容器

  */


@Override
publicvoidregisterBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry)

{

// 註冊兩個功能模組中標註了@ComponentScan的類為Bean
// 定義一個Bean定義物件,傳入第一個模組的@ComponentScan配置類

  GenericBeanDefinition functionOneScanBean = 

new

 GenericBeanDefinition();

  functionOneScanBean.setBeanClass(FunctionOneApplication

.class)

;

// 表示第二個模組的Bean定義物件

  GenericBeanDefinition functionTwoScanBean = 

new

 GenericBeanDefinition();

  functionTwoScanBean.setBeanClass(FunctionTwoApplication

.class)

;

// 將兩個定義物件進行註冊,這樣上述兩個類就會被註冊為Bean

  registry.registerBeanDefinition(

"functionOneComponentScan"

, functionOneScanBean);

  registry.registerBeanDefinition(

"functionTwoComponentScan"

, functionTwoScanBean);

 }
}

可見這個類和上述實現了ImportSelector介面的類作用一樣,都是自定義匯入其它類的邏輯,不過方式不一樣,首先我們建立GenericBeanDefinition例項,並指定需要匯入的類,然後藉助BeanDefinitionRegistry引數傳入我們的GenericBeanDefinition例項,實現將對應的類匯入並註冊為Bean。
同樣地,現在只需要再在某個配置類中使用@Import指定這個實現了ImportBeanDefinitionRegistrar介面的類即可:
package

 com.gitee.swsk33.mainmodule.config;

import

 com.gitee.swsk33.mainmodule.selector.DemoImportSelector;

import

 org.springframework.context.annotation.Configuration;

import

 org.springframework.context.annotation.Import;

/**

 * 指定匯入實現了ImportSelector的類,然後就會根據其中selectImports方法返回值,實現自定義匯入指定類

 */


@Configuration
@Import

(DemoImportSelector

.

class

)

publicclassFunctionImportConfig

{

}

啟動:
可見這和ImportSelector大同小異,過程當然也是差不多的:初始化配置類FunctionImportConfig時,讀取到@Import註解中指定的類,並執行該類介面方法registerBeanDefinitions,完成對Bean的註冊。

4,結合Spring Bean條件註解

除了實現對應的介面來實現自定義的匯入邏輯之外,事實上我們還可以藉助Spring Bean的條件註解來透過配置或者其它方式來控制@Import是否觸發生效。
比如說在標註@Import的配置類上使用@ConditionalOnProperty註解:
package

 com.gitee.swsk33.mainmodule.config;

import

 com.gitee.swsk33.functionone.FunctionOneApplication;

import

 org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;

import

 org.springframework.context.annotation.Configuration;

import

 org.springframework.context.annotation.Import;

/**

 * 用於匯入其它模組的配置,使得其它模組中的Bean也能夠交給IoC託管

 * 還可以藉助Spring的條件註解例如

@ConditionalOnProperty

,實現透過配置或者其它條件動態控制這個配置類是否載入,進而實現控制

@Import

是否生效

 */


@Configuration
@Import

(FunctionOneApplication

.

class

)

@

ConditionalOnProperty

(

prefix

"com.gitee.swsk33.function-one"

, name = 

"enabled"

)

publicclassFunctionImportConfig

{

}

@ConditionalOnProperty註解可以用來根據配置檔案條件,控制某個類是否被初始化為Bean,例如上述註解配置表示:配置檔案中必須存在配置項com.gitee.swsk33.function-one.enabled且其值必須為true時,這個配置類FunctionImportConfig才會被載入並例項化為Bean,只有這樣@Import才會被讀取到,進而觸發匯入。
這時,就可以透過application.properties控制是否匯入功能模組1了:

# 開啟功能

1

com.gitee.swsk33.function-one.enabled=

true

事實上,Spring提供了好幾個能夠控制Bean是否載入和例項化的條件註解,我們可以使用這些註解設定一些條件,使得Bean可以根據我們的條件來決定是否載入並例項化。

5,總結

可見藉助@Import註解,可以很方便地實現自定義匯入對應的配置類,甚至是新增掃描起點,這對於我們Spring Boot模組化開發是一個有利的工具。也可見該註解功能非常強大,可以透過實現對應介面方法,完成靈活地自定義匯入。
大家需要理解@Import註解具體作用是什麼,以及該註解可以傳入哪些類作為引數,以及其大致的工作流程。

歡迎加入我的知識星球,全面提升技術能力。
👉 加入方式,長按”或“掃描”下方二維碼噢
星球的內容包括:專案實戰、面試招聘、原始碼解析、學習路線。
文章有幫助的話,在看,轉發吧。
謝謝支援喲 (*^__^*)

相關文章