Java策略模式:从加锁到文件存储,封装"变化的那一步"

文章标签:Java 设计模式 策略模式

摘要:同样时“加锁-执行-释放”这个流程,synchronizedReentrantLockRedis锁、数据库锁的实现天差地别。同样时文件存储,本地磁盘、S3桶、阿里云OSS的存储方式完全不同。策略模式的本质不是消除if-else,而是把变化的那一步抽象出来封装成可替换的策略,让不变的骨架保持稳定。本文用加锁和文件存储两个贴近日常开发的场景,展示策略模式的真实应用方式。


一、问题场景:当"稳定流程"遇上"多变实现"

你有没有写过这样的代码?

一个方法里,核心流程只有三四步,但其中某一步因为"运行环境"不同要写好几个版本。单机部署用本地锁(synchronizedReentrantLock)就够了,扩容到多实例就得换成 Redis 分布式锁。“运行环境”不同,加锁方式就不同。

或者另一个场景:文件上传功能上线时用的本地存储,三个月后业务量上来要迁到 S3,半年后公司选了阿里云要求全部切到 OSS。每次换存储,FileUploadService 都要经历一次大手术。

这两个问题的共同本质:一个稳定的流程骨架,其中某一步有多个实现,而且这些实现很可能在运行时切换、在将来增加。


二、直观解法(反面教材):硬编码的代价

1. 场景一:加锁方案反复切换

一个简单的"扣库存"方法,一开始为了省事直接在方法签名上加 synchronized

// ❌ 反面教材:锁策略直接硬编码在业务方法上
public class InventoryService {
    public synchronized boolean deductStock(String sku, int quantity) {
        int current = inventoryRepo.findBySku(sku).getQuantity();
        if (current < quantity) return false;
        inventoryRepo.update(sku, current - quantity);
        log.info("deduct_stock sku={} quantity={}", sku, quantity);
        return true;
    }
}

一切正常,直到有一天需要引入 Redis 分布式锁——因为服务扩容到了多实例:

// 改成了 Redis 锁,但锁逻辑混在业务代码里
public class InventoryService {
    private final RedisTemplate<String, String> redis;

    public boolean deductStock(String sku, int quantity) {
        String lockKey = "lock:stock:" + sku;
        Boolean locked = redis.opsForValue()
            .setIfAbsent(lockKey, "1", Duration.ofSeconds(5));
        if (!Boolean.TRUE.equals(locked))
            throw new LockAcquisitionException("获取锁失败:" + sku);
        try {
            int current = inventoryRepo.findBySku(sku).getQuantity();
            if (current < quantity) return false;
            inventoryRepo.update(sku, current - quantity);
            log.info("deduct_stock sku={} quantity={}", sku, quantity);
            return true;
        } finally {
            redis.delete(lockKey);
        }
    }
}

问题所在:每次改锁方案都要修改 deductStock 方法本身,且业务逻辑和锁逻辑纠缠在一起。

2. 场景二:文件存储方式频繁变更

// ❌ 反面教材:存储逻辑直接写在业务方法中
public class FileUploadService {
    @Value("${upload.dir}")
    private String uploadDir;

    public String upload(MultipartFile file) {
        String filename = UUID.randomUUID() + "_" + file.getOriginalFilename();
        // …… 文件元数据处理
        try {
            Path targetPath = Path.of(uploadDir, filename);
            Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);
            return filename;
        } catch (IOException e) {
            throw new FileUploadException("文件上传失败", e);
        }
    }
}

切到 S3 时,upload 方法又得重写一遍。更糟糕的是,如果同时支持多种存储(不同客户用不同后端),代码会迅速膨胀为 if-else 的泥潭。


三、策略模式重构:把"变化的那一步"抽出去

这两个场景的共同解法:把"变化的那一步"提取成策略接口,让业务方法(骨架)只依赖接口,不依赖具体实现。

策略模式的核心原则:

识别"骨架"和"可替换步骤",把可替换步骤封装成接口,让骨架稳定,让策略独立演化。

这个思想在两个场景中的体现:

场景 不变的骨架 变化的策略
加锁 获取锁 → 执行业务 → 释放锁 如何获取/释放锁
文件存储 文件元数据处理 → 存储 → 记录日志 把文件存到哪里、怎么存

1. 定义策略接口

加锁策略接口:

@FunctionalInterface
public interface ILockCall<R> {
    R call() throws Exception;
}

public interface ILockStrategy {
    /**
     * 执行锁逻辑
     *
     * @param lockKey     锁键
     * @param lockBuilder 锁构建器参数,锁超时时间、超时提示等信息
     * @param callback    执行函数
     * @return 执行结果
     */
    <R> R execute(String lockKey, LockBuilder lockBuilder, ILockCall<R> callback) throws Exception {
}

文件存储策略接口:

public interface IFileStorageStrategy {
    /** 
     * 保存文件,返回可访问的文件路径/URL 
     */
    String save(String filename, InputStream content, long size);

    /** 
      * 删除文件 
      */
    void delete(String filename);

