前篇博客介绍完异步处理请求的第一种方式,下面介绍第二种——一边异步处理请求一边生成HTTP
响应。
一边异步处理请求一边生成HTTP
响应的方式为将一个HTTP
响应分割成多个事件返回,这种方式是基于HTTP/1.1
的分块传输编码(Chunked transfer encoding
)。
package com.example.component;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import java.io.IOException;
@Controller
@RequestMapping("/streaming")
public class StreamingController {
@Autowired
AsyncHelper asyncHelper;
@RequestMapping(method = RequestMethod.GET)
public ResponseBodyEmitter streaming(@RequestParam(defaultValue = "1") long eventNumber, @RequestParam(defaultValue = "0") long intervalSec) throws IOException {
Console.println("Start get.");
ResponseBodyEmitter emitter = new ResponseBodyEmitter();
asyncHelper.streaming(emitter, eventNumber, intervalSec);
Console.println("End get.");
return emitter;
}
}
@Component
public class AsyncHelper {
// ...
@Async
public void streaming(ResponseBodyEmitter emitter, long eventNumber, long intervalSec) throws IOException {
Console.println("Start Async processing.");
for (long i = 1; i <= eventNumber; i++) {
sleep(intervalSec);
emitter.send("msg" + i + "\r\n");
}
emitter.complete();
Console.println("End Async processing.");
}
// ...
}
使用CURL
或者浏览器按照下面的参数进行访问,一开始便显示如下内容:
HTTP/1.1 200
Transfer-Encoding: chunked
Date: Wed, 05 Oct 2016 16:00:22 GMT
1
秒后,再增多一条信息
msg1
再1
秒后,又增多一条信息,然后最终返回结果。
msg2
所谓的Sse
其实就是Server-Sent Events
,即服务器推送事件,属于HTML5
的一项新功能,常用于服务器主动通知客户端有相关信息的更新。其他替代方法一般有WebSocket
和客户端定时轮询,前者过于复杂,后者又过于低效而笨拙。SseEmitter
属于ResponseBodyEmitter
的子类,可以生成text/event-stream
格式的信息。
package com.example.component;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
@Controller
@RequestMapping("/sse")
public class SseController {
@Autowired
AsyncHelper asyncHelper;
@RequestMapping(method = RequestMethod.GET)
public SseEmitter sse(@RequestParam(defaultValue = "1") long eventNumber, @RequestParam(defaultValue = "0") long intervalSec) throws IOException {
Console.println("Start get.");
SseEmitter emitter = new SseEmitter();
asyncHelper.sse(emitter, eventNumber, intervalSec);
Console.println("End get.");
return emitter;
}
}
@Component
public class AsyncHelper {
// ...
@Async
public void sse(SseEmitter emitter, long eventNumber, long intervalSec) throws IOException {
Console.println("Start Async processing.");
for (long i = 1; i <= eventNumber; i++) {
sleep(intervalSec);
emitter.send(SseEmitter.event()
.comment("This is test event")
.id(UUID.randomUUID().toString())
.name("onlog")
.reconnectTime(3000)
.data("msg" + i));
}
emitter.complete();
Console.println("End Async processing.");
}
// ...
}
返回信息如下:
HTTP/1.1 200
Content-Type: text/event-stream;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 05 Oct 2016 16:31:35 GMT
:This is test event
id:c62ae77f-85fe-41ac-bfff-c03b0020a816
event:onlog
retry:3000
data:msg1
:This is test event
id:d283757e-9d67-4be5-b858-3c6b543c7b5d
event:onlog
retry:3000
data:msg2
ResponseBodyEmitter
和SseEmitter
可以在调用构造函数时传入一个数值,用来指定每次请求处理的时间上限。如:
SseEmitter emitter = new SseEmitter(0L);//永不超时
当超时发生时,会报AsyncRequestTimeoutException
异常,这时就会向客户端发送类似于如下的JSON
字符串:
{"timestamp":"2016-10-05T16:35:31.060+0000","status":200,"error":"OK","exception":"org.springframework.web.context.request.async.AsyncRequestTimeoutException","message":"No message available","path":"/sse"}
当然,你也可以重写handleAsyncRequestTimeoutException
方法定制自己的超时返回信息,做法如下:
@Controller
@RequestMapping("/sse")
public class SseController {
private static final Logger logger = LoggerFactory.getLogger(SseController.class);
// ...
@ExceptionHandler
@ResponseBody
public String handleAsyncRequestTimeoutException(AsyncRequestTimeoutException e) {
logger.error("timeout error is occurred.", e);
return SseEmitter.event().data("timeout!!").build().stream()
.map(d -> d.getData().toString())
.collect(Collectors.joining());
}
}
此后,客户端接受到的超时信息就会变成如下:
HTTP/1.1 200
Transfer-Encoding: chunked
Date: Wed, 05 Oct 2016 17:01:53 GMT
msg1
timeout!!