我正在开发一个服务于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的语义导致这种现象提出了建议。
答案 0 :(得分:0)
根据您的描述,使用IE或Edge时会有一些不同的行为
对于第一种行为,建议您使用fiddler至trace 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处理请求之前才起作用。