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

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

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

然后瀏覽器中訪問一下請求,可以看到超時了,如下

對應(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、啟動異步處理:調(diào)用req.startAsync(request,response)方法,獲取異步處理上下文對象AsyncContext
AsyncContext asyncContext = request.startAsync(request, response);
//設(shè)置異步處理超時時間為1秒
asyncContext.setTimeout(1000);
//3、調(diào)用start方法異步處理,調(diào)用這個方法之后主線程就結(jié)束了
asyncContext.start(() -> {
long stSon = System.currentTimeMillis();
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-start");
try {
//這里休眠2秒,模擬業(yè)務(wù)耗時
TimeUnit.SECONDS.sleep(2);
//這里是子線程,請求在這里被處理了
asyncContext.getResponse().getWriter().write(System.currentTimeMillis() + ",ok");
//4、調(diào)用complete()方法,表示異步請求處理完成
asyncContext.complete();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end,耗時(ms):" + (System.currentTimeMillis() - stSon));
});
System.out.println("主線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end,耗時(ms):" + (System.currentTimeMillis() - st));
}
}
8、案例 5:設(shè)置監(jiān)聽器
還可以為異步處理添加監(jiān)聽器,當(dāng)異步處理完成、發(fā)生異常錯誤、出現(xiàn)超時的時候,會回調(diào)監(jiān)聽器中對應(yīng)的方法,如下:
//添加監(jiān)聽器
asyncContext.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) throws IOException {
//異步處理完成會被回調(diào)
event.getAsyncContext().getResponse().getWriter().write("
onComplete");
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
//超時會被回調(diào)
event.getAsyncContext().getResponse().getWriter().write("
onTimeout");
}
@Override
public void onError(AsyncEvent event) throws IOException {
//發(fā)生錯誤會被回調(diào)
event.getAsyncContext().getResponse().getWriter().write("
onError");
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
//開啟異步請求調(diào)用的方法
event.getAsyncContext().getResponse().getWriter().write("
onStartAsync");
}
});
案例代碼如下,代碼@1通過請求參數(shù)中的 timeout 來控制超時時間,@2中讓異步處理休眠了 2 秒,稍后我們會模擬超時和不超時兩種情況,大家注意關(guān)注 tomcat 控制臺日志及瀏覽器中日志,可以看到監(jiān)聽器中哪些方法會被調(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、啟動異步處理:調(diào)用req.startAsync(request,response)方法,獲取異步處理上下文對象AsyncContext
AsyncContext asyncContext = request.startAsync(request, response);
response.setContentType("text/html;charset=UTF-8");
//@1:設(shè)置異步處理超時時間
Long timeout = Long.valueOf(request.getParameter("timeout"));
asyncContext.setTimeout(timeout);
//添加監(jiān)聽器
asyncContext.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) throws IOException {
//異步處理完成會被回調(diào)
System.out.println(Thread.currentThread() + "-" + System.currentTimeMillis() + "-onComplete()");
event.getAsyncContext().getResponse().getWriter().write("
onComplete");
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
//超時會被回調(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ā)生錯誤會被回調(diào)
System.out.println(Thread.currentThread() + "-" + System.currentTimeMillis() + "-onError()");
event.getAsyncContext().getResponse().getWriter().write("
onError");
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
//開啟異步請求調(diào)用的方法
System.out.println(Thread.currentThread() + "-" + System.currentTimeMillis() + "-onStartAsync()");
event.getAsyncContext().getResponse().getWriter().write("
onStartAsync");
}
});
//3、調(diào)用start方法異步處理,調(diào)用這個方法之后主線程就結(jié)束了
asyncContext.start(() -> {
long stSon = System.currentTimeMillis();
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-start");
try {
//@2:這里休眠2秒,模擬業(yè)務(wù)耗時
TimeUnit.SECONDS.sleep(2);
//這里是子線程,請求在這里被處理了
asyncContext.getResponse().getWriter().write(System.currentTimeMillis() + ",ok");
//4、調(diào)用complete()方法,表示異步請求處理完成
asyncContext.complete();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end,耗時(ms):" + (System.currentTimeMillis() - stSon));
});
System.out.println("主線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end,耗時(ms):" + (System.currentTimeMillis() - st));
}
}
模擬非超時請求,訪問下面地址
http://localhost:8080/asyncServlet5?timeout=5000
輸出:

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

下面模擬超時請求,訪問下面地址
http://localhost:8080/asyncServlet5?timeout=100

tomcat 控制臺輸出
主線程:Thread[http-nio-8080-exec-8,5,main]-1617378344250-start
主線程:Thread[http-nio-8080-exec-8,5,main]-1617378344251-end,耗時(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)的請求已經(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,耗時(ms):2009
代碼中出現(xiàn)了異常,為什么?
是因為發(fā)生超時的時候,onTimeOut 方法執(zhí)行完畢之后,異步處理就結(jié)束了,此時,子線程還在運(yùn)行,子線程執(zhí)行到下面這樣代碼,向客戶端輸出信息,所以報錯了。
asyncContext.getResponse().getWriter().write(System.currentTimeMillis() + ",ok");
9、案例 6:對案例 5 進(jìn)行改造
對案例 5 進(jìn)行改造,如下代碼,看一下@3處的代碼,通過一個原子變量來控制請求是否處理完畢了,代碼中有 3 處可能會修改這個變量,通過 cas 操作來控制誰會修改成功,修改成功者,將結(jié)果設(shè)置到 request.setAttribute 中,然后調(diào)用asyncContext.dispatch();轉(zhuǎn)發(fā)請求,這種處理方式很好的解決案例 5 中異常問題,springmvc 中異步處理過程這個過程類似,所以這段代碼大家一定要好好看看,若能夠理解,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、啟動異步處理:調(diào)用req.startAsync(request,response)方法,獲取異步處理上下文對象AsyncContext
AsyncContext asyncContext = request.startAsync(request, response);
//@1:設(shè)置異步處理超時時間
Long timeout = Long.valueOf(request.getParameter("timeout"));
asyncContext.setTimeout(timeout);
//@3:用來異步處理是否結(jié)束,在這3個地方(子線程中處理完畢時、onComplete、onTimeout)將其更新為true
AtomicBoolean finish = new AtomicBoolean(false);
//添加監(jiān)聽器
asyncContext.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) throws IOException {
//異步處理完成會被回調(diào)
System.out.println(Thread.currentThread() + "-" + System.currentTimeMillis() + "-onComplete()");
if (finish.compareAndSet(false, true)) {
event.getAsyncContext().getRequest().setAttribute("result", "onComplete");
//轉(zhuǎn)發(fā)請求
asyncContext.dispatch();
}
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
//超時會被回調(diào)
System.out.println(Thread.currentThread() + "-" + System.currentTimeMillis() + "-onTimeout()");
if (finish.compareAndSet(false, true)) {
event.getAsyncContext().getRequest().setAttribute("result", "onTimeout");
//轉(zhuǎn)發(fā)請求
asyncContext.dispatch();
}
}
@Override
public void onError(AsyncEvent event) throws IOException {
//發(fā)生錯誤會被回調(diào)
System.out.println(Thread.currentThread() + "-" + System.currentTimeMillis() + "-onError()");
event.getAsyncContext().getResponse().getWriter().write("
onError");
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
//開啟異步請求調(diào)用的方法
System.out.println(Thread.currentThread() + "-" + System.currentTimeMillis() + "-onStartAsync()");
event.getAsyncContext().getResponse().getWriter().write("
onStartAsync");
}
});
//3、調(diào)用start方法異步處理,調(diào)用這個方法之后主線程就結(jié)束了
asyncContext.start(() -> {
long stSon = System.currentTimeMillis();
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-start");
try {
//@2:這里休眠2秒,模擬業(yè)務(wù)耗時
TimeUnit.SECONDS.sleep(2);
if (finish.compareAndSet(false, true)) {
asyncContext.getRequest().setAttribute("result", "ok");
//轉(zhuǎn)發(fā)請求
asyncContext.dispatch();
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("子線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end,耗時(ms):" + (System.currentTimeMillis() - stSon));
});
System.out.println("主線程:" + Thread.currentThread() + "-" + System.currentTimeMillis() + "-end,耗時(ms):" + (System.currentTimeMillis() - st));
}
}
}
模擬超時請求
http://localhost:8080/asyncServlet6?timeout=100

tomcat 控制臺輸出

模擬非超時請求
http://localhost:8080/asyncServlet6?timeout=5000

tomcat 控制臺輸出
主線程:Thread[http-nio-8080-exec-6,5,main]-1617379567665-start
主線程:Thread[http-nio-8080-exec-6,5,main]-1617379567666-end,耗時(ms):1
子線程:Thread[http-nio-8080-exec-6,5,main]-1617379567667-start
子線程:Thread[http-nio-8080-exec-6,5,main]-1617379569667-end,耗時(ms):2000
Thread[http-nio-8080-exec-10,5,main]-1617379569668-onComplete()
10、案例 7:模擬一個業(yè)務(wù)場景
業(yè)務(wù)場景
ServiceA 接受到一個請求之后,將請求發(fā)送到 mq,然后主線程就結(jié)束了,另外一個服務(wù) ServiceB 從 mq 中取出這條消息,然后對消息進(jìn)行處理,將處理結(jié)果又丟到 mq 中,ServiceA 中監(jiān)聽器監(jiān)聽 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"));
}
}
}
測試過程
step1、啟動項目
step2、瀏覽器中訪問:http://localhost:8080/asyncServlet7,會發(fā)現(xiàn)瀏覽器中請求一直處于等待中
step3、等待5秒,用來模擬ServiceB處理耗時
step4、瀏覽器中訪問:http://localhost:8080/asyncServlet7?orderId=1&result=success;用來模擬將結(jié)果通知給請求者,這步執(zhí)行完畢之后,step2會立即收到響應(yīng)

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

13、領(lǐng)取《Spring 系列高清 pdf》
獲取方式,掃碼發(fā)送:spring


14、領(lǐng)取《計算機(jī)必讀電子書》
計算機(jī)必讀電子書,進(jìn)行了詳細(xì)的分類,自己整理的,絕不是在網(wǎng)上那種打包下載的,而是自己需要學(xué)到某個方向的時候去網(wǎng)上挨個找的,最后匯總而成,這部分我是會不斷把它完善的,當(dāng)成自己的小電子書庫,不多,但貴在精。

獲取方式,掃碼發(fā)送:計算機(jī)
