找回密码
 立即注册
首页 业界区 业界 一次并发请求导致图片错乱的排查与修复 ...

一次并发请求导致图片错乱的排查与修复

拼匍弦 2025-7-9 16:16:33
在开发过程中,我们时常会遇到与并发请求相关的问题。最近,我在实现一个图片预览功能时,发现多个并发请求时会引发资源加载错乱的问题。经过排查,最终锁定问题与 Spring Bean 的作用域设置有关。本文将详细记录问题的发现、分析及解决过程。
问题发现

我需要实现的功能是在用户管理的表格中显示用户头像的预览图。前端通过url获取图片。但是却出现了一个意外的行为:每次刷新页面,图片可能会出现随机混乱,原本的图片1展示的可能却是图片2。我先后进行了如下排查:

  • 浏览器f12的网络选项卡中查看,发现获取到的图片也是错乱的,单独打开一个标签页,直接访问图片的url却没有问题。尝试更换浏览器问题仍然存在。此时排除浏览器的问题。
  • 给el-table添加了row-key和key标识问题仍然存在。
  • 将图片url换成了其他的图片链接,刷新图片不再出现错乱。此时问题应该出现在后端api上。
问题描述

在用户管理模块中,我需要通过后端 API 提供图片预览功能,接口代码如下:
  1. @RestController
  2. @RequestMapping("/api/media")
  3. public class MediaApi {
  4.     private final MediaService fileService;
  5.     private final CustomResourceHttpRequestHandler customResourceHttpRequestHandler;
  6.     @Autowired
  7.     public MediaApi(MediaService mediaService, CustomResourceHttpRequestHandler customResourceHttpRequestHandler) {
  8.         this.fileService = mediaService;
  9.         this.customResourceHttpRequestHandler = customResourceHttpRequestHandler;
  10.     }
  11.     //预览文件
  12.     @GetMapping("/preview/{fileId}")
  13.     public ResponseEntity<?> preview(@PathVariable Long fileId, HttpServletRequest request, HttpServletResponse response) {
  14.         try {
  15.             // 解析文件路径
  16.             Path filePath = fileService.getFileFSPath(fileId);
  17.             // 设置资源路径
  18.             customResourceHttpRequestHandler.setResource(filePath);
  19.             // 设置响应头,inline 会在浏览器中显示或播放文件
  20.             response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline; filename="" + filePath.getFileName() + """);
  21.             // 让 CustomResourceHttpRequestHandler 处理请求
  22.             customResourceHttpRequestHandler.handleRequest(request, response);
  23.             return new ResponseEntity<>(HttpStatus.OK);
  24.         } catch (GeneralException e) {
  25.             return ResponseEntity.ok().body(RestBean.failure(400, e.getMessage()));
  26.         } catch (Exception e) {
  27.             return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
  28.         }
  29.     }
  30. }
复制代码
其中,customResourceHttpRequestHandler 是一个自定义的 ResourceHttpRequestHandler,用于处理静态资源。代码如下:
  1. @Component
  2. public class CustomResourceHttpRequestHandler extends ResourceHttpRequestHandler {
  3.     private Resource resource;
  4.     @Override
  5.     protected Resource getResource(@NonNull HttpServletRequest request) {
  6.         return this.resource;
  7.     }
  8.     public void setResource(Path filePath) throws MalformedURLException {
  9.         this.resource = new UrlResource(filePath.toUri());
  10.         setLocations(List.of(this.resource));
  11.     }
  12. }
复制代码
最初,接口可以正常返回图片,但在并发请求的情况下会出现资源错乱的问题。比如,访问 /preview/1 和 /preview/3 时,返回的图片可能都是 1 或者 3,而不是各自的资源。
原因分析

通过排查发现,问题出在 CustomResourceHttpRequestHandler 的 resource 属性上。该属性在 setResource 方法中被动态修改,但 CustomResourceHttpRequestHandler 默认是单例的(singleton 作用域),因此多个请求共享了同一个 resource,导致资源错乱。
单例模式下的线程安全问题

Spring 默认会将 @Component 标注的 Bean 定义为单例。当多个线程并发访问时,Bean 的共享属性会存在线程安全问题。
解决方案

为了解决这个问题,我们需要让 CustomResourceHttpRequestHandler 实例在每个请求中独立使用,避免共享状态。最简单的方式是将其作用域改为 request:
修改作用域
  1. @Component
  2. @Scope("request", proxyMode = ScopedProxyMode.TARGET_CLASS) //这里必须设置request作用域,以避免并发请求时资源出现混乱的问题
  3. public class CustomResourceHttpRequestHandler extends ResourceHttpRequestHandler {
  4.     private Resource resource; //问题根源,默认的作用域可能会导致多个请求共用一个资源,导致响应内容错乱
  5.     @Override
  6.     protected Resource getResource(@NonNull HttpServletRequest request) {
  7.         return this.resource;
  8.     }
  9.     public void setResource(Path filePath) throws MalformedURLException {
  10.         this.resource = new UrlResource(filePath.toUri());
  11.         setLocations(List.of(this.resource));
  12.     }
  13. }
复制代码
关键点

  • @Scope(value = "request"):让 Spring 在每次 HTTP 请求时创建一个新的实例。
  • proxyMode = ScopedProxyMode.TARGET_CLASS:通过代理的方式注入 Bean,从而支持注入到单例 Bean 中。
这样,每次请求都会创建一个独立的 CustomResourceHttpRequestHandler,避免了资源错乱的问题。
注意事项

配置 request 作用域时,控制台可能会出现如下警告:
  1. Unable to proxy interface-implementing method ... because it is marked as final
复制代码
这是由于 CGLIB 代理对 final 方法的限制,可以忽略这个警告,因为它不会影响功能。
想要消除警告也可以将对应的controller同样设置成request作用域,不使用代理注入:
  1. @RestController
  2. @Scope("request") //设置为request作用域
  3. @RequestMapping("/api/media")
  4. public class MediaApi {
  5. }
复制代码
  1. @Component
  2. @Scope("request") //设置为request作用域
  3. public class CustomResourceHttpRequestHandler extends ResourceHttpRequestHandler {
  4. }
复制代码
总结

通过将 Bean 的作用域调整为 request 或者移除共享状态,我们可以有效避免 Spring Bean 的并发问题。在实际开发中:

  • 单例 Bean 不应包含可变的共享状态。
  • 使用 @Scope("request") 是解决每请求独立实例化问题的常见方式。
  • 遇到复杂场景时,方法注入也是一种有效的手段。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册