Seamless OSS Switching with Adapter Pattern and Nacos Dynamic Configuration
This article demonstrates how to use the Adapter pattern together with Nacos dynamic configuration to abstract multiple OSS providers (Minio, Aliyun), configure Spring beans for hot‑refreshable storage selection, and integrate the solution into a microservice’s service, file, and controller layers.
Introduction
In a microservice project, the OSS storage service may need to support multiple vendors such as Alibaba Cloud, Tencent Cloud, and Minio, and new vendors can appear later. Directly changing the concrete vendor forces modifications in controller and service layers, violating low‑coupling principles. The Adapter pattern can solve this problem.
Adapter Pattern Refactor
The existing utility classes MinioUtils and AliyunUtils implement vendor‑specific logic. To expose a common interface, we define a target interface StorageAdapter that declares createBucket, uploadFile, and getUrl methods.
public interface StorageAdapter {
void createBucket(String bucket);
void uploadFile(MultipartFile multipartFile, String bucket, String objectName);
String getUrl(String bucket, String objectName);
}Each OSS vendor implements this interface.
Minio Adapter
@Component
@Log4j2
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;
}
}Aliyun Adapter
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";
}
}Dynamic Configuration with Nacos
We read the current storage type from Nacos using a @Value("${storage.service.type}") field. Adding a new OSS only requires a new adapter class and an additional else branch in the bean factory.
Note: The adapter implementation is instantiated with new instead of using @Service to avoid having to register every new implementation as a Spring bean.
@Configuration
public class StorageConfig {
@Value("${storage.service.type}")
private String storageType;
@Bean
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 processor found");
}
}
}FileService Anti‑Corruption Layer
@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 multipartFile, String bucket, String objectName) {
storageAdapter.uploadFile(multipartFile, bucket, objectName);
String finalObjectName = (StringUtils.isEmpty(objectName) ? "" : objectName + "/") + multipartFile.getOriginalFilename();
return storageAdapter.getUrl(bucket, finalObjectName);
}
}Controller Layer
@RestController
@Log4j2
public class FileController {
@Resource
private FileService fileService;
@PostMapping("/upload")
public Result<String> upload(MultipartFile uploadFile, String bucket, String objectName) throws Exception {
Preconditions.checkArgument(!ObjectUtils.isEmpty(uploadFile), "File cannot be empty");
Preconditions.checkArgument(!StringUtils.isEmpty(bucket), "Bucket cannot be empty");
if (log.isInfoEnabled()) {
log.info("FileController.upload: {}, bucket:{}, objectName:{}", uploadFile.getOriginalFilename(), bucket, objectName);
}
String url = fileService.uploadFile(uploadFile, bucket, objectName);
return Result.ok(url);
}
}Nacos Deployment
Run Nacos in standalone mode with Docker, exposing ports 8848 (HTTP) and 9848 (gRPC). Example command:
docker pull nacos/nacos-server
docker run -d \
--name nacos \
--privileged \
--cgroupns host \
--env JVM_XMX=256m \
--env MODE=standalone \
--env JVM_XMS=256m \
-p 8848:8848/tcp \
-p 9848:9848/tcp \
--restart=always \
-w /home/nacos \
nacos/nacos-serverProject Dependencies
Add the following Maven dependencies to enable Nacos config and Log4j2 logging:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
<version>2.4.2</version>
</dependency>Configuration Files
Write Nacos‑related settings into bootstrap.yml so they are loaded first:
spring:
application:
name: jc-club-oss # microservice name
profiles:
active: dev # development profile
cloud:
nacos:
server-addr: 117.72.118.73:8848
config:
file-extension: yamlThe data ID follows the pattern
${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}, e.g., jc-club-oss-dev.yaml.
Hot Refresh with @RefreshScope
Annotate the configuration class and the @Bean method with @RefreshScope. When the Nacos configuration changes (e.g., storage.service.type), the bean is re‑initialized automatically, and the new storage adapter takes effect without restarting the service.
@Configuration
@RefreshScope
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 processor found");
}
}
}Testing
When storage.service.type is set to aliyun, the controller returns the string aliyun. After changing the value to minio, file uploads succeed and the returned URL points to the Minio bucket. Nacos logs the refresh event:
2024-12-03 17:05:50.719 INFO Refresh keys changed: [storage.service.type]
The screenshots in the original article confirm successful uploads for both configurations.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
