简体   繁体   English

SpringBoot:拦截器的 preHandle 在流端点上运行两次

[英]SpringBoot: Interceptor's preHandle run twice on streaming endpoint

I want to use an interceptor to manage the number of currently active connections with the server.我想使用拦截器来管理当前与服务器的活动连接数。 Besides the usual JSON endpoints, my API also offers endpoints for streaming bytes.除了通常的 JSON 端点外,我的 API 还提供流字节的端点。 I implemented a session manager, that keeps track of the session count, a limiting interceptor and several API endpoints.我实现了一个 session 管理器,它跟踪 session 计数、一个限制拦截器和几个 API 端点。 Below is some exemplary code.下面是一些示例代码。

The usual JSON endpoints are running well with the interceptor.通常的 JSON 端点与拦截器运行良好。 However, the streaming endpoint actually calls the interceptor's preHandle method twice, but the afterCompletion only once.但是,流端点实际上调用了拦截器的preHandle方法两次,而afterCompletion只调用了一次。 The second call to preHandle happens after the first call's response computation was finished.第二次调用preHandle发生在第一次调用的响应计算完成之后。 When I remove the session manager from the interceptor, this behavior does not occur anymore.当我从拦截器中删除 session 管理器时,此行为不再发生。

Minimal working example:最小的工作示例:

Configuration:配置:

@Configuration
@RequiredArgsConstructor
public class AppConfig implements WebMvcConfigurer {
    private final Interceptor interceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(this.interceptor).addPathPatterns("/numbers", "/numbers/*");
    }
}

Interceptor:拦截器:

@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();
    }
}

Session manager: Session 经理:

@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);
    }
}

Controller: Controller:

@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.在 stackoverflow 上找到了一个类似的主题,但是那里给出的建议对我来说不起作用。 The behavior does not change when removing @Component from my interceptor and instantiating interceptor and session manager manually in the addInterceptors method.从我的拦截器中删除@Component并在addInterceptors方法中手动实例化拦截器和 session 管理器时,行为不会改变。

Log (maximum session count = 2):日志(最大 session 计数 = 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.日志显示preHandle被调用了两次,而afterCompletion只被调用了一次。

From the document文件

Note: Will only be called if this interceptor's preHandle method has successfully completed and returned true!注意:仅当此拦截器的 preHandle 方法已成功完成并返回 true 时才会调用!

afterCompletion() method will be called only if the preHandle() method returns true .只有当preHandle()方法返回true时才会afterCompletion()方法。 In your case, you are incrementing the session count and returning false.在您的情况下,您正在增加 session 计数并返回 false。

If you want to call sessionManager.leave();如果你想调用sessionManager.leave(); always(irrespective of what the preHandle() returns), use the postHandle() instead of afterCompletion() .总是(不管 preHandle() 返回什么),使用postHandle()而不是afterCompletion()

So to start with the issue which you are having we need to start with the dispatch types supported by the Servlet environment and particularly the ASYNC dispatch type which was added as part of the Servlet 3.0 specification.因此,从您遇到的问题开始,我们需要从 Servlet 环境支持的调度类型开始,特别是作为 Servlet 3.0 规范的一部分添加的 ASYNC 调度类型。 In a nutshell, the ASYNC dispatch type was added to support long-running / heavy requests which in older times were blocking the servlet container from handling other requests, as the handler thread was blocked waiting for the heavy/long-running task to be finished.简而言之,添加了 ASYNC 调度类型以支持长时间运行/繁重的请求,这些请求在过去会阻止 servlet 容器处理其他请求,因为处理程序线程被阻止等待繁重/长时间运行的任务完成. So clever guys out there decided that the long-running and heavy jobs can be executed in a parallel thread and the main worker thread can be released to the pool so new small requests can be handled.如此聪明的人决定可以在并行线程中执行长时间运行和繁重的工作,并且可以将主工作线程释放到池中,以便可以处理新的小请求。

So let's get back to your issue: So have two endpoints one returning the JSON or a simple object which can be handled by the handler thread and the second one which returns the StreamingResponseBody .所以让我们回到你的问题:所以有两个端点,一个返回 JSON 或一个简单的 object 可以由处理程序线程处理,第二个返回StreamingResponseBody For the first one, no special handling is defined, so Spring handles the request as a usual request and just generates a payload and returns it back to the client.对于第一个,没有定义特殊处理,因此 Spring 将请求作为普通请求处理,并仅生成有效负载并将其返回给客户端。 For the second one spring has a custom response handler called StreamingResponseBodyReturnValueHandler which basically creates a ASYNC version of the request (coping all of the attributes but changing the dispatch type) and pushes the execution to the new thread via WebAsyncManager .对于第二个 spring 有一个名为StreamingResponseBodyReturnValueHandler的自定义响应处理程序,它基本上创建请求的 ASYNC 版本(处理所有属性但更改调度类型)并通过WebAsyncManager将执行推送到新线程。

So why does the preHandle() called twice?那么为什么preHandle()调用了两次呢? That is because once it is called as part of the first execution based on the REQUEST dispatch and just after pre handle Spring starts handling the request and understands that it should be handled in ASYNC mode as the return type is a stream (basically unsized thing), so a copy of the request is made it it is once again executed in the new thread.这是因为一旦它作为基于REQUEST调度的第一次执行的一部分被调用,并且在预处理 Spring 开始处理请求并理解它应该在ASYNC模式下处理,因为返回类型是 stream (基本上没有大小的东西) ,所以请求的副本被制作,它再次在新线程中执行。 So if you look into preHandle() you will notice that it is called from different threads, with the same request data but different dispatch type on request.因此,如果您查看preHandle() ,您会注意到它是从不同的线程调用的,请求数据相同,但请求的调度类型不同。

So what you can do?那你能做什么? Your Interceptor should be a bit clever to not blindly call sessionManager.accept();你的Interceptor应该有点聪明,不要盲目地调用sessionManager.accept(); but check if the request was already handled before.但请检查该请求之前是否已处理。

So very dummy version will look like this所以非常虚拟的版本看起来像这样

@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();
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM