1.场景描述
在做项目的时候,有这样一个场景,以下是简化版描述:
浏览器A发起http请求到服务器,但响应数据却需要从浏览器B发起的http请求中获取,相当于请求/响应在两个方法,我在B中拿到的数据怎么响应给A的请求呢?
2.同步处理方法
既然问题的核心在于A发送的http请求,在用websocket通知B之后就结束了,但客户端却什么都没收到;等B的请求到达b方法的时候,虽然拿到了数据,但却没法再找到A请求了,无法将真正的数据返给客户端。那我让a方法阻塞直到b方法中拿到数据,不就可以解决这个问题了吗。A、B两个请求是两个线程,现在两个线程需要通信了,线程通信有哪些方式呢?wait/notify不就是用来线程通信的嘛,多线程的知识终于排上用场了。
看代码:
//存放wait/notify使用的对象
public Map<String,MsgData> map1 = new HashMap<>();
@ResponseBody
@RequestMapping("/aToServer1")
public MsgData aToServer1() throws InterruptedException {
MsgData msg = new MsgData();
//将要返回的对象放入Map
map1.put("1", msg);
synchronized (msg) {
//利用msg来阻塞A线程,在b中需要利用同一个msg来通知A线程
msg.wait();
}
System.out.println("A请求结束:");
return msg;
}
@RequestMapping("/bToServer1")
public void bToServer1() throws IOException {
MsgData msg = map1.get("1");
msg.setDesc("B响应");
synchronized (msg) {
msg.notify();
}
System.out.println("B响应结束:");
}
class MsgData implements Serializable{
private String title;
private Integer status;
private String desc;
//get set方法...
}
通过wait/notify机制,将A请求和B响应建立了关系,让两者可以相互通信。问题是解决了,但这样做却有一个很严重的问题。
3.同步处理方法的问题
使用wait将A请求的线程阻塞,直到超时或者B的响应到来,将会导致在此期间,A请求所占用的线程无法释放,如果访问量很大,将会导致大量线程处于阻塞状态,对于新的请求必须不断从线程池取新的线程来处理,最终导致无法再响应请求。那有没有更好的方式呢?
4.异步处理方法
异步处理方法和上面的同步方法最大的区别是,在A请求等待B响应的这段时间内,处理A请求的线程会被释放,但请求不会结束,待B响应到来后,给A响应数据后,A请求才会真正结束。
1.Servlet的异步处理支持
Servlet3.0之后,支持异步处理,通过request.startAsync(request, response)
来将同步请求转为异步,该方法返回一个AsyncContext 对象,在A中将该对象保存起来,等B的响应到来的时候,就可以利用该对象来拿到response,向A反馈响应了。
注意需要在servlet中开启异步支持,加入这一行:
<async-supported>true</async-supported>
如果是SpringMVC,直接加到DispatcherServlet中。同时注意需要在所有的Filter中也要加入异步支持,否则会报错。
Servlet异步处理的代码:
//存放AsyncContext对象
public Map<String,AsyncContext> map2 = new HashMap<>();
@RequestMapping("/aToServer2")
public void aToServer2(HttpServletRequest request,HttpServletResponse response) throws IOException {
//将请求转为异步处理
AsyncContext asyncContext = request.startAsync(request, response);
//设置超时
asyncContext.setTimeout(10000);
//将异步状态保存下来
map2.put("1", asyncContext);
//可以监听一些事件
asyncContext.addListener(new AsyncListener() {
@Override
public void onTimeout(AsyncEvent event) throws IOException {
response.getWriter().write("捕获超时事件");
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
}
@Override
public void onError(AsyncEvent event) throws IOException {
}
@Override
public void onComplete(AsyncEvent event) throws IOException {
System.out.println("捕获完成事件");
}
});
System.out.println("A请求结束");
}
@RequestMapping("/bToServer2")
public void bToServer2(HttpServletRequest request,HttpServletResponse response) throws IOException {
//拿到之前的异步状态
AsyncContext asyncContext = map2.get("1");
ServletResponse mobileResponse = asyncContext.getResponse();
mobileResponse.getWriter().write("B响应");
//记得要手动调用complete,A请求才会结束
asyncContext.complete();
System.out.println("B响应结束");
}
2.SpringMVC异步处理的支持
SpringMVC提供了DeferredResult,和上面的AsyncContext作用类似,但是使用更为简单方便。最终采用了这种方式。
直接看代码:
//存放DeferredResult对象 泛型指定返回的数据类型
public Map<String,DeferredResult<String>> map3 = new HashMap<>();
@ResponseBody
@RequestMapping("/aToServer3")
public DeferredResult<String> aToServer3() {
// 超时时间为10s,超时返回"timeout"
DeferredResult<String> deferredResult = new DeferredResult<String>(10000L, "timeout");
map3.put("1", deferredResult);
System.out.println("A请求结束");
return deferredResult;
}
@RequestMapping("/bToServer3")
public void bToServer3() throws IOException {
DeferredResult<String> deferredResult = map3.get("1");
deferredResult.setResult("B响应");
System.out.println("B响应结束");
}
5.参考资料
异步处理主要参考了如下资料: