Applying the Adapter Pattern for Multi‑Cloud OSS Storage in a Spring Microservice
This article demonstrates how to use the Adapter pattern to abstract multiple OSS providers such as MinIO and Aliyun in a Spring microservice, configure the concrete adapters via Nacos dynamic configuration, and expose a unified upload API through a service and controller layer, complete with deployment and testing steps.
In a microservice project, the OSS storage service often needs to support multiple cloud vendors (Aliyun, Tencent Cloud, MinIO) and may need to add new providers later, which would otherwise cause changes in both controller and service layers, violating low‑coupling principles.
To solve this, the Adapter pattern is introduced: a StorageAdapter interface defines the common operations (createBucket, uploadFile, getUrl), and each vendor implements its own adapter class.
Example MinIO adapter implementation:
@Component
public class MinioStorageAdapter implements StorageAdapter {
@Resource
private MinioUtil minioUtil;
@Value("${minio.url}")
private String url;
@Override
@SneakyThrows
public void createBucket(String bucket) {
minioUtil.createBucket(bucket);
}
@Override
@SneakyThrows
public void uploadFile(MultipartFile multipartFile, String bucket, String objectName) {
minioUtil.createBucket(bucket);
if (objectName != null) {
minioUtil.uploadFile(multipartFile.getInputStream(), bucket, objectName + "/" + multipartFile.getOriginalFilename());
} else {
minioUtil.uploadFile(multipartFile.getInputStream(), bucket, multipartFile.getOriginalFilename());
}
}
@Override
public String getUrl(String bucket, String objectName) {
return url + "/" + bucket + "/" + objectName;
}
}Example Aliyun adapter implementation:
public class AliStorageAdapter implements StorageAdapter {
@Override
public void createBucket(String bucket) {
System.out.println("aliyun");
}
@Override
public void uploadFile(MultipartFile multipartFile, String bucket, String objectName) {
// implementation omitted for brevity
}
@Override
public String getUrl(String bucket, String objectName) {
return "aliyun";
}
}The StorageConfig class reads the current storage.service.type from Nacos using @Value and creates the appropriate adapter bean; @RefreshScope enables hot‑reloading when the configuration changes.
@Configuration
public class StorageConfig {
@Value("${storage.service.type}")
private String storageType;
@Bean
@RefreshScope
public StorageAdapter storageAdapter() {
if ("minio".equals(storageType)) {
return new MinioStorageAdapter();
} else if ("aliyun".equals(storageType)) {
return new AliStorageAdapter();
} else {
throw new IllegalArgumentException("No matching storage adapter");
}
}
}A FileService acts as an anti‑corruption layer, delegating calls to the injected StorageAdapter and constructing the final file URL.
@Component
public class FileService {
private final StorageAdapter storageAdapter;
public FileService(StorageAdapter storageAdapter) {
this.storageAdapter = storageAdapter;
}
public void createBucket(String bucket) {
storageAdapter.createBucket(bucket);
}
public String uploadFile(MultipartFile file, String bucket, String objectName) {
storageAdapter.uploadFile(file, bucket, objectName);
String finalName = (StringUtils.isEmpty(objectName) ? "" : objectName + "/") + file.getOriginalFilename();
return storageAdapter.getUrl(bucket, finalName);
}
}The FileController exposes a /upload endpoint that validates input, calls FileService.uploadFile , and returns the file URL.
@RestController
public class FileController {
@Resource
private FileService fileService;
@PostMapping("/upload")
public Result
upload(MultipartFile uploadFile, String bucket, String objectName) throws Exception {
Preconditions.checkArgument(!ObjectUtils.isEmpty(uploadFile), "文件不能为空");
Preconditions.checkArgument(!StringUtils.isEmpty(bucket), "bucket桶名称不能为空");
String url = fileService.uploadFile(uploadFile, bucket, objectName);
return Result.ok(url);
}
}The article also provides Nacos deployment commands (Docker run with privileged mode, cgroup host, JVM settings) and the necessary Spring Boot configuration (bootstrap.yml, @RefreshScope usage) to enable dynamic switching of storage providers without code changes.
Testing shows that switching storage.service.type from aliyun to minio updates the behavior at runtime, with successful file uploads and correct URL generation, confirming the effectiveness of the adapter‑based design.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.