趣學Spring:一文搞懂Aware、異步編程、計劃任務

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

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

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

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

「哎呀,三妹,瞧你那火燒眉毛的大眼神,就好像昨晚上月亮同樣圓,同樣大。」github

0一、Spring Aware

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

「沒問題啊。」編程

Bean 通常不須要了解容器的狀態和直接使用容器,可是在某些狀況下,須要在 Bean 中直接對容器進行操做,這時候,就能夠經過特定的 Aware 接口來完成。常見的 Spring Aware 接口有下面這些:微信

Aware 子接口 描述
BeanNameAware 獲取容器中 Bean 的名稱
BeanFactoryAware Bean 被容器建立之後,會有一個相應的 BeanFactory,能夠直接經過它來訪問容器
ApplicationContextAware Bean 被初始化後,會被注入到 ApplicationContext,能夠直接經過它來訪問容器
MessageSourceAware 獲取 Message Source 的相關文本信息
ResourceLoaderAware 獲取資源加載器,以獲取外部資源文件

1)BeanNameAwaremarkdown

新建一個 MyBeanName 類,內容以下:app

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 框架耦合。

0二、異步編程

「二哥,聽說 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。

0三、計劃任務

「二哥,聽說 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 秒執行一次
*/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 秒。

「二哥,這篇文章中的示例代碼你上傳到 GitHub 了嗎?」

「你到挺貼心啊,三妹。傳送門~

「二哥,你教得真不錯,我徹底學會了,一點也不枯燥。」

「那必須得啊,期待下一篇吧?」

「那是固然啊,期待,很是期待,望眼欲穿的感受。」

請容許我熱情地吐槽一下,這篇文章我不但願再被噴了,看在我這麼辛苦搞原創(創意+乾貨+有趣)的份上,多鼓勵鼓勵好很差?別瞅了,點讚唄,你最美你最帥

若是以爲文章對你有點幫助,請微信搜索「 沉默王二 」第一時間閱讀,回覆【666】【1024】更有我爲你精心準備的 500G 高清教學視頻(已分門別類),以及大廠技術牛人整理的面經一份。

相關文章
相關標籤/搜索