Spring Boot中@Import三種使用方式!

雨點的名字發表於2022-11-22
Spring Boot中@Import三種使用方式!

需要注意的是:ImportSelector、ImportBeanDefinitionRegistrar這兩個介面都必須依賴於@Import一起使用,而@Import可以單獨使用。

@Import是一個非常有用的註解,它的長處在於你可以透過配置來控制是否注入該Bean,也可以透過條件來控制注入哪些Bean到Spring容器中。

比如我們熟悉的:@EnableAsync@EnableCaching@EnableScheduling等等統一採用的都是藉助@Import註解來實現的。

下面我們就透過示例來了解@Import三種用法!

一、引入普通類

有個使用者類如下

@Data
public class UserConfig {  
    /** 使用者名稱*/
    private String username;

    /**手機號*/
    private String phone;
}

那麼如何透過@Import注入容器呢?

@Import(UserConfig.class)
@Configuration
public class UserConfiguration { 
}

當在@Configuration標註的類上使用@Import引入了一個類後,就會把該類注入容器中。

當然除了@Configuration 比如@Component、@Service等一樣也可以。

測試

@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTest {

    @Autowired
    private UserConfig userConfig;
    
    @Test
    public void getUser() {
        String name = userConfig.getClass().getName();
        System.out.println("name = " + name);
    }
}

控制檯輸出

name = com.jincou.importselector.model.UserConfig

如果@Import的功能僅僅是這樣,那其實它並沒什麼特別的價值,我們可以透過其它方式實現?

@Configuration
public class UserConfiguration {

    @Bean
    public UserConfig userConfig() {
        return new UserConfig();
    }   
}

再比如直接新增@Configuration註解

@Configuration
public class UserConfig {
  // ......
}

確實如果注入靜態的Bean到容器中,那完全可以用上面的方式代替,但如果需要動態的帶有邏輯性的注入Bean,那才更能體現@Import的價值。


二、引入ImportSelector的實現類

說到ImportSelector這個介面就不得不說這裡面最重要的一個方法:selectImports()

public interface ImportSelector {

	String[] selectImports(AnnotationMetadata importingClassMetadata);
}

這個方法的返回值是一個字串陣列,只要在配置類被引用了,這裡返回的字串陣列中的類名就會被Spring容器new出來,然後再把這些物件注入IOC容器中。

所以這有啥用呢?我們還是用一個例子演示一下。

1、靜態import場景(注入已知的類)

我們先將上面的示例改造下:

自定義MyImportSelector實現ImportSelector介面,重寫selectImports方法

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        //這裡目的是將UserConfig 注入容器中
        return new String[]{"com.jincou.importselector.model.UserConfig"};
    }
}

然後在配置類引用

@Import(MyImportSelector.class)
@Configuration
public class UserConfiguration {

}

這樣一來同樣可以透過成功將UserConfig注入容器中。

如果看到這,你肯定會有疑問。我這又是新建MyImportSelector類,又是實現ImportSelector重寫selectImports方法,然後我這麼做有個卵用呢?

直接把類上加個@Component注入進去不香嗎?這個ImportSelector把簡單的功能搞這麼複雜。

接下來就要說說如何動態注入Bean了。

2、動態import場景(注入指定條件的類)

我們來思考一種場景,就是你想透過開關來控制是否注入該Bean,或者說透過配置來控制注入哪些Bean,這個時候就有了ImportSelector的用武之地了。

我們來舉個例子,透過ImportSelector的使用實現條件選擇是注入本地快取還是Redis快取

1)、定義快取介面和實現類

頂層介面

public interface CacheService {
    
    void setData(String key);
}

本地快取 實現類

public class LocalServicempl implements CacheService {
    
    @Override
    public void setData(String key) {
        System.out.println("本地儲存儲存資料成功 key= " + key); 
    }
}

redis快取實現類

public class RedisServicempl implements CacheService {

    @Override
    public void setData(String key) {
        System.out.println("redis儲存資料成功 key= " + key); 
    }
}

2)、定義ImportSelector實現類

以下程式碼中根據EnableMyCache註解中的不同值來切換快取的實現類再spring中的註冊。

