Spring和异步控制器+ HandlerInterceptor + IE / Edge

时间:2018-10-10 19:03:09

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

我正在开发一个服务于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的语义导致这种现象提出了建议。

2 个答案:

答案 0 :(得分:0)

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

  • 似乎浏览器正在等待其他东西
  • 连接似乎仍然打开
  • 如果删除HandlerInterceptor并在身份验证逻辑中使用硬代码,则效果很好

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

答案 1 :(得分:0)

在服务器上进行了很多跟踪并阅读了AsyncHandlerInterceptor的JavaDocs注释之后,我得以解决此问题。对于对异步控制器方法的请求,任何拦截器的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的后处理中超时。

所以对我来说,简单的解决方案是在servlet已经处理了请求之后在preHandle中添加检查是否被调用。我更新了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处理请求之前才起作用。