
本文介绍一种基于 Spring @ConditionalOnProperty 的优雅方案,通过接口抽象与条件化 Bean 注册,在同一服务中复用核心逻辑,实现“仅日志预演”与“真实执行”两种模式的无缝切换。
本文介绍一种基于 spring `@conditionalonproperty` 的优雅方案,通过接口抽象与条件化 bean 注册,在同一 service 中复用核心逻辑,实现“仅日志预演”与“真实执行”两种模式的无缝切换。
在现代 Spring 应用开发中,常遇到一类典型需求:同一套业务决策逻辑(如比对 JSON 配置与数据库状态、识别新增/删除项),需支持两种运行模式——真实执行 CRUD 与 只预演、仅记录将要执行的操作(即 dry-run 模式)。硬编码 if (readOnlyAndLog) { log(...) } else { save/delete(...) } 不仅破坏单一职责,还导致测试困难、逻辑耦合、扩展性差。
最佳实践是面向接口设计 + 条件化 Bean 注入。核心思路是:将“执行动作”的职责从主服务中剥离,定义统一行为契约(接口),再由 Spring 根据配置自动注入对应实现。
✅ 步骤一:定义行为契约接口
public interface SubjectHandler {
void handleSubject(Subject subject, OperationType operation);
enum OperationType {
CREATE, UPDATE, DELETE, EXISTS_IN_DB, MISSING_IN_JSON
}
}该接口封装了对单个 Subject 的所有可能操作语义,便于统一日志格式与后续扩展。
✅ 步骤二:提供两种实现类
1. 日志预演实现(dry-run 模式)
@Component
@ConditionalOnProperty(name = "app.readOnlyAndLog", havingValue = "true", matchIfMissing = false)
public class ReadOnlySubjectHandler implements SubjectHandler {
private static final Logger log = LoggerFactory.getLogger(ReadOnlySubjectHandler.class);
@Override
public void handleSubject(Subject subject, OperationType operation) {
switch (operation) {
case CREATE -> log.info("{} will be created", subject.getName());
case UPDATE -> log.info("{} will be updated", subject.getName());
case DELETE -> log.info("{} will be deleted", subject.getName());
case EXISTS_IN_DB -> log.info("{} is already defined in the DB", subject.getName());
case MISSING_IN_JSON -> log.warn("{} exists in DB but is missing from config", subject.getName());
}
}
}2. 真实执行实现(生产模式)
@Component
@ConditionalOnProperty(name = "app.readOnlyAndLog", havingValue = "false", matchIfMissing = true)
public class DatabaseSubjectHandler implements SubjectHandler {
private final SubjectRepository repository;
public DatabaseSubjectHandler(SubjectRepository repository) {
this.repository = repository;
}
@Override
public void handleSubject(Subject subject, OperationType operation) {
switch (operation) {
case CREATE -> repository.save(subject);
case UPDATE -> repository.save(subject);
case DELETE -> repository.deleteByName(subject.getName());
case EXISTS_IN_DB -> {} // no-op, just a check result
case MISSING_IN_JSON -> repository.deleteByName(subject.getName());
}
}
}? 关键点:@ConditionalOnProperty 的 matchIfMissing = true 表示当配置未显式设置时,默认启用数据库执行模式,保障生产环境安全;而 havingValue = "true" 明确匹配布尔字符串值(注意 Spring Boot 2.4+ 对 true/false 字符串解析更严格,推荐使用 Boolean.parseBoolean() 或 @Value("${app.readOnlyAndLog:false}") 做兜底)。
✅ 步骤三:主服务注入并复用决策逻辑
@Service
public class SubjectSyncService {
private final SubjectHandler subjectHandler;
private final SubjectRepository repository;
public SubjectSyncService(SubjectHandler subjectHandler, SubjectRepository repository) {
this.subjectHandler = subjectHandler;
this.repository = repository;
}
@Transactional
public void syncSubjectsFromJson(Resource jsonResource) throws IOException {
JsonNode rootNode = new ObjectMapper().readTree(jsonResource.getInputStream());
List<Subject> configSubjects = parseSubjects(rootNode);
// Step 1: 获取当前 DB 中所有 subject(按 name 建索引)
Map<String, Subject> dbMap = repository.findAll().stream()
.collect(Collectors.toMap(Subject::getName, Function.identity()));
// Step 2: 遍历配置项 —— 决策创建或更新
for (Subject configSub : configSubjects) {
if (dbMap.containsKey(configSub.getName())) {
subjectHandler.handleSubject(configSub, SubjectHandler.OperationType.UPDATE);
// 可选:实际更新字段(若需要)
} else {
subjectHandler.handleSubject(configSub, SubjectHandler.OperationType.CREATE);
}
}
// Step 3: 找出 DB 中存在但配置中缺失的项 —— 决策删除
dbMap.keySet().stream()
.filter(dbName -> configSubjects.stream().noneMatch(s -> s.getName().equals(dbName)))
.forEach(missingName -> {
Subject missingSubject = dbMap.get(missingName);
subjectHandler.handleSubject(missingSubject, SubjectHandler.OperationType.DELETE);
});
}
private List<Subject> parseSubjects(JsonNode rootNode) {
return StreamSupport.stream(
rootNode.path("subjects").spliterator(), false)
.map(this::mapToSubject)
.toList();
}
private Subject mapToSubject(JsonNode node) {
return new Subject(
node.path("name").asText(),
node.path("description").asText(),
node.path("price").asInt()
);
}
}该服务完全不关心“是否真实执行”,它只负责识别差异、做出决策、委托处理——真正的执行策略由 Spring 在启动时根据 app.readOnlyAndLog 配置动态绑定,实现了关注点彻底分离。
⚠️ 注意事项与进阶建议
- 配置命名规范:建议使用 app.readOnlyAndLog 而非裸名 readOnlyAndLog,避免与 Spring Boot 内部属性冲突;
- 日志级别选择:预演模式推荐使用 INFO 级别(便于运维查看),而非 DEBUG,确保关键预演信息可被生产日志系统捕获;
- 增强可观测性:可在 SubjectSyncService 中添加 @Timed(Micrometer)或自定义指标,统计预演/执行耗时、操作数量等;
- 扩展性预留:未来若需支持“生成 SQL 脚本”模式,只需新增 SqlScriptSubjectHandler 并添加对应 @ConditionalOnProperty 即可,主服务零修改;
- 测试友好性:单元测试时可通过 @MockBean SubjectHandler 精准验证不同场景下的调用行为,无需启动整个容器。
通过这一设计,你不仅满足了“开关控制执行模式”的原始需求,更构建了一个高内聚、低耦合、易测试、可演进的服务架构。真正的业务价值,永远藏在清晰的抽象与克制的条件分支之中。










