javaspringspring-bootgenericsspring-bean

Generic Service implementation required a single bean, but 2 were found


I was trying to implement a generic ReadService with the methods that I want to implement for many entities (findById, findByIdToDto, findAll, findAllToDto), so in that way I will avoid writing the same code over and over again.

@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());
    }

}

The problem is that got the following error:

***************************
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

I use the ReadService in the 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);
    }

}

Why it founds two beans of MAPPER if ReadService is parametrized? Each time I want to use it, I'll have to parametrize it with the MAPPER that it has to use.

What is the best way to solve it?

P.S. It's important to have those generic implementation cause it saves me a lot of time. Consider also that I already implemented Create, Update and Delete and they worked perfectly fine:

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;

Solution

  • I removed the @Service on the ReadService

    @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;
    
        private final 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());
        }
    
    }
    

    I think that was not a great idea to use the ReadService inside Controller, so I decided to move it on the ContinentService, and then use directly the ContinentService on the Controller side.

    I'm creating the instance of ReadService inside the constructor of the ContinentService.

    @Service
    public class ContinentService {
    
        public ContinentService(ContinentMapper continentMapper, ContinentRepository continentRepository) {
            readService = new ReadService<>(continentMapper, continentRepository, Continent.class);
        }
    
        private final ReadService<Integer, Continent, ContinentDto, ContinentMapper, ContinentRepository> readService;
    
        public ContinentDto findByIdToDto(Integer id) throws FunctionalException {
            return readService.findByIdToDto(id);
        }
    
        public Collection<ContinentDto> findAllToDto() {
            return readService.findAllToDto();
        }
    
    }
    

    and so this is the Controller

    @RestController
    @RequiredArgsConstructor
    @RequestMapping("/api/country")
    public class CountryRestController {
    
        private final CountryService countryService;
    
        @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
        public @ResponseBody CountryDto findById(@PathVariable Integer id) throws FunctionalException {
            return countryService.findByIdToDto(id);
        }
    
        @GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE)
        public @ResponseBody Collection<CountryDto> findAll() {
            return countryService.findAllToDto();
        }
    
    }