文章背景
有一個封裝 RocketMq 的 client 的需求,用來提供給各專案收、發訊息,但是專案當中常常只使用收或者發訊息的單一功能,而且不同的專案 group
等並不相同而且不會變化,可以在專案當中配置,其餘的 topic
等配置資訊因有變動則遷移到配置中心去,因此萌生了如下想法
提供一個自定義註解來啟用收、發訊息其中之一或者全部的公共元件
研究之後,決定採用 @Import
來實現該功能
一、Java註解的簡單介紹
註解,也叫Annotation
、標註,是 Java 5 帶來的新特性。
-
可使用範圍
類、欄位、方法、引數、建構函式、包等,具體可參閱列舉類
java.lang.annotation.ElementType
-
生命週期(摘自 劉大飛的部落格 )
RetentionPolicy.SOURCE
註解只保留在原始檔,當Java檔案編譯成class檔案的時候,註解被遺棄RetentionPolicy.CLASS
註解被保留到class檔案,但 jvm 載入class檔案時候被遺棄,這是預設的生命週期RetentionPolicy.RUNTIME
註解不僅被儲存到class檔案中,jvm 載入class檔案之後,仍然存在
-
使用方式
可以使用反射獲取註解的內容,具體如何使用請自己百度,可參考這篇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的靈活構建,如果你有更好更優雅的方式,歡迎留言指教。