You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

泛型ReadService注入报错:期望单个Bean却找到两个Mapper实例

问题:通用ReadService依赖注入冲突,指定泛型仍出现多Bean匹配

背景

我尝试实现一个通用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。

改造步骤:

  1. 修改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());
    }

}
  1. 为每个实体创建专属服务子类,比如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处理
    }

}
  1. 修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 13:43:16