HUAWEI CLOUD OBSはUeditorを統合していますが、画像をバッチでアップロードすると、部分的なアップロードのみが成功し、多くのファイルがアップロードに失敗します。分析の結果、Beanスコープがシングルトンモードの場合、SpringIoCコンテナには共有Beanが1つしかないことがわかりました。インスタンスは、参照する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インスタンスの
デフォルトのスコープはシングルトンモードです。SpringIoCコンテナには共有Beanインスタンスが1つだけあります。参照するBeanの数に関係なく、常に同じオブジェクトを指します。このモードはマルチスレッドでは安全ではありません。同じBeanオブジェクトが異なるオブジェクトに挿入される可能性があります。同時実行性が高い場合、依存オブジェクトが複数のスレッドで共有される可能性があり、マルチスレッドの安全性の問題が発生します。
例:FileServiceクラスはFileControllerクラスに依存しており、FileService Beanオブジェクトは@Autowiredを介してFileControllerオブジェクトに挿入されます。FileServiceのBeanインスタンスはシングルトンモードであるため、同時実行性が高い場合、同じFileServiceBeanオブジェクトインスタンスが異なるFileControllerオブジェクトに挿入されます。したがって、FileService Beanオブジェクトは複数のスレッドで共有され、セキュリティの問題が発生します。例:スレッドがFileService BeanオブジェクトのobsClient.putObject()メンバー変数を使用してファイルをアップロードし、別のスレッドがFileService BeanオブジェクトのobsClient.close()メンバーを使用してリンクを閉じるため、ファイルアップロードの一部が失敗します。
解決策:
1。obsClientのような変数をメンバー変数としてではなく、ローカル変数として宣言して、obsClientが複数のスレッドで共有されてセキュリティ上の問題が発生しないようにします。
2. @Scope( "prototype")を使用して、FileService Beanスコープをプロトタイプモードとして定義します。プロトタイプによって定義されたBeanがSpringコンテナーを介して取得されるたびに、コンテナーは
新しい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つのスコープ、つまりシングルトン(シングルトン)、プロトタイプ(プロトタイプ)、リクエスト、セッション、グローバルセッションを定義しています。オーバーヘッドを削減し、スレッドの安全性を確保するには、さまざまなアプリケーションシナリオでさまざまなスコープを選択する必要があります。 springbootがBeanのスコープを定義するときは、@ Scopeアノテーションを使用してクラスを装飾します。例えば:
//定义单例模式
@Scope("singleton")
@Component
public class SingleScopeTest {
}
//定义原型模式
@Scope("prototype")
@Component
public class PrototypeScoreTest {
}