华为云OBS整合了Ueditor,但是在批量上传图片时,只能部分上传成功,很多文件会上传失败,经过分析发现:bean作用域为单例模式时,Spring IoC 容器中只会存在一个共享的 Bean 实例,无论有多少个Bean 引用它,始终指向同一对象,该模式在多线程下是不安全的,特此记录。
错误代码:
@Service
@Slf4j
public class FileServiceImpl implements FileService {
@Value("${files.path}")
private String filesPath;
@Value("${files.prefix}")
private String FilesPrefix;
@Value("${huaWeiObs.AccessKeyId}")
private String AccessKeyId;
@Value("${huaWeiObs.AccessKeySecret}")
private String AccessKeySecret;
@Value("${huaWeiObs.BucketName}")
private String BucketName;
@Value("${huaWeiObs.Endpoint}")
private String Endpoint;
@Value("${huaWeiObs.ObsFilesPath}")
private String ObsFilesPath;
private ObsClient obsClient;
@Autowired
private FileInfoMapper fileInfoMapper;
@Override
public FileInfo huaWeiObsUpload(MultipartFile file) {
PutObjectResult putObjectResult = null;
String md5 = null;
try {
//验证文件格式
String fileOrigName = file.getOriginalFilename();
if (!fileOrigName.contains(".")) {
throw new IllegalArgumentException("缺少后缀名");
}
//对文件流进行MD5加密
md5 = FileUtil.fileMd5(file.getInputStream());
FileInfo fileInfo = fileInfoMapper.getById(md5);
//根据MD字符串查看文件是否已经上传,已经上传的文件直接返回文件信息不用调取华为云上传接口
if (fileInfo != null) {
fileInfo.setFilePrefix(FilesPrefix);
fileInfoMapper.update(fileInfo);
return fileInfo;
}
String fileSuffix = fileOrigName.substring(fileOrigName.lastIndexOf("."));
String pathname = ObsFilesPath+FileUtil.getPath()+md5+fileSuffix;
//实例化ObsClient,并将实例化引用赋值给成员变量obsClient
obsClient = new ObsClient(AccessKeyId,AccessKeySecret,Endpoint);
putObjectResult = obsClient.putObject(BucketName, pathname, file.getInputStream());
//保存上传记录到数据库
long size = file.getSize();
String contentType = file.getContentType();
String fullPath = putObjectResult.getObjectUrl();
fileInfo = new FileInfo();
fileInfo.setId(md5);
fileInfo.setContentType(contentType);
fileInfo.setSize(size);
fileInfo.setPath(fullPath);
fileInfo.setUrl(pathname);
fileInfo.setType(contentType.startsWith("image/") ? 1 : 0);
fileInfo.setFilePrefix(FilesPrefix);
fileInfo.setOriginalName(fileOrigName);
fileInfo.setSuffix(fileSuffix);
fileInfoMapper.save(fileInfo);
log.info("文件上传成功{}", fullPath);
//返回文件对象
return fileInfo;
} catch (ObsException e) {
log.info("Response Code: {}",e.getResponseCode());
log.info("Error Message: {}",e.getErrorMessage());
log.info("Error Code: {}",e.getErrorCode());
log.info("Request ID: {}",e.getErrorRequestId());
log.info("Host ID: {}",e.getErrorHostId());
} catch (IOException e) {
e.printStackTrace();
} finally {
//不管有没有返回,最后一定会执行
if (obsClient != null) {
try {
//关闭最后连接
obsClient.close();
} catch (IOException e) {
}
}
}
//返回空对象
return null;
}
}
错误原因:
bean实例默认作用域是单例模式,,Spring IoC 容器中只会存在一个共享的 Bean 实例,无论有多少个Bean 引用它,始终指向同一对象,该模式在多线程下是不安全的,同一个bean对象,可能被注入到了不同的对象中,在高并发的情况下,被依赖的对象可能会多个线程共享,会引发多线程安全问题。
例如:FileService类被FileController类依赖,FileService bean对象通过@Autowired注入到了FileController对象,因为FileService 的bean实例是单例模式,在高并发情况下,同一个FileService bean对象实例会被注入到不同FileController对象,因此FileService bean对象被多个线程共享,引起了安全问题。比如:一个线程正在使用FileService bean对象的成员变量obsClient.putObject()上传文件,另外一个线程使用FileService bean对象成员obsClient.close()关闭了链接,则就导致了部分文件上传失败。
解决方法:
1、像obsClient这样的变量不要声明为成员变量,声明为局部变量,就可以避免obsClient被多个线程共享引发安全问题。
2、使用@Scope(“prototype”)定义FileService bean作用域为原型模式,每次通过 Spring 容器获取 prototype 定义的 bean 时,容器都将创建
一个新的 Bean 实例,就不会引起多线程安全问题。
2、使用sychornized修饰上传文件的代码,锁住对象实例,例如:
Synchronized(this){
//上传代码
//因为同一个FileService bean对象被注入到不象线程,是可以被锁住的
}
但是这个加锁的方法可能会导致严重的阻塞和性能开销,不推荐使用这个方法。
方法1的代码:
@Service
@Slf4j
public class FileServiceImpl_bak implements FileService {
@Value("${files.path}")
private String filesPath;
@Value("${files.prefix}")
private String FilesPrefix;
@Value("${huaWeiObs.AccessKeyId}")
private String AccessKeyId;
@Value("${huaWeiObs.AccessKeySecret}")
private String AccessKeySecret;
@Value("${huaWeiObs.BucketName}")
private String BucketName;
@Value("${huaWeiObs.Endpoint}")
private String Endpoint;
@Value("${huaWeiObs.ObsFilesPath}")
private String ObsFilesPath;
@Autowired
private FileInfoMapper fileInfoMapper;
@Override
public FileInfo huaWeiObsUpload(MultipartFile file) {
//局部变量,不被多个线程共享,这避免了多线程导致的安全问题
ObsClient obsClient = null;
PutObjectResult putObjectResult = null;
String md5 = null;
try {
//验证文件格式
String fileOrigName = file.getOriginalFilename();
if (!fileOrigName.contains(".")) {
throw new IllegalArgumentException("缺少后缀名");
}
//对文件流进行MD5加密
md5 = FileUtil.fileMd5(file.getInputStream());
FileInfo fileInfo = fileInfoMapper.getById(md5);
//根据MD5字符串查看文件是否已经上传,已经上传的文件直接返回文件信息,不用调取华为云上传接口
if (fileInfo != null) {
fileInfo.setFilePrefix(FilesPrefix);
fileInfoMapper.update(fileInfo);
return fileInfo;
}
//文件后缀名与上传路径
String fileSuffix = fileOrigName.substring(fileOrigName.lastIndexOf("."));
String pathname = ObsFilesPath+FileUtil.getPath()+md5+fileSuffix;
obsClient = new ObsClient(AccessKeyId,AccessKeySecret,Endpoint);
putObjectResult = obsClient.putObject(BucketName, pathname, file.getInputStream());
//保存上传记录到数据库
long size = file.getSize();
String contentType = file.getContentType();
String fullPath = putObjectResult.getObjectUrl();
fileInfo = new FileInfo();
fileInfo.setId(md5);
fileInfo.setContentType(contentType);
fileInfo.setSize(size);
fileInfo.setPath(fullPath);
fileInfo.setUrl(pathname);
fileInfo.setType(contentType.startsWith("image/") ? 1 : 0);
fileInfo.setFilePrefix(FilesPrefix);
fileInfo.setOriginalName(fileOrigName);
fileInfo.setSuffix(fileSuffix);
fileInfoMapper.save(fileInfo);
log.info("文件上传成功{}", fullPath);
//返回文件对象
return fileInfo;
} catch (ObsException e) {
log.info("Response Code: {}",e.getResponseCode());
log.info("Error Message: {}",e.getErrorMessage());
log.info("Error Code: {}",e.getErrorCode());
log.info("Request ID: {}",e.getErrorRequestId());
log.info("Host ID: {}",e.getErrorHostId());
} catch (IOException e) {
e.printStackTrace();
} finally {
//不管有没有返回,最后一定会执行
if (obsClient != null) {
try {
//关闭最后连接
obsClient.close();
} catch (IOException e) {
}
}
}
return null;
}
}
总结:
Spring 3 中为 Bean 定义了 5 种作用域,分别为 singleton(单例)、prototype(原型)、request、session 和 global session,不同的应用场景需要选择不同的作用域降低开销和保证线程安全,springboot 定义bean的作用域时,使用@Scope注解修饰类。例如:
//定义单例模式
@Scope("singleton")
@Component
public class SingleScopeTest {
}
//定义原型模式
@Scope("prototype")
@Component
public class PrototypeScoreTest {
}