SpringBoot重构贫血模型为富领域模型依赖注入报错解决方案
问题根因
报错直接原因是你在Score类上添加了@Repository注解,将其标记为Spring管理的Bean。Spring启动时实例化单例Bean会尝试自动注入构造函数的所有入参,但构造函数要求的String类型gameId、List<BatsmanScore>类型batsmanScores都是业务运行时动态传入的参数,不是容器中预注册的Bean,因此抛出找不到匹配Bean的错误。
这个报错本质是对富领域模型的分层边界理解错误,把领域对象、仓储组件的职责完全混淆了。
核心设计误区
- 富领域模型的核心是领域对象自身持有状态和对应业务逻辑,这类对象是业务流程执行过程中按需创建的(比如新比赛开局才生成对应ID的分数实例),生命周期由业务逻辑控制,绝对不能直接加
@Service/@Repository这类注解交给Spring单例容器管理 - 修改后的
Score类同时混杂了三种完全不同的职责:全局分数存储的仓储能力(持有全局scoreRepository的Map)、单场比赛分数的领域对象属性(gameId、单场batsmanScores)、分数操作的业务方法,本身违背单一职责原则,就算绕过注入问题,单例模式下多场比赛的分数数据会互相覆盖污染 - 为了适配Spring注入给领域模型加无参构造、把核心字段改成非final是典型的反模式,会直接破坏领域对象的不变性,允许生成状态不合法的对象实例,完全违背富领域模型的设计初衷,没有必要采用。
标准重构实践
Spring生态下做富领域模型重构,首先要明确三类组件的严格边界,绝对不能混写:
- 仓储组件:只负责数据的增删改查,不包含业务逻辑,交给Spring单例容器管理
- 领域对象:封装自身状态和核心业务逻辑,不依赖Spring容器,通过构造函数初始化所有必填字段,核心字段尽量声明为final保证不变性,生命周期由业务流程控制
- 服务组件:仅做流程协调,负责串联参数校验、领域对象创建/查询、业务方法调用、持久化操作这类跨组件的流程逻辑,不承载核心业务规则,交给Spring容器管理
对应重构后的代码如下:
1. 富领域模型(无Spring依赖)
// 纯领域对象,不添加任何Spring注解,不交给容器管理 public class Score { // 核心字段全部声明为final,保证对象创建后状态合法 private final String gameId; private final List<BatsmanScore> batsmanScores; // 强制传入必填字段,杜绝无参构造生成非法状态的对象 public Score(String gameId, List<BatsmanScore> batsmanScores) { // 构造函数内直接做字段合法性校验,是富模型的核心优势 if (gameId == null || gameId.isBlank()) { throw new IllegalArgumentException("比赛ID不能为空"); } if (batsmanScores == null) { throw new IllegalArgumentException("分数列表不能为空"); } this.gameId = gameId; this.batsmanScores = new ArrayList<>(batsmanScores); } // 业务逻辑内聚到领域对象内部,不散落在Service层 public void addBatsmanScores(List<BatsmanScore> newScores) { if (newScores == null || newScores.isEmpty()) { return; } // 此处可添加分数校验、规则计算等核心业务逻辑,比如击球手分数不能为负 this.batsmanScores.addAll(newScores); } // 只提供必要的getter,不对外暴露setter public List<BatsmanScore> getBatsmanScores() { // 返回不可变副本,防止外部代码篡改内部状态 return List.copyOf(batsmanScores); } public String getGameId() { return gameId; } }
2. 仓储层(Spring管理,仅做数据存取)
@Repository public class ScoreRepository { // 内存存储示例,实际项目可替换为数据库操作 private final Map<String, Score> scoreStore = new HashMap<>(); public Score findByGameId(String gameId) { return scoreStore.get(gameId); } public void save(Score score) { scoreStore.put(score.getGameId(), score); } }
3. 服务层(Spring管理,仅做流程协调)
@Service public class ScoreService { // 仅注入仓储依赖,不直接持有领域对象 private final ScoreRepository scoreRepository; public ScoreService(ScoreRepository scoreRepository) { this.scoreRepository = scoreRepository; } public List<BatsmanScore> getBatsmanScores(String gameId) { Score score = scoreRepository.findByGameId(gameId); if (score == null) { return List.of(); } return score.getBatsmanScores(); } public void storeScores(String gameId, List<BatsmanScore> batsmanScores) { Score existingScore = scoreRepository.findByGameId(gameId); if (existingScore == null) { // 不存在对应比赛分数时,通过构造函数创建合法的领域对象 existingScore = new Score(gameId, batsmanScores); } else { // 存在对应分数时,调用领域对象自身的业务方法更新状态 existingScore.addBatsmanScores(batsmanScores); } // 通过仓储完成持久化 scoreRepository.save(existingScore); } }
重构注意事项
- 不要给领域对象添加任何Spring组件注解,不要试图让Spring直接帮你注入创建领域对象。如果领域方法需要用到容器管理的依赖(比如计分规则组件),直接通过方法入参传入即可,不要用字段注入
- 领域对象必须保证自身状态合法:只保留包含所有必填字段的构造函数,不要添加无参构造,不要为字段添加setter方法,返回集合类型字段时优先返回不可变副本,避免外部代码随意篡改对象内部状态
- 核心业务规则(比如分数合法性校验、得分计算逻辑)要全部内聚在领域对象内部,不要散落在Service层。Service层要尽量薄,只做流程编排,避免把领域对象退化成只有getter/setter的贫血数据结构
- 少数场景下需要用工厂模式统一创建领域对象时,可以单独写工厂类交给Spring管理,不要把注解直接加在领域类上
内容的提问来源于stack exchange,提问作者user3310115
相关产品推荐
相关产品推荐

