Spring-MVC異步請求之Servlet異步處理
在Servlet3.0的規(guī)范中新增了對異步請求的支持,SpringMVC又在此基礎(chǔ)上對異步請求提供了方便。
異步請求是在處理比較耗時的業(yè)務時先將request返回,然后另起線程處理耗時的業(yè)務,處理完后在返回給用戶。
異步請求可以給我們帶來很多方便,最直接的用法就是處理耗時的業(yè)務,比如,需要查詢數(shù)據(jù)庫,需要調(diào)用別的服務器來處理等情況下可以先將請求返回給客戶端,然后啟用新線程處理耗時業(yè)務。
如果我們合適的擴展可以實現(xiàn)訂閱者模式的消息訂閱功能,比如,當有異常情況發(fā)生時可以主動將相關(guān)信息發(fā)送給運維人員,還有現(xiàn)在的很多郵箱自動回復都是使用這種技術(shù)。
Http協(xié)議是單向的,只能客戶端自己拉不能服務器主動推,Servlet對異步請求的支持并沒有修改Http,而是對Http的巧妙利用。異步請求的核心原理主要分為兩大類,一類是輪詢,另一類是長連接。
輪詢就是定時自動發(fā)起請求檢查有沒有需要返回的數(shù)據(jù),這種對資源浪費比較大。長連接的原理是客戶端發(fā)起請求,服務端處理并返回后并不結(jié)束連接,這樣就可以在后面再次返回給客戶端數(shù)據(jù)。
Servlet對異步請求的支持其實采用的是長連接的方式,也就是說,異步請求中在原始的請求返回的時候并沒有關(guān)閉連接,關(guān)閉的只是處理請求的那個縣城,只有在異步請求全部處理完之后才會關(guān)閉連接。
Servlet3.0對異步請求的支持
在Servlet3.0規(guī)范中使用異步處理請求非常簡單,只需要在請求處理過程中調(diào)用request的startAsync返回AsyncContext。
什么是AsyncContext在異步請求中充當著非常重要的角色,可以稱為異步請求上下文也可以稱為異步請求容器。類似于ServletContext.我們多次調(diào)用startAsync都是返回的同一個AsyncContext。代碼如下:
public interface AsyncContext {
String ASYNC_REQUEST_URI = "javax.servlet.async.request_uri";
String ASYNC_CONTEXT_PATH = "javax.servlet.async.context_path";
String ASYNC_PATH_INFO = "javax.servlet.async.path_info";
String ASYNC_SERVLET_PATH = "javax.servlet.async.servlet_path";
String ASYNC_QUERY_STRING = "javax.servlet.async.query_string";
ServletRequest getRequest();
ServletResponse getResponse();
boolean hasOriginalRequestAndResponse();
void dispatch();
void dispatch(String var1);
void dispatch(ServletContext var1, String var2);
void complete();
void start(Runnable var1);
void addListener(AsyncListener var1);
void addListener(AsyncListener var1, ServletRequest var2, ServletResponse var3);
<T extends AsyncListener> T createListener(Class<T> var1) throws ServletException;
void setTimeout(long var1);
long getTimeout();
}
getResponse() 用于獲取response。dispatch用于分發(fā)新地址。complete用于通知容器已經(jīng)處理完了,start方法用于啟動實際處理線程,addListener用于添加監(jiān)聽器;setTimeout方法用于修改超時時間。
Servlet3.0處理異步請求實例
@WebServlet(
name = “WorkServlet”,
urlPatterns = “/work”,
asyncSupported = true
)
public class WorkServlet extends HttpServlet {
private static final long serialVersionUID =1L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//設置ContentType,關(guān)閉緩存
resp.setContentType("text/plain;charset=UTF-8");
resp.setHeader("Cache-Control","private");
resp.setHeader("Pragma","no-cache");
final PrintWriter writer= resp.getWriter();
writer.println("老師檢查作業(yè)了");
writer.flush();
List<String> zuoyes=new ArrayList<String>();
for (int i = 0; i < 10; i++) {
zuoyes.add("zuoye"+i);;
}
final AsyncContext ac=req.startAsync();//開啟異步請求
doZuoye(ac,zuoyes);
writer.println("老師布置作業(yè)");
writer.flush();
}
private void doZuoye(final AsyncContext ac, final List<String> zuoyes) {
ac.setTimeout(1*60*60*1000L);
ac.start(new Runnable() {
@Override
public void run() {
//通過response獲得字符輸出流
try {
PrintWriter writer=ac.getResponse().getWriter();
for (String zuoye:zuoyes) {
writer.println("\""+zuoye+"\"請求處理中");
Thread.sleep(1*1000L);
writer.flush();
}
ac.complete();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}

異步請求監(jiān)聽器
在上面的程序是我們最基本的異步請求,不過不夠完善。老師是需要思考宏觀問題,所以在寫完作業(yè)之后需要給老師匯報哪些題難,哪些題目有問題或者自己的這次經(jīng)驗總結(jié),不過這些事不應該由做作業(yè)的學生來做,應該由專門的學習匯報員來統(tǒng)計分析。所以就有了監(jiān)聽器。
public class TeacherListener implements AsyncListener {
final SimpleDateFormat formatter=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public void onComplete(AsyncEvent event) throws IOException {
System.out.println("在"+formatter.format(new Date())+"工作處理完成");
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
System.out.println("在"+formatter.format(new Date())+"工作超時");
}
@Override
public void onError(AsyncEvent event) throws IOException {
System.out.println("在"+formatter.format(new Date())+"工作處理錯誤");
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
System.out.println("在"+formatter.format(new Date())+"工作處理開始");
}
}
所有代碼具體參照github地址
https://github.com/lzggsimida123/ServletAsync
補充:SpringMVC對Servlet3異步請求的支持
SpringMVC對Servlet3異步請求的支持有兩種方式,分別是通過處理器方法返回Callable和DeferredResult。
按照Servlet3的規(guī)范,支持異步請求時需要配置對應的Servlet和Filter支持異步請求,為了使SpringMVC支持異步請求的處理,需要在定義DispatcherServlet時配置其支持異步請求,在DispatcherServlet之前定義的Filter也需要配置支持異步請求。
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<!-- 啟用異步支持 -->
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
返回Callable
當處理器的返回方法是Callable類型時會默認發(fā)起異步請求,并使用一個TaskExecutor來調(diào)用返回的Callable,之后的處理就跟正常的SpringMVC請求是一樣的。Callable的返回結(jié)果也跟正常請求SpringMVC的一樣,可以返回Model、ModelAndView、String、Object等,也可以結(jié)合@ResponseBody使用,具體可以參考CallableMethodReturnValueHandler的handleReturnValue()。
@RequestMapping("/callable")
public Callable<String> forCallable(Model model) throws Exception {
return () -> {
TimeUnit.SECONDS.sleep(1);//睡眠1秒,模仿某些業(yè)務操作
model.addAttribute("a", "aaaaaaa");
return "async_request_callable";
};
}
如果需要針對于單個Callable請求指定超時時間,我們可以把Callable用一個WebAsyncTask包裹起來。然后還可以指定超時回調(diào)和正常處理完成的回調(diào)。
@RequestMapping("/callable/timeout")
public WebAsyncTask<String> forCallableWithTimeout(Model model) throws Exception {
long timeout = 5 * 1000L;
WebAsyncTask<String> asyncTask = new WebAsyncTask<>(timeout, () -> {
TimeUnit.MILLISECONDS.sleep(timeout + 10);
model.addAttribute("a", "aaaaaaa");
return "async_request_callable";
});
asyncTask.onTimeout(() -> {
System.out.println("響應超時回調(diào)");
return "async_request_callable_timeout";
});
asyncTask.onCompletion(() -> {
System.out.println("響應callable調(diào)用完成的回調(diào)");
});
return asyncTask;
}
返回DeferredResult
使用DeferredResult的返回結(jié)果的編程通常是在處理器方法中創(chuàng)建一個DeferredResult實例,把它保存起來后再進行返回,比如保存到一個隊列中,然后在另外的一個線程中會從這個隊列中拿到相應的DeferredResult對象進行相應的業(yè)務處理后會往DeferredResult中設置對應的返回值。返回了DeferredResult后SpringMVC將創(chuàng)建一個DeferredResultHandler用于監(jiān)聽DeferredResult,一旦DeferredResult中設置了返回值后,DeferredResultHandler就將對返回值進行處理。DeferredResult的處理過程見DeferredResultMethodReturnValueHandler的handleReturnValue()。
@RequestMapping("/deferredresult")
public DeferredResult<String> forDeferredResult() throws Exception {
DeferredResult<String> result = new DeferredResult<>();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
result.setResult("async_request_deferredresult");
}).start();
return result;
}
對于DeferredResult也是可以單獨指定超時時間和超時后的回調(diào)的,它的超時時間可以直接通過構(gòu)造函數(shù)傳遞,單位是毫秒。
@RequestMapping("/deferredresult/timeout")
public DeferredResult<String> forDeferredResultWithTimeout() throws Exception {
DeferredResult<String> result = new DeferredResult<>(10 * 1000);
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(31);
} catch (InterruptedException e) {
e.printStackTrace();
}
result.setResult("async_request_deferredresult");
}).start();
result.onTimeout(() -> {
System.out.println("響應超時回調(diào)函數(shù)");
});
result.onCompletion(() -> {
System.out.println("響應完成的回調(diào)函數(shù)");
});
return result;
}
配置
可以通過<mvc:annotation-driven/>的子元素<mvc:async-support/>來定義處理異步請求默認的超時時間和需要使用的TaskExecutor。如果不指定默認超時時間則默認會使用容器的異步請求超時時間,如果不指定需要使用的TaskExecutor,則默認會使用一個SimpleAsyncTaskExecutor。在下面的配置中我們就配置了默認的超時時間是15秒,且處理異步請求的TaskExecutor是bean容器中名為asyncTaskExecutor的TaskExecutor。
<mvc:annotation-driven> <mvc:async-support default-timeout="15000" task-executor="asyncTaskExecutor"/> </mvc:annotation-driven>
攔截器
返回Callable類型的請求可以通過實現(xiàn)CallableProcessingInterceptor接口自定義一個攔截器來攔截,也可以通過繼承CallableProcessingInterceptorAdapter抽象類來定義攔截器,這樣就只需要選擇自己感興趣的方法進行實現(xiàn)。CallableProcessingInterceptor接口定義如下:
public interface CallableProcessingInterceptor {
static final Object RESULT_NONE = new Object();
static final Object RESPONSE_HANDLED = new Object();
/**
* Invoked <em>before</em> the start of concurrent handling in the original
* thread in which the {@code Callable} is submitted for concurrent handling.
*
* <p>
* This is useful for capturing the state of the current thread just prior to
* invoking the {@link Callable}. Once the state is captured, it can then be
* transferred to the new {@link Thread} in
* {@link #preProcess(NativeWebRequest, Callable)}. Capturing the state of
* Spring Security's SecurityContextHolder and migrating it to the new Thread
* is a concrete example of where this is useful.
* </p>
*
* @param request the current request
* @param task the task for the current async request
* @throws Exception in case of errors
*/
<T> void beforeConcurrentHandling(NativeWebRequest request, Callable<T> task) throws Exception;
/**
* Invoked <em>after</em> the start of concurrent handling in the async
* thread in which the {@code Callable} is executed and <em>before</em> the
* actual invocation of the {@code Callable}.
*
* @param request the current request
* @param task the task for the current async request
* @throws Exception in case of errors
*/
<T> void preProcess(NativeWebRequest request, Callable<T> task) throws Exception;
/**
* Invoked <em>after</em> the {@code Callable} has produced a result in the
* async thread in which the {@code Callable} is executed. This method may
* be invoked later than {@code afterTimeout} or {@code afterCompletion}
* depending on when the {@code Callable} finishes processing.
*
* @param request the current request
* @param task the task for the current async request
* @param concurrentResult the result of concurrent processing, which could
* be a {@link Throwable} if the {@code Callable} raised an exception
* @throws Exception in case of errors
*/
<T> void postProcess(NativeWebRequest request, Callable<T> task, Object concurrentResult) throws Exception;
/**
* Invoked from a container thread when the async request times out before
* the {@code Callable} task completes. Implementations may return a value,
* including an {@link Exception}, to use instead of the value the
* {@link Callable} did not return in time.
*
* @param request the current request
* @param task the task for the current async request
* @return a concurrent result value; if the value is anything other than
* {@link #RESULT_NONE} or {@link #RESPONSE_HANDLED}, concurrent processing
* is resumed and subsequent interceptors are not invoked
* @throws Exception in case of errors
*/
<T> Object handleTimeout(NativeWebRequest request, Callable<T> task) throws Exception;
/**
* Invoked from a container thread when async processing completes for any
* reason including timeout or network error.
*
* @param request the current request
* @param task the task for the current async request
* @throws Exception in case of errors
*/
<T> void afterCompletion(NativeWebRequest request, Callable<T> task) throws Exception;
}
它的配置是通過<mvc:callable-interceptors/>配置的。
<mvc:annotation-driven>
<mvc:async-support default-timeout="15000" task-executor="asyncTaskExecutor">
<mvc:callable-interceptors>
<bean class="YourCallableProcessingInterceptor"/>
</mvc:callable-interceptors>
</mvc:async-support>
</mvc:annotation-driven>
返回DeferredResult的也可以進行攔截,這需要我們實現(xiàn)DeferredResultProcessingInterceptor接口或者繼承自DeferredResultProcessingInterceptorAdapter。DeferredResultProcessingInterceptor接口定義如下:
public interface DeferredResultProcessingInterceptor {
/**
* Invoked immediately before the start of concurrent handling, in the same
* thread that started it. This method may be used to capture state just prior
* to the start of concurrent processing with the given {@code DeferredResult}.
*
* @param request the current request
* @param deferredResult the DeferredResult for the current request
* @throws Exception in case of errors
*/
<T> void beforeConcurrentHandling(NativeWebRequest request, DeferredResult<T> deferredResult) throws Exception;
/**
* Invoked immediately after the start of concurrent handling, in the same
* thread that started it. This method may be used to detect the start of
* concurrent processing with the given {@code DeferredResult}.
*
* <p>The {@code DeferredResult} may have already been set, for example at
* the time of its creation or by another thread.
*
* @param request the current request
* @param deferredResult the DeferredResult for the current request
* @throws Exception in case of errors
*/
<T> void preProcess(NativeWebRequest request, DeferredResult<T> deferredResult) throws Exception;
/**
* Invoked after a {@code DeferredResult} has been set, via
* {@link DeferredResult#setResult(Object)} or
* {@link DeferredResult#setErrorResult(Object)}, and is also ready to
* handle the concurrent result.
*
* <p>This method may also be invoked after a timeout when the
* {@code DeferredResult} was created with a constructor accepting a default
* timeout result.
*
* @param request the current request
* @param deferredResult the DeferredResult for the current request
* @param concurrentResult the result to which the {@code DeferredResult}
* @throws Exception in case of errors
*/
<T> void postProcess(NativeWebRequest request, DeferredResult<T> deferredResult, Object concurrentResult) throws Exception;
/**
* Invoked from a container thread when an async request times out before
* the {@code DeferredResult} has been set. Implementations may invoke
* {@link DeferredResult#setResult(Object) setResult} or
* {@link DeferredResult#setErrorResult(Object) setErrorResult} to resume processing.
*
* @param request the current request
* @param deferredResult the DeferredResult for the current request; if the
* {@code DeferredResult} is set, then concurrent processing is resumed and
* subsequent interceptors are not invoked
* @return {@code true} if processing should continue, or {@code false} if
* other interceptors should not be invoked
* @throws Exception in case of errors
*/
<T> boolean handleTimeout(NativeWebRequest request, DeferredResult<T> deferredResult) throws Exception;
/**
* Invoked from a container thread when an async request completed for any
* reason including timeout and network error. This method is useful for
* detecting that a {@code DeferredResult} instance is no longer usable.
*
* @param request the current request
* @param deferredResult the DeferredResult for the current request
* @throws Exception in case of errors
*/
<T> void afterCompletion(NativeWebRequest request, DeferredResult<T> deferredResult) throws Exception;
}
自定義的DeferredResultProcessingInterceptor是通過<mvc:deferred-result-interceptors>配置的。
<mvc:annotation-driven>
<mvc:async-support default-timeout="15000" task-executor="asyncTaskExecutor">
<mvc:deferred-result-interceptors>
<bean class="YourDeferredResultProcessingInterceptor"/>
</mvc:deferred-result-interceptors>
</mvc:async-support>
</mvc:annotation-driven>
當發(fā)起異步請求時,SpringMVC傳統(tǒng)的HandlerInterceptor的postHandle()和afterCompletion()不會執(zhí)行,但是等異步請求結(jié)束后它們還是會執(zhí)行的。如果需要在異步處理完成之后做一些事情,也可以選擇實現(xiàn)AsyncHandlerInterceptor接口的afterConcurrentHandlingStarted(),AsyncHandlerInterceptor接口繼承了HandlerInterceptor。
(注:本文是基于Spring4.1.0所寫)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。如有錯誤或未考慮完全的地方,望不吝賜教。
相關(guān)文章
使用mybatis框架連接mysql數(shù)據(jù)庫的超詳細步驟
MyBatis是目前java項目連接數(shù)據(jù)庫的最流行的orm框架了,下面這篇文章主要給大家介紹了關(guān)于使用mybatis框架連接mysql數(shù)據(jù)庫的超詳細步驟,文中通過實例代碼和圖文介紹的非常詳細,需要的朋友可以參考下2023-04-04
Spring學習筆記2之表單數(shù)據(jù)驗證、文件上傳實例代碼
這篇文章主要介紹了Spring學習筆記2之表單數(shù)據(jù)驗證、文件上傳 的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-07-07
Mybatis-Plus默認主鍵策略導致自動生成19位長度主鍵id的坑
這篇文章主要介紹了Mybatis-Plus默認主鍵策略導致自動生成19位長度主鍵id的坑,本文一步步給大家分享解決方法,給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-12-12
MyBatis入門之增刪改查+數(shù)據(jù)庫字段和實體字段不一致問題處理方法
這篇文章主要介紹了MyBatis入門之增刪改查+數(shù)據(jù)庫字段和實體字段不一致問題處理方法,需要的朋友可以參考下2017-05-05
多模塊maven的deploy集成gitlab?ci自動發(fā)版配置
這篇文章主要為大家介紹了多模塊maven項目deploy集成gitlab?ci自動發(fā)版的配置流程步驟,有需要的朋友可以借鑒參考下,希望能夠有所幫助2022-02-02

