作者:會煉鋼的小白龍
https://www.cnblogs.com/baixianlong/p/10661591.html
一、Spring Boot中異步請求的使用
1、異步請求與同步請求


特點:
可以先釋放容器分配給請求的執行緒與相關資源,減輕系統負擔,釋放了容器所分配執行緒的請求,其回應將被延后,可以在耗時處理完成(例如長時間的運算)時再對客戶端進行回應,
一句話:增加了服務器對客戶端請求的吞吐量(實際生產上我們用的比較少,如果并發請求量很大的情況下,我們會通過nginx把請求負載到集群服務的各個節點上來分攤請求壓力,當然還可以通過訊息佇列來做請求的緩沖),
2、異步請求的實作
方式一:Servlet方式實作異步請求
@RequestMapping(value = "/email/servletReq", method = GET)
public void servletReq (HttpServletRequest request, HttpServletResponse response) {
AsyncContext asyncContext = request.startAsync();
//設定監聽器:可設定其開始、完成、例外、超時等事件的回呼處理
asyncContext.addListener(new AsyncListener() {
@Override
public void onTimeout(AsyncEvent event) throws IOException {
System.out.println("超時了...");
//做一些超時后的相關操作...
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
System.out.println("執行緒開始");
}
@Override
public void onError(AsyncEvent event) throws IOException {
System.out.println("發生錯誤:"+event.getThrowable());
}
@Override
public void onComplete(AsyncEvent event) throws IOException {
System.out.println("執行完成");
//這里可以做一些清理資源的操作...
}
});
//設定超時時間
asyncContext.setTimeout(20000);
asyncContext.start(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10000);
System.out.println("內部執行緒:" + Thread.currentThread().getName());
asyncContext.getResponse().setCharacterEncoding("utf-8");
asyncContext.getResponse().setContentType("text/html;charset=UTF-8");
asyncContext.getResponse().getWriter().println("這是異步的請求回傳");
} catch (Exception e) {
System.out.println("例外:"+e);
}
//異步請求完成通知
//此時整個請求才完成
asyncContext.complete();
}
});
//此時之類 request的執行緒連接已經釋放了
System.out.println("主執行緒:" + Thread.currentThread().getName());
}
方式二:使用很簡單,直接回傳的引數包裹一層callable即可,可以繼承WebMvcConfigurerAdapter類來設定默認執行緒池和超時處理
@RequestMapping(value = "/email/callableReq", method = GET)
@ResponseBody
public Callable<String> callableReq () {
System.out.println("外部執行緒:" + Thread.currentThread().getName());
return new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(10000);
System.out.println("內部執行緒:" + Thread.currentThread().getName());
return "callable!";
}
};
}
@Configuration
public class RequestAsyncPoolConfig extends WebMvcConfigurerAdapter {
@Resource
private ThreadPoolTaskExecutor myThreadPoolTaskExecutor;
@Override
public void configureAsyncSupport(final AsyncSupportConfigurer configurer) {
//處理 callable超時
configurer.setDefaultTimeout(60*1000);
configurer.setTaskExecutor(myThreadPoolTaskExecutor);
configurer.registerCallableInterceptors(timeoutCallableProcessingInterceptor());
}
@Bean
public TimeoutCallableProcessingInterceptor timeoutCallableProcessingInterceptor() {
return new TimeoutCallableProcessingInterceptor();
}
}
方式三:和方式二差不多,在Callable外包一層,給WebAsyncTask設定一個超時回呼,即可實作超時處理
@RequestMapping(value = "/email/webAsyncReq", method = GET)
@ResponseBody
public WebAsyncTask<String> webAsyncReq () {
System.out.println("外部執行緒:" + Thread.currentThread().getName());
Callable<String> result = () -> {
System.out.println("內部執行緒開始:" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(4);
} catch (Exception e) {
// TODO: handle exception
}
logger.info("副執行緒回傳");
System.out.println("內部執行緒回傳:" + Thread.currentThread().getName());
return "success";
};
WebAsyncTask<String> wat = new WebAsyncTask<String>(3000L, result);
wat.onTimeout(new Callable<String>() {
@Override
public String call() throws Exception {
// TODO Auto-generated method stub
return "超時";
}
});
return wat;
}
方式四:DeferredResult可以處理一些相對復雜一些的業務邏輯,最主要還是可以在另一個執行緒里面進行業務處理及回傳,即可在兩個完全不相干的執行緒間的通信,
@RequestMapping(value = "/email/deferredResultReq", method = GET)
@ResponseBody
public DeferredResult<String> deferredResultReq () {
System.out.println("外部執行緒:" + Thread.currentThread().getName());
//設定超時時間
DeferredResult<String> result = new DeferredResult<String>(60*1000L);
//處理超時事件 采用委托機制
result.onTimeout(new Runnable() {
@Override
public void run() {
System.out.println("DeferredResult超時");
result.setResult("超時了!");
}
});
result.onCompletion(new Runnable() {
@Override
public void run() {
//完成后
System.out.println("呼叫完成");
}
});
myThreadPoolTaskExecutor.execute(new Runnable() {
@Override
public void run() {
//處理業務邏輯
System.out.println("內部執行緒:" + Thread.currentThread().getName());
//回傳結果
result.setResult("DeferredResult!!");
}
});
return result;
}
二、Spring Boot中異步呼叫的使用
1、介紹
異步請求的處理,除了異步請求,一般上我們用的比較多的應該是異步呼叫,通常在開發程序中,會遇到一個方法是和實際業務無關的,沒有緊密性的,比如記錄日志資訊等業務,這個時候正常就是啟一個新執行緒去做一些業務處理,讓主執行緒異步的執行其他業務,
2、使用方式(基于spring下)
需要在啟動類加入@EnableAsync使異步呼叫@Async注解生效
在需要異步執行的方法上加入此注解即可@Async("threadPool"),threadPool為自定義執行緒池,
代碼略,,,就倆標簽,自己試一把就可以了
3、注意事項
在默認情況下,未設定TaskExecutor時,默認是使用SimpleAsyncTaskExecutor這個執行緒池,但此執行緒不是真正意義上的執行緒池,因為執行緒不重用,每次呼叫都會創建一個新的執行緒,可通過控制臺日志輸出可以看出,每次輸出執行緒名都是遞增的,所以最好我們來自定義一個執行緒池,
呼叫的異步方法,不能為同一個類的方法(包括同一個類的內部類),簡單來說,因為Spring在啟動掃描時會為其創建一個代理類,而同類呼叫時,還是呼叫本身的代理類的,所以和平常呼叫是一樣的,
其他的注解如@Cache等也是一樣的道理,說白了,就是Spring的代理機制造成的,所以在開發中,最好把異步服務單獨抽出一個類來管理,下面會重點講述,,
4、什么情況下會導致@Async異步方法會失效?
呼叫同一個類下注有@Async異步方法:
在spring中像@Async和@Transactional、cache等注解本質使用的是動態代理,其實Spring容器在初始化的時候Spring容器會將含有AOP注解的類物件“替換”為代理物件(簡單這么理解),那么注解失效的原因就很明顯了,就是因為呼叫方法的是物件本身而不是代理物件,因為沒有經過Spring容器,那么解決方法也會沿著這個思路來解決,
呼叫的是靜態(static )方法
呼叫(private)私有化方法
5、解決4中問題1的方式(其它2,3兩個問題自己注意下就可以了)
將要異步執行的方法單獨抽取成一個類,原理就是當你把執行異步的方法單獨抽取成一個類的時候,這個類肯定是被Spring管理的,其他Spring組件需要呼叫的時候肯定會注入進去,這時候實際上注入進去的就是代理類了,
其實我們的注入物件都是從Spring容器中給當前Spring組件進行成員變數的賦值,由于某些類使用了AOP注解,那么實際上在Spring容器中實際存在的是它的代理物件,那么我們就可以通過背景關系獲取自己的代理物件呼叫異步方法,
@Controller
@RequestMapping("/app")
public class EmailController {
//獲取ApplicationContext物件方式有多種,這種最簡單,其它的大家自行了解一下
@Autowired
private ApplicationContext applicationContext;
@RequestMapping(value = "/email/asyncCall", method = GET)
@ResponseBody
public Map<String, Object> asyncCall () {
Map<String, Object> resMap = new HashMap<String, Object>();
try{
//這樣呼叫同類下的異步方法是不起作用的
//this.testAsyncTask();
//通過背景關系獲取自己的代理物件呼叫異步方法
EmailController emailController = (EmailController)applicationContext.getBean(EmailController.class);
emailController.testAsyncTask();
resMap.put("code",200);
}catch (Exception e) {
resMap.put("code",400);
logger.error("error!",e);
}
return resMap;
}
//注意一定是public,且是非static方法
@Async
public void testAsyncTask() throws InterruptedException {
Thread.sleep(10000);
System.out.println("異步任務執行完成!");
}
}
開啟cglib代理,手動獲取Spring代理類,從而呼叫同類下的異步方法,首先,在啟動類上加上@EnableAspectJAutoProxy(exposeProxy = true)注解,代碼實作,如下:
@Service
@Transactional(value = "transactionManager", readOnly = false, propagation = Propagation.REQUIRED, rollbackFor = Throwable.class)
public class EmailService {
@Autowired
private ApplicationContext applicationContext;
@Async
public void testSyncTask() throws InterruptedException {
Thread.sleep(10000);
System.out.println("異步任務執行完成!");
}
public void asyncCallTwo() throws InterruptedException {
//this.testSyncTask();
// EmailService emailService = (EmailService)applicationContext.getBean(EmailService.class);
// emailService.testSyncTask();
boolean isAop = AopUtils.isAopProxy(EmailController.class);//是否是代理物件;
boolean isCglib = AopUtils.isCglibProxy(EmailController.class); //是否是CGLIB方式的代理物件;
boolean isJdk = AopUtils.isJdkDynamicProxy(EmailController.class); //是否是JDK動態代理方式的代理物件;
//以下才是重點!!!
EmailService emailService = (EmailService)applicationContext.getBean(EmailService.class);
EmailService proxy = (EmailService) AopContext.currentProxy();
System.out.println(emailService == proxy ? true : false);
proxy.testSyncTask();
System.out.println("end!!!");
}
}
三、異步請求與異步呼叫的區別
兩者的使用場景不同,異步請求用來解決并發請求對服務器造成的壓力,從而提高對請求的吞吐量;而異步呼叫是用來做一些非主線流程且不需要實時計算和回應的任務,比如同步日志到kafka中做日志分析等,
異步請求是會一直等待response相應的,需要回傳結果給客戶端的;而異步呼叫我們往往會馬上回傳給客戶端回應,完成這次整個的請求,至于異步呼叫的任務后臺自己慢慢跑就行,客戶端不會關心,
四、總結
異步請求和異步呼叫的使用到這里基本就差不多了,有問題還希望大家多多指出,這邊文章提到了動態代理,而spring中Aop的實作原理就是動態代理,后續會對動態代理做詳細解讀,還望多多支持哈~
近期熱文推薦:
1.Java 15 正式發布, 14 個新特性,重繪你的認知!!
2.終于靠開源專案弄到 IntelliJ IDEA 激活碼了,真香!
3.我用 Java 8 寫了一段邏輯,同事直呼看不懂,你試試看,,
4.吊打 Tomcat ,Undertow 性能很炸!!
5.《Java開發手冊(嵩山版)》最新發布,速速下載!
覺得不錯,別忘了隨手點贊+轉發哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/182714.html
標籤:Java