    /** 
      * 判断文件是否存在 
      */
    boolean exists(String filename);
}

2. 多种策略实现各司其职

加锁策略一:synchronized 单机策略

public class SynchronizedLockStrategy implements ILockStrategy {
    private final ConcurrentHashMap<String, Object> keyLocks = new ConcurrentHashMap<>();

    @Override
    public <R> R execute(String lockKey, LockBuilder lockBuilder, ILockCall<R> callback) throws Exception {
        Object lock = keyLocks.computeIfAbsent(lockKey, k -> new Object());
        synchronized (lock) {
            return callback.call();
        }
    }
}

加锁策略二:ReentrantLock 超时可中断策略

public class ReentrantLockStrategy implements ILockStrategy {

    private final Map<String, ReentrantLock> lockMap = new ConcurrentHashMap<>();

    @Override
    public <R> R execute(String lockKey, LockBuilder lockBuilder, ILockCall<R> callback) throws Exception {
        ReentrantLock lock = this.lockMap.computeIfAbsent(lockKey, (k) -> new ReentrantLock());

        try {
            if (lock.tryLock(lockBuilder.getLockTimeout(), lockBuilder.getLockTimeoutUnit())) {
                return callback.call();
            } else {
                String failMsg = StrUtil.i18n(lockBuilder.getFailMsgCn(), lockBuilder.getFailMsgEn());
                failMsg = StrUtil.orElse(failMsg, "Lock failed");
                throw new LockException(failMsg);
            }
        } catch (InterruptedException var10) {
            Thread.currentThread().interrupt();
            throw new LockException("Lock interrupted");
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

加锁策略三:Redis 分布式锁(多实例集群)

public class RedisLockStrategy implements LockStrategy {
    private static final String LOCK_KEY_FORMAT = "LockManagement:RedisLockHandler:%s";
    private static final String LOCK_VALUE = "RedisLockValue";
    private StringRedisTemplate redisTemplate;

    public void setRedisTemplate(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public <R> R execute(String lockKey, LockBuilder lockBuilder, ILockCall<R> callback) throws Exception {
        lockKey = getLockKey(lockKey);
        Boolean hasLock = this.redisTemplate.opsForValue().setIfAbsent(lockKey, LOCK_VALUE, lockBuilder.getLockTimeout(), lockBuilder.getLockTimeoutUnit());
        if (Boolean.TRUE.equals(hasLock)) {
            try {
                return callback.call();
            } finally {
                this.redisTemplate.delete(lockKey);
            }
        } else {
            String failMsg = StrUtil.i18n(lockBuilder.getFailMsgCn(), lockBuilder.getFailMsgEn());
            failMsg = StrUtil.orElse(failMsg, "Lock failed");
            throw new LockException(failMsg);
        }
    }

    private static String getLockKey(String lockKey) {
        return String.format(Locale.ROOT, LOCK_KEY_FORMAT, lockKey);
    }
}

文件存储策略一:本地文件存储

public class LocalFileStorage implements IFileStorageStrategy {
    private final Path basePath;

    public LocalFileStorage(String baseDir) {
        this.basePath = Path.of(baseDir);
    }

    @Override
    public String save(String filename, InputStream content, long size) {
        try {
            Path target = basePath.resolve(filename);
            Files.createDirectories(target.getParent());
            Files.copy(content, target, StandardCopyOption.REPLACE_EXISTING);
            return target.toAbsolutePath().toString();
        } catch (IOException e) {
            throw new FileUploadException("本地存储写入失败", e);
        }
    }

    @Override
    public void delete(String filename) {
        try {
            Files.deleteIfExists(basePath.resolve(filename));
        } catch (IOException e) {
            throw new FileUploadException("本地存储删除失败", e);
        }
    }

    @Override
    public boolean exists(String filename) {
        return Files.exists(basePath.resolve(filename));
    }
}

文件存储策略二:AWS S3

public class S3FileStorage implements IFileStorageStrategy {
    private final AmazonS3 s3Client;
    private final String bucket;

    public S3FileStorage(AmazonS3 s3Client, String bucket) {
        this.s3Client = s3Client;
        this.bucket = bucket;
    }

    @Override
    public String save(String filename, InputStream content, long size) {
        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentLength(size);
        s3Client.putObject(bucket, filename, content, metadata);
        return String.format("https://%s.s3.amazonaws.com/%s", bucket, filename);
    }

    @Override
    public void delete(String filename) {
        s3Client.deleteObject(bucket, filename);
    }

    @Override
    public boolean exists(String filename) {
        return s3Client.doesObjectExist(bucket, filename);
    }
}

文件存储策略三:阿里云 OSS

public class OssFileStorage implements IFileStorageStrategy {
    private final OSS ossClient;
    private final String bucket;

    public OssFileStorage(OSS ossClient, String bucket) {
        this.ossClient = ossClient;
        this.bucket = bucket;
    }

    @Override
    public String save(String filename, InputStream content, long size) {
        PutObjectRequest request = new PutObjectRequest(bucket, filename, content);
        ossClient.putObject(request);
        return String.format("https://%s.oss-cn-hangzhou.aliyuncs.com/%s", bucket, filename);
    }

    @Override
    public void delete(String filename) {
        ossClient.deleteObject(bucket, filename);
    }

    @Override
    public boolean exists(String filename) {
        return ossClient.doesObjectExist(bucket, filename);
    }
}

3. 业务代码只关心"骨架"

加锁场景重构后——业务代码不再关心锁的实现:

@Service
public class InventoryService {
    private final LockStrategy lockStrategy;

    // 锁策略通过构造器注入——运行时可以切换
    public InventoryService(LockStrategy lockStrategy) {
        this.lockStrategy = lockStrategy;
    }

    public boolean deductStock(String sku, int quantity) {
        return lockStrategy.executeWithLock("lock:stock:" + sku, () -> {
            // ✅ 这里只写业务逻辑,完全不关心锁是怎么实现的
            int current = inventoryRepo.findBySku(sku).getQuantity();
            if (current < quantity) return false;
            inventoryRepo.update(sku, current - quantity);
            log.info("deduct_stock sku={} quantity={}", sku, quantity);
            return true;
        });
    }
}

文件存储场景重构后——业务代码不关心存储在哪里:

@Service
public class FileUploadService {
    private final FileStorageStrategy storageStrategy;

    public FileUploadService(FileStorageStrategy storageStrategy) {
        this.storageStrategy = storageStrategy;
    }

    public FileUploadResult upload(MultipartFile file) {
        String filename = UUID.randomUUID() + "_" + file.getOriginalFilename();
        try (InputStream content = file.getInputStream()) {
            String url = storageStrategy.save(filename, content, file.getSize());
            log.info("file_uploaded filename={} size={}", filename, file.getSize());
            return new FileUploadResult(filename, url);
        } catch (IOException e) {
            throw new FileUploadException("文件上传失败", e);
        }
    }

    public void delete(String filename) {
        storageStrategy.delete(filename);
    }
}

现代框架让策略注册几乎零配置——Spring 可以自动收集所有 IFileStorageStrategy 的实现:

@Component
public class FileUploadService {
    private final Map<String, FileStorageStrategy> strategies;

    public FileUploadService(Map<String, FileStorageStrategy> strategies) {
        this.strategies = strategies;  // Spring 自动注入所有策略 Bean
    }

    public FileUploadResult upload(MultipartFile file, String storageType) {
        FileStorageStrategy strategy = strategies.get(storageType);
        if (strategy == null) {
            throw new IllegalArgumentException("不支持的存储类型:" + storageType);
        }
        String filename = UUID.randomUUID() + "_" + file.getOriginalFilename();
        try (InputStream content = file.getInputStream()) {
            String url = strategy.save(filename, content, file.getSize());
            return new FileUploadResult(filename, url);
        } catch (IOException e) {
            throw new FileUploadException("文件上传失败", e);
        }
    }
}

这样,同一个服务可以同时支持多种存储方式,客户端通过参数指定使用哪种策略。

4. 重构后的收益对比

维度 重构前 重构后
切换锁方案 修改业务代码,风险高 切换注入的 Bean,不动业务代码
新增存储方式 upload 方法,加 if-else 新增策略实现类,符合开闭原则
代码可读性 业务逻辑与技术实现混杂 骨架清晰,实现隔离

四、使用指南:什么时候用,什么时候别用

✅ 适合使用策略模式的情况:

  • 同一个行为(算法)有多个不同的实现方式,且这些实现可能在运行时切换(环境、配置、用户选择)
  • 你发现自己在一个方法中写了大量条件分支来选择算法
  • 算法类之间差异不大,但实现各有不同

❌ 不要强行使用的情况:

  • 实现方式只有两三种且未来几乎不可能变化——YAGNI(你不会需要它)
  • 策略的选择逻辑本身依赖于策略的执行结果(这时应该用状态模式)
  • 策略之间差异极大,共享的接口过于宽泛(考虑模板方法模式)

💡 判断信号:

  1. “又要换一种方式”,这个需求出现第二次
  2. 业务方法的同行评审里有人问"这里用了 XX 方案,如果将来换 YY 怎么办"
  3. 同一个流程的 N 个实现开始出现重复的骨架代码

五、总结

策略模式的真正价值不在于消除 if-else —— 那只是一种表象。它的核心是将变与不变分离,让稳定的流程不受变化实现的影响。简单来说策略模式封装了变化

从本文两个例子可以看到策略模式的两个常见切入角度:

角度 加锁场景 文件存储场景
问题 锁方案经常切换 存储方式经常切换
不变 "加锁→执行→释放"流程 "生成文件名→保存→返回URL"流程
变化 如何加锁/释放 把文件存到哪里、怎么存
策略接口 ILockStrategy IFileStorageStrategy
收益 业务代码与锁解耦 业务代码与存储解耦

下次写代码时,如果发现一个方法里既有"做什么"也有"怎么做",停下来想一想:能不能把"怎么做"抽出去?

更多推荐