Spring中如何使用自定義注解搭配@Import引入內外部配置並完成某一功能的啟用


有些網站第一時間爬取了我的原創文章,並且沒有注明出處,不得已在這里加上說明。

轉載請注明出處:來自博客園-去哪里吃魚 https://www.cnblogs.com/qnlcy/p/15012443.html

文章背景

有一個封裝 RocketMq 的 client 的需求,用來提供給各項目收、發消息,但是項目當中常常只使用收或者發消息的單一功能,而且不同的項目 group 等並不相同而且不會變化,可以在項目當中配置,其余的 topic 等配置信息因有變動則遷移到配置中心去,因此萌生了如下想法

提供一個自定義注解來啟用收、發消息其中之一或者全部的公共組件

研究之后,決定采用 @Import 來實現該功能

一、Java注解的簡單介紹

注解,也叫Annotation、標注,是 Java 5 帶來的新特性。

  1. 可使用范圍

    類、字段、方法、參數、構造函數、包等,具體可參閱枚舉類 java.lang.annotation.ElementType

  2. 生命周期(摘自 劉大飛的博客

    • RetentionPolicy.SOURCE 注解只保留在源文件,當Java文件編譯成class文件的時候,注解被遺棄
    • RetentionPolicy.CLASS 注解被保留到class文件,但 jvm 加載class文件時候被遺棄,這是默認的生命周期
    • RetentionPolicy.RUNTIME 注解不僅被保存到class文件中,jvm 加載class文件之后,仍然存在
  3. 使用方式

    可以使用反射獲取注解的內容,具體如何使用請自己百度,可參考這篇Java注解完全解析,這里不是重點,不多做介紹

二、Spring的 @Import 注解

@Import 注解是Spring用來注入 Spring Bean 的一種方式,可以用來修飾別的注解,也可以直接在Springboot配置類上使用。

它只有一個value屬性需要設置,來看一下源碼

public @interface Import {
    Class<?>[] value();
}

這里的 value屬性只接受三種類型的Class:

  • @Configuration 修飾的配置類
  • 接口 org.springframework.context.annotation.ImportBeanDefinitionRegistrar 的實現類
  • 接口 org.springframework.context.annotation.ImportSelector 的實現類

下面針對三種類型的 Class 分別做簡單介紹,文章后面有自定義注解與外部配置的結合使用方式。

三、被 @Configuration 修飾的配置類

這種類可以像 Springboot 中的配置類一樣使用,需要注意的是,如果該類的包路徑已在Springboot啟動類上配置的掃描路徑下,則不需要再重新使用 @Import 導入了,因為 @Import 的目的是注入bean,Springboot 啟動類上的 @SpringBootApplication 注解已經自動掃描、注入你想通過@Import 導入的bean了。

這種Class可以進行如下拓展

  • 繼承各種 Aware 接口, 獲取對應的信息(如果不清楚 Aware 接口在Spring當中的作用,請自行百度),如,繼承 EnviromentAware,可以拿到Spring的環境配置信息,進而從中拿到 @Value 所需要的值,如 environment.getProperty("user.username")
  • 使用 @Autowire@Resource@Value 注入各種所需 Spring 資源
  • 使用 @Bean 聲明各種 Spring 資源
  • 像普通 Spring Bean 一樣使用該類

更多使用方式,請自行百度。

本案例當中,使用這種配置類用來導入外部配置(使用 @Value 的形式)。

四、接口org.springframework.context.annotation.ImportBeanDefinitionRegistrar的實現類

當實現類的 Class 傳入 @Import 注解的時候,就會調用該類對應的方法注入相應的 BeanDefinition 信息,方便后面獲取 bean 時候使用。我們可以在此定義我們要注入 Spring 的 bean 的屬性,這里的屬性信息參數來源於自定義注解當中傳來的值。

來看一下接口定義

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

    /**
    * importingClassMetadata: 被@Import修飾的 自定義注解 的元信息,可以獲得屬性集合
    * registry:               Spring bean注冊中心
    **/
    default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    }

通過這種方式,我們可以根據自定義注解配置的屬性值來注入Spring Bean 信息。

