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.

Java Companion
Java Companion
Java Companion
Seamless OSS Switching with Adapter Pattern and Nacos Dynamic Configuration

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-server

Project 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: yaml

The 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavamicroservicesDynamic ConfigurationNacosSpring BootAdapter PatternOSS
Java Companion
Written by

Java Companion

A highly professional Java public account

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.