public class MyCacheSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(EnableMyCache.class.getName());
        //透過 不同type注入不同的快取到容器中
        CacheType type = (CacheType) annotationAttributes.get("type");
        switch (type) {
            case LOCAL: {
                return new String[]{LocalServicempl.class.getName()};
            }
            case REDIS: {
                return new String[]{RedisServicempl.class.getName()};
            }
            default: {
                throw new RuntimeException(MessageFormat.format("unsupport cache type {0}", type.toString()));
            }
        }
    }
}

3)、定義註解

@EnableMyCache註解就像一個開關,透過這個開關來是否將特定的Bean注入容器。

定義一個列舉

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(MyCacheSelector.class)
public @interface EnableMyCache {
    CacheType type() default CacheType.REDIS;
}
public enum CacheType {
    LOCAL, REDIS;
}

4)、測試

這裡選擇本地快取。

@EnableMyCache(type = CacheType.LOCAL)
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTest {

    @Autowired
    private CacheService cacheService;

    @Test
    public void test() {
        cacheService.setData("key");
    }
}

控制檯輸出

本地儲存儲存資料成功 key= key

切換成redis快取

@EnableMyCache(type = CacheType.REDIS)
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTest {

    @Autowired
    private CacheService cacheService;

    @Test
    public void test() {
        cacheService.setData("key");
    }
}

控制檯輸出

redis儲存資料成功 key= key

這個示例不是就是Bean的動態注入了嗎?

3、Spring如何使用ImportSelector的場景

SpringBoot有兩個常用註解 @EnableAsync @EnableCaching 其實就是透過ImportSelector來動態注入Bean

看下@EnableAsync註解,它有透過@Import({AsyncConfigurationSelector.class})

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AsyncConfigurationSelector.class})
public @interface EnableAsync {
    Class<? extends Annotation> annotation() default Annotation.class;

    boolean proxyTargetClass() default false;

    AdviceMode mode() default AdviceMode.PROXY;

    int order() default 2147483647;
}

AsyncConfigurationSelector.class

public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> {
    private static final String ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME = "org.springframework.scheduling.aspectj.AspectJAsyncConfiguration";

    public AsyncConfigurationSelector() {
    }

    @Nullable
    public String[] selectImports(AdviceMode adviceMode) {
        switch(adviceMode) {
        case PROXY:
            return new String[]{ProxyAsyncConfiguration.class.getName()};
        case ASPECTJ:
            return new String[]{"org.springframework.scheduling.aspectj.AspectJAsyncConfiguration"};
        default:
            return null;
        }
    }
}

是不是和我上面寫的示例一樣。

總之,向這種還不能決定去注入哪個處理器(如果你能決定,那就直接@Import那個類好了,沒必要實現介面了),就可以實現此介面,寫出一些判斷邏輯,不同的配置情況注入不同的處理類。


三、引入ImportBeanDefinitionRegister的實現類

當配置類實現了 ImportBeanDefinitionRegistrar 介面,你就可以自定義往容器中註冊想注入的Bean。

這個介面相比與 ImportSelector 介面的主要區別就是,ImportSelector介面是返回一個類,你不能對這個類進行任何操作,但是 ImportBeanDefinitionRegistrar 是可以自己注入 BeanDefinition,可以新增屬性之類的。

public class MyImportBean implements ImportBeanDefinitionRegistrar {

    /**
     * @param importingClassMetadata 當前類的註解資訊
     * @param registry               註冊類,其registerBeanDefinition()可以註冊bean
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

    }
}

1、舉一個簡單的示例

我們透過先透過一個簡單的小示例,來理解它的基本使用

假設有個使用者配置類如下

@Data
public class UserConfig {
    /** 使用者名稱*/
    private String username;
    /**手機號*/
    private String phone;
}

我們透過實現ImportBeanDefinitionRegistrar的方式來完成注入。

public class MyImportBean implements ImportBeanDefinitionRegistrar {

