大家在日常業務開發工作中相信多多少少遇到過下面這樣的幾個場景:
當某一個特定事件或動作發生以後,需要執行很多聯動動作,如果序列去執行的話太耗時,如果引入訊息中介軟體的話又太重了;想要針對不同的傳參執行不同的策略,也就是我們常說的策略模式,但10個人可能有10種不同的寫法,夾雜在一起總感覺不那麼優雅;自己的系統想要呼叫其他系統提供的能力,但其他系統總是偶爾給你一點“小驚喜”,可能因網路問題報超時異常或被呼叫的某一臺分散式應用機器突然宕機,我們想要優雅無侵入式地引入重試機制。其實上面提到的幾個典型業務開發場景Spring都為我們提供了很好的特性支援,我們只需要引入Spring相關依賴就可以方便快速的在業務程式碼當中使用啦,而不用引入過多的三方依賴包或自己重複造輪子。下面我們就來看看Spring提供的強大魔力吧。
使用Spring優雅實現觀察者模式觀察者模式定義物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都得到通知並被自動更新,其主要解決一個物件狀態改變給其他關聯物件通知的問題,保證易用和低耦合。一個典型的應用場景是:當用戶註冊以後,需要給使用者傳送郵件,傳送優惠券等操作,如下圖所示。
使用觀察者模式後:
UserService 在完成自身的使用者註冊邏輯之後,僅僅只需要釋出一個 UserRegisterEvent 事件,而無需關注其它拓展邏輯。其它 Service 可以自己訂閱 UserRegisterEvent 事件,實現自定義的拓展邏輯。Spring的事件機制主要由3個部分組成。
ApplicationEvent: 透過繼承它,實現自定義事件。另外,透過它的 source 屬性可以獲取事件源,timestamp 屬性可以獲得發生時間。ApplicationEventPublisher: 透過實現它,來發布變更事件。ApplicationEventListener: 透過實現它,來監聽指定型別事件並響應動作。這裡就以上面的使用者註冊為例,來看看程式碼示例。首先定義使用者註冊事件 UserRegisterEvent。publicclass UserRegisterEvent extends ApplicationEvent { /** * 使用者名稱 */ private String username; public UserRegisterEvent(Object source) { super(source); } public UserRegisterEvent(Object source, String username) { super(source); this.username = username; } public String getUsername() { return username; }}
然後定義使用者註冊服務類,實現 ApplicationEventPublisherAware 介面,從而將 ApplicationEventPublisher 注入進來。從下面程式碼可以看到,在執行完註冊邏輯後,呼叫了 ApplicationEventPublisher的 publishEvent(ApplicationEvent event) 方法,釋出了 UserRegisterEvent 事件。
@Servicepublicclass UserService implements ApplicationEventPublisherAware { // <1> private Logger logger = LoggerFactory.getLogger(getClass()); private ApplicationEventPublisher applicationEventPublisher; @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } public void register(String username) { // ... 執行註冊邏輯 logger.info("[register][執行使用者({}) 的註冊邏輯]", username); // <2> ... 釋出 applicationEventPublisher.publishEvent(new UserRegisterEvent(this, username)); }}
建立郵箱Service,實現 ApplicationListener 介面,透過 E 泛型設定感興趣的事件,實現 onApplicationEvent(E event) 方法,針對監聽的 UserRegisterEvent 事件,進行自定義處理。
@Servicepublicclass EmailService implements ApplicationListener<UserRegisterEvent> { // <1> private Logger logger = LoggerFactory.getLogger(getClass()); @Override @Async// <3> public void onApplicationEvent(UserRegisterEvent event) { // <2> logger.info("[onApplicationEvent][給使用者({}) 傳送郵件]", event.getUsername()); }}
建立優惠券Service,不同於上面的實現 ApplicationListener 介面方式,在方法上,新增 @EventListener 註解,並設定監聽的事件為 UserRegisterEvent。這是另一種使用方式。
@Servicepublicclass CouponService { private Logger logger = LoggerFactory.getLogger(getClass()); @EventListener// <1> public void addCoupon(UserRegisterEvent event) { logger.info("[addCoupon][給使用者({}) 發放優惠劵]", event.getUsername()); }}
看到這裡,細心的同學可能想到了釋出訂閱模式,其實觀察者模式與釋出訂閱還是有區別的,簡單來說,釋出訂閱模式屬於廣義上的觀察者模式,在觀察者模式的 Subject 和 Observer 的基礎上,引入 Event Channel 這個中介,進一步解耦。圖示如下,可以看出,觀察者模式更加輕量,通常用於單機,而釋出訂閱模式相對而言更重一些,通常用於分散式環境下的訊息通知場景。
使用Spring Retry優雅引入重試機制如今,Spring Retry是一個獨立的包了(早期是Spring Batch的一部分),下面是使用Spring Retry框架進行重試的幾個重要步驟。第一步:加入Spring Retry依賴包
<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> <version>1.1.2.RELEASE</version></dependency>
第二步:在應用中包含main()方法的類或者在包含@Configuration的類上加上@EnableRetry註解 第三步:在想要進行重試的方法(可能發生異常)上加上@Retryable註解
@Retryable(maxAttempts=5,backoff = @Backoff(delay = 3000))public void retrySomething() throws Exception{ logger.info("printSomething{} is called"); thrownew SQLException();}
在上面這個案例當中的重試策略就是重試5次,每次延時3秒。詳細的使用文件看這裡,它的主要配置引數有下面這樣幾個。其中exclude、include、maxAttempts、value幾個屬性很容易理解,比較看不懂的是backoff屬性,它也是個註解,包含delay、maxDelay、multiplier、random四個屬性。
delay: 如果不設定的話預設是1秒maxDelay: 最大重試等待時間multiplier: 用於計算下一個延遲時間的乘數(大於0生效)random: 隨機重試等待時間(一般不用)Spring Retry的優點很明顯,第一,屬於Spring大生態,使用起來不會太生硬;第二,只需要在需要重試的方法上加上註解並配置重試策略屬性就好,不需要太多侵入程式碼。
但同時也存在兩個主要不足,第一,由於Spring Retry用到了Aspect增強,所以就會有使用Aspect不可避免的坑——方法內部呼叫,如果被 @Retryable 註解的方法的呼叫方和被呼叫方處於同一個類中,那麼重試將會失效;第二,Spring的重試機制只支援對異常進行捕獲,而無法對返回值進行校驗判斷重試。如果想要更靈活的重試策略可以考慮使用Guava Retry,也是一個不錯的選擇。
優雅使用Spring特性完成業務策略模式策略模式相信大家都應該比較熟悉,它定義了一系列的演算法,並將每一個演算法封裝起來,使每個演算法可以相互替代,使演算法本身和使用演算法的客戶端分割開來,相互獨立。
其適用的場景是這樣的:一個大功能,它有許多不同型別的實現(策略類),具體根據客戶端來決定採用哪一個策略類。比如下單優惠策略、物流對接策略等,應用場景還是非常多的。
舉一個簡單的例子,業務背景是這樣的:平臺需要根據不同的業務進行鑑權,每個業務的鑑權邏輯不一樣,都有自己的一套獨立的判斷邏輯,因此需要根據傳入的 bizType 進行鑑權操作,首先我們定義一個許可權校驗處理器介面如下。
/** * 業務許可權校驗處理器 */publicinterface PermissionCheckHandler { /** * 判斷是否是自己能夠處理的許可權校驗型別 */ boolean isMatched(BizType bizType); /** * 許可權校驗邏輯 */ PermissionCheckResultDTO permissionCheck(Long userId, String bizCode);}業務1的鑑權邏輯我們假設是這樣的:/** * 冷啟動許可權校驗處理器 */@Componentpublicclass ColdStartPermissionCheckHandlerImpl implements PermissionCheckHandler { @Override public boolean isMatched(BizType bizType) { return BizType.COLD_START.equals(bizType); } @Override public PermissionCheckResultDTO permissionCheck(Long userId, String bizCode) { //業務特有鑑權邏輯 }}業務2的鑑權邏輯我們假設是這樣的:/** * 趨勢業務許可權校驗處理器 */@Componentpublicclass TrendPermissionCheckHandlerImpl implements PermissionCheckHandler { @Override public boolean isMatched(BizType bizType) { return BizType.TREND.equals(bizType); } @Override public PermissionCheckResultDTO permissionCheck(Long userId, String bizCode){ //業務特有鑑權邏輯 }}
可能還有很多其他的業務鑑權邏輯,這裡就不一一列舉了,實現邏輯像上面這樣組織就好了。接著就到了關鍵的地方了,上面我們定義了這麼多策略,應該怎麼優雅地組織起來呢,這就需要用到Spring提供的一些擴充套件特性了,Spring主要為我們提供了三類擴充套件點,分別對應不同Bean生命週期階段:
Aware介面BeanPostProcessorInitializingBean 和 init-method我們這裡用到的主要是 Aware 介面和 InitializingBean 兩個擴充套件點,其主要用法如下程式碼所示,關鍵點就在於實現 ApplicationContextAware 介面的 setApplicationContext 方法和 InitializingBean 介面的 afterPropertiesSet 方法。
實現 ApplicationContextAware 介面的目的就是要拿到 Spring 容器的資源,從而方便的使用它提供的 getBeansOfType 方法(該方法返回的是 map 型別,key 對應 beanName, value 對應 bean);而實現 InitializingBean 介面的目的則是方便為 Service 類的 handlers 屬性執行定製初始化邏輯。
可以很明顯地看出,如果以後還有一些其他的業務需要制定相應的鑑權邏輯,我們只需要編寫對應的策略類就好了,無需再破壞當前 Service 類的邏輯,很好的保證了開閉原則。
/** * 許可權校驗服務類 */@Slf4j@Servicepublicclass PermissionServiceImpl implements PermissionService, ApplicationContextAware, InitializingBean { private ApplicationContext applicationContext; //注:這裡可以使用Map,偷個懶 private List<PermissionCheckHandler> handlers = new ArrayList<>(); @Override public PermissionCheckResultDTO permissionCheck(ArtemisSellerBizType artemisSellerBizType, Long userId, String bizCode) { //省略一些前置邏輯 PermissionCheckHandler handler = getHandler(artemisSellerBizType); return handler.permissionCheck(userId, bizCode); } private PermissionCheckHandler getHandler(ArtemisSellerBizType artemisSellerBizType) { for (PermissionCheckHandler handler : handlers) { if (handler.isMatched(artemisSellerBizType)) { return handler; } } returnnull; } @Override public void afterPropertiesSet() throws Exception { for (PermissionCheckHandler handler : applicationContext.getBeansOfType(PermissionCheckHandler.class) .values()) { handlers.add(handler); log.warn("load permission check handler [{}]", handler.getClass().getName()); } } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; }}
當然在這裡相信不少同學會有疑問,那就是這裡在獲取 handler 處理器 bean 的時候,所有的 bean 是不是已經初始化好了?會不會存在有的 handler 還沒有初始化好的情況?
答案是不會的,Spring Bean 的宣告週期保證了這一點(當然前提是 handler 自身不會有特殊的初始化邏輯)。經過實際驗證,所有的 handler 會在 Service 初始化操作前 ready,感興趣的同學可以編寫程式碼驗證,可以先在相應鉤子處打上日誌直接輸出結果驗證,然後在 Spring 原始碼關鍵處打上斷點 debug,相信會有不少收穫。
總結&思考公司裡的有些程式碼有點年齡,有些類寫得又臭又長,很多地方充斥著程式碼壞味道,如重複的程式碼,過長的引數列,散彈式修改,基本型偏執等等,不一一展開。每天要面對這些程式碼進行開發,不僅消磨了我們對技術的熱情也讓人變得毫無鬥志,很多同學會想——反正都已經這樣了,那我也就這麼來吧,相信不少小夥伴都有這樣的遭遇與困惑。
但唯一不能停下來的就是進步,即使面對惡龍還是不能放棄抵抗。當然,在做需求的時候,很多時候也不能去修改那些程式碼,太耗時太費勁,風險太大。那自己起碼也要思考一下如何設計程式碼才能去避免以後出現同樣的情況,讓自己下次不要犯同樣的錯誤。
當我們在實際編寫程式碼的時候,需要留意探索一下Spring有沒有為我們提供一些已有的工具類和擴充套件點。一方面,使用Spring提供的這些特性可以讓我們少造輪子,避免引入其他比較重的類庫;另一方面,Spring對JDK等庫提供的一些類和規範進行了抽象封裝,易用性更好,更貼合開發者需求。
原文連結: https://mp.weixin.qq.com/s?__biz=MzAxNDEwNjk5OQ==&mid=2650418200&idx=1&sn=a7fff4046ef926c7fe3868de7b4df718