refactor: 存储模式动态检查环境变量,改PAR_STORAGE_TYPE无需重启
- CachedStorageService 每次调用读 PAR_STORAGE_TYPE 环境变量 - QiniuStorageService 首次调用懒初始化,读 QINIU_* 环境变量 - 去掉 @ConditionalOnProperty/@ConditionalOnMissingBean
This commit is contained in:
@@ -6,9 +6,8 @@ import com.par.core.entity.SiteConfig;
|
||||
import com.par.core.enums.ConfigStatus;
|
||||
import com.par.core.mapper.SiteConfigMapper;
|
||||
import com.par.core.service.ConfigService;
|
||||
import com.par.core.storage.StorageService;
|
||||
import com.par.core.storage.CachedStorageService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -20,14 +19,12 @@ import java.util.stream.Collectors;
|
||||
public class ConfigServiceImpl implements ConfigService {
|
||||
|
||||
private final SiteConfigMapper siteConfigMapper;
|
||||
private final StorageService storageService;
|
||||
private final CachedStorageService storageService;
|
||||
|
||||
public ConfigServiceImpl(SiteConfigMapper siteConfigMapper,
|
||||
@Autowired(required = false) StorageService storageService) {
|
||||
CachedStorageService storageService) {
|
||||
this.siteConfigMapper = siteConfigMapper;
|
||||
// 未注入 StorageService 时(极端情况),回退到本地文件存储
|
||||
this.storageService = storageService != null ? storageService
|
||||
: new com.par.core.storage.LocalStorageService("./data/static");
|
||||
this.storageService = storageService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,61 +1,58 @@
|
||||
package com.par.core.storage;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 缓存存储:七牛(主) + 本地(读缓存/备份)。
|
||||
* <p>
|
||||
* 写:先写七牛,成功后再缓存到本地
|
||||
* 读:本地缓存命中 → 返回;未命中 → 七牛拉取 → 缓存本地 → 返回
|
||||
* 动态存储:运行时判断 PAR_STORAGE_TYPE 环境变量。
|
||||
* qiniu → 七牛主存储 + 本地缓存,否则 → 纯本地存储。
|
||||
* 修改环境变量后无需重启,下次请求即生效。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@ConditionalOnBean(QiniuStorageService.class)
|
||||
public class CachedStorageService implements StorageService {
|
||||
|
||||
private final QiniuStorageService primary;
|
||||
private final StorageService cache;
|
||||
private final QiniuStorageService qiniu;
|
||||
private final LocalStorageService local;
|
||||
|
||||
public CachedStorageService(QiniuStorageService primary, LocalStorageService cache) {
|
||||
this.primary = primary;
|
||||
this.cache = cache;
|
||||
log.info("缓存存储就绪: 七牛(主) + 本地(缓存)");
|
||||
public CachedStorageService(QiniuStorageService qiniu, LocalStorageService local) {
|
||||
this.qiniu = qiniu;
|
||||
this.local = local;
|
||||
log.info("动态存储就绪: {} 模式", isQiniuMode() ? "qiniu+本地缓存" : "本地");
|
||||
}
|
||||
|
||||
private boolean isQiniuMode() {
|
||||
return "qiniu".equalsIgnoreCase(System.getenv("PAR_STORAGE_TYPE"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void store(String path, String content) {
|
||||
// 先写七牛
|
||||
primary.store(path, content);
|
||||
// 再写本地缓存(失败不影响)
|
||||
try {
|
||||
cache.store(path, content);
|
||||
} catch (Exception e) {
|
||||
log.warn("写本地缓存失败 {}: {}", path, e.getMessage());
|
||||
if (isQiniuMode()) {
|
||||
qiniu.store(path, content);
|
||||
try { local.store(path, content); } catch (Exception ignored) {}
|
||||
} else {
|
||||
local.store(path, content);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String read(String path) {
|
||||
// 本地缓存命中
|
||||
String cached = cache.read(path);
|
||||
if (cached != null) return cached;
|
||||
|
||||
// 从七牛拉取
|
||||
String remote = primary.read(path);
|
||||
if (remote != null) {
|
||||
// 回写本地缓存
|
||||
try {
|
||||
cache.store(path, remote);
|
||||
} catch (Exception ignored) {}
|
||||
// qiniu 模式:本地缓存优先
|
||||
if (isQiniuMode()) {
|
||||
String cached = local.read(path);
|
||||
if (cached != null) return cached;
|
||||
String remote = qiniu.read(path);
|
||||
if (remote != null) {
|
||||
try { local.store(path, remote); } catch (Exception ignored) {}
|
||||
}
|
||||
return remote;
|
||||
}
|
||||
return null;
|
||||
return local.read(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(String path) {
|
||||
return cache.exists(path) || primary.exists(path);
|
||||
if (isQiniuMode()) return local.exists(path) || qiniu.exists(path);
|
||||
return local.exists(path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.par.core.storage;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -18,7 +17,6 @@ import java.nio.file.Paths;
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@ConditionalOnMissingBean(QiniuStorageService.class)
|
||||
public class LocalStorageService implements StorageService {
|
||||
|
||||
private final Path baseDir;
|
||||
|
||||
@@ -7,8 +7,6 @@ import com.qiniu.storage.Region;
|
||||
import com.qiniu.storage.UploadManager;
|
||||
import com.qiniu.util.Auth;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.net.URI;
|
||||
@@ -18,46 +16,48 @@ import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* 七牛云存储(主存储)。
|
||||
* 读文件通过公开 URL 直接 HTTP GET,不依赖 SDK 内部 API。
|
||||
* 七牛云存储。
|
||||
* 每次调用时从环境变量读取配置,支持热更新无需重启。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@ConditionalOnProperty(name = "par.storage.type", havingValue = "qiniu")
|
||||
public class QiniuStorageService implements StorageService {
|
||||
|
||||
private final UploadManager uploadManager;
|
||||
private final BucketManager bucketManager;
|
||||
private final Auth auth;
|
||||
private final String bucket;
|
||||
private final String cdnDomain;
|
||||
private final HttpClient httpClient;
|
||||
private final HttpClient httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(5))
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.build();
|
||||
|
||||
public QiniuStorageService(
|
||||
@Value("${par.storage.qiniu.access-key:}") String ak,
|
||||
@Value("${par.storage.qiniu.secret-key:}") String sk,
|
||||
@Value("${par.storage.qiniu.bucket:par-configs}") String bucket,
|
||||
@Value("${par.storage.qiniu.region:z2}") String region,
|
||||
@Value("${par.storage.qiniu.domain:}") String domain) {
|
||||
private UploadManager uploadManager;
|
||||
private BucketManager bucketManager;
|
||||
private Auth auth;
|
||||
private String bucket;
|
||||
private String cdnDomain;
|
||||
|
||||
this.bucket = bucket;
|
||||
private synchronized void ensureInit() {
|
||||
if (bucket != null) return;
|
||||
String ak = env("QINIU_AK");
|
||||
String sk = env("QINIU_SK");
|
||||
String region = env("QINIU_REGION", "z2");
|
||||
this.bucket = env("QINIU_BUCKET", "par-configs");
|
||||
this.cdnDomain = env("QINIU_DOMAIN", "http://" + bucket + ".qiniucdn.com");
|
||||
this.auth = Auth.create(ak, sk);
|
||||
this.cdnDomain = domain != null && !domain.isBlank()
|
||||
? domain : "http://" + bucket + ".qiniucdn.com";
|
||||
|
||||
Configuration cfg = new Configuration(getRegion(region));
|
||||
Configuration cfg = new Configuration(switch (region) {
|
||||
case "z0" -> Region.region0();
|
||||
case "z1" -> Region.region1();
|
||||
case "z2" -> Region.region2();
|
||||
case "na0" -> Region.regionNa0();
|
||||
case "as0" -> Region.regionAs0();
|
||||
default -> Region.autoRegion();
|
||||
});
|
||||
this.uploadManager = new UploadManager(cfg);
|
||||
this.bucketManager = new BucketManager(auth, cfg);
|
||||
this.httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(5))
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.build();
|
||||
|
||||
log.info("Qiniu 存储就绪: bucket={}, domain={}", bucket, cdnDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void store(String path, String content) {
|
||||
ensureInit();
|
||||
try {
|
||||
uploadManager.put(content.getBytes(), path, auth.uploadToken(bucket));
|
||||
log.debug("Qiniu 上传: {}", path);
|
||||
@@ -69,16 +69,13 @@ public class QiniuStorageService implements StorageService {
|
||||
|
||||
@Override
|
||||
public String read(String path) {
|
||||
ensureInit();
|
||||
try {
|
||||
String url = cdnDomain + "/" + path;
|
||||
var req = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.timeout(Duration.ofSeconds(10))
|
||||
.GET().build();
|
||||
var req = HttpRequest.newBuilder().uri(URI.create(url))
|
||||
.timeout(Duration.ofSeconds(10)).GET().build();
|
||||
var resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
|
||||
if (resp.statusCode() == 200) {
|
||||
return resp.body();
|
||||
}
|
||||
if (resp.statusCode() == 200) return resp.body();
|
||||
log.warn("Qiniu 读取失败 {}: HTTP {}", path, resp.statusCode());
|
||||
} catch (Exception e) {
|
||||
log.warn("Qiniu 读取失败 {}: {}", path, e.getMessage());
|
||||
@@ -88,6 +85,7 @@ public class QiniuStorageService implements StorageService {
|
||||
|
||||
@Override
|
||||
public boolean exists(String path) {
|
||||
ensureInit();
|
||||
try {
|
||||
return bucketManager.stat(bucket, path) != null;
|
||||
} catch (QiniuException e) {
|
||||
@@ -95,14 +93,9 @@ public class QiniuStorageService implements StorageService {
|
||||
}
|
||||
}
|
||||
|
||||
private Region getRegion(String region) {
|
||||
return switch (region) {
|
||||
case "z0" -> Region.region0();
|
||||
case "z1" -> Region.region1();
|
||||
case "z2" -> Region.region2();
|
||||
case "na0" -> Region.regionNa0();
|
||||
case "as0" -> Region.regionAs0();
|
||||
default -> Region.autoRegion();
|
||||
};
|
||||
private static String env(String key) { return System.getenv(key); }
|
||||
private static String env(String key, String def) {
|
||||
String v = System.getenv(key);
|
||||
return v != null && !v.isEmpty() ? v : def;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user