    /**
     * @param importingClassMetadata 當前類的註解資訊
     * @param registry               註冊類,其registerBeanDefinition()可以註冊bean
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //構建一個 BeanDefinition , Bean的型別為 UserConfig,這個Bean的屬性username的值為後端元宇宙
        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserConfig.class)
                .addPropertyValue("username", "後端元宇宙")
                .getBeanDefinition();
        //把 UserConfig 這個Bean的定義註冊到容器中
        registry.registerBeanDefinition("userConfig", beanDefinition);
    }
}

透過配置類 中引入MyImportBean物件。

@Import(MyImportBean.class)
@Configuration
public class UserImportConfiguration {

}

我們再來測試下

@EnableMyCache(type = CacheType.REDIS)
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTest {

    @Autowired
    private UserConfig userConfig;

    @Test
    public void test() {
        String username = userConfig.getUsername();
        System.out.println("username = " + username);
    }
}

控制檯輸出

username = 後端元宇宙

說明透過ImportBeanDefinitionRegistrar方式,已經把UserConfig注入容器成功,而且還為給bean設定了新屬性。

然後我們再來思考一個問題,就比如我們在其它地方已經將UserConfig注入容器,這裡會不會出現衝突,或者不衝突的情況下,屬效能不能設定成功?

我們來試下

@Import(MyImportBean.class)
@Configuration
public class UserImportConfiguration {

    /**
     * 這裡透過@Bean註解,將UserConfig注入Spring容器中,而且名稱也叫userConfig
     */
    @Bean
    public UserConfig userConfig() {
        return new UserConfig();
    }
}

然後我們再來跑下上面的測試用例,發現報錯了。

Spring Boot中@Import三種使用方式!

2、舉一個複雜點的例子

Mybatis的@MapperScan就是用這種方式實現的,@MapperScan註解,指定basePackages,掃描Mybatis Mapper介面類注入到容器中。

這裡我們自定義一個註解@MyMapperScan來掃描包路徑下所以帶@MapperBean註解的類,並將它們注入到IOC容器中。

1)、先定義一個@MapperBean註解,就相當於我們的@Mapper註解

/**
 * 定義包路徑。(指定包下所有新增了MapperBean註解的類作為bean)
 * 注意這裡 @Import(MyMapperScanImportBean.class) 的使用
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface MapperBean {
}

2)、一個需要注入的bean,這裡加上@MapperBean註解。

package com.jincou.importselector.mapperScan;
import com.jincou.importselector.config.MapperBean;

@MapperBean
public class User {
}

3)、再定一個掃描包路徑的註解@MyMapperScan 就相當於mybatis的@MapperScan註解。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MyMapperScanImportBean.class)
public @interface MyMapperScan {

   /**
    * 掃描包路徑
    */
    String[] basePackages() default {};
}

4)、MyMapperScanImportBean實現ImportBeanDefinitionRegistrar介面

public class MyMapperScanImportBean implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {

    private final static String PACKAGE_NAME_KEY = "basePackages";
    private ResourceLoader resourceLoader;
    
    /**
     * 搜尋指定包下所有新增了MapperBean註解的類,並且把這些類新增到ioc容器裡面去
     * 
     * @param importingClassMetadata 當前類的註解資訊
     * @param registry               註冊類,其registerBeanDefinition()可以註冊bean
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //1. 從BeanIocScan註解獲取到我們要搜尋的包路徑
        AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MyMapperScan.class.getName()));
        if (annoAttrs == null || annoAttrs.isEmpty()) {
            return;
        }
        String[] basePackages = (String[]) annoAttrs.get(PACKAGE_NAME_KEY);
        // 2. 找到指定包路徑下所有新增了MapperBean註解的類,並且把這些類新增到IOC容器裡面去
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, false);
        scanner.setResourceLoader(resourceLoader);
        //路徑包含MapperBean的註解的bean
        scanner.addIncludeFilter(new AnnotationTypeFilter(MapperBean.class));
        //掃描包下路徑
        scanner.scan(basePackages);
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }
}

5)測試

這裡掃描的路徑就是上面User實體的位置

@RunWith(SpringRunner.class)
@SpringBootTest
@MyMapperScan(basePackages = {"com.jincou.importselector.mapperScan"})
public class UserServiceTest {

    @Autowired
    private User user;

    @Test
    public void test() {
        System.out.println("username = " + user.getClass().getName());
    }
}

執行結果

username = com.jincou.importselector.mapperScan.User

完美,成功!

實現它的基本思想是:當自己需要操作BeanFactory裡面的Bean的時候,那就必須只有它才能做到了。而且它還有個方便的地方,那就是做包掃描的時候,比如@MapperScan類似這種的時候,用它處理更為方便(因為掃描到了直接註冊即可)



宣告: 公眾號如需轉載該篇文章,發表文章的頭部一定要 告知是轉至公眾號: 後端元宇宙。同時也可以問本人要markdown原稿和原圖片。其它情況一律禁止轉載!

相關文章