Spring非同步程式設計 | 你的@Async就真的非同步嗎 ☞ 非同步歷險奇遇記

錦成同學發表於2019-08-05

Spring非同步程式設計 | 你的@Async就真的非同步嗎 ☞ 非同步歷險奇遇記


引言有點長

前端的寶寶會用ajax,用非同步程式設計到快樂的不行~ 我們java也有非同步,用起來比他們還快樂~ 我們bia~ji~一個注(gǒupí)解(gāoyào),也是快樂風男...

Spring非同步程式設計 | 你的@Async就真的非同步嗎 ☞ 非同步歷險奇遇記
且看下面的栗子:

註冊一個使用者,給他的賬戶初始化積分(也可以想象成註冊獎勵),再給使用者發個註冊通知簡訊,再發個郵件,(只是舉栗子,切莫牛角大法),這樣一個流程,簡訊和郵件我覺得完全可以拆分出來,沒必要拖在在主流程上來(再補充上事務[ACID:原子性,一致性,隔離性,永續性]就好了):

Spring非同步程式設計 | 你的@Async就真的非同步嗎 ☞ 非同步歷險奇遇記
今天就這點業務,我在暗想,這不是一個註解就搞掂的嘛~~~ ~哈哈哈~

Spring非同步程式設計 | 你的@Async就真的非同步嗎 ☞ 非同步歷險奇遇記
結果不是想象中的那麼完美~~ 來看我的異(dind)步(ding)歷險記
Spring非同步程式設計 | 你的@Async就真的非同步嗎 ☞ 非同步歷險奇遇記

我的首發原創部落格地址:你的@Async就真的非同步嗎 ☞ 非同步歷險奇遇記 裡面有gitHub專案地址,關注我,裡面實戰多多哦~


奇遇一 迴圈依賴異常

