Servlet3.0新特性:異步處理,太好用了?。?!
最近有粉絲問(wèn)我,講 springboot 為什么需要從 servlet 說(shuō)起,在這里給大家解釋一下:servlet 屬于非?;A(chǔ)的知識(shí),可能現(xiàn)在開(kāi)發(fā)中很少直接用 servlet 了,但是 springmvc 就是在 servlet 的基礎(chǔ)上整起來(lái)的,所以基礎(chǔ)的東西必須要吃透,基礎(chǔ)扎實(shí)了,其他的就很容易了,還有 spring 系列還未學(xué)完的同學(xué),最近趕緊回頭去補(bǔ)補(bǔ),spring 系列吃透之后,springboot 就是小菜一碟了,springboot 中的一切技術(shù)都源于 spring。
spring系列地址:https://mp.weixin.qq.com/s/E7wNLtU-453b9YC3XoUvqQ
好了,咱們繼續(xù)今天的內(nèi)容。
springmvc 中的 controller 支持異步處理的功能,不知大家是否有接觸過(guò),其內(nèi)部原理是依靠 servlet 中的異步實(shí)現(xiàn)的,所以咱們需要先了解 servlet 中的異步處理。
1、早期 servlet 請(qǐng)求處理流程
servlet3.0 之前,一個(gè)請(qǐng)求過(guò)來(lái)之后,處理過(guò)程如下圖:

從上圖可以看出:請(qǐng)求過(guò)來(lái)后,從主線程池獲取一個(gè)線程,處理業(yè)務(wù),響應(yīng)請(qǐng)求,然后將線程還回線程池,整個(gè)過(guò)程都是由同一個(gè)主線程在執(zhí)行。
這里存在一個(gè)問(wèn)題,通常 web 容器中的主線程數(shù)量是有限的,若執(zhí)行業(yè)務(wù)的比較耗時(shí),大量請(qǐng)求過(guò)來(lái)之后,主線程被耗光,新來(lái)的請(qǐng)求就會(huì)處于等待狀態(tài)。
而 servlet3.0 中對(duì)這個(gè)過(guò)程做了改進(jìn),主線程可以將請(qǐng)求轉(zhuǎn)交給其他線程去處理,比如開(kāi)發(fā)者可以自定義一個(gè)線程,然后在自定義的線程中處理請(qǐng)求。
2、servlet3.0 異步處理流程
如下圖:
在主線程中開(kāi)啟異步處理,主線程將請(qǐng)求交給其他線程去處理,主線程就結(jié)束了,被放回了主線程池,由其他線程繼續(xù)處理請(qǐng)求。

