前言
原文地址:https://www.cnblogs.com/qnlcy/p/15905682.html
一、宗旨
在如日中天的 Spring 架構體系下,不管是什麼樣的元件,不管它採用的接入方式如何眼花繚亂,它們永遠只有一個目的:
接入Spring容器
二、Spring 容器
Spring 容器內可以認為只有一種東西,那就是 bean
,但是圍繞 bean
的生命週期,Spring 新增了許多東西
2.1 bean
的生命週期
2.1.1 例項化 bean 例項
例項化 bean 例項是 spring 針對 bean 作的擴充最多的週期
它包括:
- bean 的掃描
- bean 的解析
- bean 例項化
常見掃描相關內容:
@Component
、@Service
、@Controller
、@Configuration
、applicationContext.xml
spring/springboot 在啟動的時候,會掃描到這些註解或配置檔案修飾的類資訊
根據拿到的類資訊,經過第二步解析後,轉換成 BeanDefintion
存入到 spring 容器當中,BeanDefintion
描述 bean 的 class、scop、beanName 等資訊
在 bean 的解析過程中,我們常用到的 Properties 讀取 、 @Configuration
配置類的處理 會在這一步完成
bean 的例項化實際有自動完成和呼叫 getBean()
時候完成,還有容器初始化完畢之後例項化 bean ,他們都是根據 bean 的定義 BeanDefintion
來反射目標 bean 類,並放到 bean 容器當中
這就是大名鼎鼎的 bean 容器,就是一個 Map
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
2.1.2 設定例項屬性
這一階段是 @Value
、@Autowired
、@Resource
註解起作用的階段
2.1.3 bean 前置處理
BeanPostProcessor
前置處理方法
2.1.4 bean init 處理
@PostConstruct
註解起作用的階段
2.1.5 bean 後置處理
BeanPostProcessor
後置處理方法
2.1.6 正常使用
2.1.7 bean 銷燬
@PreDestroy
註解起作用的階段
bean
的銷燬過程中,主要的作用就是釋放一些需要手動釋放的資源和一些收尾工作,如檔案歸併、連線池釋放等
在瞭解了 Spring bean 的生命週期後,我們接下來介紹自建 Spring 元件的接入方式
三、使用簡單配置類接入方式
使用配置類接入 Spring ,一般需要搭配 PostConstruct
來使用,並且要確保 Spring 能掃描到配置
如,在元件 quartz-configable
1.0 版本當中,就是使用的這種方式
quartz-configable
需要掃描使用者自定義的 job
來註冊到 quartz-configable
自動建立的排程器 Scheduler
當中,並啟動排程器 Scheduler
在註冊 Job
的過程當中,又新增了自定義的 TriggerListener
監聽器,來監聽配置的變動,以動態調整 Job
執行時機
@Configuration
public class QuartzInitConfig {
@Autowired
private Scheduler scheduler;
@Autowired
private CustomTriggerListener customTriggerListener;
@PostConstruct
public void init() {
//先把所有jobDetail放到map裡
initJobMap();
//新增自定義Trigger監聽器,進行任務開關的監聽和故障定位的配置
addTriggerListener(scheduler, customTriggerListener);
//新增任務到任務排程器中
addJobToScheduler(scheduler);
//啟動任務排程器
try {
scheduler.start();
} catch (SchedulerException e) {
log.error("任務排程器啟動失敗", e);
throw new RuntimeException("任務排程器啟動失敗");
}
log.info("任務排程器已啟動");
}
private void initJobMap() {
//省略部分程式碼
}
private void addJobToScheduler(Scheduler scheduler) {
//省略部分程式碼
}
private void addTriggerListener(Scheduler scheduler, CustomTriggerListener customTriggerListener) {
//省略部分程式碼
}
}
QuartzInitConfig
類的作用是把掃描到的任務類放入排程器當中,並新增自定義監聽(用於動態修改 cron 表示式)
此類載入有兩個過程:
- 注入元件初始化需要的資源
- 根據注入的資源初始化元件
步驟 1
所需要的功能與 Spring 的注入功能完美契合,而恰好 @Configuration
修飾的類也被當作了一個 Spring bean
,所以才能順利注入元件需要的資源
步驟 2
的初始化任務,極為契合 Spring bean
建立完畢後的初始化動作 @PostConstruct
當中,它同樣是資源注入完畢後的初始化動作。
四、帶有條件的簡單配置類
有時候,我們希望通過開關或者特定的配置來啟用應用內具備的功能,這時候,我們可以使用 @ConditionalOnProperty
來解決問題
risk
元件掃描出符合規則的切點,在切點執行之前,去執行傳送風控資料到風控平臺的動作
@Configuration
@ConditionalOnProperty({"risk.expression", "risk.appid", "risk.appsecret", "risk.url"})
public class RiskAspectConfig {
//專案內配置
@Value("${risk.expression}")
private String riskExpression;
@Bean
public DefaultPointcutAdvisor defaultPointcutAdvisor() {
SpringBeans springBeans = springBeans();
RiskSenderDelegate riskSenderDelegate = new RiskSenderDelegate(springBeans);
GrjrMethodInterceptor grjrMethodInterceptor = new GrjrMethodInterceptor(riskSenderDelegate);
JdkRegexpMethodPointcut jdkRegexpMethodPointcut = new JdkRegexpMethodPointcut();
jdkRegexpMethodPointcut.setPattern(riskExpression);
log.info("切面準備完畢,切點為{}", riskExpression);
return new DefaultPointcutAdvisor(jdkRegexpMethodPointcut, grjrMethodInterceptor);
}
//省略部分程式碼
}
雖然類 RiskAspectConfig
是一個 Spring 配置類,方法 defaultPointcutAdvisor()
建立了一個切點顧問,用來在切點方法處實現風控的功能,但是,並不是應用啟動之後,切點就會生效,這是因為有 @ConditionalOnProperty
的存在
@ConditionalOnProperty
的作用:
根據提供的條件判斷對應的屬性是否存在,存在,則載入此配置類,不存在,則忽略。
當應用中存在如下配置時:
grjr:
risk:
expression: xxxx
appid: xxx
appsecret: xxx
url: xxx
RiskAspectConfig
配置類才會被載入,才會生成切點顧問 DefaultPointcutAdvisor
,因此切點就會生效
當需要的配置逐漸增多的時候,一條條新增進 @ConditionalOnProperty
顯得比較冗長複雜,這時候該如何處理呢?
五、使用對應的 Properties 配置類來封裝配置
在專案 fastdfs-spring-boot-starter
當中,像上述需要的配置有很多,那麼它是怎麼處理的呢?
它把需要的配置放到了一個 Java 類裡
@ConfigurationProperties(prefix = "fastdfs.boot")
public class FastDfsProperties {
private String trackerServerHosts;
private int trackerHttpPort = 80;
private int connectTimeout = 5000;
private int networkTimeout = 30000;
private boolean antiStealToken = false;
private String charset = "ISO8859-1";
private String secretKey;
//省略欄位 get set 方法
}
其中, @ConfigurationProperties
指定了配置的 prefix
,上述配置相當於
fastdfs:
boot:
trackerServerHosts: xxx
trackerHttpPort: 80
connectTimeout: 5000
networkTimeout: 30000
antiStealToken: false
charset: ISO8859-1
secretKey: xxx
這種類到現在為止還不可以和 Spring 結合起來,尚需要把它宣告為 Spring bean
才生效
宣告為 Spring bean
有兩種形式
- 在類本身上新增
@Component
註解,標識這是一個Spring bean
- 在
@Configuration
類上使用@EnableConfigurationProperties
來啟用配置
通常的,在開發元件的時候,我們使用第二種方式,把 Properties 的啟用,交給 @Configuration
配置類來管理,大家可以想想為什麼
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(FastDfsClient.class) // 當 Spring 容器中不存在 FastDfsClient 時才載入這個類
@EnableConfigurationProperties(FastDfsProperties.class) //啟用上面的 FastDfsProperties
public class FastDfsAutoConfiguration {
/**
* 建立 FastDfsClient 放到 Spring 容器當中
*/
@Bean
@ConditionalOnProperty("fastdfs.boot.trackerServerHosts")
FastDfsClient fastDFSClient(FastDfsProperties fastDfsProperties) {
globalInit(fastDfsProperties);
return new FastDfsClient();
}
/**
* 根據 properties 來配置 fastdfs
*/
private void globalInit(FastDfsProperties fastDFSProperties) {
// 省略部分程式碼
}
//省略部分程式碼
}
@EnableConfigurationProperties(FastDfsProperties.class)
啟用了括號內的 Properties 類,並把它們注入到 Spring 容器當中,使其可以被其他 Spring bean
匯入
六、使用 META-INF/spring.factories 檔案來代替掃描
有時候,我們開發的元件的類路徑和應用的類路徑不同,比如,應用類路徑常常為 com.xxx.xxx
,而元件的類路徑常常為 com.xxx.yyy
,這時候,經常需要為 Spring 指定掃描路徑,才能把我們的元件載入進去,如果在自己專案當中載入上述 quartz-configable
元件,元件類路徑為 com.xxx.yyy
:
@ComponentScan({"com.xxx.xxx", "com.xxx.yyy"})
@SpringBootApplication
public class GrjrFundBatch {
public static void main(String[] args) {
SpringApplication.run(GrjrFundBatch.class);
}
}
如果新增了類似這樣的 quartz-configable
元件,就需要改動 @ComponentScan
程式碼,這對啟動類是有侵入性的,也是繁瑣的,也極有可能寫錯,當元件路徑有改動的時候也需要跟著改動
怎樣避免這種硬編碼形式的注入呢?
Springboot 在載入類的時候,會掃描 classpath
下的 META-INF/spring.factories
檔案,當發現了 spring.factories
檔案後,根據檔案中的配置來載入類
其中一項配置為 org.springframework.boot.autoconfigure.EnableAutoConfiguration=xxx.xxx.xxx.xxxx
,它宣告瞭 Springboot 要載入的自動配置類,Springboot根據配置自動去載入配置類
借用這個規則,現在來升級我們的 quartz-configable
元件
我們在元件專案 resources
目錄下新增 META-INF/spring.factories
檔案,檔案內容如下
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.grjr.quartz.config.GjSchedulerAutoConfiguration
然後在應用啟動類當中刪除已經無用的 @Component
註解即可
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
此時,quartz-configable
依然能生效
使用 META-INF/spring.factories
雖然帶來了簡潔和便利,但是它總是去自動載入配置類,所以我們在設計元件的時候,一定要搭配 @ConditionOnxxxx
註解,有條件的去載入我們的元件
七、使用自定義 @EnableXxxx 註解的形式開啟元件功能
就像上面說的一樣,使用 META-INF/spring.factories
總會去載入配置類,自定義掃描路徑有可能會寫錯類路徑,那麼,還有沒有其他方式呢?
有,使用自定義註解來注入自己的元件,就像 dubbo
的 starter 元件一樣,我們自己造一個 @EnableXxx
註解
7.1 自定義註解的核心
自定義註解的核心是 Spring 的 @Import
註解,它基於 @Import
註解來注入元件自身需要的資源和初始化元件自身
7.2 @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 分別做簡單介紹,中間穿插自定義註解與外部配置的結合使用方式。
7.2.1 被@Configuration
修飾的配置類
像 Springboot 中的配置類一樣正常使用,需要注意的是,如果該類的包路徑已在 Springboot 啟動類上配置的掃描路徑下,則不需要再重新使用 @Import
匯入了,因為 @Import
的目的是注入 bean,但是 Springboot 啟動類自動掃描已經可以注入你想通過 @Import
匯入的 bean 了。
7.2.2 介面 org.springframework.context.annotation.ImportBeanDefinitionRegistrar
的實現類
當 @Import
修飾自定義註解時候,通常會匯入這個介面的實現類。
來看一下介面定義
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 資訊。
來看如下案例,我們通過一個註解,啟動 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
修飾的配置類
我們看第一個類 XXXRegistrar
,這個類的功能是注入一個自定義的 DefaultMQProducer
到Spring 容器中,使業務方可以直接通過 @Autowired
注入 DefaultMQProducer
使用
public class XXXRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//獲取註解裡配置的屬性
AnnotationAttributes attributes = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(EnableMqProducer.class.getName()));
//根據配置的屬性注入自定義 bean 到 spring 容器當中
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();
}
到這裡,通過自定義註解和外部配置的結合,一個完整的訊息傳送器就可以使用了,但方式有取巧之嫌,因為在訊息傳送器啟動之前,不知道還有沒有別的類使用了這個例項,這是不安全的。
7.2.3 介面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;
}
}
這個介面的實現類如果沒有進行 Spring Aware
介面擴充,功能比較單一,因為我們無法參與 Spring Bean 的構建過程,只是告訴 Spring 要注入的 Bean 的名字。不再詳述。
八、總結
綜上所述,我們一共聊了三種形式的元件建立方式
- 相同路徑下,
@Configuration
修飾的配置類 - 使用
META-INF/spring.factories
檔案接入 - 結合
@Import
註解注入
其中穿插了 @ConditionOnXxxx
選擇性啟動、Properties
封裝的技術,快去試一下吧