【问题标题】:issue with Spring and asynchronous controller + HandlerInterceptor + IE/EdgeSpring 和异步控制器 + HandlerInterceptor + IE/Edge 的问题
【发布时间】:2019-03-15 18:32:51
【问题描述】:

我正在开发一个提供 REST 端点的 Spring 应用程序。其中一个端点本质上充当 HTML 客户端和第三方云存储提供商之间的代理。此端点从存储提供程序检索文件并将它们代理回客户端。类似于以下内容(请注意,同一端点有同步和异步版本):

@Controller
public class CloudStorageController {

  ...    

  @RequestMapping(value = "/fetch-image/{id}", method = RequestMethod.GET, produces = MediaType.IMAGE_JPEG_VALUE)
  public ResponseEntity<byte[]> fetchImageSynchronous(@PathVariable final Long id) {
    final byte[] imageFileContents = this.fetchImage(id);
    return ResponseEntity.ok().body(imageFileContents);
  }

  @RequestMapping(value = "/fetch-image-async/{id}", method = RequestMethod.GET, produces = MediaType.IMAGE_JPEG_VALUE)
  public Callable<ResponseEntity<byte[]>> fetchImageAsynchronous(@PathVariable final Long id) {
    return () -> {
      final byte[] imageFileContents = this.fetchImage(id);
      return ResponseEntity.ok().body(imageFileContents);
    };
  }

  private byte[] fetchImage(final long id) {
    // fetch the file from cloud storage and return as byte array
    ...
  }

  ...

}

由于客户端应用程序 (HTML5 + ajax) 的性质以及此端点的使用方式,用户身份验证提供给此端点的方式与其他端点不同。为了处理这个问题,开发了一个 HandlerInterceptor 来处理这个端点的身份验证:

@Component("cloudStorageAuthenticationInterceptor")
public class CloudStorageAuthenticationInterceptor extends HandlerInterceptorAdapter {

  @Override
  public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) {
    // examine the request for the authentication information and verify it
    final Authentication authenticated = ...
    if (authenticated == null) {
      try {
        pResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
      return false;
    }
    else {
      try {
        request.login(authenticated.getName(), (String) authenticated.getCredentials());
      } catch (final ServletException e) {
        throw new BadCredentialsException("Bad credentials");
      }
    }
    return true;
  }

}

拦截器是这样注册的:

@Configuration
@EnableWebMvc
public class ApiConfig extends WebMvcConfigurerAdapter {

  @Autowired
  @Qualifier("cloudStorageAuthenticationInterceptor")
  private HandlerInterceptor cloudStorageAuthenticationInterceptor;

  @Override
  public void addInterceptors(final InterceptorRegistry registry) {
    registry.addInterceptor(this.cloudStorageAuthenticationInterceptor)
        .addPathPatterns(
            "/fetch-image/**",
            "/fetch-image-async/**"
        );
  }

  @Override
  public void configureAsyncSupport(final AsyncSupportConfigurer configurer) {
    final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(this.asyncThreadPoolCoreSize);
    executor.setMaxPoolSize(this.asyncThreadPoolMaxSize);
    executor.setQueueCapacity(this.asyncThreadPoolQueueCapacity);
    executor.setThreadNamePrefix(this.asyncThreadPoolPrefix);
    executor.initialize();
    configurer.setTaskExecutor(executor);
    super.configureAsyncSupport(configurer);
  }

}

理想情况下,图像获取将异步完成(使用 /fetch-image-asyc/{id} 端点),因为它必须调用可能有一些延迟的第三方 Web 服务。

同步端点 (/fetch-image/{id}) 适用于所有浏览器。但是,如果使用异步端点 (/fetch-image-async/{id}),Chrome 和 Firefox 可以正常工作。

但是,如果客户端是 Microsoft IE 或 Microsoft Edge,我们似乎有些奇怪的行为。端点被正确调用并且响应成功发送(至少从服务器的角度来看)。但是,浏览器似乎正在等待一些额外的东西。在 IE/Edge DevTools 窗口中,图像的网络请求显示为等待 30 秒,然后似乎超时,更新成功,图像成功显示。似乎与服务器的连接仍然打开,因为数据库连接等服务器端资源没有释放。在其他浏览器中,异步响应的接收和处理在一秒钟或更短的时间内完成。

如果我删除了 HandlerInterceptor 并且只是硬连线了一些用于调试的凭据,那么该行为就会消失。所以这似乎与 HandlerInterceptor 和异步控制器方法之间的交互有关,并且只针对部分客户端展示。

有人对为什么 IE/Edge 的语义会导致这种行为有任何建议吗?

【问题讨论】:

    标签: spring spring-mvc internet-explorer asynchronous microsoft-edge


    【解决方案1】:

    在服务器上进行大量跟踪并阅读 AsyncHandlerInterceptor 的 JavaDocs cmets 后,我能够解决问题。对于异步控制器方法的请求,任何拦截器的 preHandle 方法都会被调用两次。在将请求传递给处理请求的 servlet 之前调用它,并在 servlet 处理请求之后再次调用它。在我的例子中,拦截器试图对两种场景的请求进行身份验证(请求前和请求后处理)。应用程序的身份验证提供程序检查数据库中的凭据。由于某种原因,如果客户端是 IE 或 Edge,则在 servlet 处理请求后从拦截器中的 preHandle 调用时,身份验证提供程序无法获得数据库连接。会抛出以下异常:

    ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DataAccessResourceFailureException: Could not open connection; nested exception is org.hibernate.exception.JDBCConnectionException: Could not open connection] with root cause
    java.sql.SQLTransientConnectionException: HikariPool-0 - Connection is not available, request timed out after 30001ms.
    

    因此 servlet 将成功处理请求并发送响应,但过滤器将挂起 30 秒,等待数据库连接在调用 preHandle 的后处理超时。

    所以对我来说,简单的解决方案是在 preHandle 中添加一个检查,如果它在 servlet 已经处理了请求之后被调用。我更新了 preHandle 方法如下:

    @Override
    public boolean preHandle(final HttpServletRequest pRequest, final HttpServletResponse pResponse, final Object pHandler) {
      if (pRequest.getDispatcherType().equals(DispatcherType.REQUEST)) {
        ... perform authentication ...
      }
      return true;
    }
    

    这为我解决了这个问题。它没有解释一切(即为什么只有 IE/Edge 会导致问题),但似乎 preHandle 应该只在 servlet 处理请求之前才起作用。

    【讨论】:

      【解决方案2】:

      根据您的描述,在使用 IE 或 Edge 时会有一些不同的行为

      • 似乎浏览器正在等待其他内容
      • 连接似乎仍然打开
      • 如果删除 HandlerInterceptor 并在身份验证逻辑中使用硬代码,它可以正常工作

      对于第一种行为,我建议您使用fiddlertrace all http requests。如果您可以通过 fiddler (1) 在 chrome 上运行,2) 在 edge 上运行来比较两个不同的操作,那就更好了。仔细检查请求和响应中的所有 http 标头,看看是否有一些不同的部分。对于其他行为,我建议您编写日志以找出哪个部分花费的时间最多。它将为您提供有用的信息进行故障排除。

      【讨论】:

      • 感谢您的建议。我确实跟踪了 Edge 与 Chrome 的请求,并且发现请求或响应标头没有区别。我确实缩小了问题范围,并将更新我的描述。
      猜你喜欢
      • 2021-05-11
      • 1970-01-01
      • 1970-01-01
      • 2019-09-27
      • 1970-01-01
      • 1970-01-01
      • 2012-10-14
      • 2015-10-31
      • 1970-01-01
      相关资源
      最近更新 更多