可能有些朋友會(huì)說(shuō),直接提升主線程的數(shù)量不就可以了么?
老鐵們,確實(shí)可以,但是咱們的目標(biāo)是使用最少的線程做更多的事情。
異步處理的流程適合業(yè)務(wù)處理比較耗時(shí)而導(dǎo)致主線程長(zhǎng)時(shí)間等待的場(chǎng)景,稍后我會(huì)給大家上一些案例。
下面咱們來(lái)看看 servlet 中異步處理如何使用?
3、servlet3.0 中異步處理使用步驟
step1:開(kāi)啟異步支持
設(shè)置@WebServlet 的 asyncSupported 屬性為 true,表示支持異步處理
@WebServlet(asyncSupported = true)
step2:?jiǎn)?dòng)異步請(qǐng)求
啟動(dòng)異步處理:調(diào)用 req.startAsync(request,response)方法,獲取異步處理上下文對(duì)象 AsyncContext
AsyncContext asyncContext = request.startAsync(request, response);
step3:異步處理業(yè)務(wù)&完成異步處理
其他線程中執(zhí)行業(yè)務(wù)操作,輸出結(jié)果,并調(diào)用 asyncContext.complete()完成異步處理,比如下面 2 種方式:
方式 1:?jiǎn)?dòng)一個(gè)新的線程來(lái)處理請(qǐng)求,代碼如下:
new Thread(()->{
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-start");
try {
//這里休眠2秒,模擬業(yè)務(wù)耗時(shí)
TimeUnit.SECONDS.sleep(2);
//這里是子線程,請(qǐng)求在這里被處理了
asyncContext.getResponse().getWriter().write("ok");
//調(diào)用complete()方法,表示請(qǐng)求請(qǐng)求處理完成
asyncContext.complete();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end");
})
方式 2:如下代碼,調(diào)用 asyncContext.start 方法來(lái)處理請(qǐng)求,傳遞的是一個(gè) Runnable 對(duì)象,asyncContext.start 會(huì)將傳遞的 Runnable 放在新的線程中去執(zhí)行
asyncContext.start(() -> {
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-start");
try {
//這里休眠2秒,模擬業(yè)務(wù)耗時(shí)
TimeUnit.SECONDS.sleep(2);
//這里是子線程,請(qǐng)求在這里被處理了
asyncContext.getResponse().getWriter().write("ok");
//5、調(diào)用complete()方法,表示請(qǐng)求請(qǐng)求處理完成
asyncContext.complete();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end");
});
下面來(lái)看幾個(gè)案例,案例是精華,通過(guò)案例可以全面掌握異步處理。
4、案例 1:使用 asyncContext.start 處理異步請(qǐng)求
下面案例代碼會(huì)輸出 4 條日志,注意日志中包含的信息:時(shí)間、線程信息、耗時(shí),通過(guò)這些信息可以分析主線程什么時(shí)候結(jié)束的。
package com.javacode2018.springboot.lesson002.demo1;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
//1.設(shè)置@WebServlet的asyncSupported屬性為true,表示支持異步處理
@WebServlet(name = "AsyncServlet1",
urlPatterns = "/asyncServlet1",
asyncSupported = true
)
public class AsyncServlet1 extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
long st = System.currentTimeMillis();
System.out.println("主線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-start");
//2、啟動(dòng)異步處理:調(diào)用req.startAsync(request,response)方法,獲取異步處理上下文對(duì)象AsyncContext
AsyncContext asyncContext = request.startAsync(request, response);
//3、調(diào)用start方法異步處理,調(diào)用這個(gè)方法之后主線程就結(jié)束了
asyncContext.start(() -> {
long stSon = System.currentTimeMillis();
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-start");
try {
//這里休眠2秒,模擬業(yè)務(wù)耗時(shí)
TimeUnit.SECONDS.sleep(2);
//這里是子線程,請(qǐng)求在這里被處理了
asyncContext.getResponse().getWriter().write("ok");
//4、調(diào)用complete()方法,表示請(qǐng)求請(qǐng)求處理完成
asyncContext.complete();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end,耗時(shí)(ms):" + (System.currentTimeMillis() - stSon));
});
System.out.println("主線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end,耗時(shí)(ms):" + (System.currentTimeMillis() - st));
}
}
發(fā)布到 tomcat 中,瀏覽器中訪問(wèn)下面地址:
http://localhost:8080/asyncServlet1
tomcat 控制臺(tái)輸出
主線程:Thread[http-nio-8080-exec-5,5,main]-1617373260994-start
主線程:Thread[http-nio-8080-exec-5,5,main]-1617373260994-end,耗時(shí)(ms):0
子線程:Thread[http-nio-8080-exec-6,5,main]-1617373260994-start
子線程:Thread[http-nio-8080-exec-6,5,main]-1617373262995-end,耗時(shí)(ms):2001
主線程耗時(shí) 0 毫秒,并不是耗時(shí)是 0,而是小于 1 毫秒,太快了,子線程中 sleep 了 2 秒,所以耗時(shí)是 2000 毫秒。
大家注意看下瀏覽器中的請(qǐng)求,在asyncContext.complete();被調(diào)用之前,瀏覽器中的請(qǐng)求一直處于阻塞狀態(tài),當(dāng)這個(gè)方法執(zhí)行完畢之后,瀏覽器端才會(huì)受到響應(yīng)。如果沒(méi)有asyncContext.complete();這行代碼,請(qǐng)求等上一段時(shí)間會(huì)超時(shí),異步請(qǐng)求是默認(rèn)是有超時(shí)時(shí)間的,tomcat 默認(rèn)是 30 秒,大家可以試試,在瀏覽器中通過(guò) F12 可以看到 30 秒后會(huì)響應(yīng)超時(shí)。
5、案例 2:自定義線程處理異步請(qǐng)求
案例 1 中,我們使用asyncContext.start來(lái)處理異步請(qǐng)求,start 方法內(nèi)部會(huì)使用 web 容器中默認(rèn)的線程池來(lái)處理請(qǐng)求,我們也可以自定義線程來(lái)處理異步請(qǐng)求,將案例 1 中asyncContext.start代碼替換為下面代碼,大家也可以自定義一個(gè)線程池,將請(qǐng)求丟到線程池中去處理。
//3、自定義一個(gè)線程來(lái)處理異步請(qǐng)求
Thread thread = new Thread(() -> {
long stSon = System.currentTimeMillis();
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-start");
try {
//這里休眠2秒,模擬業(yè)務(wù)耗時(shí)
TimeUnit.SECONDS.sleep(2);
//這里是子線程,請(qǐng)求在這里被處理了
asyncContext.getResponse().getWriter().write(System.currentTimeMillis() + ",ok");
//4、調(diào)用complete()方法,表示異步請(qǐng)求處理完成
asyncContext.complete();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end,耗時(shí)(ms):" + (System.currentTimeMillis() - stSon));
});
thread.setName("自定義線程");
thread.start();
6、案例 3:通過(guò) asyncContext.dispatch 結(jié)束異步請(qǐng)求
上面 2 個(gè)案例都是通過(guò)asyncContext.complete()來(lái)結(jié)束異步請(qǐng)求的,結(jié)束請(qǐng)求還有另外一種方式,子線程中處理完畢業(yè)務(wù)之后,將結(jié)果放在 request 中,然后調(diào)用asyncContext.dispatch()轉(zhuǎn)發(fā)請(qǐng)求,此時(shí)請(qǐng)求又會(huì)進(jìn)入當(dāng)前 servlet,此時(shí)需在代碼中判斷請(qǐng)求是不是異步轉(zhuǎn)發(fā)過(guò)來(lái)的,如果是的,則從 request 中獲取結(jié)果,然后輸出,這種方式就是 springmvc 處理異步的方式,所以這種看懂了,springmvc 就一目了然了,代碼如下
package com.javacode2018.springboot.lesson002.demo1;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
//1.設(shè)置@WebServlet的asyncSupported屬性為true,表示支持異步處理
@WebServlet(name = "AsyncServlet3",
urlPatterns = "/asyncServlet3",
asyncSupported = true
)
public class AsyncServlet3 extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("請(qǐng)求類型:" + request.getDispatcherType());
//@1:判斷請(qǐng)求類型,如果是異步類型(DispatcherType.ASYNC),則說(shuō)明是異步轉(zhuǎn)發(fā)過(guò)來(lái)的,將結(jié)果輸出
if (request.getDispatcherType() == DispatcherType.ASYNC) {
System.out.println("響應(yīng)結(jié)果:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-start");
//從request中獲取結(jié)果,然后輸出
Object result = request.getAttribute("result");
response.getWriter().write(result.toString());
System.out.println("響應(yīng)結(jié)果:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end");
} else {
long st = System.currentTimeMillis();
System.out.println("主線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-start");
//2、啟動(dòng)異步處理:調(diào)用req.startAsync(request,response)方法,獲取異步處理上下文對(duì)象AsyncContext
AsyncContext asyncContext = request.startAsync(request, response);
//3、調(diào)用start方法異步處理,調(diào)用這個(gè)方法之后主線程就結(jié)束了
asyncContext.start(() -> {
long stSon = System.currentTimeMillis();
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-start");
try {
//這里休眠2秒,模擬業(yè)務(wù)耗時(shí)
TimeUnit.SECONDS.sleep(2);
//將結(jié)果丟到request中
asyncContext.getRequest().setAttribute("result", "ok");
//轉(zhuǎn)發(fā)請(qǐng)求,調(diào)用這個(gè)方法之后,請(qǐng)求又會(huì)被轉(zhuǎn)發(fā)到當(dāng)前的servlet,又會(huì)進(jìn)入當(dāng)前servlet的service方法
//此時(shí)請(qǐng)求的類型(request.getDispatcherType())是DispatcherType.ASYNC,所以通過(guò)這個(gè)值可以判斷請(qǐng)求是異步轉(zhuǎn)發(fā)過(guò)來(lái)的
//然后在request中將結(jié)果取出,對(duì)應(yīng)代碼@1,然后輸出
asyncContext.dispatch();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end,耗時(shí)(ms):" + (System.currentTimeMillis() - stSon));
});
System.out.println("主線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end,耗時(shí)(ms):" + (System.currentTimeMillis() - st));
}
}
}
瀏覽器中訪問(wèn)
http://localhost:8080/asyncServlet3
tomcat 控制臺(tái)輸出
請(qǐng)求類型:REQUEST
主線程:Thread[http-nio-8080-exec-1,5,main]-1617375432076-start
主線程:Thread[http-nio-8080-exec-1,5,main]-1617375432084-end,耗時(shí)(ms):8
子線程:Thread[http-nio-8080-exec-2,5,main]-1617375432084-start
子線程:Thread[http-nio-8080-exec-2,5,main]-1617375434092-end,耗時(shí)(ms):2008
請(qǐng)求類型:ASYNC
響應(yīng)結(jié)果:Thread[http-nio-8080-exec-3,5,main]-1617375434100-start
響應(yīng)結(jié)果:Thread[http-nio-8080-exec-3,5,main]-1617375434102-end
7、案例 4:設(shè)置異步處理超時(shí)時(shí)間
異步請(qǐng)求總不能讓他一直執(zhí)行吧,所以咱們可以設(shè)置超時(shí)時(shí)間。
asyncContext.setTimeout(超時(shí)時(shí)間,毫秒,默認(rèn)是30秒);
我們案例 1 的代碼進(jìn)行改造,添加一行代碼,如下,設(shè)置超時(shí)時(shí)間為 1 秒

然后瀏覽器中訪問(wèn)一下請(qǐng)求,可以看到超時(shí)了,如下

對(duì)應(yīng)的案例源碼
package com.javacode2018.springboot.lesson002.demo1;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
//1.設(shè)置@WebServlet的asyncSupported屬性為true,表示支持異步處理
@WebServlet(name = "AsyncServlet4",
urlPatterns = "/asyncServlet4",
asyncSupported = true
)
public class AsyncServlet4 extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
long st = System.currentTimeMillis();
System.out.println("主線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-start");
//2、啟動(dòng)異步處理:調(diào)用req.startAsync(request,response)方法,獲取異步處理上下文對(duì)象AsyncContext
AsyncContext asyncContext = request.startAsync(request, response);
//設(shè)置異步處理超時(shí)時(shí)間為1秒
asyncContext.setTimeout(1000);
//3、調(diào)用start方法異步處理,調(diào)用這個(gè)方法之后主線程就結(jié)束了
asyncContext.start(() -> {
long stSon = System.currentTimeMillis();
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-start");
try {
//這里休眠2秒,模擬業(yè)務(wù)耗時(shí)
TimeUnit.SECONDS.sleep(2);
//這里是子線程,請(qǐng)求在這里被處理了
asyncContext.getResponse().getWriter().write(System.currentTimeMillis() + ",ok");
//4、調(diào)用complete()方法,表示異步請(qǐng)求處理完成
asyncContext.complete();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end,耗時(shí)(ms):" + (System.currentTimeMillis() - stSon));
});
System.out.println("主線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end,耗時(shí)(ms):" + (System.currentTimeMillis() - st));
}
}
8、案例 5:設(shè)置監(jiān)聽(tīng)器
還可以為異步處理添加監(jiān)聽(tīng)器,當(dāng)異步處理完成、發(fā)生異常錯(cuò)誤、出現(xiàn)超時(shí)的時(shí)候,會(huì)回調(diào)監(jiān)聽(tīng)器中對(duì)應(yīng)的方法,如下:
//添加監(jiān)聽(tīng)器
asyncContext.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) throws IOException {
//異步處理完成會(huì)被回調(diào)
event.getAsyncContext().getResponse().getWriter().write("
onComplete");
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
//超時(shí)會(huì)被回調(diào)
event.getAsyncContext().getResponse().getWriter().write("
onTimeout");
}
@Override
public void onError(AsyncEvent event) throws IOException {
//發(fā)生錯(cuò)誤會(huì)被回調(diào)
event.getAsyncContext().getResponse().getWriter().write("
onError");
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
//開(kāi)啟異步請(qǐng)求調(diào)用的方法
event.getAsyncContext().getResponse().getWriter().write("
onStartAsync");
}
});
案例代碼如下,代碼@1通過(guò)請(qǐng)求參數(shù)中的 timeout 來(lái)控制超時(shí)時(shí)間,@2中讓異步處理休眠了 2 秒,稍后我們會(huì)模擬超時(shí)和不超時(shí)兩種情況,大家注意關(guān)注 tomcat 控制臺(tái)日志及瀏覽器中日志,可以看到監(jiān)聽(tīng)器中哪些方法會(huì)被調(diào)用。
package com.javacode2018.springboot.lesson002.demo1;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.AsyncEvent;
import jakarta.servlet.AsyncListener;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
//1.設(shè)置@WebServlet的asyncSupported屬性為true,表示支持異步處理
@WebServlet(name = "AsyncServlet5",
urlPatterns = "/asyncServlet5",
asyncSupported = true
)
public class AsyncServlet5 extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
long st = System.currentTimeMillis();
System.out.println("主線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-start");
//2、啟動(dòng)異步處理:調(diào)用req.startAsync(request,response)方法,獲取異步處理上下文對(duì)象AsyncContext
AsyncContext asyncContext = request.startAsync(request, response);
response.setContentType("text/html;charset=UTF-8");
//@1:設(shè)置異步處理超時(shí)時(shí)間
Long timeout = Long.valueOf(request.getParameter("timeout"));
asyncContext.setTimeout(timeout);
//添加監(jiān)聽(tīng)器
asyncContext.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) throws IOException {
//異步處理完成會(huì)被回調(diào)
System.out.println(Thread.currentThread() + "-" + System.currentTimeMillis() + "-onComplete()");
event.getAsyncContext().getResponse().getWriter().write("
onComplete");
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
//超時(shí)會(huì)被回調(diào)
System.out.println(Thread.currentThread() + "-" + System.currentTimeMillis() + "-onTimeout()");
event.getAsyncContext().getResponse().getWriter().write("
onTimeout");
}
@Override
public void onError(AsyncEvent event) throws IOException {
//發(fā)生錯(cuò)誤會(huì)被回調(diào)
System.out.println(Thread.currentThread() + "-" + System.currentTimeMillis() + "-onError()");
event.getAsyncContext().getResponse().getWriter().write("
onError");
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
//開(kāi)啟異步請(qǐng)求調(diào)用的方法
System.out.println(Thread.currentThread() + "-" + System.currentTimeMillis() + "-onStartAsync()");
event.getAsyncContext().getResponse().getWriter().write("
onStartAsync");
}
});
//3、調(diào)用start方法異步處理,調(diào)用這個(gè)方法之后主線程就結(jié)束了
asyncContext.start(() -> {
long stSon = System.currentTimeMillis();
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-start");
try {
//@2:這里休眠2秒,模擬業(yè)務(wù)耗時(shí)
TimeUnit.SECONDS.sleep(2);
//這里是子線程,請(qǐng)求在這里被處理了
asyncContext.getResponse().getWriter().write(System.currentTimeMillis() + ",ok");
//4、調(diào)用complete()方法,表示異步請(qǐng)求處理完成
asyncContext.complete();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end,耗時(shí)(ms):" + (System.currentTimeMillis() - stSon));
});
System.out.println("主線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end,耗時(shí)(ms):" + (System.currentTimeMillis() - st));
}
}
模擬非超時(shí)請(qǐng)求,訪問(wèn)下面地址
http://localhost:8080/asyncServlet5?timeout=5000
輸出:

tomcat 控制臺(tái)輸出,可以看出 onComplete 被調(diào)用了。

下面模擬超時(shí)請(qǐng)求,訪問(wèn)下面地址
http://localhost:8080/asyncServlet5?timeout=100

tomcat 控制臺(tái)輸出
主線程:Thread[http-nio-8080-exec-8,5,main]-1617378344250-start
主線程:Thread[http-nio-8080-exec-8,5,main]-1617378344251-end,耗時(shí)(ms):1
子線程:Thread[http-nio-8080-exec-9,5,main]-1617378344251-start
Thread[http-nio-8080-exec-10,5,main]-1617378344634-onTimeout()
Thread[http-nio-8080-exec-10,5,main]-1617378344634-onComplete()
java.lang.IllegalStateException: AsyncContext關(guān)聯(lián)的請(qǐng)求已經(jīng)完成處理。
at org.apache.catalina.core.AsyncContextImpl.check(AsyncContextImpl.java:522)
at org.apache.catalina.core.AsyncContextImpl.getResponse(AsyncContextImpl.java:228)
at com.javacode2018.springboot.lesson002.demo1.AsyncServlet5.lambda$service$0(AsyncServlet5.java:70)
at org.apache.catalina.core.AsyncContextImpl$RunnableWrapper.run(AsyncContextImpl.java:548)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
子線程:Thread[http-nio-8080-exec-9,5,main]-1617378346260-end,耗時(shí)(ms):2009
代碼中出現(xiàn)了異常,為什么?
是因?yàn)榘l(fā)生超時(shí)的時(shí)候,onTimeOut 方法執(zhí)行完畢之后,異步處理就結(jié)束了,此時(shí),子線程還在運(yùn)行,子線程執(zhí)行到下面這樣代碼,向客戶端輸出信息,所以報(bào)錯(cuò)了。
asyncContext.getResponse().getWriter().write(System.currentTimeMillis() + ",ok");
9、案例 6:對(duì)案例 5 進(jìn)行改造
對(duì)案例 5 進(jìn)行改造,如下代碼,看一下@3處的代碼,通過(guò)一個(gè)原子變量來(lái)控制請(qǐng)求是否處理完畢了,代碼中有 3 處可能會(huì)修改這個(gè)變量,通過(guò) cas 操作來(lái)控制誰(shuí)會(huì)修改成功,修改成功者,將結(jié)果設(shè)置到 request.setAttribute 中,然后調(diào)用asyncContext.dispatch();轉(zhuǎn)發(fā)請(qǐng)求,這種處理方式很好的解決案例 5 中異常問(wèn)題,springmvc 中異步處理過(guò)程這個(gè)過(guò)程類似,所以這段代碼大家一定要好好看看,若能夠理解,springmvc 中異步處理的代碼可以秒懂。
package com.javacode2018.springboot.lesson002.demo1;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
//1.設(shè)置@WebServlet的asyncSupported屬性為true,表示支持異步處理
@WebServlet(name = "AsyncServlet6",
urlPatterns = "/asyncServlet6",
asyncSupported = true
)
public class AsyncServlet6 extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
if (request.getDispatcherType() == DispatcherType.ASYNC) {
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write(request.getAttribute("result").toString());
} else {
long st = System.currentTimeMillis();
System.out.println("主線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-start");
//2、啟動(dòng)異步處理:調(diào)用req.startAsync(request,response)方法,獲取異步處理上下文對(duì)象AsyncContext
AsyncContext asyncContext = request.startAsync(request, response);
//@1:設(shè)置異步處理超時(shí)時(shí)間
Long timeout = Long.valueOf(request.getParameter("timeout"));
asyncContext.setTimeout(timeout);
//@3:用來(lái)異步處理是否結(jié)束,在這3個(gè)地方(子線程中處理完畢時(shí)、onComplete、onTimeout)將其更新為true
AtomicBoolean finish = new AtomicBoolean(false);
//添加監(jiān)聽(tīng)器
asyncContext.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) throws IOException {
//異步處理完成會(huì)被回調(diào)
System.out.println(Thread.currentThread() + "-" + System.currentTimeMillis() + "-onComplete()");
if (finish.compareAndSet(false, true)) {
event.getAsyncContext().getRequest().setAttribute("result", "onComplete");
//轉(zhuǎn)發(fā)請(qǐng)求
asyncContext.dispatch();
}
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
//超時(shí)會(huì)被回調(diào)
System.out.println(Thread.currentThread() + "-" + System.currentTimeMillis() + "-onTimeout()");
if (finish.compareAndSet(false, true)) {
event.getAsyncContext().getRequest().setAttribute("result", "onTimeout");
//轉(zhuǎn)發(fā)請(qǐng)求
asyncContext.dispatch();
}
}
@Override
public void onError(AsyncEvent event) throws IOException {
//發(fā)生錯(cuò)誤會(huì)被回調(diào)
System.out.println(Thread.currentThread() + "-" + System.currentTimeMillis() + "-onError()");
event.getAsyncContext().getResponse().getWriter().write("
onError");
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
//開(kāi)啟異步請(qǐng)求調(diào)用的方法
System.out.println(Thread.currentThread() + "-" + System.currentTimeMillis() + "-onStartAsync()");
event.getAsyncContext().getResponse().getWriter().write("
onStartAsync");
}
});
//3、調(diào)用start方法異步處理,調(diào)用這個(gè)方法之后主線程就結(jié)束了
asyncContext.start(() -> {
long stSon = System.currentTimeMillis();
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-start");
try {
//@2:這里休眠2秒,模擬業(yè)務(wù)耗時(shí)
TimeUnit.SECONDS.sleep(2);
if (finish.compareAndSet(false, true)) {
asyncContext.getRequest().setAttribute("result", "ok");
//轉(zhuǎn)發(fā)請(qǐng)求
asyncContext.dispatch();
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end,耗時(shí)(ms):" + (System.currentTimeMillis() - stSon));
});
System.out.println("主線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end,耗時(shí)(ms):" + (System.currentTimeMillis() - st));
}
}
}
模擬超時(shí)請(qǐng)求
http://localhost:8080/asyncServlet6?timeout=100

tomcat 控制臺(tái)輸出

模擬非超時(shí)請(qǐng)求
http://localhost:8080/asyncServlet6?timeout=5000

tomcat 控制臺(tái)輸出
主線程:Thread[http-nio-8080-exec-6,5,main]-1617379567665-start
主線程:Thread[http-nio-8080-exec-6,5,main]-1617379567666-end,耗時(shí)(ms):1
子線程:Thread[http-nio-8080-exec-6,5,main]-1617379567667-start
子線程:Thread[http-nio-8080-exec-6,5,main]-1617379569667-end,耗時(shí)(ms):2000
Thread[http-nio-8080-exec-10,5,main]-1617379569668-onComplete()
10、案例 7:模擬一個(gè)業(yè)務(wù)場(chǎng)景
業(yè)務(wù)場(chǎng)景
ServiceA 接受到一個(gè)請(qǐng)求之后,將請(qǐng)求發(fā)送到 mq,然后主線程就結(jié)束了,另外一個(gè)服務(wù) ServiceB 從 mq 中取出這條消息,然后對(duì)消息進(jìn)行處理,將處理結(jié)果又丟到 mq 中,ServiceA 中監(jiān)聽(tīng)器監(jiān)聽(tīng) mq 中的結(jié)果,然后將結(jié)果再輸出。
案例代碼
package com.javacode2018.springboot.lesson002.demo1;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
//1.設(shè)置@WebServlet的asyncSupported屬性為true,表示支持異步處理
@WebServlet(name = "AsyncServlet7",
urlPatterns = "/asyncServlet7",
asyncSupported = true
)
public class AsyncServlet7 extends HttpServlet {
Map orderIdAsyncContextMap = new ConcurrentHashMap<>();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String orderId = request.getParameter("orderId");
String result = request.getParameter("result");
AsyncContext async;
if (orderId != null && result != null && (async = orderIdAsyncContextMap.get(orderId)) != null) {
async.getResponse().getWriter().write(String.format("
" +
"%s:%s:result:%s", Thread.currentThread(), System.currentTimeMillis(), result));
async.complete();
} else {
AsyncContext asyncContext = request.startAsync(request, response);
orderIdAsyncContextMap.put("1", asyncContext);
asyncContext.getResponse().setContentType("text/html;charset=utf-8");
asyncContext.getResponse().getWriter().write(String.format("%s:%s:%s", Thread.currentThread(), System.currentTimeMillis(), "start"));
}
}
}
測(cè)試過(guò)程
step1、啟動(dòng)項(xiàng)目
step2、瀏覽器中訪問(wèn):http://localhost:8080/asyncServlet7,會(huì)發(fā)現(xiàn)瀏覽器中請(qǐng)求一直處于等待中
step3、等待5秒,用來(lái)模擬ServiceB處理耗時(shí)
step4、瀏覽器中訪問(wèn):http://localhost:8080/asyncServlet7?orderId=1&result=success;用來(lái)模擬將結(jié)果通知給請(qǐng)求者,這步執(zhí)行完畢之后,step2會(huì)立即收到響應(yīng)

