天天看點

Spring及SpringBoot @Async配置步驟及注意事項

  • 前言

    最近在做一個使用者回報的功能,就是當使用者回報意見或建議後服務端将意見儲存然後發郵件給相關子產品的開發者。考慮發郵件耗時的情況是以我想用異步的方法去執行,于是就開始研究Spring的@Async了。但網上的許多教程都是互相抄襲且直接複制代碼的,根本沒有去自己實踐或者更改代碼,是以在這兩天我遇到了許多問題,使得@Async無效,也一直沒有找到很好的文章去詳細的說明@Async的正确及錯誤的使用方法及需要注意的地方,這裡Sring是以配置檔案的形式來開啟@Async,而SpringBoot則是以注解的方式開啟。

  • 教程
  1. Spring配置檔案

    配置檔案的話有兩種方式一種是精簡式:

    直接在applicationContext.xml中加入開啟異步并添加task的命名空間

    <task:executor id="WhifExecutor" pool-size="10"/>
    <task:annotation-driven executor="WhifExecutor" />           
    xmlns:task="http://www.springframework.org/schema/task"
    http://www.springframework.org/schema/task
    http://www.springframework.org/schema/task/spring-task.xsd           

    這樣好處是簡單快速,缺點是線程池配置簡單,線程建立不能定時關閉。

    是以我推薦下面這種,定義一個線程池,然後引入。其實兩種方法原理是一樣隻不過下面這種隻是線程池配置更加詳細,線程在空閑後會根據存活時間配置進行關閉。

    在applicationContext.xml同目錄下建立檔案threadPool.xml内容如下,然後在applicationContext.xml中引入threadPool.xml:<import resource="threadPool.xml" />

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xmlns:task="http://www.springframework.org/schema/task"
    	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                            http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">
                            
    	<!-- 開啟異步,并引入線程池 -->
    	<task:annotation-driven executor="threadPool" />
    	
    	<!-- 定義線程池 -->
    	<bean id="threadPool"
    		class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
    		<!-- 核心線程數,預設為1 -->
    		<property name="corePoolSize" value="5" />
    
    		<!-- 最大線程數,預設為Integer.MAX_VALUE -->
    		<property name="maxPoolSize" value="20" />
    
    		<!-- 隊列最大長度,一般需要設定值>=notifyScheduledMainExecutor.maxNum;預設為Integer.MAX_VALUE -->
    		<property name="queueCapacity" value="500" />
    
    		<!-- 線程池維護線程所允許的空閑時間,預設為60s -->
    		<property name="keepAliveSeconds" value="30" />
    
    		<!-- 完成任務自動關閉 , 預設為false-->
    		<property name="waitForTasksToCompleteOnShutdown" value="true" />
    
    		<!-- 核心線程逾時退出,預設為false -->
    		<property name="allowCoreThreadTimeOut" value="true" />
    
    		<!-- 線程池對拒絕任務(無線程可用)的處理政策,目前隻支援AbortPolicy、CallerRunsPolicy;預設為後者 -->
    		<property name="rejectedExecutionHandler">
    			<!-- AbortPolicy:直接抛出java.util.concurrent.RejectedExecutionException異常 -->
    			<!-- CallerRunsPolicy:主線程直接執行該任務,執行完之後嘗試添加下一個任務到線程池中,可以有效降低向線程池内添加任務的速度 -->
    			<!-- DiscardOldestPolicy:抛棄舊的任務、暫不支援;會導緻被丢棄的任務無法再次被執行 -->
    			<!-- DiscardPolicy:抛棄目前任務、暫不支援;會導緻被丢棄的任務無法再次被執行 -->
    			<bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" />
    		</property>
    	</bean>
    </beans>           
  2. SpringBoot則是添加@EnableAsync注解
    @EnableAsync
    @SpringBootApplication
    @ServletComponentScan
    @MapperScan("com.cmc.schedule.model.mapper") //配置掃描mapper接口的位址
    public class NLPApplication extends SpringBootServletInitializer {
    
    	//不使用springboot内嵌tomcat啟動方式
    	@Override
    	protected SpringApplicationBuilder configure(
    			SpringApplicationBuilder application) {
    		return application.sources(NLPApplication.class);
    	}
    
    	public static void main(String[] args) {
    		SpringApplication.run(NLPApplication.class, args);
    	}
    
    	//預設使用fastjson解析
    	@Bean
    	public HttpMessageConverters fastJsonHttpMessageConverters() {
    		FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
    		FastJsonConfig fastJsonConfig = new FastJsonConfig();
    		fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
    		fastConverter.setFastJsonConfig(fastJsonConfig);
    		HttpMessageConverter<?> converter = fastConverter;
    		return new HttpMessageConverters(converter);
    	}
    }           
  3. 這一步Spring和SpringBoot都是一樣的,建立一個異步方法的類
    package com.cmc.tst;
    
    import org.springframework.scheduling.annotation.Async;
    import org.springframework.stereotype.Component;
    
    /**
     * @Component 注解必須要有,否則無法将此bean注入
     * 當然也可以使用其他的注解,隻要可以裝配就行
     * 
     * @author chenmc
     * @date 2017年9月4日 下午3:38:29
     */
    @Component
    public class MyAsync {
    
    	/**
    	 * @Async 表明這是一個異步方法,也就是說當調用這個方法時,
    	 * spring會建立一條線程來執行這個方法。
    	 * 注意:不能使用static來修飾此方法,否則@Async無效
    	 * 
    	 * @author chenmc
    	 * @date 2017年9月4日 下午3:34:24
    	 */
    	@Async
    	public void asyncMethod(){
    		System.out.println(Thread.currentThread().getName());
    	}
    }           
  4. 測試調用異步方法,我這裡使用的是JUnit4測試
    @Autowired
    	MyAsync async;
    	
    	@Test
    	public void test() {
    		System.out.println(Thread.currentThread().getName() + "start");
    		//MyAsync async = new MyAsync(); //自己new出來的對象@Async将無效,必須要spring注入的
    		async.asyncMethod();
    		try {
    			Thread.sleep(500);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		System.out.println(Thread.currentThread().getName() + "end");
    	}           
  5. 總結

    這裡總結一下@Async注解無效的可能點

    一、異步方法使用static修飾

    二、異步類沒有使用@Component注解(或其他注解)導緻spring無法掃描到異步類

    三、測試異步方法不能與異步方法在同一個類中

    四、測試類中需要使用@Autowired或@Resource等注解自動注入,不能自己手動new對象

    五、如果使用SpringBoot架構必須在啟動類中增加@EnableAsync注解

  • 結語

    spring的@Async真的極大的友善了java的異步(多線程)開發,心裡默念三遍:感謝spring!感謝spring!感謝spring!