我想使用攔截器來管理當前與服務器的活動連接數。除了通常的 JSON 端點之外,我的 API 還提供了用于流式傳輸位元組的端點。我實作了一個會話管理器,它跟蹤會話計數、一個限制攔截器和幾個 API 端點。下面是一些示例代碼。
通常的 JSON 端點與攔截器運行良好。然而,流端點實際上呼叫了攔截器的preHandle方法兩次,但afterCompletion只有一次。第二次呼叫preHandle發生在第一個呼叫的回應計算完成之后。當我從攔截器中洗掉會話管理器時,這種行為不再發生。
最小作業示例:
配置:
@Configuration
@RequiredArgsConstructor
public class AppConfig implements WebMvcConfigurer {
private final Interceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(this.interceptor).addPathPatterns("/numbers", "/numbers/*");
}
}
攔截器:
@Component
@RequiredArgsConstructor
@Slf4j
public class Interceptor implements HandlerInterceptor {
private final SessionManager sessionManager;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
log.info("Pre-handle {}", this.hashCode());
return this.sessionManager.accept();
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) {
log.info("After completion {}", this.hashCode());
this.sessionManager.leave();
}
}
會話管理器:
@Component
@Slf4j
public class SessionManager {
private static final int MAX_SESSION_COUNT = 1;
private final AtomicInteger sessionCount = new AtomicInteger(0);
public synchronized boolean accept() {
int sessionCount = this.sessionCount.get();
if (sessionCount >= MAX_SESSION_COUNT) {
log.error("Upper session limit hit! Currently active sessions: {}, maximum allowed active sessions: {}", sessionCount, MAX_SESSION_COUNT);
return false;
}
sessionCount = this.sessionCount.incrementAndGet();
log.debug("Accept new session. Currently active sessions: {}, maximum allowed active sessions: {}", sessionCount, MAX_SESSION_COUNT);
return true;
}
public void leave() {
int sessionCount = this.sessionCount.decrementAndGet();
log.debug("Decrement session count to {}", sessionCount);
}
}
控制器:
@RestController
@RequestMapping("/numbers")
@Slf4j
public class Controller {
private final Random random = new Random();
@PostMapping("")
public ResponseEntity<List<Integer>> number() {
log.info("Generate numbers");
List<Integer> bytes = IntStream.range(0, 1_000)
.map(ignored -> this.random.nextInt(255))
.boxed()
.collect(Collectors.toList());
return ResponseEntity.ok(bytes);
}
@PostMapping("/stream")
public ResponseEntity<StreamingResponseBody> numberStream() {
log.info("Generate stream start");
StreamingResponseBody responseBody = outputStream -> {
for (int i = 0; i < 1_000_000; i ) {
outputStream.write(this.random.nextInt(255));
}
};
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(responseBody);
}
}
I found a similar topic on stackoverflow, but the advice given there does not work in my case. The behavior does not change when removing @Component from my interceptor and instantiating interceptor and session manager manually in the addInterceptors method.
Log (maximum session count = 2):
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.6.1)
2021-12-10 14:21:25.999 INFO 17112 --- [ main] c.e.untitled2.Untitled2Application : Starting Untitled2Application using Java 1.8.0_312 on HOSTNAME with PID 17112 (D:\IntelliJProjects\untitled2\target\classes started by USERNAME in D:\IntelliJProjects\untitled2)
2021-12-10 14:21:26.001 INFO 17112 --- [ main] c.e.untitled2.Untitled2Application : No active profile set, falling back to default profiles: default
2021-12-10 14:21:26.626 INFO 17112 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2021-12-10 14:21:26.632 INFO 17112 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2021-12-10 14:21:26.632 INFO 17112 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.55]
2021-12-10 14:21:26.701 INFO 17112 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2021-12-10 14:21:26.701 INFO 17112 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 669 ms
2021-12-10 14:21:26.907 INFO 17112 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2021-12-10 14:21:26.913 INFO 17112 --- [ main] c.e.untitled2.Untitled2Application : Started Untitled2Application in 1.197 seconds (JVM running for 1.84)
#### Call /numbers
2021-12-10 14:21:49.494 INFO 17112 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-12-10 14:21:49.494 INFO 17112 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2021-12-10 14:21:49.494 INFO 17112 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 0 ms
2021-12-10 14:21:49.502 INFO 17112 --- [nio-8080-exec-1] com.example.untitled2.Interceptor : Interceptor 1184674729 pre-handles request 1068123396: POST /numbers
2021-12-10 14:21:49.503 INFO 17112 --- [nio-8080-exec-1] com.example.untitled2.SessionManager : Accept new session. Currently active sessions: 1, maximum allowed active sessions: 2
2021-12-10 14:21:49.508 INFO 17112 --- [nio-8080-exec-1] com.example.untitled2.Controller : Generate numbers
2021-12-10 14:21:49.536 INFO 17112 --- [nio-8080-exec-1] com.example.untitled2.Interceptor : After completion 1184674729
2021-12-10 14:21:49.536 INFO 17112 --- [nio-8080-exec-1] com.example.untitled2.SessionManager : Decrement session count to 0
#### Call /numbers again
2021-12-10 14:21:57.054 INFO 17112 --- [nio-8080-exec-3] com.example.untitled2.Interceptor : Interceptor 1184674729 pre-handles request 1068123396: POST /numbers
2021-12-10 14:21:57.054 INFO 17112 --- [nio-8080-exec-3] com.example.untitled2.SessionManager : Accept new session. Currently active sessions: 1, maximum allowed active sessions: 2
2021-12-10 14:21:57.054 INFO 17112 --- [nio-8080-exec-3] com.example.untitled2.Controller : Generate numbers
2021-12-10 14:21:57.055 INFO 17112 --- [nio-8080-exec-3] com.example.untitled2.Interceptor : After completion 1184674729
2021-12-10 14:21:57.055 INFO 17112 --- [nio-8080-exec-3] com.example.untitled2.SessionManager : Decrement session count to 0
#### Call /numbers/stream
2021-12-10 14:22:06.375 INFO 17112 --- [nio-8080-exec-4] com.example.untitled2.Interceptor : Interceptor 1184674729 pre-handles request 1068123396: POST /numbers/stream
2021-12-10 14:22:06.376 INFO 17112 --- [nio-8080-exec-4] com.example.untitled2.SessionManager : Accept new session. Currently active sessions: 1, maximum allowed active sessions: 2
2021-12-10 14:22:06.376 INFO 17112 --- [nio-8080-exec-4] com.example.untitled2.Controller : Generate stream start
2021-12-10 14:22:06.414 INFO 17112 --- [nio-8080-exec-5] com.example.untitled2.Interceptor : Interceptor 1184674729 pre-handles request 317286159: POST /numbers/stream
2021-12-10 14:22:06.414 INFO 17112 --- [nio-8080-exec-5] com.example.untitled2.SessionManager : Accept new session. Currently active sessions: 2, maximum allowed active sessions: 2
2021-12-10 14:22:06.416 INFO 17112 --- [nio-8080-exec-5] com.example.untitled2.Interceptor : After completion 1184674729
2021-12-10 14:22:06.416 INFO 17112 --- [nio-8080-exec-5] com.example.untitled2.SessionManager : Decrement session count to 1
#### Call /numbers/stream again
2021-12-10 14:22:17.857 INFO 17112 --- [nio-8080-exec-6] com.example.untitled2.Interceptor : Interceptor 1184674729 pre-handles request 1068123396: POST /numbers/stream
2021-12-10 14:22:17.857 INFO 17112 --- [nio-8080-exec-6] com.example.untitled2.SessionManager : Accept new session. Currently active sessions: 2, maximum allowed active sessions: 2
2021-12-10 14:22:17.857 INFO 17112 --- [nio-8080-exec-6] com.example.untitled2.Controller : Generate stream start
2021-12-10 14:22:17.889 INFO 17112 --- [nio-8080-exec-7] com.example.untitled2.Interceptor : Interceptor 1184674729 pre-handles request 1473864520: POST /numbers/stream
2021-12-10 14:22:17.889 ERROR 17112 --- [nio-8080-exec-7] com.example.untitled2.SessionManager : Upper session limit hit! Currently active sessions: 2, maximum allowed active sessions: 2
#### Call /numbers/stream again
2021-12-10 14:22:26.443 INFO 17112 --- [nio-8080-exec-8] com.example.untitled2.Interceptor : Interceptor 1184674729 pre-handles request 1068123396: POST /numbers/stream
2021-12-10 14:22:26.443 ERROR 17112 --- [nio-8080-exec-8] com.example.untitled2.SessionManager : Upper session limit hit! Currently active sessions: 2, maximum allowed active sessions: 2
The logs show that preHandle is called twice while afterCompletion is called only once.
uj5u.com熱心網友回復:
因此,從您遇到的問題開始,我們需要從 Servlet 環境支持的調度型別開始,尤其是作為 Servlet 3.0 規范的一部分添加的 ASYNC 調度型別。簡而言之,添加了 ASYNC 調度型別以支持長時間運行/繁重的請求,這些請求在過去會阻止 servlet 容器處理其他請求,因為處理程式執行緒被阻塞,等待繁重/長時間運行的任務完成. 所以聰明的人決定可以在并行執行緒中執行長時間運行和繁重的作業,并且可以將主作業執行緒釋放到池中,以便處理新的小請求。
所以讓我們回到你的問題:所以有兩個端點,一個回傳 JSON 或一個可以由處理程式執行緒處理的簡單物件,第二個回傳StreamingResponseBody. 對于第一個,沒有定義特殊處理,因此 Spring 將請求作為普通請求處理,只生成有效負載并將其回傳給客戶端。對于第二個 spring,有一個自定義回應處理程式呼叫StreamingResponseBodyReturnValueHandler,它基本上創建了請求的 ASYNC 版本(處理所有屬性但更改調度型別)并通過WebAsyncManager.
那么為什么preHandle()呼叫了兩次呢?那是因為一旦它作為基于REQUEST調度的第一次執行的一部分被呼叫,并且在預處理之后 Spring 開始處理請求并理解它應該以ASYNC模式處理,因為回傳型別是一個流(基本上沒有大小的東西),所以請求的副本被制作出來,它再次在新執行緒中執行。因此,如果您仔細查看,preHandle()您會注意到它是從不同的執行緒呼叫的,具有相同的請求資料,但在請求時具有不同的調度型別。
更多資訊在這里:
Github 問題
代碼
那你能做什么?你Interceptor應該有點聰明,不要盲目地打電話,sessionManager.accept();而是檢查請求之前是否已經處理過。
所以非常虛擬的版本看起來像這樣
@Component
@RequiredArgsConstructor
@Slf4j
public class Interceptor implements HandlerInterceptor {
private final SessionManager sessionManager;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
log.info("Pre-handle {}", this.hashCode());
if(BooleanUtils.isTrue(request.getAttribute("accepted")) || DispatcherType.ASYNC == request.getDispatcherType()){
return true;
}
boolean accepted = this.sessionManager.accept();
if (accepted){
request.setAttribute("accepted", true);
}
return accepted;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) {
log.info("After completion {}", this.hashCode());
this.sessionManager.leave();
}
}
uj5u.com熱心網友回復:
從檔案
注意:只有在此攔截器的 preHandle 方法成功完成并回傳 true 時才會呼叫!
afterCompletion()只有當preHandle()方法回傳時才會呼叫方法true。在您的情況下,您正在增加會話計數并回傳 false。
如果您想sessionManager.leave();始終呼叫(無論 preHandle() 回傳什么),請使用postHandle()代替afterCompletion()。
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/385845.html
標籤:java spring spring-mvc streaming interceptor
