泛型ReadService注入报错:期望单个Bean却找到两个Mapper实例
背景
我尝试实现一个通用ReadService,封装findById、findByIdToDto、findAll、findAllToDto等方法,避免多个实体重复编码,代码如下:
@Service @Transactional(readOnly = true) @RequiredArgsConstructor public class ReadService< ID, ENTITY, DTO, MAPPER extends BaseMapperToDTO<ENTITY, DTO>, REPOSITORY extends JpaRepository<ENTITY, ID>> { private final MAPPER mapper; private final REPOSITORY repository; @Setter private Class<ENTITY> entityClass; public ENTITY findById(ID id) throws FunctionalException { return repository .findById(id) .orElseThrow(() -> new FunctionalException( "No " + entityClass.getSimpleName() + " found with the id: " + id)); } public DTO findByIdToDto(ID id) throws FunctionalException { return mapper.toDto(findById(id)); } public Collection<ENTITY> findAll() { return repository.findAll(); } public Collection<DTO> findAllToDto() { return mapper.toDtos(findAll()); } }
启动报错
但项目启动时出现如下错误:
*************************** APPLICATION FAILED TO START *************************** Description: Parameter 0 of constructor in com.paulmarcelinbejan.toolbox.web.service.ReadService required a single bean, but 2 were found: - continentMapperImpl: defined in file [/projects/HyperBank/HyperBank-Maps/target/classes/com/hyperbank/maps/continent/mapper/ContinentMapperImpl.class] - countryMapperImpl: defined in file [/projects/HyperBank/HyperBank-Maps/target/classes/com/hyperbank/maps/country/mapper/CountryMapperImpl.class] Action: Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
控制器使用代码
我在Controller中使用该服务的代码如下:
@RestController @RequiredArgsConstructor @RequestMapping("/api/continent") public class ContinentRestController { private final ReadService<Integer, Continent, ContinentDto, ContinentMapper, ContinentRepository> readService; @PostConstruct private void injectClass() { readService.setEntityClass(Continent.class); } @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) public @ResponseBody ContinentDto findById(@PathVariable Integer id) throws FunctionalException { return readService.findByIdToDto(id); } }
疑问
我已经为ReadService指定了泛型类型,为什么Spring仍然找到两个Mapper Bean?而且我实现的Create、Update、Delete通用服务可以正常运行:
private final CreateService<Integer, Continent, ContinentDto, ContinentMapper, ContinentRepository> createService; private final UpdateService<Integer, Continent, ContinentDto, ContinentMapper, ContinentRepository> updateService; private final DeleteService<Integer, Continent, ContinentDto, ContinentMapper, ContinentRepository> deleteService;
请问最优解决方案是什么?
问题根源
Spring依赖注入处理泛型类时,运行时会执行类型擦除,直接把ReadService标记为@Service会让Spring将其视为单一Bean类型,无法区分不同泛型参数的实例。你的增删改服务能正常运行,大概率是因为它们没有被标记为@Service,而是通过子类或显式实例化的方式,为每个实体创建了独立的Bean。
最优方案:为每个实体创建专属ReadService子类(推荐)
不要让ReadService本身成为Spring Bean,而是将其作为抽象基类,为每个实体创建对应的子类并标记为@Service,让Spring明确区分不同Bean,自动注入匹配的Mapper和Repository。
改造步骤:
- 修改ReadService,移除@Service注解,保留事务和构造器注解,将其改为抽象类:
@Transactional(readOnly = true) @RequiredArgsConstructor public abstract class ReadService< ID, ENTITY, DTO, MAPPER extends BaseMapperToDTO<ENTITY, DTO>, REPOSITORY extends JpaRepository<ENTITY, ID>> { private final MAPPER mapper; private final REPOSITORY repository; @Setter private Class<ENTITY> entityClass; // 原有方法保持不变 public ENTITY findById(ID id) throws FunctionalException { return repository .findById(id) .orElseThrow(() -> new FunctionalException( "No " + entityClass.getSimpleName() + " found with the id: " + id)); } public DTO findByIdToDto(ID id) throws FunctionalException { return mapper.toDto(findById(id)); } public Collection<ENTITY> findAll() { return repository.findAll(); } public Collection<DTO> findAllToDto() { return mapper.toDtos(findAll()); } }
- 为每个实体创建专属服务子类,比如Continent的服务:
@Service public class ContinentReadService extends ReadService<Integer, Continent, ContinentDto, ContinentMapper, ContinentRepository> { public ContinentReadService(ContinentMapper mapper, ContinentRepository repository) { super(mapper, repository); this.setEntityClass(Continent.class); // 直接在子类中设置实体类,无需Controller处理 } }
- 修改Controller注入专属服务:
@RestController @RequiredArgsConstructor @RequestMapping("/api/continent") public class ContinentRestController { private final ContinentReadService readService; @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) public @ResponseBody ContinentDto findById(@PathVariable Integer id) throws FunctionalException { return readService.findByIdToDto(id); } }
替代方案:使用@Qualifier手动指定Bean(不推荐)
如果不想创建子类,可以在Controller中通过@Qualifier指定具体的Mapper和Repository,手动构造ReadService实例,但这种方式会增加代码冗余,违背通用服务的设计初衷:
@RestController @RequestMapping("/api/continent") public class ContinentRestController { private final ReadService<Integer, Continent, ContinentDto, ContinentMapper, ContinentRepository> readService; @Autowired public ContinentRestController(@Qualifier("continentMapperImpl") ContinentMapper mapper, ContinentRepository repository) { this.readService = new ReadService<>(mapper, repository); this.readService.setEntityClass(Continent.class); } // 原有接口方法不变 }
增删改服务正常运行的原因
大概率是因为你没有给这些服务标记@Service,而是通过子类或手动实例化的方式,为每个实体创建了独立的Bean实例,Spring不会将它们视为单一的泛型Bean,因此不会出现多Bean匹配冲突。
内容的提问来源于stack exchange,提问作者Paul Marcelin Bejan