看code:

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    UserService userService;
    
     @Override
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    public int save(UserDTO userDTO) {
        User user = new User();
        BeanCopyUtils.copy(userDTO, user);
        int insert = userMapper.insert(user);
        System.out.println("User 儲存使用者成功:" + user);
        userService.senMsg(user);
        userService.senEmail(user);
        return insert;
    }

    @Async
    @Override
    public Boolean senMsg(User user) {
        try {
            TimeUnit.SECONDS.sleep(2);
            System.out.println("傳送簡訊中:.....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "給使用者id:" + user.getId() + ",手機號:" + user.getMobile() + "傳送簡訊成功");
        return true;
    }

    @Async
    @Override
    public Boolean senEmail(User user) {
        try {
            TimeUnit.SECONDS.sleep(3);
            System.out.println("傳送郵件中:.....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "給使用者id:" + user.getId() + ",郵箱:" + user.getEmail() + "傳送郵件成功");
        return true;
    }
複製程式碼

結果:啟動不起來,Spring迴圈依賴問題。 Spring不是解決了迴圈依賴問題嗎,它是支援迴圈依賴的呀?怎麼會呢?

不可否認,在這之前我也是這麼堅信的,倘若你目前也和我有一樣堅挺的想法,那就讓異常UnsatisfiedDependencyException,has been injected into other beans [userServiceImpl] in its raw version as part of a circular reference,,來鼓勵你,擁抱你, 就是這麼的不給面子,赤裸裸的circular reference

談到Spring Bean的迴圈依賴,有的小夥伴可能比較陌生,畢竟開發過程中好像對迴圈依賴這個概念無感知。其實不然,你有這種錯覺,那是因為你工作在Spring的襁褓中,從而讓你“高枕無憂”~ 其實我們的程式碼中肯定被我們寫了迴圈依賴,比如像這樣:

@Service
public class AServiceImpl implements AService {
    @Autowired
    private BService bService;
    ...
}
@Service
public class BServiceImpl implements BService {
    @Autowired
    private AService aService;
    ...
}
複製程式碼

通過實驗總結出,出現使用@Async導致迴圈依賴問題的必要條件:

  1. 已開啟@EnableAsync的支援
  2. @Async註解所在的Bean被迴圈依賴了

奇遇二 非同步失效異常

那麼既然不能迴圈依賴,我們就不迴圈依賴,我們這麼來:

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    UserMapper userMapper;
    @Autowired
    SendService sendService;
    
    @Override
    @Transactional()
    public int save(UserDTO userDTO) {
        User user = new User();
        BeanCopyUtils.copy(userDTO, user);
        int insert = userMapper.insert(user);
        System.out.println("User 儲存使用者成功:" + user);
        this.senMsg(user);
        this.senEmail(user);
        return insert;
    }


    @Async
    @Override
    public Boolean senMsg(User user) {
        System.out.println(Thread.currentThread().getName() + "給使用者id:" + user.getId() + ",手機號:" + user.getMobile() + "傳送簡訊成功");
        return true;
    }

    @Async
    @Override
    public Boolean senEmail(User user) {
        System.out.println(Thread.currentThread().getName() + "給使用者id:" + user.getId() + ",郵箱:" + user.getEmail() + "傳送郵件成功");
        return true;
    }
複製程式碼

結果我們測試了幾把,我列印一下結果:

2019-08-05 21:59:32.304  INFO 14360 --- [nio-8080-exec-3] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
2019-08-05 21:59:32.346 DEBUG 14360 --- [nio-8080-exec-3] c.b.lea.mybot.mapper.UserMapper.insert   : ==>  Preparing: insert into t_user (username, sex, mobile,email) values (?, ?, ?,?) 
2019-08-05 21:59:32.454 DEBUG 14360 --- [nio-8080-exec-3] c.b.lea.mybot.mapper.UserMapper.insert   : ==> Parameters: 王麻子(String), 男(String), 18820158833(String), 22qq@qq.com(String)
2019-08-05 21:59:32.463 DEBUG 14360 --- [nio-8080-exec-3] c.b.lea.mybot.mapper.UserMapper.insert   : <==    Updates: 1
User 儲存使用者成功:User(id=101, username=王麻子, mobile=18820158833, email=22qq@qq.com, sex=男, password=123435, createTime=Mon Aug 05 12:20:51 CST 2019, updateTime=null)
傳送簡訊中:.....
http-nio-8080-exec-3給使用者id:101,手機號:18820158833傳送簡訊成功
傳送郵件中:.....
http-nio-8080-exec-3給使用者id:101,郵箱:22qq@qq.com傳送郵件成功
複製程式碼

這不白瞎了嗎?感知不到我的愛,白寫了,難受~~執行緒依然是http-nio-8080-exec-3,那麼為什麼了呢? 下面會講的哦,先說結論:

通過實驗總結出,出現使用@Async導致非同步失效的原因:

  1. 在本類中使用了非同步是不支援非同步的
  2. 呼叫者其實是this,是當前物件,不是真正的代理物件userService,spring無法截獲這個方法呼叫 所以不在不在本類中去呼叫,網上的解決方法有applicationContext.getBean(UserService.class)AopContext.currentProxy()

奇遇三 事務失效異常

那麼,既然不能用當前物件,那我們用代理,AopContext.currentProxy(),然鵝,你發現,報錯了,對Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.就他:

Spring非同步程式設計 | 你的@Async就真的非同步嗎 ☞ 非同步歷險奇遇記
但是你去網上百度就會發現,都這麼搞

@EnableAspectJAutoProxy(exposeProxy = true)
複製程式碼

我也這麼搞,但是又報錯了,細細的看報錯內容,才發現少了個jar包這東西要配合切面織入,要配合,懂嗎?,來上包

  <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.2</version>
        </dependency>
複製程式碼

再來看為撒: 這是一個性感的話題,exposeProxy = true它的作用就是啟用切面的自動代理,說人話就是暴露當前代理物件到當前執行緒繫結, 看個報錯Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available. 就是AopContext搞得鬼.

public final class AopContext {
	private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal<>("Current AOP proxy");
	private AopContext() {
	}

	// 該方法是public static方法,說明可以被任意類進行呼叫
	public static Object currentProxy() throws IllegalStateException {
		Object proxy = currentProxy.get();

		// 它丟擲異常的原因是當前執行緒並沒有繫結物件
		// 而給執行緒繫結物件的方法在下面:特別有意思的是它的訪問許可權是default級別,也就是說只能Spring內部去呼叫
		if (proxy == null) {
			throw new IllegalStateException("Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.");
		}
		return proxy;
	}

	// 它最有意思的地方是它的訪問許可權是default的,表示只能給Spring內部去呼叫
	// 呼叫它的類有CglibAopProxy和JdkDynamicAopProxy
	@Nullable
	static Object setCurrentProxy(@Nullable Object proxy) {
		Object old = currentProxy.get();
		if (proxy != null) {
			currentProxy.set(proxy);
		} else {
			currentProxy.remove();
		}
		return old;
	}

}

複製程式碼

所以我們要做啟用代理設定,讓代理生效,來走起,主執行緒的方法使用來呼叫非同步方法,來測試走起: no code said niao:


@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserMapper userMapper;

    @Override@Transactional(propagation = Propagation.REQUIRED)
    public int save(UserDTO userDTO) {
        User user = new User();
        BeanCopyUtils.copy(userDTO, user);
        int insert = userMapper.insert(user);
        System.out.println("User 儲存使用者成功:" + user);
        UserService currentProxy = UserService.class.cast(AopContext.currentProxy());
        currentProxy.senMsg(user);
        currentProxy.senEmail(user);
        int i = 1 / 0;
        return insert;
    }
    @Async  @Override  @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void senMsg(User user) {
        user.setUsername(Thread.currentThread().getName()+"發簡訊測試事務...."+ new Random().nextInt());
        userMapper.insert(user);
        System.out.println(Thread.currentThread().getName() + "給使用者id:" + user.getId() + ",手機號:" + user.getMobile() + "傳送簡訊成功");
    }
    @Async @Override @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void senEmail(User user) {
        user.setUsername("發郵件測試事務...."+ new Random().nextInt());
         userMapper.insert(user);
         int i = 1 / 0;
        System.out.println(Thread.currentThread().getName() + "給使用者id:" + user.getId() + ",郵箱:" + user.getEmail() + "傳送郵件成功");
    }
}    
複製程式碼

測試結果:

Spring非同步程式設計 | 你的@Async就真的非同步嗎 ☞ 非同步歷險奇遇記
如我們所願,事務和非同步都生效了

1. 非同步執行緒SimpleAsyncTaskExecutor-1和SimpleAsyncTaskExecutor-2分別發簡訊和郵件,主執行緒儲存使用者
2. 實際結果,主執行緒儲存的那個使用者失敗,名字叫'發郵件'的也儲存失敗,只有叫'傳送簡訊'的使用者插入成功
3. 那麼就做到了事務的執行緒隔離,事務的互不影響,完美
4. 親,你這麼寫了嗎?這麼寫不優美,雖然有用,但是寫的另闢蹊徑啊
複製程式碼

奇遇四 非同步巢狀異常

來,我們看個更騷氣的,非同步中巢狀非同步,來上code:


@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserMapper userMapper;

    @Override@Transactional(propagation = Propagation.REQUIRED)
    public int save(UserDTO userDTO) {
        User user = new User();
        BeanCopyUtils.copy(userDTO, user);
        int insert = userMapper.insert(user);
        System.out.println("User 儲存使用者成功:" + user);
        UserService currentProxy = UserService.class.cast(AopContext.currentProxy());
        currentProxy.send(user);
        return insert;
    }

    @Async  @Override  @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void send(User user) {
        //發簡訊
        user.setUsername(Thread.currentThread().getName()+"發簡訊測試事務...."+ new Random().nextInt());
        userMapper.insert(user);
        System.out.println(Thread.currentThread().getName() + "給使用者id:" + user.getId() + ",手機號:" + user.getMobile() + "傳送簡訊成功");
        //發郵件
        UserService currentProxy = UserService.class.cast(AopContext.currentProxy());
        currentProxy.senEmail(user);
    }
    
     @Async @Override @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void senEmail(User user) {
        user.setUsername("發郵件測試事務...."+ new Random().nextInt());
         userMapper.insert(user);
        System.out.println(Thread.currentThread().getName() + "給使用者id:" + user.getId() + ",郵箱:" + user.getEmail() + "傳送郵件成功");
    }
}
複製程式碼

看我們猜下結果? 資料庫會新增幾個資料?3個?2個?1個?0個?納尼報錯?

哈哈``` 上結果:

Spring非同步程式設計 | 你的@Async就真的非同步嗎 ☞ 非同步歷險奇遇記
答案:只有一條資料主執行緒儲存成功,簡訊和郵件的插入都失敗了,最主要的是還報錯了,又是那個~~~Set 'exposeProxy' property on Advised to 'true'磨人的小妖精

通過實驗總結出,出現導致非同步巢狀使用失敗的原因:

  1. 在本類中使用了非同步巢狀非同步是不支援的
  2. 呼叫者其實被代理過一次了,再巢狀會出現'二次代理',其實是達不到代理了效果,因為已經非同步了.即時你在巢狀中不使用代理去獲取,即使不儲存,但是事務和非同步效果都會跟隨當前的代理,即巢狀的效果是達不到再次非同步的.
  3. 解決辦法應該有,但是我覺得我還沒找到.這個寫法是我們應該規避的,我們應該遵循規範,啟用新的服務類去完成我們的非同步工作

下面我們舉個例子:正確的寫法,優雅的寫法


@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserMapper userMapper;

    @Autowired
    SendService sendService;

    @Override@Transactional(propagation = Propagation.REQUIRED)
    public int save(UserDTO userDTO) {
        User user = new User();
        BeanCopyUtils.copy(userDTO, user);
        int insert = userMapper.insert(user);
        System.out.println("User 儲存使用者成功:" + user);
        sendService.senMsg(user);
        sendService.senEmail(user);
        return insert;
    }
}

---------------無責任分割線--------------------


@Service
public class SendServiceImpl implements SendService {


    @Override
    @Async
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Boolean senMsg(User user) {
        try {
            TimeUnit.SECONDS.sleep(2);
            System.out.println("傳送簡訊中:.....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        int i = 1 / 0;
        System.out.println(Thread.currentThread().getName() + "給使用者id:" + user.getId() + ",手機號:" + user.getMobile() + "傳送簡訊成功");
        return true;
    }

    @Async
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Boolean senEmail(User user) {
        try {
            TimeUnit.SECONDS.sleep(3);
            System.out.println("傳送郵件中:.....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "給使用者id:" + user.getId() + ",郵箱:" + user.getEmail() + "傳送郵件成功");
        return true;
    }
}
複製程式碼

結果肯定完美:

Spring非同步程式設計 | 你的@Async就真的非同步嗎 ☞ 非同步歷險奇遇記

因此當你看到你同事就在本類寫個方法標註上@Async然後呼叫,請制止他吧,做的無用功~~~(關鍵自己還以為有用,這是最可怕的深坑~)

那我補充點: @EnableAspectJAutoProxy(exposeProxy = true)的作用: 此註解它匯入了AspectJAutoProxyRegistrar,最終設定此註解的兩個屬性的方法為:

public abstract class AopConfigUtils {
    ...正在加(sheng)載(lue)程式碼中 請稍後....
	public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) {
		if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
			BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
			definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE);
		}
	}
	public static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry registry) {
		if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
			BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
			definition.getPropertyValues().add("exposeProxy", Boolean.TRUE);
		}
	}
}
複製程式碼

看到此註解標註的屬性值最終都被設定到了internalAutoProxyCreator身上,也就是它:自動代理建立器。 首先我們需要明晰的是:@Async的代理物件並不是由自動代理建立器來建立的,而是由AsyncAnnotationBeanPostProcessor一個單純的BeanPostProcessor實現的,很顯然當執行AopContext.currentProxy()這句程式碼的時候報錯了。 @EnableAsync給容器注入的是AsyncAnnotationBeanPostProcessor,它用於給@Async生成代理,但是它僅僅是個BeanPostProcessor並不屬於自動代理建立器,因此exposeProxy = true對它無效。 所以AopContext.setCurrentProxy(proxy);這個set方法肯定就不會執行,所以,因此,但凡只要業務方法中呼叫AopContext.currentProxy()方法就鐵定拋異常~~

奇遇五 基本型別異常

看嘛,發簡訊其實是一些閘道器呼叫,我想寫個看簡訊,郵件傳送成功的標誌,是否呼叫成功的狀態,來走起

....省略...UserService
---------------無責任分割線--------------------

@Service
public class SendServiceImpl implements SendService {


    @Override
    @Async
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public boolean senMsg(User user) {
        try {
            TimeUnit.SECONDS.sleep(2);
            System.out.println("傳送簡訊中:.....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "給使用者id:" + user.getId() + ",手機號:" + user.getMobile() + "傳送簡訊成功");
        return true;
    }

    @Async
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public boolean senEmail(User user) {
        try {
            TimeUnit.SECONDS.sleep(3);
            System.out.println("傳送郵件中:.....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "給使用者id:" + user.getId() + ",郵箱:" + user.getEmail() + "傳送郵件成功");
        return true;
    }
}
複製程式碼

瞪大眼睛看,我的返回結果是boolean,屬於基本型別,雖然沒有用,但是報錯了:

org.springframework.aop.AopInvocationException: Null return value from advice does not match primitive return type for: 
public boolean com.boot.lea.mybot.service.impl.SendServiceImpl.senMsg(com.boot.lea.mybot.entity.User)
複製程式碼

導致我的資料庫一條資料都沒有,影響到主執行緒了,可見問題發生在主執行緒觸發非同步執行緒的時候,那我們找原因: 是走代理觸發的:我先找這個類 CglibAopProxy再順藤摸瓜

/**
	 * Process a return value. Wraps a return of {@code this} if necessary to be the
	 * {@code proxy} and also verifies that {@code null} is not returned as a primitive.
	 */
	private static Object processReturnType(Object proxy, Object target, Method method, Object retVal) {
		// Massage return value if necessary
		if (retVal != null && retVal == target &&
				!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
			// Special case: it returned "this". Note that we can't help
			// if the target sets a reference to itself in another returned object.
			retVal = proxy;
		}
		Class<?> returnType = method.getReturnType();
		if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
			throw new AopInvocationException(
					"Null return value from advice does not match primitive return type for: " + method);
		}
		return retVal;
	}
複製程式碼

在這retVal == null && returnType != Void.TYPE && returnType.isPrimitive(),因為我們的這種非同步其實是不支援友好的返回結果的,我們的結果應該是void,因為這個非同步執行緒被主執行緒觸發後其實被當做一個任務提交到Spring的非同步的一個執行緒池中進行非同步的處理任務了,執行緒之間的通訊是不能之間返回的,其實用這種寫法我們就應該用void去非同步執行,不要有返回值,而且我們的返回值是isPrimitive(),是基本型別,剛好達標....

那麼我大聲喊出,使用非同步的時候儘量不要有返回值,實在要有你也不能用基本型別.

奇遇六 返回非同步結果

有些人就是難受,就是想要返回結果,那麼也是可以滴:但是要藉助Furtrue小姐姐的get()來進行執行緒之間的阻塞通訊,畢竟小姐姐⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄害羞.

醬紫寫,你就可以阻塞等到執行任務有結果的時候去獲取真正的結果了,這個寫法和我之前的文章 JAVA併發非同步程式設計 原來十個介面的活現在只需要一個介面就搞定!是一樣的道理了

import com.boot.lea.mybot.service.AsyncService;
import com.boot.lea.mybot.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;

@Service
@Async("taskExecutor")
public class AsyncServiceImpl implements AsyncService {

    @Autowired
    private UserService userService;

    @Override
    public Future<Long> queryUserMsgCount(final Long userId) {
        System.out.println("當前執行緒:" + Thread.currentThread().getName() + "=-=====queryUserMsgCount");
        long countByUserId = userService.countMsgCountByUserId(userId);
        return new AsyncResult<>(countByUserId);
    }

    @Override
    public Future<Long> queryCollectCount(final Long userId) {
        System.out.println("當前執行緒:" + Thread.currentThread().getName() + "=-====queryCollectCount");
        long collectCount = userService.countCollectCountByUserId(userId);
        return new AsyncResult<>(collectCount);
    }
複製程式碼

你需要知道的非同步注意事項

  1. 儘量不要在本類中非同步呼叫
  2. 儘量不要有返回值
  3. 不能使用本類的私有方法或者非介面化加註@Async,因為代理不到失效
  4. 非同步方法不能使用static修飾
  5. 非同步類沒有使用@Component註解(或其他註解)導致spring無法掃描到非同步類
  6. 類中需要使用@Autowired或@Resource等註解自動注入,不能自己手動new物件
  7. 如果使用SpringBoot框架必須在啟動類中增加@EnableAsync註解
  8. 在呼叫Async方法的方法上標註@Transactional是管理呼叫方法的事務的
  9. 在Async方法上標註@Transactional是管理非同步方法的事務,事務因執行緒隔離

你需要懂的非同步原理

@Async的非同步:

  • 實際是spring 在掃描bean的時候會掃描方法上是否包含@Async的註解,如果包含的,spring會為這個bean動態的生成一個子類,我們稱之為代理類(jdkProxy), 代理類是繼承我們所寫的bean的,然後把代理類注入進來,那此時,在執行此方法的時候,會到代理類中,代理類判斷了此方法需要非同步執行,就不會呼叫父類 (我們原本寫的bean)的對應方法。
  • spring自己維護了一個佇列,他會把需要執行的方法,放入佇列中,等待執行緒池去讀取這個佇列,完成方法的執行, 從而完成了非同步的功能。
  • 我們可以關注到再配置task的時候,是有引數讓我們配置執行緒池的數量的。因為這種實現方法,所以在同一個類中的方法呼叫,新增@Async註解是失效的!,原因是當你在同一個類中的時候,方法呼叫是在類體內執行的,spring無法截獲這個方法呼叫(為什麼呢,這個就是下文講的...奸笑...嘻嘻嘻嘻...)。
  • 那在深入一步,Spring為我們提供了AOP,面向切面的功能。他的原理和非同步註解的原理是類似的,spring在啟動容器的時候,會掃描切面所定義的 類。在這些類被注入的時候,所注入的也是代理類,當你呼叫這些方法的時候,本質上是呼叫的代理類。通過代理類再去執行父類相對應的方法,那spring只需要在呼叫之前和之後執行某段程式碼就完成了AOP的實現了!

SpringBoot環境中,要使用@Async註解,我們需要先在啟動類上加上@EnableAsync註解。這個與在SpringBoot中使用@Scheduled註解需要在啟動類中加上@EnableScheduling是一樣的道理(當然你使用古老的XML配置也是可以的,但是在SpringBoot環境中,建議的是全註解開發),具體原理下面會分析。加上@EnableAsync註解後,如果我們想在呼叫一個方法的時候開啟一個新的執行緒開始非同步操作,我們只需要在這個方法上加上@Async註解,當然前提是,這個方法所在的類必須在Spring環境中。

示例:非spingboot專案
<task:annotation-driven executor="annotationExecutor" />
<!-- 支援 @Async 註解 -->
<task:executor id="annotationExecutor" pool-size="20"/>
複製程式碼

執行流程:

  1. 掃描是否開啟註解EnableAsync,@EnableAsync註解上有個@Import(AsyncConfigurationSelector.class),springboot的注入老套路了
  2. 請您再移步AsyncConfigurationSelector,看到selectImports方法了沒,這裡使用的是預設使用的是ProxyAsyncConfiguration這個配置類
  3. 繼續觀摩ProxyAsyncConfiguration繼承AbstractAsyncConfiguration,它裡面的的setConfigurers說明了我們可以通過實現AsyncConfigurer介面來完成執行緒池以及異常處理器的配置,而且在Spring環境中只能配置一個實現類,否則會丟擲異常。 上一點程式碼:
    /**
	 * Collect any {@link AsyncConfigurer} beans through autowiring.
	 */
	@Autowired(required = false)
	void setConfigurers(Collection<AsyncConfigurer> configurers) {
      
		if (CollectionUtils.isEmpty(configurers)) {
			return;
		}
      //AsyncConfigurer用來配置執行緒池配置以及異常處理器,而且在Spring環境中最多隻能有一個,在這裡我們知道了,如果想要自己去配置執行緒池,只需要實現AsyncConfigurer介面,並且不可以在Spring環境中有多個實現AsyncConfigurer的類。
		if (configurers.size() > 1) {
			throw new IllegalStateException("Only one AsyncConfigurer may exist");
		}
		AsyncConfigurer configurer = configurers.iterator().next();
		this.executor = configurer.getAsyncExecutor();
		this.exceptionHandler = configurer.getAsyncUncaughtExceptionHandler();
	}
複製程式碼
  1. ProxyAsyncConfiguration注入的bean AsyncAnnotationBeanPostProcessor,這個BeanPostBeanPostProcessor很顯然會對帶有能夠引發非同步操作的註解(比如@Async)的Bean進行處理
  2. 我們注意到AsyncAnnotationBeanPostProcessor有重寫父類的setBeanFactory,這個方法是不是有點熟悉呢,它是BeanFactoryAware介面中的方法,AsyncAnnotationBeanPostProcessor的父類實現了這個介面,在我們很久之前分析過的Bean的初始化中,是有提到過這個介面的,實現了Aware型別介面的Bean,會在初始化Bean的時候呼叫相應的初始化方法,具體可以檢視AbstractAutowireCapableBeanFactory#initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd)方法
  3. 處理Bean的postProcessAfterInitialization方法在祖先類AbstractAdvisingBeanPostProcessor中。從原始碼中可以看到。AsyncAnnotationBeanPostProcessor是對Bean進行後置處理的BeanPostProcessor
  4. 最後代理到JdkDynamicAopProxy的invoke方法中,是用了責任鏈模式:List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);,將代理進行攔截來執行,通知鏈會包含setBeanFactory()方法生成的通知,執行鏈會用於建立ReflectiveMethodInvocation物件,最終是呼叫ReflectiveMethodInvocationproceed()來完成對方法的增強處理,proceed()方法在這裡會執行最後一個分支
  5. 具體執行的是AsyncExecutionInterceptorinvoke()
  6. 注意:雖然上文Spring環境中只能有一個AsyncConfigurer實現類,但是不意味著,在Spring環境中只能配置一個執行緒池,在Spring環境中是可以配置多個執行緒池,而且我們可以在使用@Async註解進行非同步操作的時候,通過在value屬性上指定執行緒池BeanName,這樣就可以指定相應的執行緒池來作為任務的載體,參見:determineAsyncExecutor

小結兄弟:

當我們想要在SpringBoot中方便的使用@Async註解開啟非同步操作的時候,只需要實現AsyncConfigurer介面(這樣就配置了預設執行緒池配置,當然該類需要在Spring環境中,因為是預設的,所以只能有一個,沒有多個實現類排優先順序的說法),實現對執行緒池的配置,並在啟動類上加上@EnableAsync註解,即可使得@Async註解生效。

我們甚至可以不顯式的實現AsyncConfigurer,我們可以在Spring環境中配置多個Executor型別的Bean,在使用@Async註解時,將註解的value指定為你Executor型別的BeanName,就可以使用指定的執行緒池來作為任務的載體,這樣就使用執行緒池也更加靈活。

相關文章