Java策略模式:从加锁到文件存储,封装“变化的那一步”
Java策略模式:从加锁到文件存储,封装"变化的那一步"
文章标签:
Java设计模式策略模式摘要:同样时“加锁-执行-释放”这个流程,
synchronized、ReentrantLock、Redis锁、数据库锁的实现天差地别。同样时文件存储,本地磁盘、S3桶、阿里云OSS的存储方式完全不同。策略模式的本质不是消除if-else,而是把变化的那一步抽象出来封装成可替换的策略,让不变的骨架保持稳定。本文用加锁和文件存储两个贴近日常开发的场景,展示策略模式的真实应用方式。
一、问题场景:当"稳定流程"遇上"多变实现"
你有没有写过这样的代码?
一个方法里,核心流程只有三四步,但其中某一步因为"运行环境"不同要写好几个版本。单机部署用本地锁(synchronized 或 ReentrantLock)就够了,扩容到多实例就得换成 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(你不会需要它)
- 策略的选择逻辑本身依赖于策略的执行结果(这时应该用状态模式)
- 策略之间差异极大,共享的接口过于宽泛(考虑模板方法模式)
💡 判断信号:
- “又要换一种方式”,这个需求出现第二次
- 业务方法的同行评审里有人问"这里用了 XX 方案,如果将来换 YY 怎么办"
- 同一个流程的 N 个实现开始出现重复的骨架代码
五、总结
策略模式的真正价值不在于消除 if-else —— 那只是一种表象。它的核心是将变与不变分离,让稳定的流程不受变化实现的影响。简单来说策略模式封装了变化。
从本文两个例子可以看到策略模式的两个常见切入角度:
| 角度 | 加锁场景 | 文件存储场景 |
|---|---|---|
| 问题 | 锁方案经常切换 | 存储方式经常切换 |
| 不变 | "加锁→执行→释放"流程 | "生成文件名→保存→返回URL"流程 |
| 变化 | 如何加锁/释放 | 把文件存到哪里、怎么存 |
| 策略接口 | ILockStrategy |
IFileStorageStrategy |
| 收益 | 业务代码与锁解耦 | 业务代码与存储解耦 |
下次写代码时,如果发现一个方法里既有"做什么"也有"怎么做",停下来想一想:能不能把"怎么做"抽出去?
更多推荐


所有评论(0)