五、接口org.springframework.context.annotation.ImportSelector的實現類

首先看一下接口

public interface ImportSelector {

    /**
     * importingClassMetadata 注解元信息,可獲取自定義注解的屬性集合
     * 根據自定義注解的屬性,或者沒有屬性,返回要注入Spring的Class全限定類名集合
     如:XXX.class.getName(),Spring會自動注入XXX的一個實例
     */
    String[] selectImports(AnnotationMetadata importingClassMetadata);

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

}

這個接口的實現類如果沒有進行@Aware拓展,功能比較單一,因為我們無法參與Spring Bean 的構建過程,只是告訴Spring 要注入的Bean的名字。不再詳述。

六、案例

來看如下案例,我們通過一個注解,啟動RocketMq的消息發送器:

@SpringBootApplication
@EnableMqProducer(group="xxx")
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class);
    }
}

這是一個服務項目的啟動類,這個服務開啟了RocketMq的一個發送器,並且分到xxx組里。

來看一下@EnableMqProducer注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({XXXRegistrar.class,XXXConfig.class})
public @interface EnableMqProducer {

    String group() default "DEFAULT_PRODUCER_GROUP";

    String instanceName() default "defaultProducer";

    boolean retryAnotherBrokerWhenNotStoreOK() default true;
}

這里使用@Import導入了兩個配置類,第一個是接口org.springframework.context.annotation.ImportBeanDefinitionRegistrar的實現類,第二個是被@Configuration 修飾的配置類

我們看第一個類,這個類注入了一個 DefaultMQProducer 的實例到Spring 容器中,使業務方可以直接通過@Autowired注入使用

public class XXXRegistrar implements ImportBeanDefinitionRegistrar {  
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        AnnotationAttributes attributes = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(EnableMqProducer.class.getName()));
        registerBeanDefinitions(attributes, registry);
    }

    private void registerBeanDefinitions(AnnotationAttributes attributes, BeanDefinitionRegistry registry) {
        //獲取配置
        String group = attributes.getString("group");
        //省略部分代碼...
        //添加要注入的類的字段值
        Map<String, Object> values = new HashMap<>();
        //這里有的同學可能不清楚為什么key是這個
        //這里的key就是DefaultMQProducer類的字段名
        values.put("producerGroup", group);
        //省略部分代碼

        //注冊到Spring中
        BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, DefaultMQProducer.class.getName(), DefaultMQProducer.class, values);
    }

到這里,我們已經注入了一個DefaultMQProducer的實例到Spring容器中,但是這個實例,還不完整,比如,還沒有啟動,nameServer地址還沒有配置,可外部配置的屬性還沒有覆蓋實例已有的值(nameServer地址建議外部配置)。好消息是,我們已經可以通過注入來使用這個實例了。

上面遺留的問題,就是第二個類接下來要做的事。

來看第二個配置類

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@EnableConfigurationProperties(XxxProperties.class)  //Spring提供的配置自動映射功能,配置后可直接注入
public class XXXConfig {

    @Resource //直接注入外部配置,可能來源於外部配置文件、配置中心、啟動參數
    private XxxProperties XxxProperties;

    @Autowired //注入上一步生成的實例
    private DefaultMQProducer producer;

    @PostConstruct
    public void init() {
        //省略部分代碼
        //獲取外部配置的值
        String nameServer = XxxProperties.getNameServer();
        //修改實例
        producer.setNamesrvAddr(nameServer);
        //啟動實例
        try {
            this.producer.start();
        } catch (MQClientException e) {
            throw new RocketMqException("mq消息發送實例啟動失敗", e);
        }
    }

    @PreDestroy
    public void destroy() {
        producer.shutdown();
    }

到這里,通過自定義注解和外部配置的結合,一個完整的消息發送器就可以使用了,但方式有取巧之嫌,因為在消息發送器啟動之前,不知道還有沒有別的類使用了這個實例,這是不安全的。

七、總結

通過接口和配置類的靈活結合,可以實現基於自定義注解結合內外配置化的設計,歸根到底是Spring Bean的靈活構建,如果你有更好更優雅的方式,歡迎留言指教。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM