­

教妹學Spring:Aware、非同步編程、計劃任務

你好呀,我是沉默王二,一個和黃家駒一樣身高,劉德華一樣顏值的程式設計師(不信圍觀朋友圈唄)。從 2 位偶像的年紀上,你就可以斷定我的碼齡至少在 10 年以上,但實話實說,我一直堅信自己只有 18 歲,因為好學使我年輕。本篇文章就打算通過我和三妹對話的形式來聊一聊「Spring 的 Aware、非同步編程、計劃任務」。

教妹學 Spring,沒見過這麼放肆的標題吧?「語不驚人死不休」,沒錯,本篇文章的標題就是這麼酷炫,不然你怎麼會點進來?

我有一個漂亮如花的妹妹(見上圖,怎麼又變了?還不能一天做個夢),她叫什麼呢?我想聰明的讀者能猜得出:沉默王三,沒錯,年方三六。父母正考慮讓她向我學習,做一名正兒八經的 Java 程式設計師。我一開始是反對的,因為程式設計師這行業容易掉頭髮,女生可不適合掉頭髮。但家命難為啊,與其反對,不如做點更積極的事情,比如說寫點有趣的文章教教她。

「二哥,聽說今天要學習 Spring 的 Aware、非同步編程、計劃任務,真的是翹首以盼啊。」

「哎呀,三妹,瞧你那迫不及待的大眼神,就好像昨晚上月亮一樣圓,一樣大。」

01、Spring Aware

「二哥,據說 Aware 的目的是讓 Bean 獲取 Spring 容器的服務,你能給我具體說說嗎?」

「沒問題啊。」

Bean 一般不需要了解容器的狀態和直接使用容器,但是在某些情況下,需要在 Bean 中直接對容器進行操作,這時候,就可以通過特定的 Aware 介面來完成。常見的 Spring Aware 介面有下面這些:

Aware 子介面

描述

BeanNameAware

獲取容器中 Bean 的名稱

BeanFactoryAware

Bean 被容器創建以後,會有一個相應的 BeanFactory,可以直接通過它來訪問容器

ApplicationContextAware

Bean 被初始化後,會被注入到 ApplicationContext,可以直接通過它來訪問容器

MessageSourceAware

獲取 Message Source 的相關文本資訊

ResourceLoaderAware

獲取資源載入器,以獲取外部資源文件

1)BeanNameAware

新建一個 MyBeanName 類,內容如下:

public class MyBeanName implements BeanNameAware {      @Override      public void setBeanName(String beanName) {          System.out.println(beanName);      }  }  

MyBeanName 實現了 BeanNameAware 介面,並重寫了 setBeanName() 方法。beanName 參數表示 Bean 在 Spring 容器中註冊的 name。

新建一個 Config 類,內容如下:

@Configuration  public class Config {      @Bean(name = "myCustomBeanName")      public MyBeanName getMyBeanName() {          return new MyBeanName();      }  }  

@Bean 註解用在 getMyBeanName() 方法上,表明當前方法返回一個 Bean 對象(MyBeanName),並通過 name 屬性指定 Bean 的名字為「myCustomBeanName」。

新建 BeanNameMain 類,程式碼如下:

public class BeanNameMain {      public static void main(String[] args) {          AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BeanNameConfig.class);          MyBeanName myBeanName = context.getBean(MyBeanName.class);          context.close();      }  }  

程式輸出的結果如下所示:

myCustomBeanName  

如果把 @Bean() 註解中的 (name = "myCustomBeanName)" 去掉的話,程式輸出的內容將會是 BeanNameConfig 類的 getMyBeanName() 的方法名「getMyBeanName」。

2)BeanFactoryAware

新建一個 MyBeanFactory 類,內容如下:

public class MyBeanFactory implements BeanFactoryAware {      private BeanFactory beanFactory;        @Override      public void setBeanFactory(BeanFactory beanFactory) throws BeansException {          this.beanFactory = beanFactory;      }        public void getMyBeanName() {          MyBeanName myBeanName = beanFactory.getBean(MyBeanName.class);          System.out.println(beanFactory.isSingleton("myCustomBeanName"));          System.out.println(beanFactory.isSingleton("getMyBeanFactory"));      }  }  

藉助 setBeanFactory() 方法,可以將容器中的 BeanFactory 賦值給 MyBeanFactory 類的成員變數 beanFactory,這樣就可以在 getMyBeanName() 方法中使用 BeanFactory 了。

通過 getBean() 方法可以獲取 Bean 的實例;通過 isSingleton() 方法判斷 Bean 是否為一個單例。

在 Config 類中追加 MyBeanFactory 的 Bean:

@Configuration  public class Config {      @Bean(name = "myCustomBeanName")      public MyBeanName getMyBeanName() {          return new MyBeanName();      }        @Bean      public MyBeanFactory getMyBeanFactory() {          return new MyBeanFactory();      }  }  

新建 BeanFactoryMain 類,程式碼如下:

public class BeanFactoryMain {      public static void main(String[] args) {          AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);          MyBeanFactory myBeanFactory = context.getBean(MyBeanFactory.class);          myBeanFactory.getMyBeanName();          context.close();      }  }  

初始化 MyBeanFactory 後就可以調用 getMyBeanName() 方法了,程式輸出的結果如下所示:

myCustomBeanName  true  true  

結果符合我們的預期:MyBeanName 的名字為「myCustomBeanName」,MyBeanName 和 MyBeanFactory 的 scope 都是 singleton。

3)其他幾個 Aware 介面就不再舉例說明了。通常情況下,不要實現 Aware 介面,因為它會使 Bean 和 Spring 框架耦合。

02、非同步編程

「二哥,據說 Spring 可以通過 @Async 來實現非同步編程,你能給我詳細說說嗎?」

「沒問題啊。」

新建一個 AsyncService 類,內容如下:

public class AsyncService {      @Async      public void execute() {          System.out.println(Thread.currentThread().getName());      }  }  

@Async 註解用在 public 方法上,表明 execute() 方法是一個非同步方法。

新建一個 AsyncConfig 類,內容如下:

@Configuration  @EnableAsync  public class AsyncConfig {      @Bean      public AsyncService getAsyncService() {          return new AsyncService();      }  }  

在配置類上使用 @EnableAsync 註解用以開啟非同步編程,否則 @Async 註解不會起作用。

新建一個 AsyncMain 類,內容如下:

public class AsyncMain {      public static void main(String[] args) {          AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AsyncConfig.class);          AsyncService service = context.getBean(AsyncService.class);          for (int i = 0; i < 10; i++) {              service.execute();          }      }  

程式輸出結果如下:

SimpleAsyncTaskExecutor-1  SimpleAsyncTaskExecutor-9  SimpleAsyncTaskExecutor-7  SimpleAsyncTaskExecutor-8  SimpleAsyncTaskExecutor-10  SimpleAsyncTaskExecutor-3  SimpleAsyncTaskExecutor-2  SimpleAsyncTaskExecutor-4  SimpleAsyncTaskExecutor-6  SimpleAsyncTaskExecutor-5  

OK,結果符合我們的預期,非同步編程實現了。就像你看到的那樣,Spring 提供了一個默認的 SimpleAsyncTaskExecutor 用來執行執行緒,我們也可以在方法級別和應用級別上對執行器進行配置。

1)方法級別

新建 AsyncConfig 類,內容如下:

@Configuration  @EnableAsync  public class AsyncConfig {      @Bean      public AsyncService getAsyncService() {          return new AsyncService();      }        @Bean(name = "threadPoolTaskExecutor")      public Executor threadPoolTaskExecutor() {          ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();          executor.setCorePoolSize(5);          return executor;      }  }  

在配置類中創建了一個返回類型為 Executor 的 Bean,其名稱定義為「threadPoolTaskExecutor」,並且重新設置了 ThreadPoolTaskExecutor 的核心執行緒池大小,默認為 1,現在修改為 5。

新進 AsyncService 類,內容如下:

public class AsyncService {      @Async("threadPoolTaskExecutor")      public void execute() {          System.out.println(Thread.currentThread().getName());      }  }  

@Async 註解上需要指定我們之前配置的執行緒池執行器「threadPoolTaskExecutor」。

新建 AsyncMain 類,內容如下:

public class AsyncMain {      public static void main(String[] args) {          AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AsyncConfig.class);          AsyncService service = context.getBean(AsyncService.class);          for (int i = 0; i < 10; i++) {              service.execute();          }      }  }  

程式運行結果如下:

threadPoolTaskExecutor-1  threadPoolTaskExecutor-2  threadPoolTaskExecutor-4  threadPoolTaskExecutor-3  threadPoolTaskExecutor-5  threadPoolTaskExecutor-3  threadPoolTaskExecutor-2  threadPoolTaskExecutor-4  threadPoolTaskExecutor-1  threadPoolTaskExecutor-5  

從結果中可以看得出,執行緒池執行器變成了「threadPoolTaskExecutor」,並且大小為 5。

2)應用級別

新建 AsyncConfig 類,內容如下:

@Configuration  @EnableAsync  public class AsyncConfig implements AsyncConfigurer {      @Bean      public AsyncService getAsyncService() {          return new AsyncService();      }        @Override      public Executor getAsyncExecutor() {          ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();          executor.setCorePoolSize(3);          executor.initialize();          return executor;      }  }  

需要實現 AsyncConfigurer 介面,並重寫 getAsyncExecutor() 方法,這次設置執行緒池的大小為 3。注意執行器要執行一次 initialize() 方法。

新進 AsyncService 類,內容如下:

public class AsyncService {      @Async      public void execute() {          System.out.println(Thread.currentThread().getName());      }  }  

新建 AsyncMain 類,內容如下:

public class AsyncMain {      public static void main(String[] args) {          AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AsyncConfig.class);          AsyncService service = context.getBean(AsyncService.class);          for (int i = 0; i < 10; i++) {              service.execute();          }      }  }  

程式運行結果如下:

ThreadPoolTaskExecutor-2  ThreadPoolTaskExecutor-2  ThreadPoolTaskExecutor-2  ThreadPoolTaskExecutor-2  ThreadPoolTaskExecutor-2  ThreadPoolTaskExecutor-2  ThreadPoolTaskExecutor-2  ThreadPoolTaskExecutor-2  ThreadPoolTaskExecutor-1  ThreadPoolTaskExecutor-3  

從結果中可以看得出,執行緒池執行器變成了「ThreadPoolTaskExecutor」,並且大小為 3。

03、計劃任務

「二哥,據說 Spring 可以通過 @Scheduled 來實現計劃任務,你能給我詳細說說怎麼實現嗎?」

「沒問題啊。」

新建一個 ScheduledService 類,內容如下:

@Service  public class ScheduledService {      @Scheduled(fixedDelay = 1000)      public void scheduleFixedDelayTask() {          System.out.println(                  "固定時間段後執行任務 - " + System.currentTimeMillis() / 1000);      }        @Scheduled(fixedRate = 1000)      public void scheduleFixedRateTask() {          System.out.println(                  "固定的頻率執行任務 - " + System.currentTimeMillis() / 1000);      }        @Scheduled(cron = "0/2 * * * * ?")      public void scheduleTaskUsingCronExpression() {          long now = System.currentTimeMillis() / 1000;          System.out.println(                  "Cron 表達式執行任務 - " + now);      }  }  

@Service 註解用於指定 ScheduledService 類為一個業務層的 Bean。@Scheduled 註解用於指定當前方法(返回類型為 void,無參)為一個任務執行方法,常見的用法有以下 3 種:

1)fixedDelay 用於確保任務執行的完成時間與任務下一次執行的開始時間之間存在 n 毫秒的延遲,下一次任務執行前,上一次任務必須執行完。

2)fixedRate 用於確保每 n 毫秒執行一次計劃任務,即使最後一次調用可能仍在運行。

3)Cron 表達式比 fixedDelay 和 fixedRate 都要靈活,由 7 個部分組成,各部分之間用空格隔開,其完整的格式如下所示:

Seconds Minutes Hours Day-of-Month Month Day-of-Week Year  

單詞都很簡單,就不用我翻譯了。其中 Year 是可選項。常見的範例如下所示:

*/5 * * * * ?  每隔 5 秒執行一次  0 */1 * * * ?  每隔 1 分鐘執行一次  0 0 23 * * ?  每天 23 點執行一次  0 0 1 * * ?  每天凌晨 1 點執行一次:  0 0 1 1 * ?  每月 1 號凌晨 1 點執行一次  0 0 23 L * ?  每月最後一天 23 點執行一次  0 0 1 ? * L  每周星期天凌晨 1 點執行一次  0 26,29,33 * * * ?  在 26 分、29 分、33 分執行一次  0 0 0,13,18,21 * * ? 每天的 0 點、13 點、18 點、21 點各執行一次  

新建 ScheduledConfig 類,內容如下:

@Configuration  @EnableScheduling  @ComponentScan("high.scheduled")  public class ScheduledConfig {  }  

@EnableScheduling 註解用於開啟計劃任務。@ComponentScan 註解用於掃描當前包下的類,如果它使用了註解(比如 @Service),就將其註冊成為一個 Bean。

新建 ScheduledMain 類,內容如下:

public class ScheduledMain {      public static void main(String[] args) {          AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScheduledConfig.class);      }  }  

程式運行結果如下:

固定的頻率執行任務 - 1584666273  固定時間段後執行任務 - 1584666273  Cron 表達式執行任務 - 1584666274  固定的頻率執行任務 - 1584666274  固定時間段後執行任務 - 1584666274  固定的頻率執行任務 - 1584666275  固定時間段後執行任務 - 1584666275  Cron 表達式執行任務 - 1584666276  

從結果中可以看得出,如果任務之間沒有衝突的話,fixedDelay 任務之間的間隔和 fixedRate 任務之間的間隔是相同的,都是 1 秒;Cron 表達式任務與上一次任務之間的間隔為 2 秒。