這里稍微擴(kuò)展下
可能有些朋友已經(jīng)想到了,通常我們的項(xiàng)目是集群部署的,假如這個(gè)業(yè)務(wù)場(chǎng)景中 ServiceA 是集群部署的,有 3 臺(tái)機(jī)器【ServiceA1、ServiceA2、ServiceA3】,如果 ServiceB 將處理完成的結(jié)果消息丟到 mq 后,如果消息類型是點(diǎn)對(duì)點(diǎn)的,那么消息只能被一臺(tái)機(jī)器消費(fèi),需要確保 ServiceA 中接受用戶請(qǐng)求的機(jī)器和最終接受 mq 中消息結(jié)果的機(jī)器是一臺(tái)機(jī)器才可以,如果接受請(qǐng)求的機(jī)器是 ServceA1,而消費(fèi)結(jié)果消息的機(jī)器是 ServiceA2,那么 ServiceA1 就一直拿不到結(jié)果,直到超時(shí),如何解決?
此時(shí)需要廣播消息,ServiceB 將處理結(jié)果廣播出去,ServiceA 所有機(jī)器都會(huì)監(jiān)聽(tīng)到這條廣播消息。
可以使用 redis 的發(fā)布訂閱功能解決這個(gè)問(wèn)題,有興趣的朋友可以研究一下 redis 發(fā)布定義的功能。
11、總結(jié)
開(kāi)啟異步處理:request.startAsync(request,response) 獲取異步處理上下文對(duì)象 AsyncContext 設(shè)置異步處理超時(shí)時(shí)間:asyncContext.setTimeout(毫秒) 設(shè)置異步處理監(jiān)聽(tīng)器:asyncContext.addListener,可以添加多個(gè)監(jiān)聽(tīng)器 完成異步處理的 2 種方式:asyncContext.dispatch() 或 asyncContext.complete()
12、源碼
https://gitee.com/javacode2018/springboot-series

歡迎關(guān)注微信公眾號(hào):互聯(lián)網(wǎng)全棧架構(gòu),收取更多有價(jià)值的信息。
